Intoduction to cln4go

Go framework for Core Lightning Daemon with a flexible interface

GitHub Workflow Status GitHub go.mod Go version (subdirectory of monorepo)

Home Page Source Code

Welcome in cln4go module that it is developed under the umbrella of the coffee-tools.

Packages

This module will implement all the necessary tool to interact with the core lightning ecosystem with modern Go, and the following module are available:

CrateDescription
clng4go-clientPackage that provides means to make RPC bindings from Go code to the core lightning daemon
cln4go-pluginPackage that provides a plugin API to give the possibility to implement a plugin in Go
cln4go-commonPackage that provides common interface to interact with the core lightning ecosystem in Go

Contributing guidelines

Read our Hacking guide

Supports

If you want support this library consider to donate with the following methods

cln4go client

At the current state of the library the client provided by cln4go is really minimal and it do not provide a typed module with all the types of core lightning API, this feature is left for the future when a modular code generator will be ready.

For the moment the definition of the types are left to the user of this library, to give also the flexibility to define only the model that the user needs. However, with the go generics feature this library is still strongly typed, and allow to make typed calls to core lightning.

Basic Usage of the Client

The client is implemented with a procedural programming style due the limitation of go generics that do not allow to have generics paramters on a single method of a struct.

So, a basic usage of the client is

package main

import (
    "fmt",
    "os",
    
    cln "github.com/vincenzopalazzo/cln4go/client"
)

var rpc *cln.UnixRPC

type Map = map[string]any

func init() {
    path := os.Getenv("CLN_UNIX_SOCKET")
    if path == "" {
        err := fmt.Errorf("Unix path not exported with the CLN_UNIX_SOCKET env variable")
        panic(err)
    }
    
    rpc, _ = cln.NewUnix(path)
}

func main() {
    getinfo, err := cln.Call[Map, Map](rpc, "getinfo", map[string]any{})
    if err != nil {
        fmt.Printf("cln4go core lightning error: %s", err)
    } else { 
        fmt.Printf("cln4go getinfo: node alias %s", getinfo["alias"])
    }
}

Improve Client performance

One of the pain of golang is the reflection used to encode and decode an object from and to JSON, and this can be very downgrading while using the library in a restricted env like a raspberry pi 2/3.

In order to work around this problem, the client is able to change the encoder that it is used internally to encode and decode the json, and the definition of this encoder is left to the user of the library.

A example of custom encoder that use the library go-json that made the conversion from and to json without reflection is reported below

package json

import (
    "fmt"

    json "github.com/goccy/go-json"
)

type FastJSON struct{}

func (self *FastJSON) EncodeToByte(obj any) ([]byte, error) {
    return json.Marshal(obj)
}

func (self *FastJSON) EncodeToString(obj any) (*string, error) {
    jsonByte, err := json.Marshal(obj)
    if err != nil {
        return nil, err
    }
    jsonStr := string(jsonByte)
    return &jsonStr, nil
}

func (self *FastJSON) DecodeFromString(jsonStr *string, obj any) error {
    return json.Unmarshal([]byte(*jsonStr), &obj)
}

func (self *FastJSON) DecodeFromBytes(jsonByte []byte, obj any) error {
    if len(jsonByte) == 0 {
        return fmt.Errorf("encoding a null byte array")
    }
    return json.Unmarshal(jsonByte, &obj)
}

Now we use the encoder as

package main

import (
    "fmt",
    "os",
   
    json "./json.go"
   
    cln "github.com/vincenzopalazzo/cln4go/client"
)

var rpc *cln.UnixRPC

type Map = map[string]any

func init() {
    path := os.Getenv("CLN_UNIX_SOCKET")
    if path == "" {
        err := fmt.Errorf("Unix path not exported with the CLN_UNIX_SOCKET env variable")
        panic(err)
    }
    
    rpc, _ = cln.NewUnix(path)
    rpc.SetEncoder(&json.FastJSON{})
}

