Hello friends! It’s August which means right now the heat and humidity are at insane levels here in Barcelona! I need some water!
Today, in this post, I would like to talk about how Besu’s Plugin API works and how you too can very easily build new plugins that expand the capabilities that the client offers by default.
It’s going to be a more technical article, so I expect the reader to be familiarized with some Java programming knowledge.
Without further ado, let’s begin!
Why a Plugin API?
One of the cool additions that this enterprise-ready Ethereum client offers is the Plugin API. This alone contrasts with what Geth and OpenEthereum (previously known as Parity) are offering. When it comes to customizing certain aspects of the client, your options are limited with these systems. Let’s suppose you want to store processed blocks in another system for later analysis, you are left with only two possible solutions:
- Use JSON RPC APIs.
- Modify the client directly to perform the modification from the inside.
These two options are not ideal for multiple reasons:
For option one, practically all JSON RPC APIs are not prepared for extracting all the required information in one request, so you need to perform multiple ones. That means you need to code some non-trivial logic to do this well. On top of that, JSON RPC introduces some overhead as you need to serialize everything, send it through the wire, and deserialize back again. Moreover, in our past experience, we have found that under heavy loads, such as when syncing for the first time, the client may become unstable and periodically core dump.
On the other hand, for option two, having to create a fork of a big codebase introduces a lot of overhead and maintenance burden. Yes, you have access to all of the internals, but you need to merge the latest changes from upstream in order to be up to date. It’s a powerful option but with terrible maintenance experience.
Introducing Plugin API!
What Besu has done with its Plugin API is create a third (and in our opinion better) option. Some of the apparent benefits that this provide to the programmer:
- Easy way to register custom commands and custom options that your plugin can register.
- Have direct access to Java calls.
- Internal client data like block propagation, metrics, consensus state, and addition and removals to the transaction pool are all directly accessible.
- You don’t need to serialize anything if you don’t want to.
- There’s no necessity to keep polling data.
All of these traits combine perfectly to create powerful plugins never seen before in the Ethereum ecosystem. That’s neat!
How does the Plugin API work?
Now that you have read about the advantages and benefits of Besu’s Plugin API, you may be asking: “But how does it work?”
Before diving in deeper on the internals, it’s worth noting and explaining how Besu is architected as a client. As they say, a picture is worth a thousand words:
As you can see in the picture above, the high-level architecture is divided into three main blocks:
- Core - All basic entities that are common to the whole codebase are found here and also other key important pieces like the EVM (Ethereum Virtual Machine) or the different consensus mechanisms.
- Storage - As the name implies, all these modules and entities are in charge of keeping the state of the client.
- Networking - These modules are in charge of keeping the client synchronized, primitives for discovering peers, and other related stuff.
The reality is that the codebase is split into multiple independent but interrelated packages, so the division is not precisely as depicted in the image above but, for the sake of obtaining a basic understanding, the graphic serves us well.
Let’s take a look then to the next diagram:
As you can see in the image above, the Plugin API has access to different services. Those services are defined in this package and, for now, there are only five. It’s expected that number may grow to support more services as the codebase evolves.
It’s all about implementing an interface and following the lifecycle!
All plugins work by conforming to the Plugin interface defined in
BesuPlugin
:
/**
* Base interface for Besu plugins.
*
* <p>Plugins are discovered and loaded using {@link java.util.ServiceLoader} from jar files within
* Besu's plugin directory. See the {@link java.util.ServiceLoader} documentation for how to
* register plugins.
*/
public interface BesuPlugin {
/**
* Returns the name of the plugin. This name is used to trigger specific actions on individual
* plugins.
*
* @return an {@link Optional} wrapping the unique name of the plugin.
*/
default Optional<String> getName() {
return Optional.of(this.getClass().getName());
}
/**
* Called when the plugin is first registered with Besu. Plugins are registered very early in the
* Besu lifecycle and should use this callback to register any command-line options required via
* the PicoCLIOptions service.
*
* <p>The <code>context</code> parameter should be stored in a field in the plugin. This is the
* only time it will be provided to the plugin and is how the plugin will interact with Besu.
*
* <p>Typically the plugin will not begin operation until the {@link #start()} method is called.
*
* @param context the context that provides access to Besu services.
*/
void register(BesuContext context);
/**
* Called once Besu has loaded configuration and is starting up. The plugin should begin
* operation, including registering any event listener with Besu services and starting any
* background threads the plugin requires.
*/
void start();
/**
* Called when the plugin is being reloaded. This method will be called trough a dedicated JSON
* RPC endpoint. If not overridden this method does nothing for convenience. The plugin should
* only implement this method if it supports dynamic reloading.
*
* <p>The plugin should reload its configuration dynamically or do nothing if not applicable.
*
* @return a {@link CompletableFuture}
*/
default CompletableFuture<Void> reloadConfiguration() {
return CompletableFuture.completedFuture(null);
}
/**
* Called when the plugin is being stopped. This method will be called as part of Besu shutting
* down but may also be called at other times to disable the plugin.
*
* <p>The plugin should remove any registered listeners and stop any background threads it
* started.
*/
void stop();
}
As you can see, the contract you have to follow is not that complex. It has well defined method names which clearly signal when they are going to be called or executed.
Second, the Plugin API defines what is called the plugin lifecycle:
We can summarize the lifecycle into three distinct phases:
- Initialization
- Execution
- Halting
But, if we dig in the implementation details, the real phases to which the plugin will transition are defined in the
following Lifecycle
enum
that you can find inside BesuPluginContextImpl
class:
enum Lifecycle {
UNINITIALIZED,
REGISTERING,
REGISTERED,
STARTING,
STARTED,
STOPPING,
STOPPED
}
These are all the possible states the plugin may be in at any given time. Each time the plugin transitions to a
new/different state, the Plugin API will call related methods according to the BesuPlugin
interface.
It’s about the Context too!
Another important piece of information worth writting about is the
BesuContext
:
/** Allows plugins to access Besu services. */
public interface BesuContext {
/**
* Get the requested service, if it is available. There are a number of reasons that a service may
* not be available:
*
* <ul>
* <li>The service may not have started yet. Most services are not available before the {@link
* BesuPlugin#start()} method is called
* <li>The service is not supported by this version of Besu
* <li>The service may not be applicable to the current configuration. For example some services
* may only be available when a proof of authority network is in use
* </ul>
*
* <p>Since plugins are automatically loaded, unless the user has specifically requested
* functionality provided by the plugin, no error should be raised if required services are
* unavailable.
*
* @param serviceType the class defining the requested service.
* @param <T> the service type
* @return an optional containing the instance of the requested service, or empty if the service
* is unavailable
*/
<T> Optional<T> getService(Class<T> serviceType);
}
Usually, during the Registering
phase, the Plugin API will call the method register(BesuContext context)
on the
BesuPlugin
interface and this is the only chance the programmer has to keep a local instance of the BesuContext
that
can be used later for retrieving the services.
Also, as you may have guessed by reading the comments above, a service may not be available even to the current
configuration, so it’s best to check properly and defend your plugin implementation if the returned service is null
.
How does the Plugin loading mechanism work?
The class in charge of searching for and loading the plugins is also the same
BesuPluginContextImpl
class that we mentioned before, and more precisely the implementation is found inside the registerPlugins method. Below
is the complete implementation:
public void registerPlugins(final Path pluginsDir) {
checkState(
state == Lifecycle.UNINITIALIZED,
"Besu plugins have already been registered. Cannot register additional plugins.");
final ClassLoader pluginLoader =
pluginDirectoryLoader(pluginsDir).orElse(this.getClass().getClassLoader());
state = Lifecycle.REGISTERING;
final ServiceLoader<BesuPlugin> serviceLoader =
ServiceLoader.load(BesuPlugin.class, pluginLoader);
for (final BesuPlugin plugin : serviceLoader) {
try {
plugin.register(this);
LOG.debug("Registered plugin of type {}.", plugin.getClass().getName());
addPluginVersion(plugin);
} catch (final Exception e) {
LOG.error(
"Error registering plugin of type "
+ plugin.getClass().getName()
+ ", start and stop will not be called.",
e);
continue;
}
plugins.add(plugin);
}
LOG.debug("Plugin registration complete.");
state = Lifecycle.REGISTERED;
}
As you may have noticed, the implementation is quite simple! It’s just a for
loop that scans JARs
in a concrete
folder!
There are a couple of things worth explaining, though:
First, Besu tries to register an URLClassLoader
in the context of the plugin JAR
, so that way all your plugin classes can be found. Be aware that your plugin JAR
has to be a fat JAR
so all dependencies are included as well. If you want more information about how ClassLoader
works in Java, this tutorial made by Baeldung covers enough to get you a
decent understanding.
Second, Besu also uses the ServiceLoader
API that Java offers in order to provide a standard mechanism of loading a concrete service implementation that conforms
to a given interface (i.e. BesuPlugin
). As the official documentation says:
A ServiceLoader is an object that locates and loads service providers deployed in the run time environment at a time of an application’s choosing
In order to conform to the specification, your plugin needs to include what is called a provider-configuration
file
that tells the ServiceLoader
which is the class that it needs to instantiate.
Let’s suppose your main plugin class is called MyAwesomePlugin
and the package is io.myawesome.plugin
, you are
required to produce a JAR
with a folder named META-INF/services/io.myawesome.plugin
with the following content:
io.myawesome.plugin.MyAwesomePlugin
And that’s it! With that file the ServiceLoader
knows which classes it needs to construct.
One cool thing is, there’s no limit on how many classes the BesuPlugin
interface implements, you can specify multiple
ones like this:
io.myawesome.plugin.MyAwesomePlugin1
io.myawesome.plugin.MyAwesomePlugin2
io.myawesome.plugin.MyAwesomePlugin3
That’s pretty neat! You can have multiple classes that implement the BesuPlugin
interface and have those inside one
JAR
.
Finally, the order in which the plugins are loaded is not guaranteed at all. Besu internally uses
Files.list()
method from Java NIO
package. This means you can’t expect your plugin to run first every time. Keep that in mind if you are considering using
multiple plugins with a concrete ordering!
What else do I need to do to create a plugin?
The easiest way of doing it is by creating a regular Java or Kotlin Gradle
project.
From there, the only real important thing to add is the following dependency:
dependencies {
implementation("org.hyperledger.besu:plugin-api:1.5.1")
}
But also keep in mind that Besu requires creating what is considered a fat JAR
in order to work properly as I
mentioned before. There are a couple of good plugins out there, the one that we use at 41North is
ShadowJar.
In any case, creating the basics for a plugin is a repetitive process that you need to perform for each plugin you want
to create, for that reason we have created a Besu Plugin Starter
template in Github
which you can fork and kick-start your new Besu plugin at the speed of light.
Some of the additional benefits of this template repository are:
- It’s mainly based on working with Kotlin but easily adaptable to use vanilla Java.
- It allows you to generate fat JARs to distribute the plugin with ease.
- It allows you to check if dependencies are up to date.
- It allows you to auto-format the code.
Have a look! At the very least it will make a good guide for your own implementation.
What are the current limitations of the Plugin API?
Having worked with the Plugin API in two plugins (Besu Exflo and Besu Storage Replication) we have come across a few limitations that hopefully will be resolved soon:
- Some essential Besu services are not exposed in the
BesuContext
. The Majority of exposed services are for registering listeners or implementations but there’s no way of accessing useful entities likeBlockchain
or theWorldStateArchive
. Well you can do it, but you need to do some reflection trickery that we believe is not necessary. We are currently discussing this in this Github Issue. - Besu allows you to implement sub-commands with PicoCLI but some of the internals that it would make sense to have access are not exposed. For more info see this Github Issue.
- Right now, plugins are not able to expose custom JSON RPC methods. We believe that allowing plugin implementers to
register custom JSON RPC commands would enable greater functionality. For example, you could create a plugin that
exposes a custom JSON RPC method called
myplugin_getAllTokens
that responds with a custom response with all scanned tokens. This idea has already been implemented in Quorum. We are also discussing this in this Github Issue. - As we have mentioned before, having an ordering mechanism to load the plugins could be useful. Right now, there’s no guarantee in which order your plugin is going to be called, so having dependencies on other plugins is not possible.
More Resources
If you want to dig deeper on these topics I recommend you the following resources:
- PegaSys released a webinar in May to educate people on how to create plugins. It’s a very interesting watch if you want to expand on what I have explained here.
- Take a look at the PluginsAPIDemo project they have released as well. It will serve you as a guide or reference.
- We have created a curated list of resources related to Besu called
Awesome Besu
. You will find more plugins made by the community, so it can also be used as a source of inspiration. - The official Hyperledger Besu documentation includes briefly some concepts related to plugins.
Get in touch!
Feel free to reach out to us via our contact form or by sending an email to [email protected]. We try to respond within 48 hours and look forward to hearing from you.