Intoduction to cln4go
Go framework for Core Lightning Daemon with a flexible interface
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:
Crate | Description |
---|---|
clng4go-client | Package that provides means to make RPC bindings from Go code to the core lightning daemon |
cln4go-plugin | Package that provides a plugin API to give the possibility to implement a plugin in Go |
cln4go-common | Package 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
- Lightning address: vincenzopalazzo@coinos.io
- Bitcoin Sponsor
- Github donation
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!