Building a Core Lightning Plugin with Go

Introduction

Plugins for lightning nodes help automate functionalities, process specific actions, and provide extra functionalities. Plugins can be used to skip process like btcli4j, provides a Bitcoin Backend to safely enable pruning mode, or requestinvoice that provides a way to start a server to request invoices.

A core lightning plugin can be built for personal use or the bitcoin community at large. Community built plugins can be seen here.

This article will guide on you through on how to create a plugin for a Core Lightning node.

We will go over:

  • What is Core Lightning?
  • What does a Core Lightning Plugin do?
  • How does the Plugin work with CLN?
  • Go Plugin package
  • Building with the Go Plugin package

What is Core Lightning?

*Core Lightning (previously c-lightning) is a lightweight, highly customizable and standard-compliant implementation of the Lightning Network protocol. *

Core Lightning(CLN) helps you set up a highly customizable Lightning node running with a Bitcoin node as its backend. One way to customize a node is by using plugins with specific actions. It currently only works in Ubuntu and macOS.

What does a Core Lightning Plugin do?

Core Lightning Plugins are small, simple packages that help build and extend functionalities of the core lightning daemon. They are subprocesses that get started after been called together when starting the lightningd daemon. lightningd and plugins communicate in several forms;

  • Command line option allows plugins to register their command line options that are exposed through lightningd so that only the main process needs to be configured at once.

  • JSON-RPC command allows plugins to add their commands to the JSON-RPC interface.

  • Event subscriptions provide plugins with a push-based notification mechanism about events from the lightningd and allow plugins to perform actions based on that.

  • Hooks are a primitive that allows plugins to be notified about internal events in lightningd and alter its behaviour or inject custom behaviours.

How does the Plugin work with CLN?

Plugins communicate with Core Lightning using JSON-RPC2 over its stdin and stdout. The plugin acts as a server, where lightningd acts as a client and makes JSON-RPC requests to the plugin expecting a JSON-RPC response.

The lightningd client when started calls the plugin getmanifest() rpc, to get the manifest of the plugin, the manifest json that carries options, rpcmethods, subscriptions, hooks of the plugin.

After the getmanifest() method, the init() method is called, and the plugin options set by the user or default value is sent back to the plugin for initialization.

During runtime, lightningd communicates with the plugin, when an rpcmethod is called, or a event is triggered.

Writing all these functionalities to handle communication and default methods and still worrying about building your plugin's main functionality can be tedious, so cln4go plugin API does all these for you, while you focus on the plugin's core functionality.

Cln4go Plugin API

Then Cln4go plugin API is a package that allows create and manage CLN plugins in go. It helps generate and handle operations of the plugin allowing you focus on your plugin functionality. It has different functions it uses to achieve this;

  • New() : this allows you create a new plugin struct with access to all plugin function available.
  • RegisterOption(): Allows you register an command-line option that can used for within your plugin settings.
  • RegisterRPCMethod(): register an RPC method that performs a certain action.
  • RegisterNotification(): register to a subscription event, the function is called when that event is triggered.
  • GetOpt(): this returns the value of a CLI option of your plugin.
  • GetConf(): get lightningd configurations.
  • Start(): start the plugin

Building with Cln4go

We will be building a simple plugin that allows user get what network lightningd is running on, and to receive mails whenever a payment is made to their node.

Setting up

To get started, visit starter plugin template, and use the Use this template button and create a new repo to start working on.

Clone the new repo:

   git clone git@github.com:<github_username>/<repo_name>.git

Currently the starter plugin has an RPC method; hello, and a shutdown event subscription.

Edit the Makefile, update the OS and ARCH value based on your operating system;

    NAME=demo-cln-plugin
    //using M1 Mac
    OS=darwin
    ARCH=amd64

Update the the plugin go.mod to your github repo

    module github/<github_username>/<repo_name>

Also in cmd/plugin.go update the import;

    import (
        core "github/<github_username>/<repo_name>/pkg/plugin"
    
        "github.com/vincenzopalazzo/cln4go/plugin"
    )