func main() {
    getinfo, err := cln.Call[Map, Map](rpc, "getinfo", map[string]any{})
    if err != nil {
        fmt.Printf("cln4go core lightning error: %s", err)
    } else { 
        fmt.Printf("cln4go getinfo: node alias %s", getinfo["alias"])
    }
}

A benchmark with the popular library glightning is available here

cln4go Plugin

Implementing a plugin in Go it's really straightforward, and the cln4go provide an easy interface to implement one from scratch

An hello world plugin can be implemented with

package main

import (
    "github.com/vincenzopalazzo/cln4go/plugin"
)

// Common interface of the State!
type IPluginState interface {
    GetName() *string
    SetName(name string)
}

// PluginState - each plugin has a state that is stored 
// inside the plugin and mutated across the plugin lifecycle.
//
// This is the perfect place where store the UNIX client if 
// needed by the plugin, otherwise the plugin do not include this 
// dependencies.
type PluginState struct {
    Name string
}

func (self *PluginState) GetName() *string {
    return &self.Name
}

func (self *PluginState) SetName(name string) {
    self.Name = name
}

// A callback in cln4go is implemeted with the Command,
// So this is just a struct to implement the interface on top
type OnRPCCommand[T IPluginState] struct{}

// Implementing the callback, now the void return here, this mean that it is a notification
func (instance *OnRPCCommand[IPluginState]) Call(plugin *plugin.Plugin[IPluginState], request map[string]any) {}

// Implementing another callback
type Hello[T IPluginState] struct{}

// Implementing the callback, please note that this is not an void method, so this callback can be register 
// as RPC method or as an hook.
func (instance *Hello[IPluginState]) Call(plugin *plugin.Plugin[IPluginState], request map[string]any) (map[string]any, error) {
    return map[string]any{"message": "hello from go 1.18"}, nil
}

func main() {
    state := PluginState{
        Name: "cln4go",
    }
    plugin := plugin.New(&state, true, plugin.DummyOnInit[*PluginState])
    plugin.RegisterOption("foo", "string", "Hello Go", "An example of option", false)
    plugin.RegisterRPCMethod("hello", "", "an example of rpc method", &Hello[*PluginState]{})
    plugin.RegisterNotification("rpc_command", &OnRPCCommand[*PluginState]{})
    plugin.Start()
}

The code to write a callback is to much, and this would be better to have just a func declaration, so the API to define a callback can be improved but for now we leave this as it is because with generics we have some limitation, and a feature from the Go lang side is required. We keep track of this feature with the issue #12

Intercept on init callback

While the API is minimal and do not include the RPC API for core lightning, with the plugin it is possible put the RPC client inside the State and create the client inside the on init callback.

It is possible register the on init callback with a simple callback like the following code

package main

import (
    "github.com/vincenzopalazzo/cln4go/plugin"
    cln "github.com/vincenzopalazzo/cln4go/client"
)

type State struct {
    Client *cln.UnixRPC
}

func OnInit[T State](plugin *plugin.Plugin[T], request map[string]any) map[string]any {
    state := plugin.State()

    lightningDir, _ := plugin.GetConf("lightning-dir")
    rpcFile, _ := plugin.GetConf("rpc-file")

    rpcPath := strings.Join([]string{lightningDir.(string), rpcFile.(string)}, "/")
    rpc, err = cln.NewUnix(path)
    if err != nil {
        panic(err)
    }
    state.Client = rpc
    return map[string]any{}
}

func main() {
    state := State{}
    plugin := plugin.New(&state, true, OnInit[*PluginState])
    plugin.Start()
}

As in the client it is possible define a custom encoder and set it as we did inside the client section, but also it is register and access to a custom tracer as discussed in the common section to have a different way to log the plugin.

Plugin Template

If you do not want start from scratch, you can use the plugin template.

cln4go common

