DEV Community

Joe Miyamoto
Joe Miyamoto

Posted on • Updated on

Building c-lightning plugin with .NET

Update 2022/05/20: Based on the methodology described in this article, I have created a package for building your own plugin. please check out DotNetLightning.ClnRpc

Recently, I have been modifying my own Lapps to work as a c-lightning plugin in order to make them work with not only with LND but also with c-lightning (What are Lapps?).

I couldn't find someone doing it with .NET/C#/F#, so I will leave here a guideline for how to.

I also made a sample application, so if you are more interested in the sample than the explanation, please go there. My application is in F#, but this sample is written in C#. The language is different, but the basic approach is the same in both cases.

In this case, we will assume that there is a single class named MyAwesomeRpcServer, that has method handlers for RPC invocation. It starts out like this

using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StreamJsonRpc;

namespace HelloWorldPlugin.Server
  public class MyAwesomeRpcServerOptions
    public string GreeterName { get; set; } = "World";
    public bool IsInitiated { get; set; }

  public class MyAwesomeRpcServer
    private readonly MyAwesomeRpcServerOptions _opts;

    public MyAwesomeRpcServer(MyAwesomeRpcServerOptions opts)
      _opts = opts;

    public async Task<string> HelloAsync(string name)
      return $"hello!! {name}! This is {_opts.GreeterName} !!";
Enter fullscreen mode Exit fullscreen mode

The requirements are

  1. We want to expose an rpc method called hello.
  2. We want to return a message containing GreeterName passed as a command line argument
  3. We want to return a message that includes the name passed as an RPC parameter from the User.
  4. The number of exposed RPC may increase in the future.

1. Create a JsonRPCServer using StreamJsonRPC.

First, create an application that acts as an ordinary JsonRPC 2.0 server.
The standard way to do this is to use the StreamJsonRpc library authored by MS.

If you want to use it as a normal web server over HTTP, this sample is a good reference.
However, the c-lightning plugin communicates with lightningd itself via standard input/output, so this time we will use Console.In and Console.Out for transport.

It looks like this

var rpcServer = new MyAwesomeRpcServer();
var formatter = new JsonMessageFormatter();
var handler = new NewLineDelimitedMessageHandler(Console.OpenStandardOutput(), Console.OpenStandardInput(), formatter);
var rpc = new JsonRpc(handler);
rpc.AddLocalRpcTarget(rpcServer, new JsonRpcTargetOptions());
rpc.ExceptionStrategy = ExceptionProcessing.CommonErrorData;
Enter fullscreen mode Exit fullscreen mode

The Json RPC 2.0 specification does not specify the way of separating each messages as well as transports, so we must choose a suitable one.
Since the c-lightning plugin uses newlines to delimit messages, we use NewLineDelimitedMessageHandler as a message handler.
Also, if the communication partner is not C# (which is not in our case), it is a good to specify ExceptionStrategy as ExceptionProcessing.CommonErrorData.

2. Make read/write operations thread-safe.

Normally, the RPC server runs in multi-threaded mode, but writing to stdin/output in parallel will result in message corruption.
The easiest way to deal with this problem is to process requests sequentially.

In this case, the easiest way is to use AsyncSemaphore, as described in the official guidelines.

In the case of plugins, performance issues related to rpc calls are rare, so we decided to limit writes by acquiring the semaphore for all methods.

3. Automatic generation of methods

The c-lightning plugin must expose two RPC methods: getmanifest and init.

The former is to pass information from the plugin to c-lightning, and the latter is to pass information other way around when c-lightning is launched.

Much of the processing logic can be generated automatically from other plugin method definitions, so we use reflection to keep things generic enough.