Running the make build command creates and executable file.

Next we will building the unique functionality of our plugin.

Building Plugin Functionality

We will be adding a new subscription for when a payment is made and an rpc method that returns the what network lightningd is running on.

Now we will be writing our new RPC method and subscription in pkg/plugin/plugin.go


    type NetworkChecker[T PluginState] struct{}
    
    func (instance *NetworkChecker[PluginState]) Call(plugin *plugin.Plugin[PluginState], request map[string]any) (map[string]any, error) {
    clnPath, found := plugin.GetConf("lightning-dir")
	if !found {
		panic(found)
	}

	rpcFileName, found := plugin.GetConf("rpc-file")
	if !found {
		panic(found)
	}
	unixPath := strings.Join([]string{clnPath.(string), rpcFileName.(string)}, "/")
	client, err := client.NewUnix(unixPath)
        if err != nil {
            return map[string]any{"message": "error setting up client"}, err
        }
        response, err := client.Call("getinfo", make(map[string]interface{}))
        if err != nil {
            return map[string]any{"message": "error calling getinfo"}, err
        }
        network := response["network"]
        if !found {
            return map[string]any{"message": "unknown"}, nil
        }
        return map[string]any{"message": fmt.Sprintf("running on %s", network.(string))}, nil
    }
    
    func sendMail(email string) {
        // send mail or do something else
    }
    
    type OnPayment[T PluginState] struct{}
    
    func (instance *OnPayment[PluginState]) Call(plugin *plugin.Plugin[PluginState], request map[string]any) {
        receivingEmail, found := plugin.GetOpt("demo-email")
    
        if !found {
            receivingEmail = "bar@email.com"
        }
        sendMail(receivingEmail.(string))
    }

In the code above, the NetworkChecker() RPCMethod generates a path to make a client request, sends a get_info rpc call and returns the network lightningd is running on.

We also have a sendEmail() that will be used in sending an email when a payment is made. To make this guide short we would be leaving this blank, you can complete this by reading this article.

OnPayment() is called when an invoice is paid, and gets the email to send mail to and calls the sendMail() function.

Registering Functionalities

To register the function built in the last section, add to the cmd/pl``ugin.go file:

    plugin.RegisterRPCMethod("networktester", "", "an example of rpc method", &core.NetworkChecker[core.PluginState]{})
    
    plugin.RegisterNotification("on-payment", &core.OnPayment[core.PluginState]{}

We will be also be adding our email option;

    plugin.RegisterOption("demo-email", "string", "foo@email.com", "Email Plugin should send payment email to", false)

Testing

To test the our plugin, run make build to generate an executable test file. If an executable test file already exists, it is updated accordingly. Next, you start core lightning daemon, attached with the plugin path.

    lightningd --testnet --plugin=/path-to-exec-file/demo-cln-plugin-darwin-amd64 --demo-email=johndoe@bitcoin.org

Update the test file, tests/plugin_test.go

func TestCallNetworkMethod(t *testing.T) {
	path := os.Getenv("CLN_UNIX_SOCKET")
	client, err := client.NewUnix(path)
	if err != nil {
		panic(err)
	}
	response, err := client.Call("networktester", make(map[string]interface{}))
	if err != nil {
		panic(err)
	}

	message, found := response["message"]
	if !found {
		t.Error("The message is not found")
	}
	network := os.Getenv("NETWORK")
	if message != "running on "+network {
		t.Errorf("message received %s different from expected %s", message, "running on "+network)
	}
}

Next we export the: The NETWORK env : network you're running on. CLN_UNIX_SOCKET: path to lightning rpc file.

Run test

    go test tests/plugin_test.go

Conclusion

We have built a fully functional plugin that can work with any core lightning node. Plugins are amazing tools to build on, as they help to make lightning development better and more secure.

You can see the full code on this repo.

Resources

OTHER POSTS

Powered By Swish