Skip to content

Instantly share code, notes, and snippets.

@ShahanaFarooqui
Last active April 12, 2023 03:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShahanaFarooqui/1d7b4f9fc3d0657ffda4657262fce047 to your computer and use it in GitHub Desktop.
Save ShahanaFarooqui/1d7b4f9fc3d0657ffda4657262fce047 to your computer and use it in GitHub Desktop.
CLN Commando plugin

Reference Links:

=============================================

Benefits of the current API architecture (with c-lightning-REST):

  • 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

=============================================

Suggestions:

  • Return response with method name
  • Provide a first class REST server with CLN, to simplify developer onboarding

=============================================

Steps to follow:

Step 1: Install Commando

Install commando plugin where the core lightning node is installed.

Step 2: Create rune

Follow instructions to create rune with full/readonly access.

Step 3: Experimental Feature

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

Step 4: Activate Plugin

Restart the core lightning node with commando plugin argument.

Step 5: Initial Message and local secret key:

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

Step 6.1: Create Commando Client

	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;

Step 6.2: Create Noise for Encryption/Decryption

	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 }

Step 6.3: Create Client Instance, Connect and Call

// 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));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment