Overview of lnmessage implementation
In this live we look at the implementation of lnmessage
JS library that let us send commando
messages to a CLN node. Specifically, we look at LnMessage.commando
and NoiseState.encryptMessage
methods. This give us the opportunity to talk about BOLT #8 and BOLT #1.
Transcript with corrections and improvements
Note that if you don't know how to use lnmessage
you might want to
check first these videos:
Note that at 1h16m I wrote a wrong equality (that I don't reproduce here). The correct equality I wanted to write is:
ecdh(Alice.priv, Bob.pub) = ecdh(Bob.priv, Alice.pub)
getinfo.js
In the current directory we have the file package.json
where we can
see that lnmessage
dependency is local and point to the lnmessage
repository that we have cloned locally:
{
"type": "module",
"dependencies": {
"lnmessage": "file:./lnmessage"
}
}
We also have the Node JS script getinfo.js
which:
uses
lnmessage
library to connect to our node (that we are going to start in a moment),send a
commando
message to our node asking to run the commandgetinfo
andprint out the response that we get back from our node.
Note that we'll replace the dots when we'll start our node.
#!/usr/bin/env node
import LnMessage from 'lnmessage'
import net from 'net'
// node l1
const NODE_ID = '...'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7171'
// rune
const RUNE = '...'
const ln = new LnMessage({
remoteNodePublicKey: NODE_ID,
tcpSocket: new net.Socket(),
ip: NODE_IP,
port: NODE_PORT
})
await ln.connect()
const getinfo = await ln.commando({
method: 'getinfo',
params: [],
rune: RUNE
})
console.log(getinfo)
process.exit() // ln.disconnect() if using lnmessage 0.2.6
Start 2 Lightning nodes running on regtest
Let's start two Lightning nodes running on the Bitcoin regtest
chain
by sourcing the script lightning/contrib/startup_regtest.sh
provided
in CLN repository and by running the command start_ln
:
◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
...
◉ tony@tony:~/clnlive:
$ start_ln
...
We can check that l1-cli
is just an alias for lightning-cli
with the
base directory being /tmp/l1-regtest
:
◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
Node id and rune
To get our script working correctly, we need l1
's node id and a rune
that authorizes us to run getinfo
command using commando
messages.
Let's get l1
's node id
◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54",
"alias": "VIOLENTDEITY",
"color": "02ba4b",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.05.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a0000a0269a2",
"node": "88a0000a0269a2",
"channel": "",
"invoice": "02000002024100"
}
}
and generates an unrestricted rune:
◉ tony@tony:~/clnlive:
$ l1-cli commando-rune
{
"rune": "QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==",
"unique_id": "0",
"warning_unrestricted_rune": "WARNING: This rune has no restrictions! Anyone who has access to this rune could drain funds from your node. Be careful when giving this to apps that you don't trust. Consider using the restrictions parameter to only allow access to specific rpc methods."
}
Now, we can set the variables NODE_ID
and RUNE
in the file
getinfo.js
with the values above:
...
// node l1
const NODE_ID = '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7171'
// rune
const RUNE = 'QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA=='
...
Run getinfo.js
Before we can run getinfo.js
we need to build lnmessage
library. To
do that we open a new terminal and we move into the directory
lnmessage
. Then we install the dependencies and build the package:
# TERMINAL 2
◉ tony@tony:~/clnlive/lnmessage:
$ npm i
...
◉ tony@tony:~/clnlive/lnmessage:
$ npm run build
...
Back into the terminal 1, we can now install the dependencies by running:
◉ tony@tony:~/clnlive/lnmessage:
$ npm i
...
And we can check that lnmessage
in node_modules
is a symbolic link to
the directory where we've clone and build lnmessage
locally:
◉ tony@tony:~/clnlive:
$ ls -l node_modules/lnmessage
lrwxrwxrwx 1 tony tony 12 Aug 17 16:17 node_modules/lnmessage -> ../lnmessage/
Finally we can run getinfo.js
script:
◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
id: '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54',
alias: 'VIOLENTDEITY',
color: '02ba4b',
num_peers: 1,
num_pending_channels: 0,
num_active_channels: 0,
num_inactive_channels: 0,
address: [],
binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7171 } ],
version: 'v23.05.2',
blockheight: 1,
network: 'regtest',
fees_collected_msat: 0,
'lightning-dir': '/tmp/l1-regtest/regtest',
our_features: {
init: '08a0000a0269a2',
node: '88a0000a0269a2',
channel: '',
invoice: '02000002024100'
}
}
lnmessage connects getinfo.js to the node l1
One important thing to notice is that when we use lnmessage
we connect
the script to the node l1
and so getinfo.js
in some way act as a peer.
Let's see that.
We comment the line process.exit()
in getinfo.js
to not keep connected
connect to the node l1
after we've send the commando
message.
Before we run again the script, in the terminal 2, we check that l1
has no peers:
◉ tony@tony:~/clnlive/lnmessage:
$ alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive/lnmessage:
$ l1-cli listpeers
{
"peer": []
}
Now in the terminal 1, we run getinfo.js
◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
id: '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54',
alias: 'VIOLENTDEITY',
color: '02ba4b',
num_peers: 1,
num_pending_channels: 0,
num_active_channels: 0,
num_inactive_channels: 0,
address: [],
binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7171 } ],
version: 'v23.05.2',
blockheight: 1,
network: 'regtest',
fees_collected_msat: 0,
'lightning-dir': '/tmp/l1-regtest/regtest',
our_features: {
init: '08a0000a0269a2',
node: '88a0000a0269a2',
channel: '',
invoice: '02000002024100'
}
}
and in the terminal 2, we check that the node l1
has a peer (which is
getinfo.js
):
◉ tony@tony:~/clnlive/lnmessage:
$ l1-cli listpeers
{
"peers": [
{
"id": "027e29",
"connected": true,
"num_channels": 0,
"netaddr": [
"127.0.0.1:41568"
],
"features": "0000000000000000"
}
]
}
lnmessage
Let's start with an overview of lnmessage
using cloc
and tree
utility:
◉ tony@tony:~/clnlive/lnmessage:
$ cd src/
◉ tony@tony:~/clnlive/lnmessage/src:
$ cloc .
20 text files.
20 unique files.
0 files ignored.
github.com/AlDanial/cloc v 1.90 T=0.02 s (834.7 files/s, 125745.7 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 20 390 790 1833
-------------------------------------------------------------------------------
SUM: 20 390 790 1833
-------------------------------------------------------------------------------
◉ tony@tony:~/clnlive/lnmessage/src:
$ tree
.
├── chacha
│ ├── chacha20.ts
│ ├── index.ts
│ └── poly1305.ts
├── crypto.ts
├── index.ts
├── messages
│ ├── BigIntUtils.ts
│ ├── BitField.ts
│ ├── buf.ts
│ ├── CommandoMessage.ts
│ ├── InitFeatureFlags.ts
│ ├── InitMessage.ts
│ ├── IWireMessage.ts
│ ├── MessageFactory.ts
│ ├── PingMessage.ts
│ ├── PongMessage.ts
│ └── read-tlvs.ts
├── noise-state.ts
├── socket-wrapper.ts
├── types.ts
└── validation.ts
2 directories, 20 files
Terminal session
We ran the following commands in this order:
And below you can read the terminal session (command lines and outputs):
◉ tony@tony:~/clnlive:
$ source lightning/contrib/startup_regtest.sh
lightning-cli is /usr/local/bin/lightning-cli
lightningd is /usr/local/bin/lightningd
Useful commands:
start_ln 3: start three nodes, l1, l2, l3
connect 1 2: connect l1 and l2
fund_nodes: connect all nodes with channels, in a row
stop_ln: shutdown
destroy_ln: remove ln directories
◉ tony@tony:~/clnlive:
$ start_ln
Bitcoin Core starting
awaiting bitcoind...
Making "default" bitcoind wallet.
error code: -35
error message:
Wallet "default" is already loaded.
[1] 863417
[2] 863451
WARNING: eatmydata not found: install it for faster testing
Commands:
l1-cli, l1-log,
l2-cli, l2-log,
bt-cli, stop_ln, fund_nodes
◉ tony@tony:~/clnlive:
$ l1-cli getinfo
{
"id": "02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54",
"alias": "VIOLENTDEITY",
"color": "02ba4b",
"num_peers": 0,
"num_pending_channels": 0,
"num_active_channels": 0,
"num_inactive_channels": 0,
"address": [],
"binding": [
{
"type": "ipv4",
"address": "127.0.0.1",
"port": 7171
}
],
"version": "v23.05.2",
"blockheight": 1,
"network": "regtest",
"fees_collected_msat": 0,
"lightning-dir": "/tmp/l1-regtest/regtest",
"our_features": {
"init": "08a0000a0269a2",
"node": "88a0000a0269a2",
"channel": "",
"invoice": "02000002024100"
}
}
◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive:
$ l1-cli commando-rune
{
"rune": "QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==",
"unique_id": "0",
"warning_unrestricted_rune": "WARNING: This rune has no restrictions! Anyone who has access to this rune could drain funds from your node. Be careful when giving this to apps that you don't trust. Consider using the restrictions parameter to only allow access to specific rpc methods."
}
◉ tony@tony:~/clnlive:
$ npm i
added 144 packages, and audited 146 packages in 6s
39 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
◉ tony@tony:~/clnlive:
$ ls -l node_modules/lnmessage
lrwxrwxrwx 1 tony tony 12 Aug 17 16:17 node_modules/lnmessage -> ../lnmessage/
◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
id: '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54',
alias: 'VIOLENTDEITY',
color: '02ba4b',
num_peers: 1,
num_pending_channels: 0,
num_active_channels: 0,
num_inactive_channels: 0,
address: [],
binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7171 } ],
version: 'v23.05.2',
blockheight: 1,
network: 'regtest',
fees_collected_msat: 0,
'lightning-dir': '/tmp/l1-regtest/regtest',
our_features: {
init: '08a0000a0269a2',
node: '88a0000a0269a2',
channel: '',
invoice: '02000002024100'
}
}
◉ tony@tony:~/clnlive:
$ alias l1-cli
alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive:
$ ./getinfo.js
{
id: '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54',
alias: 'VIOLENTDEITY',
color: '02ba4b',
num_peers: 1,
num_pending_channels: 0,
num_active_channels: 0,
num_inactive_channels: 0,
address: [],
binding: [ { type: 'ipv4', address: '127.0.0.1', port: 7171 } ],
version: 'v23.05.2',
blockheight: 1,
network: 'regtest',
fees_collected_msat: 0,
'lightning-dir': '/tmp/l1-regtest/regtest',
our_features: {
init: '08a0000a0269a2',
node: '88a0000a0269a2',
channel: '',
invoice: '02000002024100'
}
}
^C
◉ tony@tony:~/clnlive:
$ ./getinfo.js
19535
◉ tony@tony:~/clnlive:
$ ./getinfo.js
19535
LO�...�e��{"id":"lnmessage:getinfo#f411188f65dbe6b7","rune":"QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==","method":"getinfo","params":[]}
◉ tony@tony:~/clnlive:
$ ./getinfo.js
19535
LO�ش...D...P{"id":"lnmessage:getinfo#a4d8b40b15440150","rune":"QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==","method":"getinfo","params":[]}
03�M!���34�ٯ�//g�*nw�'�...j�g�...�...]��%��������ö3���...__
◉ tony@tony:~/clnlive:
$ ./getinfo.js
..."nF...LOpx��...O�{"id":"lnmessage:getinfo#7078eab7d6194ff5","rune":"QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==","method":"getinfo","params":[]}
◉ tony@tony:~/clnlive:
$ ./getinfo.js
..."nF...LO�...Ù...!$S{"id":"lnmessage:getinfo#d612c39916212453","rune":"QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==","method":"getinfo","params":[]}
◉ tony@tony:~/clnlive:
$ ./getinfo.js
foo
..."nF...foo
LO�h��"+�...{"id":"lnmessage:getinfo#a568fcbb222b8513","rune":"QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA==","method":"getinfo","params":[]}
◉ tony@tony:~/clnlive:
$ ./getinfo.js
foo
16
foo
19535
# TERMINAL 2
◉ tony@tony:~/clnlive/lnmessage:
$ npm i
...
◉ tony@tony:~/clnlive/lnmessage:
$ npm run build
...
◉ tony@tony:~/clnlive/lnmessage:
$ alias l1-cli='lightning-cli --lightning-dir=/tmp/l1-regtest'
◉ tony@tony:~/clnlive/lnmessage:
$ l1-cli listpeers
{
"peer": []
}
◉ tony@tony:~/clnlive/lnmessage:
$ l1-cli listpeers
{
"peers": [
{
"id": "027e29",
"connected": true,
"num_channels": 0,
"netaddr": [
"127.0.0.1:41568"
],
"features": "0000000000000000"
}
]
}
◉ tony@tony:~/clnlive/lnmessage:
$ cd src/
◉ tony@tony:~/clnlive/lnmessage/src:
$ cloc .
20 text files.
20 unique files.
0 files ignored.
github.com/AlDanial/cloc v 1.90 T=0.02 s (834.7 files/s, 125745.7 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
TypeScript 20 390 790 1833
-------------------------------------------------------------------------------
SUM: 20 390 790 1833
-------------------------------------------------------------------------------
◉ tony@tony:~/clnlive/lnmessage/src:
$ tree
.
├── chacha
│ ├── chacha20.ts
│ ├── index.ts
│ └── poly1305.ts
├── crypto.ts
├── index.ts
├── messages
│ ├── BigIntUtils.ts
│ ├── BitField.ts
│ ├── buf.ts
│ ├── CommandoMessage.ts
│ ├── InitFeatureFlags.ts
│ ├── InitMessage.ts
│ ├── IWireMessage.ts
│ ├── MessageFactory.ts
│ ├── PingMessage.ts
│ ├── PongMessage.ts
│ └── read-tlvs.ts
├── noise-state.ts
├── socket-wrapper.ts
├── types.ts
└── validation.ts
2 directories, 20 files
Source code
getinfo.js
#!/usr/bin/env node
import LnMessage from 'lnmessage'
import net from 'net'
// node l1
const NODE_ID = '02ba4bea6bc050a311fa2688d43821b907acfd50f714fdfa7296f97188cc854c54'
const NODE_IP = '127.0.0.1'
const NODE_PORT = '7171'
// rune
const RUNE = 'QoPDe_gQX_ejD7N40QNnLUuJmIioAoxr53x9QLDfTIs9MA=='
const ln = new LnMessage({
remoteNodePublicKey: NODE_ID,
tcpSocket: new net.Socket(),
ip: NODE_IP,
port: NODE_PORT
})
await ln.connect()
const getinfo = await ln.commando({
method: 'getinfo',
params: [],
rune: RUNE
})
// console.log(getinfo)
process.exit() // ln.disconnect()
package.json
{
"type": "module",
"dependencies": {
"lnmessage": "file:./lnmessage"
}
}
LnMessage.commando()
class LnMessage {
...
async commando({
method,
params = [],
rune,
reqId
}: CommandoRequest): Promise<JsonRpcSuccessResponse['result']> {
this._log('info', `Commando request method: ${method} params: ${JSON.stringify(params)}`)
// not connected, so initiate a connection
if (this.connectionStatus$.value === 'disconnected') {
this._log('info', 'No socket connection, so creating one now')
const connected = await this.connect()
if (!connected) {
throw {
code: 2,
message: 'Could not establish a connection to node'
}
}
} else {
this._log('info', 'Ensuring we have a connection before making request')
// ensure that we are connected before making any requests
await firstValueFrom(this.connectionStatus$.pipe(filter((status) => status === 'connected')))
}
const writer = new BufferWriter()
if (!reqId) {
// create random id to match request with response
const id = createRandomBytes(8)
reqId = bytesToHex(id)
}
// write the type
writer.writeUInt16BE(MessageType.CommandoRequest)
// write the id
writer.writeBytes(Buffer.from(reqId, 'hex'))
// Unique request id with prefix, method and id
const detailedReqId = `lnmessage:${method}#${reqId}`
// write the request
writer.writeBytes(
Buffer.from(
JSON.stringify({
id: detailedReqId, // Adding id for easier debugging with commando
rune,
method,
params
})
)
)
this._log('info', 'Creating message to send')
const message = this.noise.encryptMessage(writer.toBuffer())
if (this.socket) {
this._log('info', 'Sending commando message')
this.socket.send(message)
this._log('info', `Message sent with id ${detailedReqId} and awaiting response`)
const { response } = await firstValueFrom(
this._commandoMsgs$.pipe(filter((commandoMsg) => commandoMsg.id === reqId))
)
const { result } = response as JsonRpcSuccessResponse
const { error } = response as JsonRpcErrorResponse
this._log(
'info',
result
? `Successful response received for ID: ${response.id}`
: `Error response received: ${error.message}`
)
if (error) throw error
return result
} else {
throw new Error('No socket initialised and connected')
}
}
...
}
commando_msgtype
/* We (as your local commando command) detected an error. */
#define COMMANDO_ERROR_LOCAL 0x4c4f
/* Remote (as executing your commando command) detected an error. */
#define COMMANDO_ERROR_REMOTE 0x4c50
/* Specifically: bad/missing rune */
#define COMMANDO_ERROR_REMOTE_AUTH 0x4c51
enum commando_msgtype {
/* Requests are split across multiple CONTINUES, then TERM. */
COMMANDO_MSG_CMD_CONTINUES = 0x4c4d,
COMMANDO_MSG_CMD_TERM = 0x4c4f,
/* Replies are split across multiple CONTINUES, then TERM. */
COMMANDO_MSG_REPLY_CONTINUES = 0x594b,
COMMANDO_MSG_REPLY_TERM = 0x594d,
};
NoiseState.encryptMessage()
export class NoiseState {
...
/**
* Sends an encrypted message using the shared sending key and nonce.
* The nonce is rotated once the message is sent. The sending key is
* rotated every 1000 messages.
* @param m
*/
public encryptMessage(m: Buffer): Buffer {
// step 1/2. serialize m length into int16
const l = Buffer.alloc(2)
l.writeUInt16BE(m.length, 0)
// step 3. encrypt l, using chachapoly1305, sn, sk)
const lc = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), l)
// step 3a: increment sn
if (this._incrementSendingNonce() >= 1000) this._rotateSendingKeys()
// step 4 encrypt m using chachapoly1305, sn, sk
const c = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), m)
// step 4a: increment sn
if (this._incrementSendingNonce() >= 1000) this._rotateSendingKeys()
// step 5 return m to be sent
return Buffer.concat([lc, c])
}
...
}
Resources
BOLT #8 references: