LIVE #11 - Overview of lnmessage implementation
In this live we look at the implementation of
lnmessageJS library that let us sendcommandomessages to a CLN node. Specifically, we look atLnMessage.commandoandNoiseState.encryptMessagemethods. 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:
- Create invoices with a Node.JS cli using lnmessage and commando and
- Introduction to commando and commando-rune.
Note that at 1h16m I wrote a wrong equality (that I don't reproduce here). The correct equality I wanted to write is:
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:
We also have the Node JS script getinfo.js which:
- uses
lnmessagelibrary to connect to our node (that we are going to start in a moment), - send a
commandomessage to our node asking to run the commandgetinfoand - print 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:
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¶
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¶
- https://github.com/aaronbarnardsound/lnmessage
- https://github.com/jb55/lnsocket
- lightning:doc/lightning-commando.7.md
- https://github.com/lightning/bolts
- BOLT #1: Base Protocol
- BOLT #8: Encrypted and Authenticated Transport
- BOLT #8 references:
- https://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange
- LnMessage.commando()
- lnmessage:src/index.ts
- enum commando_msgtype
- lightning:plugins/commando.c
- NoiseState.encryptMessage()
- lnmessage:src/noise-state.ts