Hello friends!
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.
41north / besu-plugin-starter
Accelerate your Besu plugin development with this starter repository
⚡ Besu Plugin Starter ⚡
Kick-start your next Besu plugin!
💡 Introduction
Do you want to create a Besu plugin super fast? This template will help you get started!
What is included:
- generate fat
JARs
to distribute the plugin with ease. - easily check if all the various Besu dependencies are up to date.
- ensure a consistent coding style with auto-formatting.
Whilst this project is mainly geared towards Kotlin it can still be used with plain old Java.
Bundled Gradle plugins:
-
ShadowJar
- For creating fatJARs
. -
Ktlint
- For automatic formatting of Kotlin code. -
Gradle Versions Plugin
- Determine which dependencies have updates
🙈 Usage
Simply fork this repository and start hacking away!
Below is a summary of some useful gradle
tasks that you will have at your disposal:
Target | Description |
---|---|
assemble | Full JAR file in build/distributions as .jar . |
assembleDist | Creates .zip and .tar archives of the distribution |
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 hello@41north.dev. We try to respond within 48 hours and look forward to hearing from you.
Top comments (0)