The common module implement a sequence of utils that help to share code between other modules. In particular it implement the JSON RPC 2.0 types and also the Tracer and encoder module.

We discussed the Custom encoder inside the client section, and in this chapter we discuss how to create a custom logger and inject it inside the client or plugin library.

A custom logger can be defined with the following code

package trace

import (
    "github.com/LNOpenMetrics/lnmetrics.utils/log"
    "github.com/vincenzopalazzo/cln4go/comm/tracer"
)

type Tracer struct{}

func (self *Tracer) Log(lebel tracer.TracerLevel, msg string) {}

func (self *Tracer) Logf(level tracer.TracerLevel, msg string, args ...any) {}

func (self *Tracer) Info(msg string) {
    log.GetInstance().Info(msg)
}

func (self *Tracer) Infof(msg string, args ...any) {
    log.GetInstance().Infof(msg, args...)
}

func (self *Tracer) Trace(msg string) {
    log.GetInstance().Error(msg)
}

func (self *Tracer) Tracef(msg string, args ...any) {
    log.GetInstance().Errorf(msg, args...)
}

and then on the Client or on the Plugin, just call SetTracer(&Tracer{})

cln4go Benchmarks

There is already a popular client in Go that is glightning, so why I need to use another library, where i need to specify the model by hand?

What is the benefit of this?

cln4go Benefits

The benefit of cln4go is the modularity and the flexibility that it give to the user to configure the Go client that the user want for core lightning.

One of the flexibility that the API give to use it to inject a custom tracer (logger) and a custom JSON encoder. This feature will give to inject a custom JSON encoder and to use a custom logger that do not interact with the core lightning logger, but use a different stream such as a File.

See how to implement a custom Encoder in plugin docs and how to write a custom logger in the common docs

In addition, this client do not exclude the possibility to have a library that include all the core lightning model and call by generating the typed client on top of the cln4go client.

It is on the road map of the library to have a autogenerate client from the core lightning json schema, so if you are planning to help with this client, you can consider to use the core lightning core generation code to generate the strongly typed library on top of the cln4go client.

Benchmarks

In this section we run benchmarks to compare the cln4go with the encoder discussed in the section regarding the client compared with the solution provided by glightning library.

TODO put here the figure!

To run yourself the benchmarks, you can run the following commands

>> export CLN_UNIX_SOCKET=/run/media/vincent/VincentSSD/.lightning/testnet/lightning-rpc
>> make dep
>> make bench_check

cd bench; go run main.go
2023/04/13 13:51:21 INFO ----------------- cln4go-listnodes -----------------
2023/04/13 13:51:21 INFO Number of run: 1000000000
2023/04/13 13:51:21 INFO Time taken: 28.793444ms
2023/04/13 13:51:21 INFO Size result: 1
2023/04/13 13:51:22 INFO ----------------- glightning-listnodes -----------------
2023/04/13 13:51:22 INFO Number of run: 1000000000
2023/04/13 13:51:22 INFO Time taken: 68.307247ms
2023/04/13 13:51:22 INFO Size result: 2
2023/04/13 13:51:23 INFO ----------------- cln4go-listchannels -----------------
2023/04/13 13:51:23 INFO Number of run: 1000000000
2023/04/13 13:51:23 INFO Time taken: 104.217164ms
2023/04/13 13:51:23 INFO Size result: 3
2023/04/13 13:51:25 INFO ----------------- glightning-listchannels -----------------
2023/04/13 13:51:25 INFO Number of run: 1000000000
2023/04/13 13:51:25 INFO Time taken: 177.613183ms
2023/04/13 13:51:25 INFO Size result: 4
2023/04/13 13:51:25 INFO json: {"bench":[{"name":"cln4go-listnodes","runs":1000000000,"times":28793444,"time_str":"28.793444ms"},{"name":"glightning-listnodes","runs":1000000000,"times":68307247,"time_str":"68.307247ms"},{"name":"cln4go-listchannels","runs":1000000000,"times":104217164,"time_str":"104.217164ms"},{"name":"glightning-listchannels","runs":1000000000,"times":177613183,"time_str":"177.613183ms"}]}

"core lightning Go Framework" HACKING guide

Table of Content

  • Introduction
  • Code Style
  • Commit Style
  • How to make the release

Introduction

Welcome to the HACKING guide and let's peek into how a day in the life of a "core lightning Go Framework" maintainer looks like.

After reading this you should be ready to contribute to the repository and also be one of the next maintainers in the future if you would like!

Let's begin

Code style

To ensure consistency throughout the source code, these rules are to be kept in mind:

  • All features or bug fixes must be tested by one or more specs (unit-tests).
  • All public API methods must be documented. (Details TBC).
  • Four spaces
  • Call make fmt before committing
  • If you can, GPG-sign at least your top commit when filing a PR

If You Don’t Know The Right Thing, Do The Simplest Thing

Sometimes the right way is unclear, so it’s best not to spend time on it. It’s far easier to rewrite simple code than complex code, too.

Use of FIXME

There are two cases in which you should use a /* FIXME: */ comment: one is where an optimization seems possible, but it’s unclear if it’s yet worthwhile, and the second one is in the case of an ugly corner case which could be improved (and may be in a following patch).

There are always compromises in code: eventually, it needs to ship. FIXME is grep-fodder for yourself and others, as well as useful warning signs if we later encounter an issue in some part of the code.

Write For Today: Unused Code Is Buggy Code

Don’t overdesign: complexity is a killer. If you need a fancy data structure, start with a brute force linked list. Once that’s working, perhaps consider your fancy structure, but don’t implement a generic thing. Use /* FIXME: ...*/ to salve your conscience.

Keep Your Patches Reviewable

Try to make a single change at a time. It’s tempting to do “drive-by” fixes as you see other things, and a minimal amount is unavoidable, but you can end up shaving infinite yaks. This is a good time to drop a /* FIXME: ...*/ comment and move on.

Commit Style

The commit style is one of the more important concepts when managing a monorepo like "core lightning Rust Framework", and in particular, the commit style is used to generate the changelog for the next release.

Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

The header is mandatory while the scope of the header is optional.

All lines in a commit message should be at most 100 characters! This ensures better readability on GitHub as well as in various git tools.

The footer should contain a closing reference to an issue if any.

Some couple of examples are:

docs(changelog): update changelog to beta.5
fix(release): need to depend on the latest rxjs and zone.js

The version in our package.json gets copied to the one we publish, and users need the latest of these.

Types

  • feat: A new feature
  • fix: A bug fix
  • deprecate: Deprecate a feature and start to the removing process (3 official release or 1 major release)
  • remove: End of life for the feature.

Scopes

  • rpc: Changes related to the core lightning RPC wrapper library
  • plugin: Changes related to the plugin module
  • common: Changes related to the common module.

Subject

The subject contains a succinct description of the change:

  • use the imperative, present tense: "change" not "changed" nor "changes"
  • don't capitalize the first letter
  • no dot (.) at the end

Body

You are free to put all the content you want inside the body, but if you are fixing an exception or some wrong behavior you must put the details or stacktrace inside the body ensure that the search engine indexes it.

An example of commit body is the following one

checker: fixes overloading operation when the type is optimized

The stacktrace is the following one

} expected `Foo` not `Foo` - both operands must be the same type for operator overloading
   11 | }
   12 |
   13 | fn (_ Foo) == (_ Foo) bool {
      |                  ~~~
   14 |     return true
   15 | }---
description: "`Rust core lightning Rust framework` HACKING guide"
---

N.B: Part of this document is stolen from core lightning docs made with from @rustyrussell 's experience.

Programs must be written for people to read, and only incidentally for machines to execute. - Someone

Cheers!

Vincent