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()
: getlightningd
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.