Plugins
Nethermind plugins are a powerful way of extending its capabilities by adding new features and functionalities. If you need a functionality missing in Nethermind, you can add it yourself as a plugin! Actually, many Nethermind features are implemented as plugins, like L2 network support such as Optimism and Taiko, health checks, and Shutter, to name a few. The sky is the limit. Almost.
Nethermind plugins are .NET assemblies (.dll) that Nethermind's process loads on startup. By default, they are located in the plugins directory. To set a different location for plugins, use the --plugins-dir command line option. In that case, move the bundled plugins to the new location to ensure the correct functionality of Nethermind.
We have a dedicated Discord channel for plugin development. Please get in touch with us if you have any issues or need functionality that is not provided by the current plugin API.
This guide will walk you through writing a simple plugin to better understand the Nethermind plugin API and its capabilities.
Creating a basic plugin
Ensure you have installed the required version of the .NET SDK. See Building from source for the details.
To write a Nethermind plugin, you need the Nethermind API to be available to your code. There are two ways of achieving that:
- Using the Nethermind.ReferenceAssemblies NuGet package. This package is updated with each Nethermind release and is versioned the same. Thus, when choosing this approach, ensure the package version is lower than or equal to your target Nethermind version.
- Checking out the Nethermind source code and reference the required projects from the plugin. While this approach seems better for debugging your code, some setups had assembly version mismatch issues after upgrading Nethermind.
In this guide, we will use the first approach. So, let's pick a working directory for the plugin and create a library project as follows:
dotnet new classlib -n DemoPlugin -o .
Now, we need to add the NuGet package to get access to the Nethermind API:
dotnet add package Nethermind.ReferenceAssemblies
As the package name implies, it provides reference assemblies that are only enough to compile the project. To see the plugin in action, put the library assembly (.dll) in the Nethermind's plugins directory and then run Nethermind. We will get to that soon.
Now, we have everything we need to begin with the actual implementation. For the sake of simplicity, we will create a basic plugin, a classic example, that simply prints the famous "Hello, world!" message.
All Nethermind plugins must implement the INethermindPlugin interface. That's how Nethermind recognizes its plugins. So, let's create a DemoPlugin class implementing that interface:
using Nethermind.Api;
using Nethermind.Api.Extensions;
namespace DemoPlugin;
public class DemoPlugin : INethermindPlugin
{
public string Name => "Demo plugin";
public string Description => "A sample plugin for demo";
public string Author => "Anonymous";
public bool Enabled => true;
// The entry point of the plugin
public Task Init(INethermindApi nethermindApi)
{
var logger = nethermindApi.LogManager.GetClassLogger();
logger.Warn("Hello, world!");
return Task.CompletedTask;
}
}
Let's examine the code above. The properties at lines 8–11 are required and self-explanatory. The Name, Description, and Author are displayed on Nethermind startup for each loaded plugin. The Enabled property at line 11 tells Nethermind whether this plugin should be initialized. Only plugins returning true for Enabled are activated; the rest are skipped. Next is the Init() method at line 14, which is the main entry point of any plugin where initialization begins. Its only argument of type INethermindApi → IApiWithNetwork → IApiWithBlockchain → IApiWithStores → IBasicApi is the main gateway to the Nethermind API, as its name implies. The INethermindApi interface provides a rich functionality set essential for plugin development and is widely used in the Nethermind codebase.
In line 16, we get the logger instance we need to print our message. Usually, that instance is stored in a private field to be available to other class members, but in our example, we don't need that. Once we have the instance, we log the message as a warning so you can spot it easily in the logs.
The INethermindPlugin interface provides default implementations for the Init(), InitNetworkProtocol(), InitRpcModules(), and InitTxTypesAndRlpDecoders() methods. You only need to override the ones your plugin requires. In this basic example, we only override Init().
To see our plugin in action, let's build it first:
dotnet build
Once built, we need to copy the DemoPlugin.dll to Nethermind's plugins directory and run Nethermind. The output should be similar to the one below:
24 Jan 18:01:37 | Nethermind is starting up
...
24 Jan 18:01:37 | Loading 14 assemblies from ...
24 Jan 18:01:37 | Loading assembly DemoPlugin
24 Jan 18:01:37 | Found plugin type DemoPlugin
24 Jan 18:01:37 | Loading assembly Nethermind.Api
...
24 Jan 18:01:39 | Detected 17 plugins
...
24 Jan 18:01:39 | EthStats by Nethermind Enabled
24 Jan 18:01:39 | Demo plugin by Anonymous Enabled
24 Jan 18:01:39 | Hello, world!
...
That's it! We created our very first Nethermind plugin.
Configuration
As Nethermind is highly configurable, so may its plugins. The same flexible configuration features that Nethermind uses internally are also available to its plugins. That means a plugin can be configured with command line options, environment variables, and configuration files by simply implementing a single interface.
Nethermind loads and runs all the plugins it finds on startup, but it only initializes those whose Enabled property returns true. This behavior is particularly useful for resource-hungry plugins or those requiring a specific network (chain) to run on. Instead of hardcoding the Enabled property to true as we did above, a typical approach is to base it on a configuration setting. Let's implement that for our Demo plugin.
All Nethermind configurations must implement the IConfig interface. It's a 2 step process.
First, we derive a new interface from the IConfig and add all the required configuration options as properties. In our case, it's a single boolean property Enabled:
using Nethermind.Config;
namespace DemoPlugin;
public interface IDemoConfig : IConfig
{
// The attribute below is optional and serves as documentation
[ConfigItem(Description = "Whether to enable the Demo plugin.", DefaultValue = "false")]
bool Enabled { get; set; }
}
Second, we implement the interface above as follows:
namespace DemoPlugin;
public class DemoConfig : IDemoConfig
{
public bool Enabled { get; set; }
}
That's it for the configuration. Now, let's update our DemoPlugin class to use the configuration. Nethermind uses Autofac for dependency injection. This means we can inject our configuration interface directly into the plugin's constructor:
using Nethermind.Api;
using Nethermind.Api.Extensions;
namespace DemoPlugin;
public class DemoPlugin(IDemoConfig demoConfig) : INethermindPlugin
{
public string Name => "Demo plugin";
public string Description => "A sample plugin for demo";
public string Author => "Anonymous";
public bool Enabled => demoConfig.Enabled;
public Task Init(INethermindApi nethermindApi)
{
var logger = nethermindApi.LogManager.GetClassLogger();
logger.Warn("Hello, world!");
return Task.CompletedTask;
}
}
The highlighted lines show the key changes. At line 6, we use a primary constructor to accept an IDemoConfig instance that Nethermind's dependency injection container resolves automatically. At line 13, Enabled delegates to the configuration value, so the plugin is only activated when the user enables it.
You can also retrieve configuration via INethermindApi.Config<T>() in the Init() method:
public Task Init(INethermindApi nethermindApi)
{
var config = nethermindApi.Config<IDemoConfig>();
// ...
}
However, for controlling the Enabled property, constructor injection is preferred since Enabled is evaluated before Init() is called.
The configuration interface name must be in the I{PluginName}Config format. In our case, it's IDemoConfig.
The naming convention is crucial for mapping the configuration options. For instance, IDemoConfig.Enabled turns into the following configuration options:
--demo-enabledor--Demo.Enabledas a command line optionNETHERMIND_DEMOCONFIG_ENABLEDas an environment variable{ "Demo": { "Enabled": true|false } }as a JSON in a configuration file
Since now we know what our configuration options are, let's build the project, copy the library to Nethermind's plugins directory, and run Nethermind as we did previously:
24 Jan 18:01:37 | Nethermind is starting up
...
24 Jan 18:01:37 | Loading 14 assemblies from ...
24 Jan 18:01:37 | Loading assembly DemoPlugin
24 Jan 18:01:37 | Found plugin type DemoPlugin
24 Jan 18:01:37 | Loading assembly Nethermind.Api
...
24 Jan 18:01:39 | Detected 17 plugins
...
24 Jan 18:01:39 | EthStats by Nethermind Enabled
24 Jan 18:01:39 | Demo plugin by Anonymous Disabled
...
There's a slight difference compared to the previous run -- the "Hello, world!" message is gone and the plugin shows as "Disabled". The reason is that the plugin's Enabled property returns false because IDemoConfig.Enabled defaults to false. Let's set it to true using the command line option as follows:
nethermind --demo-enabled
Now we see that our message is back, and the configuration option works as intended! That is how to turn plugins on or off in Nethermind and provide other configuration options.
Last, let's test our plugin configuration documentation defined at line 8 in IDemoConfig.cs:
nethermind -h
The output should be similar to the following:
Description:
Usage:
nethermind [options]
Options:
-?, -h, --help Show help and usage information
--version Show version information
...
--demo-enabled, --Demo.Enabled <value> Whether to enable the Demo plugin.
--era-exportdirectory, --Era.ExportDirectory <value> Directory of archive export.
--era-from, --Era.From <value> Block number to import/export from.
...
Dependency injection and modules
Nethermind uses Autofac as its dependency injection (DI) container. Plugins can participate in DI by providing an Autofac Module through the Module property of INethermindPlugin. This is used to register services, override default implementations, and register initialization steps.
Here is an example of a plugin that registers an initialization step:
using Autofac;
using Autofac.Core;
using Nethermind.Api.Extensions;
using Nethermind.Api.Steps;
namespace DemoPlugin;
public class DemoPlugin(IDemoConfig demoConfig) : INethermindPlugin
{
public string Name => "Demo plugin";
public string Description => "A sample plugin for demo";
public string Author => "Anonymous";
public bool Enabled => demoConfig.Enabled;
public IModule Module => new DemoModule();
}
public class DemoModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// Register an initialization step
builder.AddStep(typeof(DemoStep));
}
}
Initialization steps
Initialization steps allow plugins to hook into Nethermind's startup sequence. Each step implements the IStep interface and is resolved through Autofac, so its dependencies are injected automatically via the constructor:
using System.Threading;
using System.Threading.Tasks;
using Nethermind.Api.Steps;
using Nethermind.Logging;
namespace DemoPlugin;
public class DemoStep(ILogManager logManager) : IStep
{
public Task Execute(CancellationToken cancellationToken)
{
var logger = logManager.GetClassLogger();
logger.Warn("Hello from DemoStep!");
return Task.CompletedTask;
}
}
Steps may declare dependencies on other steps to control the startup order using the RunnerStepDependencies attribute:
using Nethermind.Api.Steps;
using Nethermind.Init.Steps;
// This step runs after InitializeBlockchain
[RunnerStepDependencies(typeof(InitializeBlockchain))]
public class DemoStep(ILogManager logManager) : IStep
{
// ...
}
Debugging
As your code grows more complex and sophisticated, you may want to debug it at some point. These are the two ways to do that:
- Attaching the debugger to the Nethermind process
- Debugging the plugin together with the Nethermind codebase
Attaching to process
This approach is preferable if you focus on your plugin only and don't need to debug the Nethermind codebase.
This guide assumes you already have installed Nethermind. If you haven't, install it before moving on.
We recommend using Visual Studio or JetBrains Rider as a debugger on Windows. On Linux and macOS, we recommend JetBrains Rider. While Visual Studio Code can also attach to and debug processes, it does not support debugging the "SingleFile" .NET distributions that Nethermind distributives are.
You may want to check out the following before moving on:
Before attaching the debugger to the Nethermind process, we need to ensure Nethermind will pick up our plugin. There are two ways:
- Run Nethermind with the
--plugins-dircommand line option set to the output directory of the plugin project. We recommend copying the other bundled plugins from the originalpluginsdirectory to the new destination as you may be required depending on your use case. - Set the plugin project output to the Nethermind's
pluginsdirectory.
Either of the above approaches will ensure Nethermind loads our plugin with the latest changes automatically. The following video demonstrates what the debugging process looks like:
Debugging with Nethermind codebase
Another way to debug plugins is to debug them along with the Nethermind codebase. That requires obtaining the Nethermind source code and debugging it with the IDE of your choice. Visual Studio and JetBrains Rider are the most popular choices. Let's try that with our DemoPlugin example.
Step 1: Clone the Nethermind repo
We highly recommend cloning a stable version of the codebase to avoid any unwanted behavior on debugging. Usually, it's the latest released version of Nethermind. For example, the command below clones Nethermind v1.30.0:
git clone -b "1.30.0" --depth 1 https://github.com/nethermindeth/nethermind.git
Step 2: Configure the startup project
In the repo's root directory, open the src/Nethermind/Nethermind.slnx and set the Nethermind.Runner as a startup project. That is the Nethermind's executable that handles everything, including plugins.
Step 3: Add the plugin project to the solution
Add the DemoPlugin project to the solution to have everything in one place. Then, let's set the DemoPlugin project output to Nethermind's plugins directory so the latest changes are always available for Nethermind.Runner to pick up. Add the following to the DemoPlugin.csproj:
<PropertyGroup>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>$(SolutionDir)/artifacts/bin/Nethermind.Runner/debug/plugins</OutputPath>
</PropertyGroup>
Step 4: Configure build dependencies
Last, let's configure build dependencies so that launching Nethermind.Runner automatically builds our DemoPlugin with its latest changes, so you don't need to build the plugin separately each time before launching the debugger. With this said, we need to make the Nethermind.Runner project depend on the DemoPlugin project. See how to configure project dependencies below:
IDE-agnostic workaround
If your IDE doesn't provide project dependency configuration, you can achieve that functionality by referencing the DemoPlugin project from the Nethermind.Runner project. Run the following from src/Nethermind:
dotnet add ./Nethermind.Runner reference path/to/DemoPlugin.csproj
Then, in the Nethermind.Runner.csproj, find the reference to DemoPlugin and disable the reference output as follows:
...
<ProjectReference Include="path/to/DemoPlugin.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
...
Thus, the DemoPlugin won't be included in the output of Nethermind.Runner. This is important to avoid dependency conflicts.
Launching the debugger
Now, we're ready to launch the debugger and check the Nethermind logs for our plugin. You may notice that the "Hello, world!" message is missing, although Nethermind logs show the plugin is loaded. That's because we made it configurable with the Demo.Enabled option, which is false by default. Let's set it to true.
The launch configurations of Nethermind.Runner are defined in launchSettings.json. For instance, if we launch it with Hoodi, we set our Demo.Enabled configuration option as follows:
- CLI
- Environment variable
...
"Hoodi": {
"commandName": "Project",
"commandLineArgs": "-c hoodi --data-dir .data --demo-enabled",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
...
...
"Hoodi": {
"commandName": "Project",
"commandLineArgs": "-c hoodi --data-dir .data",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"NETHERMIND_DEMOCONFIG_ENABLED": "true"
}
},
...
Now, if we launch the debugger with Hoodi, we will see our "Hello, world!" message again!
Plugin types
Nethermind defines the following plugin types derived from INethermindPlugin intended for specific functionality:
-
IConsensusPluginPlugins of this type provide support for consensus algorithms by implementing the block producer factory interfaces. Only one
IConsensusPlugincan be active at a time. For example, see theOptimismPluginorEthashPlugin. -
IConsensusWrapperPluginPlugins of this type extend or change the handling of the Ethereum PoS consensus algorithm by wrapping the block production pipeline. For example, see the
MergePluginorShutterPlugin.
INethermindPlugin reference
The INethermindPlugin interface has the following members. Properties Name, Description, Author, and Enabled are required. All methods have default (empty) implementations.
| Member | Description |
|---|---|
Name | The display name of the plugin. |
Description | A brief description of the plugin. |
Author | The author of the plugin. |
Enabled | Whether the plugin is enabled. Only enabled plugins are initialized. |
MustInitialize | If true, Nethermind will not start if this plugin's initialization fails. Defaults to false. |
Module | An optional Autofac IModule for registering services and initialization steps with the DI container. Defaults to null. |
Init(INethermindApi) | The main initialization entry point. Called after the DI container is built. |
InitNetworkProtocol() | Initializes the network stack. |
InitRpcModules() | Initializes the JSON-RPC modules. |
InitTxTypesAndRlpDecoders(INethermindApi) | Registers custom transaction types and RLP decoders. |
Samples
- JSON-RPC handler
- More to be added later