For example, getmanifest looks like this

    public async Task<ManifestDto> GetManifestAsync(bool allowDeprecatedApis = false, object? otherParams = null)
      using var releaser = await _semaphore.EnterAsync();
      var userDefinedMethodInfo =
          .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
          .Where(m => !m.IsSpecialName && !String.Equals(m.Name, "initasync", StringComparison.OrdinalIgnoreCase) && !String.Equals(m.Name, "getmanifestasync", StringComparison.OrdinalIgnoreCase));
      var methods =
          .Select(m =>
            var argSpec = m.GetParameters();
            var numDefaults =
              argSpec.Count(s => s.HasDefaultValue);
            var keywordArgsStartIndex = argSpec.Length - numDefaults;
            var args =
              argSpec.Where(s =>
                !string.Equals(s.Name, "plugin", StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(s.Name, "request", StringComparison.OrdinalIgnoreCase))
                .Select((s, i) => i < keywordArgsStartIndex ? s.Name : $"[{s.Name}]");

            var name = m.Name.ToLowerInvariant();
            if (name.EndsWith("async"))
              name = name[..^5];
            return new RPCMethodDTO
              Name = name,
              Usage = string.Join(' ', args),
              Description = _rpcDescriptions[name].Item1,
              LongDescription = _rpcDescriptions[name].Item2

      return new ManifestDto
        Options =
          // translate `System.CommandLine` to c-lightning compatible style.
        RpcMethods = methods.ToArray(),
        Notifications = new NotificationsDTO[]{},
        Subscriptions = new string[]{},
        Hooks = new object[] {},
        Dynamic = true,
        FeatureBits = null
Enter fullscreen mode Exit fullscreen mode

init is as follows. This is more difficult to automate than getmanifest, and the process varies depending on the requirements of the application.

    public async Task InitAsync(LnInitConfigurationDTO configuration, Dictionary<string, object> options)
      using var releaser = await _semaphore.EnterAsync();
      foreach (var op in options)
        var maybeProp =
            .FirstOrDefault(p => string.Equals(p.Name, op.Key, StringComparison.OrdinalIgnoreCase));
        maybeProp?.SetValue(_opts, op.Value);
      _opts.IsInitiated = true;
Enter fullscreen mode Exit fullscreen mode

init passes method arguments as objects.
The object has fields named configurations and options, So the method handler on the C# side must have a same name to automatically bind.

In this case, we only bind the startup options givin by the c-lightning side to _opts.
LnInitConfigurationDto's information on the c-lightning side (e.g. is an user using tor? What lightning feature bits are supported? ) can be used to freely perform the initialization process.

We set IsInitiated at the completion of the initialization process.
This is used later to suspend the server startup process until the init message is received.

4. JsonRPCLogger

The fact that c-lightning and the plugin communicate via standard input/output means that logs cannot be sent to standard output at will.

The existing c-lightning plugin sends a message to the c-lightning's log rpc method as an rpc notification and leaves the rest of the processing to the c-lightning side.
In this way, the logs are unified on the c-lightning side, making them easier to read.

.NET is bundled with ConsoleLogger, but this performs the writing process as a background task, so the aforementioned thread race may occur if we implement the logging logic in the same way.

To solve these problem, I have created my own ILoggerProvider.

5. startup process

c-lightning starts the plugin specified by the user at startup, passing the environment variable LIGHTNINGD_PLUGIN=1 when launching.

Thus, the presence or absence of this environment variable can be used to determine in the program whether the application should be run as a plugin or not.

In my case, I refer to this variable in two places
The first is where you configure the host.

Action<IHostBuilder> configureHostBuilder = hostBuilder =>
  var isPluginMode = Environment.GetEnvironmentVariable("LIGHTNINGD_PLUGIN") == "1";

  if (isPluginMode)
      .ConfigureLogging(builder => { builder.AddJsonRpcNotificationLogger();})
      .ConfigureServices(serviceCollection =>
     // configuration for running as a normal webserver.
Enter fullscreen mode Exit fullscreen mode

The other is during Host execution.
We delay the execution of IHost.RunAsync() as follows.

  var host = hostBuilder.Build();
  var isPluginMode = Environment.GetEnvironmentVariable("LIGHTNINGD_PLUGIN") == "1";
  if (isPluginMode)
    await host.StartJsonRpcServerForInitAsync(); /// initialize only rpc server and listen to `init` call.
  await host.RunAsync();
Enter fullscreen mode Exit fullscreen mode

This will delay the startup of BackgroundService and log output until the init message is received from lightningd.

6. compile as a single binary

Finally, the plugin must be compiled as an executable single binary.
Therefore, we utilize the Single file Executable Compilation feature of .NET

And that's it!
You can specify the --plugin or --plugin-dir of c-lightning to launch the plugin.

publish as a library?

At first, I thought about converting what I have done this time into a library and releasing it to the public, but since it is not that hard to do it, and the init process is difficult to automate, I will just leave the document and examples here.

If you want to make it into a library, probably the best way is to use the Source Generator

Top comments (0)