- https://twitter.com/jb55/status/1501372963433578496,
- https://twitter.com/rusty_twit/status/1419212235574894597,
- lightningd/plugins#280
- https://github.com/jb55/lnsocket
- https://github.com/lightningd/plugins
- https://github.com/lightningd/plugins/tree/master/commando
=============================================
- Better compatibility of REST standard APIs with web applications
- Data enrichment on top of standard response
- Secured with HTTPS & Macaroon
- Options to run as standalone or plugin
- No hardcoded values (chainhash, data.length < 50, etc.)
- Catastrophic error will not crash CLN (if being run as standalone server)
- Would not need change management for existing node solution vendors
=============================================
- Return response with method name
- Provide a first class REST server with CLN, to simplify developer onboarding
=============================================
Install commando plugin where the core lightning node is installed.
Follow instructions to create rune with full/readonly access.
Add 'experimental-websocket-port' setting in the core lightning config file. The web-socket port should be forwarded if used with public IP. Example: experimental-websocket-port=5001
Restart the core lightning node with commando plugin argument.
First message after establishing the connection is chainhash, ie. 001000000000
LOCAL_SECRET_KEY can be generated like below: openssl ecparam -genkey -name secp256k1 -text -noout -outform DER | xxd -p -c 1000 | sed 's/41534e31204f49443a20736563703235366b310a30740201010420//' | sed 's/a00706052b8104000aa144034200/'$'\nPubKey: /' | sed '2d
Step 6: Below sample project can be found here
const WebSocket= require('ws');
const secp256k1 = require('secp256k1');
const { randomBytes } = require('crypto');
const { NoiseState }=require('./noise.js');
const {EventEmitter} = require('events');
class CommandoClient extends EventEmitter {
constructor(nodeId, address, port, rune, localSecretKey, initMessage) {
super();
console.log('Connecting to node ' + nodeId);
this.node_id = nodeId;
this.address = address;
this.port = port;
this.rune = rune;
this.local_secret_key = localSecretKey;
this.reqcount = 0;
var link = 'ws://' + address + ':' + port;
var ls = Buffer.from(this.local_secret_key, 'hex');
var es;
do {
es = randomBytes(32);
} while (!secp256k1.privateKeyVerify(es))
var vals = { ls, es };
this.noise = new NoiseState(vals);
this.socket = new WebSocket(link);
this.rpk = Buffer.from(this.node_id,'hex');
const _self = this;
this.connectionPromise = new Promise((resolve, reject) => {
_self.socket.on('open', () => {
_self.socket.send(_self.noise.initiatorAct1(_self.rpk));
console.log('Socket Opened!');
});
_self.socket.on('close', () => {
console.log('Socket Closed!');
return ([
{ 'rn':_self.noise.rn, 'sn':_self.noise.sn },
{ 'sk':_self.noise.sk, 'rk':_self.noise.rk },
{ 'ck':_self.noise.ck }
]);
});
_self.socket.on('error', error => {
console.error('Socket Error: ' + JSON.stringify(error));
_self.emit('error', error);
});
_self.socket.on('message', (data) => {
if(data.length < 50) {
_self.emit('error', {error: 'Error Incorrect Data!'});
} else if(data.length === 50) {
_self.noise.initiatorAct2(data);
var Act3 = _self.noise.initiatorAct3();
_self.socket.send(Act3);
console.log('Connection Established!');
} else {
let len = _self.noise.decryptLength(data.slice(0,18));
let init_msg = _self.noise.decryptMessage(data.slice(18,18+len+16));
let pref = init_msg.slice(0,2).toString('hex');
let msg = init_msg.slice(2);
if(pref === '0010'){
_self.socket.send(_self.noise.encryptMessage(Buffer.from(initMessage,'hex')));
console.log('Initial Message Sent!');
resolve(true);
} else if(pref === '0011'){
console.error(msg);
_self.socket.close(1000,'Delibrate Closing After Error!');
_self.emit('error', {error: msg});
} else if (pref === '4c4f' || pref === '594d') {
_self.emit('success', init_msg.slice(10).toString());
}
}
});
});
}
call(method, args = []) {
let _self = this;
console.log('Calling ' + method);
this.reqcount++;
const command = {"method": method, "rune": this.rune, "params": args, "id": this.reqcount};
this.connectionPromise.then((res) => {
_self.socket.send(_self.noise.encryptMessage(Buffer.concat([Buffer.from('4c4f','hex'),Buffer.from([0,0,0,0,0,0,0,0]) ,Buffer.from(JSON.stringify(command))])));
});
}
}
module.exports = (nodeId, address, port, rune, localSecretKey, initMessage) => new CommandoClient(nodeId, address, port, rune, localSecretKey, initMessage);
module.exports.CommandoClient = CommandoClient;
const secp256k1 = require('secp256k1');
const sha256 = require('js-sha256');
const crypto = require('crypto');
function ecdh(pubkey, privkey){
return Buffer.from(secp256k1.ecdh(pubkey,privkey));
}
function hmacHash(key, input, hash) {
var hmac = crypto.createHmac(hash, key);
hmac.update(input);
return hmac.digest();
}
function hkdf(ikm, len, salt, info, hash) {
if (salt === void 0) { salt = Buffer.alloc(0); }
if (info === void 0) { info = Buffer.alloc(0); }
if (hash === void 0) { hash = "sha256";}
// extract step
var prk = hmacHash(salt, ikm, hash);
// expand
var n = Math.ceil(len / prk.byteLength);
if (n > 255)
throw new Error("Output length exceeds maximum");
var t = [Buffer.alloc(0)];
for (var i = 1; i <= n; i++) {
var tp = t[t.length - 1];
var bi = Buffer.from([i]);
t.push(hmacHash(prk, Buffer.concat([tp, info, bi]), hash));
}
return Buffer.concat(t.slice(1)).slice(0, len);
}
function getPublicKey(privKey, compressed = true){
return Buffer.from(secp256k1.publicKeyCreate(privKey, compressed));
}
function ccpEncrypt(k, n, ad, plaintext) {
var cipher = crypto.createCipheriv("ChaCha20-Poly1305", k, n, { authTagLength: 16 });
cipher.setAAD(ad, undefined);
var pad = cipher.update(plaintext);
cipher.final();
var tag = cipher.getAuthTag();
return Buffer.concat([pad, tag]);
}
function ccpDecrypt(k, n, ad, ciphertext) {
var decipher = crypto.createDecipheriv("ChaCha20-Poly1305", k, n, {
authTagLength: 16,
});
decipher.setAAD(ad, undefined);
if (ciphertext.length === 16) {
decipher.setAuthTag(ciphertext);
return decipher.final();
}
if (ciphertext.length > 16) {
var tag = ciphertext.slice(ciphertext.length - 16);
var pad = ciphertext.slice(0, ciphertext.length - 16);
decipher.setAuthTag(tag);
var m = decipher.update(pad);
var f = decipher.final();
m = Buffer.concat([m, f]);
return m;
}
}
class NoiseStateVar{
constructor(ls,es){
this.ls=ls;
this.es=es;
}
};
class NoiseState{
constructor(_a){
var ls = _a.ls, es = _a.es;
this.protocolName = Buffer.from("Noise_XK_secp256k1_ChaChaPoly_SHA256");
this.prologue = Buffer.from("lightning");
this.ls = ls;
this.lpk = getPublicKey(ls);
this.es = es;
this.epk = getPublicKey(es);
}
initiatorAct1 (rpk) {
this.rpk = rpk;
this._initialize(this.rpk);
// 2. h = SHA-256(h || epk)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.epk]));
// 3. es = ECDH(e.priv, rs)
var ss = ecdh(this.rpk, this.es);
// 4. ck, temp_k1 = HKDF(ck, es)
var tempK1 = hkdf(ss, 64, Buffer.from(this.ck,'hex'));
this.ck = tempK1.slice(0, 32);
this.tempK1 = tempK1.slice(32);
// 5. c = encryptWithAD(temp_k1, 0, h, zero)
var c = ccpEncrypt(this.tempK1, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0));
// 6. h = SHA-256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
// 7. m = 0 || epk || c
var m = Buffer.concat([Buffer.alloc(1), this.epk, c]);
return m;
}
initiatorAct2 (m) {
// 1. read exactly 50 bytes off the stream
if (m.length !== 50)
throw new Error("ACT2_READ_FAILED");
// 2. parse th read message m into v, re, and c
var v = m.slice(0, 1)[0];
var re = m.slice(1, 34);
var c = m.slice(34);
// 2a. convert re to public key
this.repk = re;
// 3. assert version is known version
if (v !== 0)
throw new Error("ACT2_BAD_VERSION");
// 4. sha256(h || re.serializedCompressed');
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.repk]));
// 5. ss = ECDH(re, e.priv);
var ss = ecdh(this.repk, this.es);
// 6. ck, temp_k2 = HKDF(cd, ss)
// console.log(this.ck);
var tempK2 = hkdf(ss, 64, this.ck);
this.ck = tempK2.slice(0, 32);
this.tempK2 = tempK2.slice(32);
// 7. p = decryptWithAD()
ccpDecrypt(this.tempK2, Buffer.alloc(12), Buffer.from(this.h,'hex'), c);
// 8. h = sha256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
}
initiatorAct3 () {
// 1. c = encryptWithAD(temp_k2, 1, h, lpk)
var c = ccpEncrypt(this.tempK2, Buffer.from([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]), Buffer.from(this.h,'hex'), this.lpk);
// 2. h = sha256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
// 3. ss = ECDH(re, s.priv)
var ss = ecdh(this.repk, this.ls);
// 4. ck, temp_k3 = HKDF(ck, ss)
var tempK3 = hkdf(ss, 64, this.ck);
this.ck = tempK3.slice(0, 32);
this.tempK3 = tempK3.slice(32);
// 5. t = encryptWithAD(temp_k3, 0, h, zero)
var t = ccpEncrypt(this.tempK3, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0));
// 6. sk, rk = hkdf(ck, zero)
var sk = hkdf(Buffer.alloc(0), 64, this.ck);
this.rk = sk.slice(32);
this.sk = sk.slice(0, 32);
// 7. rn = 0, sn = 0
this.sn = Buffer.alloc(12);
this.rn = Buffer.alloc(12);
// 8. send m = 0 || c || t
var m = Buffer.concat([Buffer.alloc(1), c, t]);
return m;
}
receiveAct1 (m) {
this._initialize(this.lpk);
// 1. read exactly 50 bytes off the stream
if (m.length !== 50)
throw new Error("ACT1_READ_FAILED");
// 2. parse th read message m into v,re, and c
var v = m.slice(0, 1)[0];
var re = m.slice(1, 34);
var c = m.slice(34);
this.repk = re;
// 3. assert version is known version
if (v !== 0)
throw new Error("ACT1_BAD_VERSION");
// 4. sha256(h || re.serializedCompressed');
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), re]));
// 5. ss = ECDH(re, ls.priv);
var ss = ecdh(re, this.ls);
// 6. ck, temp_k1 = HKDF(cd, ss)
var tempK1 = hkdf(ss, 64, Buffer.from(this.ck,'hex'));
this.ck = tempK1.slice(0, 32);
this.tempK1 = tempK1.slice(32);
// 7. p = decryptWithAD(temp_k1, 0, h, c)
ccpDecrypt(this.tempK1, Buffer.alloc(12), Buffer.from(this.h,'hex'), c);
// 8. h = sha256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
}
recieveAct2 () {
// 1. e = generateKey() => done in initialization
// 2. h = sha256(h || e.pub.compressed())
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.epk]));
// 3. ss = ecdh(re, e.priv)
var ss = ecdh(this.repk, this.es);
// 4. ck, temp_k2 = hkdf(ck, ss)
var tempK2 = hkdf(ss, 64, this.ck);
this.ck = tempK2.slice(0, 32);
this.tempK2 = tempK2.slice(32);
// 5. c = encryptWithAd(temp_k2, 0, h, zero)
var c = ccpEncrypt(this.tempK2, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0));
// 6. h = sha256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
// 7. m = 0 || e.pub.compressed() Z|| c
var m = Buffer.concat([Buffer.alloc(1), this.epk, c]);
return m;
}
receiveAct3 (m) {
// 1. read exactly 66 bytes from the network buffer
if (m.length !== 66)
throw new Error("ACT3_READ_FAILED");
// 2. parse m into v, c, t
var v = m.slice(0, 1)[0];
var c = m.slice(1, 50);
var t = m.slice(50);
// 3. validate v is recognized
if (v !== 0)
throw new Error("ACT3_BAD_VERSION");
// 4. rs = decryptWithAD(temp_k2, 1, h, c)
var rs = ccpDecrypt(this.tempK2, Buffer.from("000000000100000000000000", "hex"), Buffer.from(this.h,'hex'), c);
this.rpk = rs;
// 5. h = sha256(h || c)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c]));
// 6. ss = ECDH(rs, e.priv)
var ss = ecdh(this.rpk, this.es);
// 7. ck, temp_k3 = hkdf(cs, ss)
var tempK3 = hkdf(ss, 64, this.ck);
this.ck = tempK3.slice(0, 32);
this.tempK3 = tempK3.slice(32);
// 8. p = decryptWithAD(temp_k3, 0, h, t)
ccpDecrypt(this.tempK3, Buffer.alloc(12), Buffer.from(this.h,'hex'), t);
// 9. rk, sk = hkdf(ck, zero)
var sk = hkdf(Buffer.alloc(0), 64, this.ck);
this.rk = sk.slice(0, 32);
this.sk = sk.slice(32);
// 10. rn = 0, sn = 0
this.rn = Buffer.alloc(12);
this.sn = Buffer.alloc(12);
}
encryptMessage (m) {
// step 1/2. serialize m length into int16
var l = Buffer.alloc(2);
l.writeUInt16BE(m.length, 0);
// step 3. encrypt l, using chachapoly1305, sn, sk)
var 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
var 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]);
}
decryptLength (lc) {
var l = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), lc);
if (this._incrementRecievingNonce() >= 1000)
this._rotateRecievingKeys();
return l.readUInt16BE(0);
}
decryptMessage (c) {
var m = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), c);
if (this._incrementRecievingNonce() >= 1000)
this._rotateRecievingKeys();
return m;
}
// Initializes the noise state prior to Act1.
_initialize (pubkey) {
// 1. h = SHA-256(protocolName)
this.h = sha256(Buffer.from(this.protocolName));
// 2. ck = h
this.ck = this.h;
// 3. h = SHA-256(h || prologue)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), Buffer.from(this.prologue)]));
// 4. h = SHA-256(h || pubkey)
this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), pubkey]));
}
_incrementSendingNonce () {
var newValue = this.sn.readUInt16LE(4) + 1;
this.sn.writeUInt16LE(newValue, 4);
return newValue;
}
_incrementRecievingNonce () {
var newValue = this.rn.readUInt16LE(4) + 1;
this.rn.writeUInt16LE(newValue, 4);
return newValue;
}
_rotateSendingKeys () {
var result = hkdf(this.sk, 64, this.ck);
this.sk = result.slice(32);
this.ck = result.slice(0, 32);
this.sn = Buffer.alloc(12);
}
_rotateRecievingKeys () {
var result = hkdf(this.rk, 64, this.ck);
this.rk = result.slice(32);
this.ck = result.slice(0, 32);
this.rn = Buffer.alloc(12);
}
};
module.exports={ NoiseState }
// Mainnet node settings (please check port and other settings)
const NODE_ID = '024b...605';
const ADDRESS = 'local.or.public.ip';
const PORT = '5001';
const RUNE = '2...mbw==';
const LOCAL_SECRET_KEY = 'ea8d3091934f...561';
const INIT_MESSAGE = '001000000000';
// Initialize
let commandoClient = new CommandoClient(NODE_ID, ADDRESS, PORT, RUNE, LOCAL_SECRET_KEY, INIT_MESSAGE);
// Sample calls
commandoClient.call('getinfo', []);
commandoClient.call('feerates', ['perkw']);
commandoClient.call('signmessage', ['Testing Sign Message Via Commando']);
// Catch events and consume responses
commandoClient.on('success', res => console.log('Response: \n' + res));
commandoClient.on('error', err => console.error('Error: \n' + err));