├── lib ├── plasma.js ├── framer.js ├── peer.js ├── parser.js ├── elkrem.js ├── channelstate.js ├── list.js ├── aead.js ├── connection.js ├── scriptutil.js ├── wire.js └── channel.js ├── .gitignore ├── .npmignore ├── README.md ├── .jshintrc ├── LICENSE ├── package.json ├── bin └── net-test.js └── test ├── channel-test.js └── script-test.js /lib/plasma.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | docker_data/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | test/ 3 | node_modules/ 4 | .jshintrc 5 | .jscsrc 6 | jsdoc.json 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plasma 2 | 3 | A lightning network implementation on top of [bcoin][bcoin]. Designed to be 4 | lnd-compatible. 5 | 6 | Unstable. 7 | 8 | [bcoin]: https://github.com/bcoin-org/bcoin 9 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "curly": false, 4 | "eqeqeq": true, 5 | "freeze": true, 6 | "latedef": "nofunc", 7 | "maxparams": 7, 8 | "noarg": true, 9 | "shadow": "inner", 10 | "undef": true, 11 | "unused": "vars", 12 | 13 | "boss": true, 14 | "expr": true, 15 | "eqnull": true, 16 | "evil": true, 17 | "loopfunc": true, 18 | "proto": true, 19 | "supernew": true, 20 | 21 | "-W018": true, 22 | "-W064": true, 23 | "-W086": true, 24 | "-W032": true, 25 | "-W021": true, 26 | 27 | "browser": true, 28 | "browserify": true, 29 | "node": true, 30 | "nonstandard": true, 31 | "typed": true, 32 | "worker": false, 33 | 34 | "camelcase": false, 35 | "indent": 2, 36 | "maxlen": 110, 37 | "newcap": false, 38 | "quotmark": "single", 39 | 40 | "laxbreak": true, 41 | "laxcomma": true 42 | } 43 | -------------------------------------------------------------------------------- /lib/framer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var utils = bcoin.utils; 5 | var assert = utils.assert; 6 | 7 | /** 8 | * Protocol packet framer 9 | * @exports Framer 10 | * @constructor 11 | * @param {Object} options 12 | */ 13 | 14 | function Framer(options) { 15 | if (!(this instanceof Framer)) 16 | return new Framer(options); 17 | 18 | if (!options) 19 | options = {}; 20 | 21 | this.options = options; 22 | 23 | this.network = bcoin.network.get(options.network); 24 | } 25 | 26 | /** 27 | * Frame a payload with a header. 28 | * @param {String} cmd - Packet type. 29 | * @param {Buffer} payload 30 | * @returns {Buffer} Payload with header prepended. 31 | */ 32 | 33 | Framer.prototype.packet = function packet(cmd, payload) { 34 | var packet; 35 | 36 | assert(payload, 'No payload.'); 37 | 38 | packet = new Buffer(12 + payload.length); 39 | packet.writeUInt32BE(this.network.magic, 0, true); 40 | packet.writeUInt32BE(cmd, 4, true); 41 | packet.writeUInt32BE(payload.length, 8, true); 42 | payload.copy(packet, 12); 43 | 44 | return packet; 45 | }; 46 | 47 | module.exports = Framer; 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright (c) 2016, Christopher Jeffrey (https://github.com/chjj) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bcoin-plasma", 3 | "version": "0.0.1", 4 | "description": "Lightning network bike-shed", 5 | "main": "./lib/plasma.js", 6 | "bin": {}, 7 | "preferGlobal": false, 8 | "scripts": { 9 | "test": "mocha --reporter spec test/*-test.js", 10 | "browserify": "browserify --im -o plasma.js lib/plasma.js", 11 | "uglify": "uglifyjs -m -o plasma.min.js plasma.js", 12 | "clean": "rm plasma.js plasma.min.js" 13 | }, 14 | "repository": "git://github.com/bcoin-org/plasma.git", 15 | "keywords": [ 16 | "bcoin", 17 | "bitcoin", 18 | "blockchain", 19 | "lightning", 20 | "wallet" 21 | ], 22 | "author": "Christopher Jeffrey (https://github.com/chjj)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/bcoin-org/plasma/issues" 26 | }, 27 | "homepage": "https://github.com/bcoin-org/plasma", 28 | "engines": { 29 | "node": ">= 0.10.0" 30 | }, 31 | "dependencies": { 32 | "bcoin": "git://github.com/bcoin-org/bcoin.git#master" 33 | }, 34 | "optionalDependencies": {}, 35 | "devDependencies": { 36 | "browserify": "13.1.0", 37 | "hash.js": "1.0.3", 38 | "jsdoc": "3.4.0", 39 | "level-js": "2.2.4", 40 | "mocha": "3.0.2", 41 | "uglify-js": "2.7.3" 42 | }, 43 | "browser": {} 44 | } 45 | -------------------------------------------------------------------------------- /bin/net-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var utils = bcoin.utils; 5 | var crypto = bcoin.crypto; 6 | var assert = utils.assert; 7 | var wire = require('../lib/wire'); 8 | var Peer = require('../lib/peer'); 9 | 10 | var myID = bcoin.ec.generatePrivateKey(); 11 | var addr = '52.39.113.206'; 12 | var lnid = new Buffer('d64fd0c520b788b97c4a7cda33e5cd0379e2b180ace44fd9553e05776534c6a7', 'hex'); 13 | var hash = bcoin.utils.fromBase58('SZKbmbvudiHu6ScqDmz3o64ZViyy2JPcaY'); 14 | hash = hash.slice(1, 21); 15 | 16 | var peer = new Peer(myID, addr, hash, bcoin.network.get('simnet')); 17 | 18 | peer.connect(); 19 | 20 | peer.on('packet', function(msg) { 21 | console.log('Received packet:'); 22 | console.log(msg); 23 | }); 24 | 25 | peer.on('connect', function() { 26 | console.log('Handshake complete.'); 27 | 28 | var ck = bcoin.ec.publicKeyCreate(bcoin.ec.generatePrivateKey(), true); 29 | var cp = bcoin.ec.publicKeyCreate(bcoin.ec.generatePrivateKey(), true); 30 | 31 | var sfr = new wire.SingleFundingRequest(); 32 | sfr.channelID = 0; 33 | sfr.channelType = 0; 34 | sfr.coinType = 0; 35 | sfr.feeRate = 5000; 36 | sfr.fundingValue = 1000; 37 | sfr.csvDelay = 10; 38 | sfr.commitKey = ck; 39 | sfr.channelDerivationPoint = cp; 40 | sfr.deliveryScript.fromProgram(0, crypto.hash160(ck)); 41 | 42 | console.log('Sending packet:'); 43 | console.log(sfr); 44 | 45 | peer.send(sfr); 46 | }); 47 | -------------------------------------------------------------------------------- /lib/peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var bcoin = require('bcoin'); 5 | var utils = bcoin.utils; 6 | var Connection = require('./connection'); 7 | var Parser = require('./parser'); 8 | var Framer = require('./framer'); 9 | 10 | function Peer(myID, addr, lnid, network) { 11 | var self = this; 12 | 13 | EventEmitter.call(this); 14 | 15 | this.myID = myID; 16 | this.addr = addr; 17 | this.lnid = lnid; 18 | this.network = network || bcoin.network.get(); 19 | this.conn = new Connection(); 20 | this.parser = new Parser(this); 21 | this.framer = new Framer(this); 22 | 23 | this.conn.on('connect', function() { 24 | self.emit('connect'); 25 | }); 26 | 27 | this.conn.on('data', function(data) { 28 | self.parser.feed(data); 29 | }); 30 | 31 | this.conn.on('error', function(err) { 32 | self.emit('error', err); 33 | }); 34 | 35 | this.parser.on('packet', function(msg) { 36 | self.emit('packet', msg); 37 | }); 38 | } 39 | 40 | utils.inherits(Peer, EventEmitter); 41 | 42 | Peer.prototype.connect = function connect() { 43 | this.conn.connect(this.myID, this.addr, this.lnid); 44 | }; 45 | 46 | Peer.prototype.send = function send(msg) { 47 | return this.write(msg.cmd, msg.toRaw()); 48 | }; 49 | 50 | Peer.prototype.frame = function frame(cmd, payload) { 51 | return this.framer.packet(cmd, payload); 52 | }; 53 | 54 | Peer.prototype.write = function write(cmd, payload) { 55 | return this.conn.write(this.frame(cmd, payload)); 56 | }; 57 | 58 | module.exports = Peer; 59 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var bcoin = require('bcoin'); 5 | var utils = bcoin.utils; 6 | var assert = utils.assert; 7 | var constants = bcoin.constants; 8 | var wire = require('./wire'); 9 | 10 | function Parser(options) { 11 | if (!(this instanceof Parser)) 12 | return new Parser(options); 13 | 14 | if (!options) 15 | options = {}; 16 | 17 | EventEmitter.call(this); 18 | 19 | this.network = bcoin.network.get(options.network); 20 | 21 | this.pending = []; 22 | this.total = 0; 23 | this.waiting = 12; 24 | this.cmd = -1; 25 | 26 | this._init(); 27 | } 28 | 29 | utils.inherits(Parser, EventEmitter); 30 | 31 | Parser.prototype._init = function _init(str) { 32 | ; 33 | }; 34 | 35 | Parser.prototype._error = function _error(str) { 36 | this.emit('error', new Error(str)); 37 | }; 38 | 39 | Parser.prototype.feed = function feed(data) { 40 | var chunk, off, len; 41 | 42 | this.total += data.length; 43 | this.pending.push(data); 44 | 45 | while (this.total >= this.waiting) { 46 | chunk = new Buffer(this.waiting); 47 | off = 0; 48 | len = 0; 49 | 50 | while (off < chunk.length) { 51 | len = this.pending[0].copy(chunk, off); 52 | if (len === this.pending[0].length) 53 | this.pending.shift(); 54 | else 55 | this.pending[0] = this.pending[0].slice(len); 56 | off += len; 57 | } 58 | 59 | assert.equal(off, chunk.length); 60 | 61 | this.total -= chunk.length; 62 | this.parse(chunk); 63 | } 64 | }; 65 | 66 | Parser.prototype.parse = function parse(chunk) { 67 | var payload; 68 | 69 | if (chunk.length > constants.MAX_MESSAGE) { 70 | this.waiting = 12; 71 | this.cmd = -1; 72 | return this._error('Packet too large: %dmb.', utils.mb(chunk.length)); 73 | } 74 | 75 | if (this.cmd === -1) { 76 | this.cmd = this.parseHeader(chunk); 77 | return; 78 | } 79 | 80 | try { 81 | payload = this.parsePayload(this.cmd, chunk); 82 | } catch (e) { 83 | this.emit('error', e); 84 | this.waiting = 12; 85 | this.cmd = -1; 86 | return; 87 | } 88 | 89 | this.emit('packet', payload); 90 | this.waiting = 12; 91 | this.cmd = -1; 92 | }; 93 | 94 | Parser.prototype.parseHeader = function parseHeader(data) { 95 | var magic = data.readUInt32BE(0, true); 96 | var cmd = data.readUInt32BE(4, true); 97 | var size = data.readUInt32BE(8, true); 98 | 99 | if (magic !== this.network.magic) 100 | return this._error('Invalid magic value: ' + magic.toString(16)); 101 | 102 | if (size > constants.MAX_MESSAGE) { 103 | this.waiting = 12; 104 | return this._error('Packet length too large: %dmb', utils.mb(size)); 105 | } 106 | 107 | this.waiting = size; 108 | 109 | return cmd; 110 | }; 111 | 112 | Parser.prototype.parsePayload = function parsePayload(cmd, data) { 113 | return wire.fromRaw(cmd, data); 114 | }; 115 | 116 | module.exports = Parser; 117 | -------------------------------------------------------------------------------- /lib/elkrem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('bcoin/lib/utils/util'); 4 | var crypto = require('bcoin/lib/crypto/crypto'); 5 | var bcoin = require('bcoin'); 6 | var constants = bcoin.constants; 7 | 8 | /* 9 | * Constants 10 | */ 11 | 12 | var maxIndex = 281474976710654; // 2^48 - 2 13 | var maxHeight = 47; 14 | 15 | /** 16 | * Elkrem Sender 17 | */ 18 | 19 | function ElkremSender(root) { 20 | this.root = root; 21 | } 22 | 23 | ElkremSender.prototype.getIndex = function getIndex(w) { 24 | return descend(w, maxIndex, maxHeight, this.root); 25 | }; 26 | 27 | /** 28 | * Elkrem Receiver 29 | */ 30 | 31 | function ElkremReceiver(stack) { 32 | this.stack = stack || []; 33 | } 34 | 35 | ElkremReceiver.prototype.addNext = function addNext(hash) { 36 | var node = new ElkremNode(hash); 37 | var t = this.stack.length - 1; 38 | var left, right; 39 | 40 | if (t >= 0) 41 | node.i = this.stack[t].i + 1; 42 | 43 | if (t > 0 && this.stack[t - 1].h === this.stack[t].h) { 44 | node.h = this.stack[t].h + 1; 45 | 46 | left = leftHash(hash); 47 | right = rightHash(hash); 48 | 49 | if (!utils.equal(this.stack[t - 1].hash, left)) 50 | throw new Error('Left child does not match.'); 51 | 52 | if (!utils.equal(this.stack[t].hash, right)) 53 | throw new Error('Right child does not match.'); 54 | 55 | this.stack.pop(); 56 | this.stack.pop(); 57 | } 58 | 59 | this.stack.push(node); 60 | }; 61 | 62 | ElkremReceiver.prototype.getIndex = function getIndex(w) { 63 | var i, node, out; 64 | 65 | if (this.stack.length === 0) 66 | throw new Error('Nil receiver'); 67 | 68 | for (i = 0; i < this.stack.length; i++) { 69 | node = this.stack[i]; 70 | if (w <= node.i) { 71 | out = node; 72 | break; 73 | } 74 | } 75 | 76 | if (!out) { 77 | throw new Error('Receiver has max ' 78 | + this.stack[this.stack.length - 1].i 79 | + ', less than requested ' + w); 80 | } 81 | 82 | return descend(w, out.i, out.h, out.hash); 83 | }; 84 | 85 | ElkremReceiver.prototype.upTo = function upTo() { 86 | if (this.stack.length < 1) 87 | return 0; 88 | return this.stack[this.stack.length - 1].i; 89 | }; 90 | 91 | /** 92 | * Elkrem Node 93 | */ 94 | 95 | function ElkremNode(hash, h, i) { 96 | this.hash = hash || constants.ZERO_HASH; 97 | this.h = h || 0; 98 | this.i = i || 0; 99 | } 100 | 101 | /* 102 | * Helpers 103 | */ 104 | 105 | function leftHash(hash) { 106 | return crypto.hash256(hash); 107 | } 108 | 109 | function rightHash(hash) { 110 | var buf = new Buffer(33); 111 | hash.copy(buf, 0); 112 | buf[32] = 1; 113 | return crypto.hash256(buf); 114 | } 115 | 116 | function descend(w, i, h, hash) { 117 | var pow; 118 | 119 | while (w < i) { 120 | pow = Math.pow(2, h); 121 | 122 | if (w <= i - pow) { 123 | hash = leftHash(hash); 124 | i -= pow; 125 | } else { 126 | hash = rightHash(hash); 127 | i--; 128 | } 129 | 130 | if (h === 0) 131 | break; 132 | 133 | h--; 134 | } 135 | 136 | if (w !== i) 137 | throw new Error('Cannot get index ' + w + ' from ' + i); 138 | 139 | return hash; 140 | } 141 | 142 | /* 143 | * Expose 144 | */ 145 | 146 | exports.ElkremSender = ElkremSender; 147 | exports.ElkremReceiver = ElkremReceiver; 148 | -------------------------------------------------------------------------------- /lib/channelstate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var constants = bcoin.constants; 5 | var elkrem = require('./elkrem'); 6 | var ElkremSender = elkrem.ElkremSender; 7 | var ElkremReceiver = elkrem.ElkremReceiver; 8 | 9 | function ChannelState(options) { 10 | this.theirLNID = null; 11 | this.id = new bcoin.outpoint(); 12 | this.minRate = 0; 13 | this.ourCommitKey = null; // private 14 | this.ourCommitPub = null; 15 | this.theirCommitKey = null; // public 16 | this.capacity = 0; 17 | this.ourBalance = 0; 18 | this.theirBalance = 0; 19 | this.ourCommitTX = null; 20 | this.ourCommitSig = null; 21 | this.fundingInput = new bcoin.coin(); 22 | this.ourMultisigKey = null; // private 23 | this.ourMiltisigPub = null; 24 | this.theirMultisigKey = null; // public 25 | this.fundingScript = new bcoin.script(); 26 | this.localCSVDelay = 0; 27 | this.remoteCSVDelay = 0; 28 | this.theirCurrentRevocation = constants.ZERO_KEY; // public 29 | this.theirCurrentRevHash = constants.ZERO_HASH; 30 | this.localElkrem = new ElkremSender(); 31 | this.remoteElkrem = new ElkremReceiver(); 32 | this.ourDeliveryScript = new bcoin.script(); 33 | this.theirDeliveryScript = new bcoin.script(); 34 | this.numUpdates = 0; 35 | this.totalSent = 0; 36 | this.totalReceived = 0; 37 | this.totalFees = 0; 38 | this.ts = 0; 39 | this.isPrevState = false; 40 | this.db = null; 41 | 42 | if (options) 43 | this.fromOptions(options); 44 | } 45 | 46 | ChannelState.prototype.fromOptions = function(options) { 47 | if (options.id) 48 | this.id = options.id; 49 | 50 | if (options.ourCommitKey) { 51 | this.ourCommitKey = options.ourCommitKey; 52 | this.ourCommitPub = bcoin.ec.publicKeyCreate(this.ourCommitKey, true); 53 | } 54 | 55 | if (options.theirCommitKey) 56 | this.theirCommitKey = options.theirCommitKey; 57 | 58 | if (options.capacity != null) 59 | this.capacity = options.capacity; 60 | 61 | if (options.ourBalance != null) 62 | this.ourBalance = options.ourBalance; 63 | 64 | if (options.theirBalance != null) 65 | this.theirBalance = options.theirBalance; 66 | 67 | if (options.ourCommitTX) 68 | this.ourCommitTX = options.ourCommitTX; 69 | 70 | if (options.ourCommitSig) 71 | this.ourCommitSig = options.ourCommitSig; 72 | 73 | if (options.fundingInput) 74 | this.fundingInput = options.fundingInput; 75 | 76 | if (options.ourMultisigKey) { 77 | this.ourMultisigKey = options.ourMultisigKey; 78 | this.ourMultisigPub = bcoin.ec.publicKeyCreate(this.ourMultisigKey, true); 79 | } 80 | 81 | if (options.theirMultisigKey) 82 | this.theirMultisigKey = options.theirMultisigKey; 83 | 84 | if (options.fundingScript) 85 | this.fundingScript = options.fundingScript; 86 | 87 | if (options.localCSVDelay != null) 88 | this.localCSVDelay = options.localCSVDelay; 89 | 90 | if (options.remoteCSVDelay != null) 91 | this.remoteCSVDelay = options.remoteCSVDelay; 92 | 93 | if (options.theirCurrentRevocation) 94 | this.theirCurrentRevocation = options.theirCurrentRevocation; 95 | 96 | if (options.theirCurrentRevHash) 97 | this.theirCurrentRevHash = options.theirCurrentRevHash; 98 | 99 | if (options.localElkrem) 100 | this.localElkrem = options.localElkrem; 101 | 102 | if (options.remoteElkrem) 103 | this.remoteElkrem = options.remoteElkrem; 104 | 105 | if (options.ourDeliveryScript) 106 | this.ourDeliveryScript = options.ourDeliveryScript; 107 | 108 | if (options.theirDeliveryScript) 109 | this.theirDeliveryScript = options.theirDeliveryScript; 110 | 111 | if (options.numUpdates != null) 112 | this.numUpdates = options.numUpdates; 113 | 114 | if (options.db) 115 | this.db = options.db; 116 | }; 117 | 118 | ChannelState.prototype.fullSync = function() { 119 | }; 120 | 121 | ChannelState.prototype.syncRevocation = function() { 122 | }; 123 | 124 | module.exports = ChannelState; 125 | -------------------------------------------------------------------------------- /lib/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | 5 | /** 6 | * A linked list. 7 | * @exports List 8 | * @constructor 9 | */ 10 | 11 | function List() { 12 | if (!(this instanceof List)) 13 | return new List(); 14 | 15 | this.head = null; 16 | this.tail = null; 17 | } 18 | 19 | /** 20 | * Reset the cache. Clear all items. 21 | */ 22 | 23 | List.prototype.reset = function reset() { 24 | var item, next; 25 | 26 | for (item = this.head; item; item = next) { 27 | next = item.next; 28 | item.prev = null; 29 | item.next = null; 30 | } 31 | 32 | assert(!item); 33 | 34 | this.head = null; 35 | this.tail = null; 36 | }; 37 | 38 | /** 39 | * Remove the first item in the list. 40 | */ 41 | 42 | List.prototype.shiftItem = function shiftItem() { 43 | var item = this.head; 44 | 45 | if (!item) 46 | return; 47 | 48 | this.removeItem(item); 49 | 50 | return item; 51 | }; 52 | 53 | /** 54 | * Prepend an item to the linked list (sets new head). 55 | * @private 56 | * @param {ListItem} 57 | */ 58 | 59 | List.prototype.unshiftItem = function unshiftItem(item) { 60 | this.insertItem(null, item); 61 | }; 62 | 63 | /** 64 | * Append an item to the linked list (sets new tail). 65 | * @private 66 | * @param {ListItem} 67 | */ 68 | 69 | List.prototype.pushItem = function pushItem(item) { 70 | this.insertItem(this.tail, item); 71 | }; 72 | 73 | /** 74 | * Remove the last item in the list. 75 | */ 76 | 77 | List.prototype.popItem = function popItem() { 78 | var item = this.tail; 79 | 80 | if (!item) 81 | return; 82 | 83 | this.removeItem(item); 84 | 85 | return item; 86 | }; 87 | 88 | /** 89 | * Remove the first item in the list. 90 | */ 91 | 92 | List.prototype.shift = function shift() { 93 | var item = this.shiftItem(); 94 | if (!item) 95 | return; 96 | return item.value; 97 | }; 98 | 99 | /** 100 | * Prepend an item to the linked list (sets new head). 101 | * @private 102 | * @param {ListItem} 103 | */ 104 | 105 | List.prototype.unshift = function unshift(value) { 106 | var item = new ListItem(value); 107 | this.unshiftItem(item); 108 | return item; 109 | }; 110 | 111 | /** 112 | * Append an item to the linked list (sets new tail). 113 | * @private 114 | * @param {ListItem} 115 | */ 116 | 117 | List.prototype.push = function push(value) { 118 | var item = new ListItem(value); 119 | this.pushItem(item); 120 | return item; 121 | }; 122 | 123 | /** 124 | * Remove the last item in the list. 125 | */ 126 | 127 | List.prototype.pop = function pop() { 128 | var item = this.popItem(); 129 | if (!item) 130 | return; 131 | return item.value; 132 | }; 133 | 134 | /** 135 | * Insert item into the linked list. 136 | * @private 137 | * @param {ListItem|null} ref 138 | * @param {ListItem} item 139 | */ 140 | 141 | List.prototype.insertItem = function insertItem(ref, item) { 142 | assert(!item.next); 143 | assert(!item.prev); 144 | 145 | if (ref == null) { 146 | if (!this.head) { 147 | this.head = item; 148 | this.tail = item; 149 | } else { 150 | this.head.prev = item; 151 | item.next = this.head; 152 | this.head = item; 153 | } 154 | return; 155 | } 156 | 157 | item.next = ref.next; 158 | item.prev = ref; 159 | ref.next = item; 160 | 161 | if (ref === this.tail) 162 | this.tail = item; 163 | }; 164 | 165 | /** 166 | * Remove item from the linked list. 167 | * @private 168 | * @param {ListItem} 169 | */ 170 | 171 | List.prototype.removeItem = function removeItem(item) { 172 | if (item.prev) 173 | item.prev.next = item.next; 174 | 175 | if (item.next) 176 | item.next.prev = item.prev; 177 | 178 | if (item === this.head) 179 | this.head = item.next; 180 | 181 | if (item === this.tail) 182 | this.tail = item.prev || this.head; 183 | 184 | if (!this.head) 185 | assert(!this.tail); 186 | 187 | if (!this.tail) 188 | assert(!this.head); 189 | 190 | item.prev = null; 191 | item.next = null; 192 | }; 193 | 194 | /** 195 | * Convert the list to an array of items. 196 | * @returns {Object[]} 197 | */ 198 | 199 | List.prototype.toArray = function toArray() { 200 | var items = []; 201 | var item; 202 | 203 | for (item = this.head; item; item = item.next) 204 | items.push(item.value); 205 | 206 | return items; 207 | }; 208 | 209 | /** 210 | * Get the list size. 211 | * @returns {Number} 212 | */ 213 | 214 | List.prototype.size = function size() { 215 | var total = 0; 216 | var item; 217 | 218 | for (item = this.head; item; item = item.next) 219 | total += 1; 220 | 221 | return total; 222 | }; 223 | 224 | /** 225 | * Represents an LRU item. 226 | * @constructor 227 | * @private 228 | * @param {String} key 229 | * @param {Object} value 230 | */ 231 | 232 | function ListItem(value) { 233 | this.value = value; 234 | this.next = null; 235 | this.prev = null; 236 | } 237 | 238 | /* 239 | * Expose 240 | */ 241 | 242 | module.exports = List; 243 | -------------------------------------------------------------------------------- /lib/aead.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var utils = bcoin.utils; 5 | var assert = utils.assert; 6 | var chachapoly = require('bcoin/lib/crypto/chachapoly'); 7 | 8 | /** 9 | * AEAD (used for bip151) 10 | * @exports AEAD 11 | * @see https://github.com/openssh/openssh-portable 12 | * @see https://tools.ietf.org/html/rfc7539#section-2.8 13 | * @constructor 14 | */ 15 | 16 | function NonstandardAEAD() { 17 | if (!(this instanceof NonstandardAEAD)) 18 | return new NonstandardAEAD(); 19 | 20 | this.chacha20 = new chachapoly.ChaCha20(); 21 | this.poly1305 = new chachapoly.Poly1305(); 22 | this.aadLen = 0; 23 | this.cipherLen = 0; 24 | this.polyKey = null; 25 | } 26 | 27 | /** 28 | * Initialize the AEAD with a key and iv. 29 | * @param {Buffer} key 30 | * @param {Buffer} iv - IV / packet sequence number. 31 | */ 32 | 33 | NonstandardAEAD.prototype.init = function init(key, iv) { 34 | var polyKey = new Buffer(32); 35 | polyKey.fill(0); 36 | 37 | this.chacha20.init(key, iv); 38 | this.chacha20.encrypt(polyKey); 39 | this.poly1305.init(polyKey); 40 | 41 | // We need to encrypt a full block 42 | // to get the cipher in the correct state. 43 | this.chacha20.encrypt(new Buffer(32)); 44 | 45 | // Counter should be one. 46 | assert(this.chacha20.getCounter() === 1); 47 | 48 | // Expose for debugging. 49 | this.polyKey = polyKey; 50 | 51 | this.aadLen = 0; 52 | this.cipherLen = 0; 53 | }; 54 | 55 | /** 56 | * Update the aad (will be finalized 57 | * on an encrypt/decrypt call). 58 | * @param {Buffer} aad 59 | */ 60 | 61 | NonstandardAEAD.prototype.aad = function _aad(aad) { 62 | assert(this.cipherLen === 0, 'Cannot update aad.'); 63 | this.poly1305.update(aad); 64 | this.aadLen += aad.length; 65 | }; 66 | 67 | /** 68 | * Finalize AAD. 69 | */ 70 | 71 | NonstandardAEAD.prototype.finalAAD = function finalAAD() { 72 | var lo, hi, len; 73 | 74 | if (this.cipherLen !== 0) 75 | return; 76 | 77 | this.pad16(this.aadLen); 78 | 79 | len = new Buffer(8); 80 | 81 | lo = this.aadLen % 0x100000000; 82 | hi = (this.aadLen - lo) / 0x100000000; 83 | len.writeUInt32LE(lo, 0, true); 84 | len.writeUInt32LE(hi, 4, true); 85 | 86 | this.poly1305.update(len); 87 | }; 88 | 89 | /** 90 | * Encrypt a piece of data. 91 | * @param {Buffer} data 92 | */ 93 | 94 | NonstandardAEAD.prototype.encrypt = function encrypt(data) { 95 | this.finalAAD(); 96 | 97 | this.chacha20.encrypt(data); 98 | this.poly1305.update(data); 99 | this.cipherLen += data.length; 100 | 101 | return data; 102 | }; 103 | 104 | /** 105 | * Decrypt a piece of data. 106 | * @param {Buffer} data 107 | */ 108 | 109 | NonstandardAEAD.prototype.decrypt = function decrypt(data) { 110 | this.finalAAD(); 111 | 112 | this.cipherLen += data.length; 113 | this.poly1305.update(data); 114 | this.chacha20.encrypt(data); 115 | 116 | return data; 117 | }; 118 | 119 | /** 120 | * Authenticate data without decrypting. 121 | * @param {Buffer} data 122 | */ 123 | 124 | NonstandardAEAD.prototype.auth = function auth(data) { 125 | this.finalAAD(); 126 | 127 | this.cipherLen += data.length; 128 | this.poly1305.update(data); 129 | 130 | return data; 131 | }; 132 | 133 | /** 134 | * Finalize the aead and generate a MAC. 135 | * @returns {Buffer} MAC 136 | */ 137 | 138 | NonstandardAEAD.prototype.finish = function finish() { 139 | var len = new Buffer(8); 140 | var lo, hi; 141 | 142 | this.finalAAD(); 143 | 144 | this.pad16(this.cipherLen); 145 | 146 | len = new Buffer(8); 147 | 148 | lo = this.cipherLen % 0x100000000; 149 | hi = (this.cipherLen - lo) / 0x100000000; 150 | len.writeUInt32LE(lo, 0, true); 151 | len.writeUInt32LE(hi, 4, true); 152 | 153 | this.poly1305.update(len); 154 | 155 | return this.poly1305.finish(); 156 | }; 157 | 158 | /** 159 | * Pad a chunk before updating mac. 160 | * @private 161 | * @param {Number} size 162 | */ 163 | 164 | NonstandardAEAD.prototype.pad16 = function pad16(size) { 165 | // NOP 166 | }; 167 | 168 | /** 169 | * AEAD Stream 170 | * @constructor 171 | */ 172 | 173 | function AEADStream() { 174 | this.aead = new NonstandardAEAD(); 175 | 176 | this.tag = null; 177 | this.seqLo = 0; 178 | this.seqHi = 0; 179 | this.iv = new Buffer(8); 180 | this.iv.fill(0); 181 | 182 | this.highWaterMark = 1024 * (1 << 20); 183 | this.processed = 0; 184 | this.lastRekey = 0; 185 | } 186 | 187 | AEADStream.prototype.init = function init(key) { 188 | this.update(); 189 | this.aead.init(key, this.iv); 190 | this.lastRekey = utils.now(); 191 | }; 192 | 193 | AEADStream.prototype.sequence = function sequence() { 194 | // Wrap sequence number a la openssh. 195 | if (++this.seqLo === 0x100000000) { 196 | this.seqLo = 0; 197 | if (++this.seqHi === 0x100000000) 198 | this.seqHi = 0; 199 | } 200 | 201 | this.update(); 202 | 203 | // State of the ciphers is 204 | // unaltered aside from the iv. 205 | this.aead.init(null, this.iv); 206 | }; 207 | 208 | AEADStream.prototype.update = function update() { 209 | this.iv.writeUInt32BE(this.seqHi, 0, true); 210 | this.iv.writeUInt32BE(this.seqLo, 4, true); 211 | return this.iv; 212 | }; 213 | 214 | AEADStream.prototype.encryptSize = function encryptSize(size) { 215 | var data = new Buffer(2); 216 | data.writeUInt16BE(size, 0, true); 217 | // this.aead.aad(data); 218 | return data; 219 | }; 220 | 221 | AEADStream.prototype.decryptSize = function decryptSize(data) { 222 | // this.aead.aad(data); 223 | return data.readUInt16BE(0, true); 224 | }; 225 | 226 | AEADStream.prototype.encrypt = function encrypt(data) { 227 | return this.aead.encrypt(data); 228 | }; 229 | 230 | AEADStream.prototype.decrypt = function decrypt(data) { 231 | return this.aead.chacha20.encrypt(data); 232 | }; 233 | 234 | AEADStream.prototype.auth = function auth(data) { 235 | return this.aead.auth(data); 236 | }; 237 | 238 | AEADStream.prototype.finish = function finish() { 239 | this.tag = this.aead.finish(); 240 | return this.tag; 241 | }; 242 | 243 | AEADStream.prototype.verify = function verify(tag) { 244 | return chachapoly.Poly1305.verify(this.tag, tag); 245 | }; 246 | 247 | /* 248 | * Expose 249 | */ 250 | 251 | exports = NonstandardAEAD; 252 | exports.Stream = AEADStream; 253 | module.exports = exports; 254 | -------------------------------------------------------------------------------- /test/channel-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var constants = bcoin.constants; 5 | var utils = require('bcoin/lib/utils/util'); 6 | var crypto = bcoin.crypto; 7 | var assert = require('assert'); 8 | var BufferWriter = require('bcoin/lib/utils/writer'); 9 | var BufferReader = require('bcoin/lib/utils/reader'); 10 | var opcodes = constants.opcodes; 11 | var hashType = constants.hashType; 12 | var elkrem = require('../lib/elkrem'); 13 | var ElkremSender = elkrem.ElkremSender; 14 | var ElkremReceiver = elkrem.ElkremReceiver; 15 | var util = require('../lib/scriptutil'); 16 | var ChannelState = require('../lib/channelstate'); 17 | var Channel = require('../lib/channel'); 18 | var wire = require('../lib/wire'); 19 | var CommitRevocation = wire.CommitRevocation; 20 | var HTLCAddRequest = wire.HTLCAddRequest; 21 | var List = require('../lib/list'); 22 | 23 | bcoin.cache(); 24 | 25 | function alloc(num) { 26 | var buf = new Buffer(32); 27 | buf.fill(num); 28 | return buf; 29 | } 30 | 31 | function createChannels() { 32 | var hdSeed = alloc(1); 33 | var alice = alloc(2); 34 | var alicePub = bcoin.ec.publicKeyCreate(alice, true); 35 | var bob = alloc(3); 36 | var bobPub = bcoin.ec.publicKeyCreate(bob, true); 37 | var channelCapacity = 10 * 1e8; 38 | var channelBalance = channelCapacity / 2; 39 | var csvTimeoutAlice = 5; 40 | var csvTimeoutBob = 4; 41 | 42 | var redeem = util.fundingRedeem(alicePub, bobPub, channelCapacity); 43 | 44 | var fundingOutput = new bcoin.coin(); 45 | fundingOutput.hash = constants.ONE_HASH.toString('hex'); 46 | fundingOutput.index = 0; 47 | fundingOutput.value = 1 * 1e8; 48 | fundingOutput.script = redeem.output.script; 49 | 50 | var bobElkrem = new ElkremSender(util.deriveElkremRoot(bob, alicePub)); 51 | var bobFirstRevoke = bobElkrem.getIndex(0); 52 | var bobRevKey = util.deriveRevPub(alicePub, bobFirstRevoke); 53 | 54 | var aliceElkrem = new ElkremSender(util.deriveElkremRoot(alice, bobPub)); 55 | var aliceFirstRevoke = aliceElkrem.getIndex(0); 56 | var aliceRevKey = util.deriveRevPub(bobPub, aliceFirstRevoke); 57 | 58 | var aliceCommit = util.createCommitTX( 59 | fundingOutput, alicePub, bobPub, aliceRevKey, 60 | csvTimeoutAlice, channelBalance, channelBalance); 61 | 62 | var bobCommit = util.createCommitTX( 63 | fundingOutput, bobPub, alicePub, bobRevKey, 64 | csvTimeoutAlice, channelBalance, channelBalance); 65 | 66 | var aliceState = new ChannelState({ 67 | theirLNID: hdSeed, 68 | id: fundingOutput, 69 | ourCommitKey: alice, 70 | theirCommitKey: bobPub, 71 | capacity: channelCapacity, 72 | ourBalance: channelBalance, 73 | theirBalance: channelBalance, 74 | ourCommitTX: aliceCommit, 75 | fundingInput: fundingOutput, 76 | ourMultisigKey: alice, 77 | theirMultisigKey: bobPub, 78 | fundingScript: redeem.redeem, 79 | localCSVDelay: csvTimeoutAlice, 80 | remoteCSVDelay: csvTimeoutBob, 81 | theirCurrentRevocation: bobRevKey, 82 | localElkrem: aliceElkrem, 83 | remoteElkrem: new ElkremReceiver(), 84 | db: null 85 | }); 86 | 87 | var bobState = new ChannelState({ 88 | theirLNID: hdSeed, 89 | id: fundingOutput, // supposed to be prevout. do outpoint.fromOptions 90 | ourCommitKey: bob, 91 | theirCommitKey: alicePub, 92 | capacity: channelCapacity, 93 | ourBalance: channelBalance, 94 | theirBalance: channelBalance, 95 | ourCommitTX: bobCommit, 96 | fundingInput: fundingOutput, 97 | ourMultisigKey: bob, 98 | theirMultisigKey: alicePub, 99 | fundingScript: redeem.redeem, 100 | localCSVDelay: csvTimeoutBob, 101 | remoteCSVDelay: csvTimeoutAlice, 102 | theirCurrentRevocation: aliceRevKey, 103 | localElkrem: bobElkrem, 104 | remoteElkrem: new ElkremReceiver(), 105 | db: null 106 | }); 107 | 108 | var aliceChannel = new Channel({ 109 | state: aliceState 110 | }); 111 | 112 | var bobChannel = new Channel({ 113 | state: bobState 114 | }); 115 | 116 | return { alice: aliceChannel, bob: bobChannel }; 117 | } 118 | 119 | describe('Channel', function() { 120 | it('should test simple add and settle workflow', function() { 121 | var channel = createChannels(); 122 | var i, aliceNextRevoke, htlcs, bobNextRevoke; 123 | var data; 124 | 125 | for (i = 1; i < 4; i++) { 126 | aliceNextRevoke = channel.alice.extendRevocationWindow(); 127 | htlcs = channel.bob.receiveRevocation(aliceNextRevoke); 128 | assert(!htlcs || htlcs.length === 0); 129 | bobNextRevoke = channel.bob.extendRevocationWindow(); 130 | htlcs = channel.alice.receiveRevocation(bobNextRevoke); 131 | assert(!htlcs || htlcs.length === 0); 132 | } 133 | 134 | assert(channel.alice.revocationWindowEdge === 3); 135 | assert(channel.bob.revocationWindowEdge === 3); 136 | 137 | var payPreimage = alloc(4); 138 | var payHash = crypto.sha256(payPreimage); 139 | 140 | // Bob requests a payment from alice. 141 | var htlc = new HTLCAddRequest(); 142 | htlc.redemptionHashes = [payHash]; 143 | htlc.value = 1e8; 144 | htlc.expiry = 5; 145 | 146 | channel.alice.addHTLC(htlc); 147 | 148 | channel.bob.receiveHTLC(htlc); 149 | 150 | data = channel.alice.signNextCommitment(); 151 | var aliceSig = data.sig; 152 | var bobLogIndex = data.index; 153 | 154 | channel.bob.receiveNewCommitment(aliceSig, bobLogIndex); 155 | 156 | data = channel.bob.signNextCommitment(); 157 | var bobSig = data.sig; 158 | var aliceLogIndex = data.index; 159 | 160 | var bobRev = channel.bob.revokeCurrentCommitment(); 161 | 162 | channel.alice.receiveNewCommitment(bobSig, aliceLogIndex); 163 | 164 | htlcs = channel.alice.receiveRevocation(bobRev); 165 | assert(!htlcs || htlcs.length === 0); 166 | 167 | var aliceRev = channel.alice.revokeCurrentCommitment(); 168 | 169 | htlcs = channel.bob.receiveRevocation(aliceRev); 170 | assert(htlcs && htlcs.length === 1); 171 | 172 | // utils.log(channel.alice.localCommitChain.tip()); 173 | // utils.log(channel.bob.localCommitChain.tip()); 174 | 175 | var aliceBalance = 4 * 1e8; 176 | var bobBalance = 5 * 1e8; 177 | 178 | assert(channel.alice.state.ourBalance === aliceBalance); 179 | assert(channel.alice.state.theirBalance === bobBalance); 180 | assert(channel.bob.state.ourBalance === bobBalance); 181 | assert(channel.bob.state.theirBalance === aliceBalance); 182 | assert(channel.alice.currentHeight === 1); 183 | assert(channel.bob.currentHeight === 1); 184 | assert(channel.alice.revocationWindowEdge === 4); 185 | assert(channel.bob.revocationWindowEdge === 4); 186 | 187 | var preimage = utils.copy(payPreimage); 188 | var settleIndex = channel.bob.settleHTLC(preimage); 189 | 190 | channel.alice.receiveHTLCSettle(preimage, settleIndex); 191 | 192 | data = channel.bob.signNextCommitment(); 193 | var bobSig2 = data.sig; 194 | var aliceIndex2 = data.index; 195 | 196 | channel.alice.receiveNewCommitment(bobSig2, aliceIndex2); 197 | 198 | data = channel.alice.signNextCommitment(); 199 | var aliceSig2 = data.sig; 200 | var bobIndex2 = data.index; 201 | var aliceRev2 = channel.alice.revokeCurrentCommitment(); 202 | 203 | channel.bob.receiveNewCommitment(aliceSig2, bobIndex2); 204 | 205 | var bobRev2 = channel.bob.revokeCurrentCommitment(); 206 | 207 | htlcs = channel.bob.receiveRevocation(aliceRev2); 208 | assert(!htlcs || htlcs.length === 0); 209 | 210 | htlcs = channel.alice.receiveRevocation(bobRev2); 211 | assert(htlcs && htlcs.length === 1); 212 | 213 | var aliceSettleBalance = 4 * 1e8; 214 | var bobSettleBalance = 6 * 1e8; 215 | assert(channel.alice.state.ourBalance === aliceSettleBalance); 216 | assert(channel.alice.state.theirBalance === bobSettleBalance); 217 | assert(channel.bob.state.ourBalance === bobSettleBalance); 218 | assert(channel.bob.state.theirBalance === aliceSettleBalance); 219 | assert(channel.alice.currentHeight === 2); 220 | assert(channel.bob.currentHeight === 2); 221 | assert(channel.alice.revocationWindowEdge === 5); 222 | assert(channel.bob.revocationWindowEdge === 5); 223 | 224 | assert(channel.alice.ourUpdateLog.size() === 0); 225 | assert(channel.alice.theirUpdateLog.size() === 0); 226 | assert(Object.keys(channel.alice.ourLogIndex).length === 0); 227 | assert(Object.keys(channel.alice.theirLogIndex).length === 0); 228 | 229 | assert(channel.bob.ourUpdateLog.size() === 0); 230 | assert(channel.bob.theirUpdateLog.size() === 0); 231 | assert(Object.keys(channel.bob.ourLogIndex).length === 0); 232 | assert(Object.keys(channel.bob.theirLogIndex).length === 0); 233 | }); 234 | 235 | it('should test cooperative closure', function() { 236 | var channel = createChannels(); 237 | var data = channel.alice.initCooperativeClose(); 238 | var sig = data.sig; 239 | var txid = data.hash; 240 | var closeTX = channel.bob.completeCooperativeClose(sig); 241 | assert(utils.equal(txid, closeTX.hash())); 242 | 243 | channel.alice.status = Channel.states.OPEN; 244 | channel.bob.status = Channel.states.OPEN; 245 | 246 | var data = channel.bob.initCooperativeClose(); 247 | var sig = data.sig; 248 | var txid = data.hash; 249 | var closeTX = channel.alice.completeCooperativeClose(sig); 250 | assert(utils.equal(txid, closeTX.hash())); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var bcoin = require('bcoin'); 5 | var utils = bcoin.utils; 6 | var crypto = bcoin.crypto; 7 | var assert = utils.assert; 8 | var AEAD = require('./aead'); 9 | 10 | /** 11 | * Lightning Connection 12 | * @constructor 13 | */ 14 | 15 | function Connection() { 16 | if (!(this instanceof Connection)) 17 | return new Connection(); 18 | 19 | EventEmitter.call(this); 20 | 21 | this.remotePub = null; 22 | this.remoteID = null; 23 | this.authed = false; 24 | this.local = new AEAD.Stream(); 25 | this.remote = new AEAD.Stream(); 26 | this.viaPBX = false; 27 | this.pbxIncoming = null; 28 | this.pbxOutgoing = null; 29 | this.version = 0; 30 | this.socket = null; 31 | 32 | this.readQueue = []; 33 | this.readCallback = null; 34 | this.pending = []; 35 | this.total = 0; 36 | this.waiting = 2; 37 | this.hasSize = false; 38 | } 39 | 40 | utils.inherits(Connection, EventEmitter); 41 | 42 | Connection.prototype.error = function error(err) { 43 | this.emit('error', new Error(err)); 44 | }; 45 | 46 | Connection.prototype.connect = function connect(myID, addr, remoteID) { 47 | var self = this; 48 | var net = require('net'); 49 | 50 | this.socket = net.connect(10011, addr); 51 | 52 | this.socket.on('error', function(err) { 53 | self.emit('error', err); 54 | }); 55 | 56 | this.socket.on('connect', function() { 57 | self._onConnect(myID, addr, remoteID); 58 | }); 59 | 60 | this.socket.on('data', function(data) { 61 | self.feed(data); 62 | }); 63 | }; 64 | 65 | Connection.prototype._onConnect = function _onConnect(myID, addr, remoteID) { 66 | var ourPriv, ourPub; 67 | 68 | switch (remoteID.length) { 69 | case 20: 70 | this.remoteID = remoteID; 71 | break; 72 | case 33: 73 | this.remoteID = crypto.hash160(remoteID); 74 | break; 75 | default: 76 | throw new Error('Bad LNID size.'); 77 | } 78 | 79 | ourPriv = bcoin.ec.generatePrivateKey(); 80 | ourPub = bcoin.ec.publicKeyCreate(ourPriv, true); 81 | 82 | this.writeClear(ourPub); 83 | 84 | this.readClear(33, function(theirPub) { 85 | var sessionKey = crypto.sha256(bcoin.ec.ecdh(theirPub, ourPriv)); 86 | 87 | this.local.seqHi = 0; 88 | this.local.seqLo = 0; 89 | this.remote.seqHi = (1 << 31) >>> 0; 90 | this.remote.seqLo = 0; 91 | 92 | this.remote.init(sessionKey); 93 | this.local.init(sessionKey); 94 | 95 | this.remotePub = theirPub; 96 | this.authed = false; 97 | 98 | if (remoteID.length === 20) { 99 | this.authPubkeyhash(myID, remoteID, ourPub); 100 | return; 101 | } 102 | 103 | this.authPubkey(myID, remoteID, ourPub); 104 | }); 105 | }; 106 | 107 | Connection.prototype.authPubkey = function authPubkey(myID, theirPub, localPub) { 108 | var theirPKH = crypto.hash160(theirPub); 109 | var idDH = crypto.sha256(bcoin.ec.ecdh(theirPub, myID)); 110 | var myProof = crypto.hash160(concat(theirPub, idDH)); 111 | var myPub = bcoin.ec.publicKeyCreate(myID, true); 112 | var authMsg; 113 | 114 | assert(!this.authed); 115 | 116 | authMsg = new Buffer(73); 117 | myPub.copy(authMsg, 0); 118 | theirPKH.copy(authMsg, 33); 119 | myProof.copy(authMsg, 53); 120 | 121 | this.writeRaw(authMsg); 122 | 123 | this.readRaw(20, function(response) { 124 | var theirProof = crypto.hash160(concat(localPub, idDH)); 125 | 126 | assert(response.length === 20); 127 | 128 | if (!crypto.ccmp(response, theirProof)) 129 | return this.error('Invalid proof.'); 130 | 131 | this.remotePub = theirPub; 132 | this.remoteID = crypto.hash160(theirPub); 133 | this.authed = true; 134 | this.emit('connect'); 135 | }); 136 | }; 137 | 138 | Connection.prototype.authPubkeyhash = function authPubkeyhash(myID, theirPKH, localPub) { 139 | var myPub = bcoin.ec.publicKeyCreate(myID, true); 140 | var greeting; 141 | 142 | assert(!this.authed); 143 | assert(theirPKH.length === 20); 144 | 145 | greeting = new Buffer(53); 146 | myPub.copy(greeting, 0); 147 | theirPKH.copy(greeting, 33); 148 | 149 | this.writeRaw(greeting); 150 | 151 | this.readRaw(53, function(response) { 152 | var theirPub = response.slice(0, 33); 153 | var idDH = crypto.sha256(bcoin.ec.ecdh(theirPub, myID)); 154 | var theirProof = crypto.hash160(concat(localPub, idDH)); 155 | var myProof; 156 | 157 | if (!crypto.ccmp(response.slice(33), theirProof)) 158 | return this.error('Invalid proof.'); 159 | 160 | myProof = crypto.hash160(concat(this.remotePub, idDH)); 161 | 162 | this.writeRaw(myProof); 163 | 164 | this.remotePub = theirPub; 165 | this.remoteID = crypto.hash160(theirPub); 166 | this.authed = true; 167 | this.emit('connect'); 168 | }); 169 | }; 170 | 171 | Connection.prototype.writeClear = function writeClear(payload) { 172 | var packet = new Buffer(2 + payload.length); 173 | packet.writeUInt16BE(payload.length, 0, true); 174 | payload.copy(packet, 2); 175 | this.socket.write(packet); 176 | }; 177 | 178 | Connection.prototype.readClear = function readClear(size, callback) { 179 | this.readQueue.push(new QueuedRead(size, callback)); 180 | }; 181 | 182 | Connection.prototype.writeRaw = function writeRaw(data) { 183 | this.socket.write(data); 184 | }; 185 | 186 | Connection.prototype.readRaw = function readRaw(size, callback) { 187 | assert(!this.hasSize); 188 | assert(this.waiting === 2); 189 | this.waiting = size; 190 | this.readCallback = callback; 191 | }; 192 | 193 | Connection.prototype.feed = function feed(data) { 194 | var chunk; 195 | 196 | this.total += data.length; 197 | this.pending.push(data); 198 | 199 | while (this.total >= this.waiting) { 200 | chunk = this.read(this.waiting); 201 | this.parse(chunk); 202 | } 203 | }; 204 | 205 | Connection.prototype.read = function read(size) { 206 | var pending, chunk, off, len; 207 | 208 | assert(this.total >= size, 'Reading too much.'); 209 | 210 | if (size === 0) 211 | return new Buffer(0); 212 | 213 | pending = this.pending[0]; 214 | 215 | if (pending.length > size) { 216 | chunk = pending.slice(0, size); 217 | this.pending[0] = pending.slice(size); 218 | this.total -= chunk.length; 219 | return chunk; 220 | } 221 | 222 | if (pending.length === size) { 223 | chunk = this.pending.shift(); 224 | this.total -= chunk.length; 225 | return chunk; 226 | } 227 | 228 | chunk = new Buffer(size); 229 | off = 0; 230 | len = 0; 231 | 232 | while (off < chunk.length) { 233 | pending = this.pending[0]; 234 | len = pending.copy(chunk, off); 235 | if (len === pending.length) 236 | this.pending.shift(); 237 | else 238 | this.pending[0] = pending.slice(len); 239 | off += len; 240 | } 241 | 242 | assert.equal(off, chunk.length); 243 | 244 | this.total -= chunk.length; 245 | 246 | return chunk; 247 | }; 248 | 249 | Connection.prototype.parse = function parse(data) { 250 | var size, payload, tag, item; 251 | 252 | if (!this.authed) { 253 | if (this.readCallback) { 254 | this.hasSize = false; 255 | this.waiting = 2; 256 | item = this.readCallback; 257 | this.readCallback = null; 258 | item.call(this, data); 259 | return; 260 | } 261 | 262 | if (!this.hasSize) { 263 | size = data.readUInt16BE(0, true); 264 | 265 | if (size < 12) { 266 | this.waiting = 2; 267 | this.emit('error', new Error('Bad packet size.')); 268 | return; 269 | } 270 | 271 | this.hasSize = true; 272 | this.waiting = size; 273 | 274 | return; 275 | } 276 | 277 | this.hasSize = false; 278 | this.waiting = 2; 279 | 280 | if (this.readQueue.length > 0) { 281 | item = this.readQueue.shift(); 282 | if (item.size !== data.length) 283 | return this.error('Bad packet size.'); 284 | item.callback.call(this, data); 285 | return; 286 | } 287 | 288 | this.emit('data', data); 289 | 290 | return; 291 | } 292 | 293 | if (!this.hasSize) { 294 | size = this.local.decryptSize(data); 295 | 296 | if (size < 2) { 297 | this.waiting = 2; 298 | this.error('Bad packet size.'); 299 | return; 300 | } 301 | 302 | this.hasSize = true; 303 | this.waiting = size; 304 | 305 | return; 306 | } 307 | 308 | payload = data.slice(0, this.waiting - 16); 309 | tag = data.slice(this.waiting - 16, this.waiting); 310 | 311 | this.hasSize = false; 312 | this.waiting = 2; 313 | 314 | // Authenticate payload before decrypting. 315 | // This ensures the cipher state isn't altered 316 | // if the payload integrity has been compromised. 317 | this.local.auth(payload); 318 | this.local.finish(); 319 | 320 | if (!this.local.verify(tag)) { 321 | this.local.sequence(); 322 | this.error('Bad tag.'); 323 | return; 324 | } 325 | 326 | this.local.decrypt(payload); 327 | this.local.sequence(); 328 | 329 | this.emit('data', payload); 330 | }; 331 | 332 | Connection.prototype.write = function write(payload) { 333 | var packet = new Buffer(2 + payload.length + 16); 334 | 335 | this.remote.encryptSize(payload.length + 16).copy(packet, 0); 336 | this.remote.encrypt(payload).copy(packet, 2); 337 | this.remote.finish().copy(packet, 2 + payload.length); 338 | this.remote.sequence(); 339 | 340 | this.socket.write(packet); 341 | }; 342 | 343 | /* 344 | * Helpers 345 | */ 346 | 347 | function QueuedRead(size, callback) { 348 | this.size = size; 349 | this.callback = callback; 350 | } 351 | 352 | function concat(b1, b2) { 353 | var buf = new Buffer(b1.length + b2.length); 354 | b1.copy(buf, 0); 355 | b2.copy(buf, b1.length); 356 | return buf; 357 | } 358 | 359 | module.exports = Connection; 360 | -------------------------------------------------------------------------------- /test/script-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var constants = bcoin.constants; 5 | var utils = require('bcoin/lib/utils/util'); 6 | var crypto = bcoin.crypto; 7 | var assert = require('assert'); 8 | var BufferWriter = require('bcoin/lib/utils/writer'); 9 | var BufferReader = require('bcoin/lib/utils/reader'); 10 | var opcodes = constants.opcodes; 11 | var hashType = constants.hashType; 12 | var elkrem = require('../lib/elkrem'); 13 | var ElkremSender = elkrem.ElkremSender; 14 | var ElkremReceiver = elkrem.ElkremReceiver; 15 | var util = require('../lib/scriptutil'); 16 | var ChannelState = require('../lib/channelstate'); 17 | var Channel = require('../lib/channel'); 18 | var wire = require('../lib/wire'); 19 | var CommitRevocation = wire.CommitRevocation; 20 | var HTLCAddRequest = wire.HTLCAddRequest; 21 | var List = require('../lib/list'); 22 | 23 | describe('Script', function() { 24 | // TestCommitmentSpendValidation test the spendability of both outputs within 25 | // the commitment transaction. 26 | // 27 | // The following spending cases are covered by this test: 28 | // * Alice's spend from the delayed output on her commitment transaciton. 29 | // * Bob's spend from Alice's delayed output when she broadcasts a revoked 30 | // commitment transaction. 31 | // * Bob's spend from his unencumbered output within Alice's commitment 32 | // transaction. 33 | it('should test commitment spend validation', function() { 34 | var hdSeed = crypto.randomBytes(32); 35 | 36 | // Setup funding transaction output. 37 | var fundingOutput = new bcoin.coin(); 38 | fundingOutput.hash = constants.ONE_HASH.toString('hex'); 39 | fundingOutput.index = 50; 40 | fundingOutput.value = 1 * 1e8; 41 | 42 | // We also set up set some resources for the commitment transaction. 43 | // Each side currently has 1 BTC within the channel, with a total 44 | // channel capacity of 2BTC. 45 | var alice = bcoin.ec.generatePrivateKey(); 46 | var alicePub = bcoin.ec.publicKeyCreate(alice, true); 47 | var bob = bcoin.ec.generatePrivateKey(); 48 | var bobPub = bcoin.ec.publicKeyCreate(bob, true); 49 | var balance = 1 * 1e8; 50 | var csvTimeout = 5; 51 | var revImage = hdSeed; 52 | var revPub = util.deriveRevPub(bobPub, revImage); 53 | 54 | var commitTX = util.createCommitTX(fundingOutput, alicePub, bobPub, revPub, csvTimeout, balance, balance); 55 | // var delayOut = commitTX.outputs[0]; 56 | // var regularOut = commitTX.outputs[1]; 57 | var targetOut = util.commitUnencumbered(alicePub); 58 | var sweep = new bcoin.mtx(); 59 | sweep.addInput(bcoin.coin.fromTX(commitTX, 0)); 60 | var o = new bcoin.output(); 61 | o.script = targetOut; 62 | o.value = 0.5 * 1e8; 63 | sweep.addOutput(o); 64 | 65 | // First, we'll test spending with Alice's key after the timeout. 66 | var delayScript = util.commitSelf(csvTimeout, alicePub, revPub); 67 | var aliceSpend = util.commitSpendTimeout(delayScript, csvTimeout, alice, sweep); 68 | sweep.inputs[0].witness = aliceSpend; 69 | assert(sweep.verify()); 70 | 71 | // Next, we'll test bob spending with the derived revocation key to 72 | // simulate the scenario when alice broadcasts this commitmen 73 | // transaction after it's been revoked. 74 | var revPriv = util.deriveRevPriv(bob, revImage); 75 | var bobSpend = util.commitSpendRevoke(delayScript, revPriv, sweep); 76 | sweep.inputs[0].witness = bobSpend; 77 | assert(sweep.verify()); 78 | 79 | // Finally, we test bob sweeping his output as normal in the case that 80 | // alice broadcasts this commitment transaction. 81 | sweep.inputs.length = 0; 82 | sweep.addInput(bcoin.coin.fromTX(commitTX, 1)); 83 | var bobScript = util.commitUnencumbered(bobPub); 84 | var bobRegularSpend = util.commitSpendNoDelay(bobScript, bob, sweep); 85 | sweep.inputs[0].witness = bobRegularSpend; 86 | assert(sweep.verify()); 87 | }); 88 | 89 | // TestHTLCSenderSpendValidation tests all possible valid+invalid redemption 90 | // paths in the script used within the sender's commitment transaction for an 91 | // outgoing HTLC. 92 | // 93 | // The following cases are exercised by this test: 94 | // sender script: 95 | // * reciever spends 96 | // * revoke w/ sig 97 | // * HTLC with invalid pre-image size 98 | // * HTLC with valid pre-image size + sig 99 | // * sender spends 100 | // * invalid lock-time for CLTV 101 | // * invalid sequence for CSV 102 | // * valid lock-time+sequence, valid sig 103 | it('should test HTLC sender spend validation', function() { 104 | var hdSeed = crypto.randomBytes(32); 105 | 106 | var fundingOutput = new bcoin.coin(); 107 | fundingOutput.hash = constants.ONE_HASH.toString('hex'); 108 | fundingOutput.index = 50; 109 | fundingOutput.value = 1 * 1e8; 110 | 111 | var revImage = hdSeed; 112 | var revHash = crypto.sha256(revImage); 113 | var payImage = utils.copy(revHash); 114 | payImage[0] ^= 1; 115 | var payHash = crypto.sha256(payImage); 116 | 117 | var alice = bcoin.ec.generatePrivateKey(); 118 | var alicePub = bcoin.ec.publicKeyCreate(alice, true); 119 | var bob = bcoin.ec.generatePrivateKey(); 120 | var bobPub = bcoin.ec.publicKeyCreate(bob, true); 121 | var payValue = 1 * 10e8; 122 | var cltvTimeout = 8; 123 | var csvTimeout = 5; 124 | 125 | var htlc = util.createSenderHTLC( 126 | cltvTimeout, csvTimeout, alicePub, 127 | bobPub, revHash, payHash); 128 | 129 | var whtlc = util.toWitnessScripthash(htlc); 130 | 131 | // This will be Alice's commitment transaction. In this scenario Alice 132 | // is sending an HTLC to a node she has a a path to (could be Bob, 133 | // could be multiple hops down, it doesn't really matter). 134 | var senderCommit = new bcoin.mtx(); 135 | senderCommit.addInput(fundingOutput); 136 | senderCommit.addOutput({ 137 | value: payValue, 138 | script: whtlc 139 | }); 140 | 141 | var prevout = bcoin.coin.fromTX(senderCommit, 0); 142 | 143 | var sweep = new bcoin.mtx(); 144 | sweep.addInput(prevout); 145 | sweep.addOutput({ 146 | script: bcoin.script.fromRaw('doesnt matter', 'ascii'), 147 | value: 1 * 10e8 148 | }); 149 | 150 | function testHTLC(witness, result) { 151 | sweep.inputs[0].witness = witness; 152 | assert(sweep.verify() === result); 153 | } 154 | 155 | // revoke w/ sig 156 | testHTLC(util.senderSpendRevoke(htlc, bob, sweep, revImage), true); 157 | // htlc with invalid preimage size 158 | testHTLC(util.senderSpendRedeem(htlc, bob, sweep, new Buffer(45)), false); 159 | // htlc with valid preimage size & sig 160 | testHTLC(util.senderSpendRedeem(htlc, bob, sweep, payImage), true); 161 | // invalid locktime for cltv 162 | testHTLC(util.senderSpendTimeout(htlc, alice, sweep, cltvTimeout - 2, csvTimeout), false); 163 | // invalid sequence for csv 164 | testHTLC(util.senderSpendTimeout(htlc, alice, sweep, cltvTimeout, csvTimeout - 2), false); 165 | // valid locktime+sequence, valid sig 166 | testHTLC(util.senderSpendTimeout(htlc, alice, sweep, cltvTimeout, csvTimeout), true); 167 | }); 168 | 169 | // TestHTLCReceiverSpendValidation tests all possible valid+invalid redemption 170 | // paths in the script used within the reciever's commitment transaction for an 171 | // incoming HTLC. 172 | // 173 | // The following cases are exercised by this test: 174 | // * reciever spends 175 | // * HTLC redemption w/ invalid preimage size 176 | // * HTLC redemption w/ invalid sequence 177 | // * HTLC redemption w/ valid preimage size 178 | // * sender spends 179 | // * revoke w/ sig 180 | // * refund w/ invalid lock time 181 | // * refund w/ valid lock time 182 | it('should test HTLC receiver spend validation', function() { 183 | var hdSeed = crypto.randomBytes(32); 184 | 185 | var fundingOutput = new bcoin.coin(); 186 | fundingOutput.hash = constants.ONE_HASH.toString('hex'); 187 | fundingOutput.index = 50; 188 | fundingOutput.value = 1 * 1e8; 189 | 190 | var revImage = hdSeed; 191 | var revHash = crypto.sha256(revImage); 192 | var payImage = utils.copy(revHash); 193 | payImage[0] ^= 1; 194 | var payHash = crypto.sha256(payImage); 195 | 196 | var alice = bcoin.ec.generatePrivateKey(); 197 | var alicePub = bcoin.ec.publicKeyCreate(alice, true); 198 | var bob = bcoin.ec.generatePrivateKey(); 199 | var bobPub = bcoin.ec.publicKeyCreate(bob, true); 200 | var payValue = 1 * 10e8; 201 | var cltvTimeout = 8; 202 | var csvTimeout = 5; 203 | 204 | var htlc = util.createReceiverHTLC( 205 | cltvTimeout, csvTimeout, alicePub, 206 | bobPub, revHash, payHash); 207 | 208 | var whtlc = util.toWitnessScripthash(htlc); 209 | 210 | // This will be Bob's commitment transaction. In this scenario Alice 211 | // is sending an HTLC to a node she has a a path to (could be Bob, 212 | // could be multiple hops down, it doesn't really matter). 213 | var recCommit = new bcoin.mtx(); 214 | recCommit.addInput(fundingOutput); 215 | recCommit.addOutput({ 216 | value: payValue, 217 | script: whtlc 218 | }); 219 | var prevout = bcoin.coin.fromTX(recCommit, 0); 220 | 221 | var sweep = new bcoin.mtx(); 222 | sweep.addInput(prevout); 223 | sweep.addOutput({ 224 | script: bcoin.script.fromRaw('doesnt matter', 'ascii'), 225 | value: 1 * 10e8 226 | }); 227 | 228 | function testHTLC(witness, result) { 229 | sweep.inputs[0].witness = witness; 230 | assert(sweep.verify() === result); 231 | } 232 | 233 | // htlc redemption w/ invalid preimage size 234 | testHTLC(util.recSpendRedeem(htlc, bob, sweep, new Buffer(45), csvTimeout), false); 235 | // htlc redemption w/ invalid sequence 236 | testHTLC(util.recSpendRedeem(htlc, bob, sweep, payImage, csvTimeout - 2), false); 237 | // htlc redemption w/ valid preimage size 238 | testHTLC(util.recSpendRedeem(htlc, bob, sweep, payImage, csvTimeout), true); 239 | // revoke w/ sig 240 | testHTLC(util.recSpendRevoke(htlc, alice, sweep, revImage), true); 241 | // refund w/ invalid lock time 242 | testHTLC(util.recSpendTimeout(htlc, alice, sweep, cltvTimeout - 2), false); 243 | // refund w/ valid lock time 244 | testHTLC(util.recSpendTimeout(htlc, alice, sweep, cltvTimeout), true); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /lib/scriptutil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var bn = bcoin.bn; 5 | var constants = bcoin.constants; 6 | var utils = require('bcoin/lib/utils/util'); 7 | var crypto = require('bcoin/lib/crypto/crypto'); 8 | var assert = require('assert'); 9 | var opcodes = constants.opcodes; 10 | var hashType = constants.hashType; 11 | var util = exports; 12 | 13 | util.toWitnessScripthash = function(redeem) { 14 | return bcoin.script.fromProgram(0, redeem.sha256()); 15 | }; 16 | 17 | util.toMultisig = function(k1, k2) { 18 | var script = new bcoin.script(); 19 | var k; 20 | 21 | // Note: It looks like lnd orders these in reverse. 22 | if (utils.cmp(k1, k2) < 0) { 23 | k = k1; 24 | k1 = k2; 25 | k2 = k; 26 | } 27 | 28 | script.push(opcodes.OP_2); 29 | script.push(k1); 30 | script.push(k2); 31 | script.push(opcodes.OP_2); 32 | script.push(opcodes.OP_CHECKMULTISIG); 33 | script.compile(); 34 | 35 | return script; 36 | }; 37 | 38 | util.fundingRedeem = function(k1, k2, value) { 39 | var redeem = util.toMultisig(k1, k2); 40 | var output = new bcoin.output(); 41 | 42 | output.script = util.toWitnessScripthash(redeem); 43 | output.value = value; 44 | 45 | assert(value > 0); 46 | 47 | return { 48 | redeem: redeem, 49 | output: output 50 | }; 51 | }; 52 | 53 | util.spendMultisig = function(redeem, k1, s1, k2, s2) { 54 | var witness = new bcoin.witness(); 55 | 56 | witness.push(new Buffer(0)); 57 | 58 | // Note: It looks like lnd orders these in reverse. 59 | if (utils.cmp(k1, k2) < 0) { 60 | witness.push(s2); 61 | witness.push(s1); 62 | } else { 63 | witness.push(s1); 64 | witness.push(s2); 65 | } 66 | 67 | witness.push(redeem.toRaw()); 68 | witness.compile(); 69 | 70 | return witness; 71 | }; 72 | 73 | util.findOutput = function(tx, script) { 74 | var i, output; 75 | 76 | for (i = 0; i < tx.outputs.length; i++) { 77 | output = tx.outputs[i]; 78 | if (utils.equal(output.script.toRaw(), script.toRaw())) 79 | return i; 80 | } 81 | 82 | return -1; 83 | }; 84 | 85 | util.createSenderHTLC = function(absTimeout, relTimeout, senderKey, recKey, revHash, payHash) { 86 | var script = new bcoin.script(); 87 | script.push(opcodes.OP_IF); 88 | script.push(opcodes.OP_IF); 89 | script.push(revHash); 90 | script.push(opcodes.OP_ELSE); 91 | script.push(opcodes.OP_SIZE); 92 | script.push(new bn(32)); 93 | script.push(opcodes.OP_EQUALVERIFY); 94 | script.push(payHash); 95 | script.push(opcodes.OP_ENDIF); 96 | script.push(opcodes.OP_SWAP); 97 | script.push(opcodes.OP_SHA256); 98 | script.push(opcodes.OP_EQUALVERIFY); 99 | script.push(recKey); 100 | script.push(opcodes.OP_CHECKSIG); 101 | script.push(opcodes.OP_ELSE); 102 | script.push(new bn(absTimeout)); 103 | script.push(opcodes.OP_CHECKLOCKTIMEVERIFY); 104 | script.push(new bn(relTimeout)); 105 | script.push(opcodes.OP_CHECKSEQUENCEVERIFY); 106 | script.push(opcodes.OP_2DROP); 107 | script.push(senderKey); 108 | script.push(opcodes.OP_CHECKSIG); 109 | script.push(opcodes.OP_ENDIF); 110 | script.compile(); 111 | return script; 112 | }; 113 | 114 | util.senderSpendRedeem = function(commitScript, recKey, sweep, payImage) { 115 | var sig = sweep.signature(0, commitScript, recKey, hashType.ALL, 1); 116 | var witness = new bcoin.witness(); 117 | witness.push(sig); 118 | witness.push(payImage); 119 | witness.push(new bn(0)); 120 | witness.push(new bn(1)); 121 | witness.push(commitScript.toRaw()); 122 | witness.compile(); 123 | return witness; 124 | }; 125 | 126 | util.senderSpendRevoke = function(commitScript, recKey, sweep, revImage) { 127 | var sig = sweep.signature(0, commitScript, recKey, hashType.ALL, 1); 128 | var witness = new bcoin.witness(); 129 | witness.push(sig); 130 | witness.push(revImage); 131 | witness.push(new bn(1)); 132 | witness.push(new bn(1)); 133 | witness.push(commitScript.toRaw()); 134 | witness.compile(); 135 | return witness; 136 | }; 137 | 138 | util.senderSpendTimeout = function(commitScript, senderKey, sweep, absTimeout, relTime) { 139 | var sig, witness; 140 | 141 | sweep.setSequence(0, relTime); 142 | sweep.setLocktime(absTimeout); 143 | 144 | sig = sweep.signature(0, commitScript, senderKey, hashType.ALL, 1); 145 | 146 | witness = new bcoin.witness(); 147 | witness.push(sig); 148 | witness.push(new bn(0)); 149 | witness.push(commitScript.toRaw()); 150 | witness.compile(); 151 | 152 | return witness; 153 | }; 154 | 155 | util.createReceiverHTLC = function(absTimeout, relTimeout, senderKey, recKey, revHash, payHash) { 156 | var script = new bcoin.script(); 157 | script.push(opcodes.OP_IF); 158 | script.push(opcodes.OP_SIZE); 159 | script.push(new bn(32)); 160 | script.push(opcodes.OP_EQUALVERIFY); 161 | script.push(opcodes.OP_SHA256); 162 | script.push(payHash); 163 | script.push(opcodes.OP_EQUALVERIFY); 164 | script.push(new bn(relTimeout)); 165 | script.push(opcodes.OP_CHECKSEQUENCEVERIFY); 166 | script.push(opcodes.OP_DROP); 167 | script.push(recKey); 168 | script.push(opcodes.OP_CHECKSIG); 169 | script.push(opcodes.OP_ELSE); 170 | script.push(opcodes.OP_IF); 171 | script.push(opcodes.OP_SHA256); 172 | script.push(revHash); 173 | script.push(opcodes.OP_EQUALVERIFY); 174 | script.push(opcodes.OP_ELSE); 175 | script.push(new bn(absTimeout)); 176 | script.push(opcodes.OP_CHECKLOCKTIMEVERIFY); 177 | script.push(opcodes.OP_DROP); 178 | script.push(opcodes.OP_ENDIF); 179 | script.push(senderKey); 180 | script.push(opcodes.OP_CHECKSIG); 181 | script.push(opcodes.OP_ENDIF); 182 | script.compile(); 183 | return script; 184 | }; 185 | 186 | util.recSpendRedeem = function(commitScript, recKey, sweep, payImage, relTime) { 187 | var sig, witness; 188 | 189 | sweep.setSequence(0, relTime); 190 | 191 | sig = sweep.signature(0, commitScript, recKey, hashType.ALL, 1); 192 | 193 | witness = new bcoin.witness(); 194 | witness.push(sig); 195 | witness.push(payImage); 196 | witness.push(new bn(1)); 197 | witness.push(commitScript.toRaw()); 198 | witness.compile(); 199 | 200 | return witness; 201 | }; 202 | 203 | util.recSpendRevoke = function(commitScript, senderKey, sweep, revImage) { 204 | var sig = sweep.signature(0, commitScript, senderKey, hashType.ALL, 1); 205 | var witness = new bcoin.witness(); 206 | witness.push(sig); 207 | witness.push(revImage); 208 | witness.push(new bn(1)); 209 | witness.push(new bn(0)); 210 | witness.push(commitScript.toRaw()); 211 | witness.compile(); 212 | return witness; 213 | }; 214 | 215 | util.recSpendTimeout = function(commitScript, senderKey, sweep, absTimeout) { 216 | var sig, witness; 217 | 218 | sweep.setLocktime(absTimeout); 219 | 220 | sig = sweep.signature(0, commitScript, senderKey, hashType.ALL, 1); 221 | 222 | witness = new bcoin.witness(); 223 | witness.push(sig); 224 | witness.push(new bn(0)); 225 | witness.push(new bn(0)); 226 | witness.push(commitScript.toRaw()); 227 | witness.compile(); 228 | 229 | return witness; 230 | }; 231 | 232 | util.commitSelf = function commitSelf(csvTime, selfKey, revKey) { 233 | var script = new bcoin.script(); 234 | script.push(opcodes.OP_IF); 235 | script.push(revKey); 236 | script.push(opcodes.OP_CHECKSIG); 237 | script.push(opcodes.OP_ELSE); 238 | script.push(selfKey); 239 | script.push(opcodes.OP_CHECKSIGVERIFY); 240 | script.push(new bn(csvTime)); 241 | script.push(opcodes.OP_CHECKSEQUENCEVERIFY); 242 | script.push(opcodes.OP_ENDIF); 243 | script.compile(); 244 | return script; 245 | }; 246 | 247 | util.commitUnencumbered = function commitUnencumbered(key) { 248 | return bcoin.script.fromProgram(0, crypto.hash160(key)); 249 | }; 250 | 251 | util.commitSpendTimeout = function commitSpendTimeout(commitScript, blockTimeout, selfKey, sweep) { 252 | var sig, witness; 253 | 254 | sweep.setSequence(0, blockTimeout); 255 | 256 | sig = sweep.signature(0, commitScript, selfKey, hashType.ALL, 1); 257 | witness = new bcoin.witness(); 258 | witness.push(sig); 259 | witness.push(new bn(0)); 260 | witness.push(commitScript.toRaw()); 261 | witness.compile(); 262 | 263 | return witness; 264 | }; 265 | 266 | util.commitSpendRevoke = function commitSpendRevoke(commitScript, revPriv, sweep) { 267 | var sig = sweep.signature(0, commitScript, revPriv, hashType.ALL, 1); 268 | var witness = new bcoin.witness(); 269 | witness.push(sig); 270 | witness.push(new bn(1)); 271 | witness.push(commitScript.toRaw()); 272 | witness.compile(); 273 | return witness; 274 | }; 275 | 276 | util.commitSpendNoDelay = function commitSpendNoDelay(commitScript, commitPriv, sweep) { 277 | var pkh = bcoin.script.fromPubkeyhash(commitScript.get(1)); 278 | var sig = sweep.signature(0, pkh, commitPriv, hashType.ALL, 1); 279 | var witness = new bcoin.witness(); 280 | witness.push(sig); 281 | witness.push(bcoin.ec.publicKeyCreate(commitPriv, true)); 282 | witness.compile(); 283 | return witness; 284 | }; 285 | 286 | util.deriveRevPub = function(commitPub, revImage) { 287 | return bcoin.ec.publicKeyTweakAdd(commitPub, revImage, true); 288 | }; 289 | 290 | util.deriveRevPriv = function(commitPriv, revImage) { 291 | return bcoin.ec.privateKeyTweakAdd(commitPriv, revImage); 292 | }; 293 | 294 | util.deriveElkremRoot = function(localKey, remoteKey) { 295 | var secret = localKey; // private 296 | var salt = remoteKey; // public 297 | var info = new Buffer('elkrem', 'ascii'); 298 | var prk = crypto.hkdfExtract(secret, salt, 'sha256'); 299 | var root = crypto.hkdfExpand(prk, info, 32, 'sha256'); 300 | return root; 301 | }; 302 | 303 | util.createCommitTX = function( 304 | fundingOutput, selfKey, theirKey, revKey, 305 | csvTimeout, valueToSelf, valueToThem 306 | ) { 307 | var ourRedeem = util.commitSelf(csvTimeout, selfKey, revKey); 308 | var payToUs = util.toWitnessScripthash(ourRedeem); 309 | var payToThem = util.commitUnencumbered(theirKey); 310 | var tx = new bcoin.mtx(); 311 | var output; 312 | 313 | tx.version = 2; 314 | tx.addInput(fundingOutput); 315 | 316 | if (valueToSelf > 0) { 317 | output = new bcoin.output(); 318 | output.value = valueToSelf; 319 | output.script = payToUs; 320 | tx.addOutput(output); 321 | } 322 | 323 | if (valueToThem > 0) { 324 | output = new bcoin.output(); 325 | output.value = valueToThem; 326 | output.script = payToThem; 327 | tx.addOutput(output); 328 | } 329 | 330 | return tx; 331 | }; 332 | 333 | util.createCooperativeClose = function createCooperativeClose( 334 | fundingInput, ourBalance, theirBalance, 335 | ourDeliveryScript, theirDeliveryScript, 336 | initiator 337 | ) { 338 | var tx = new bcoin.mtx(); 339 | var output; 340 | 341 | tx.addInput(fundingInput); 342 | 343 | if (initiator) 344 | ourBalance -= 5000; 345 | else 346 | theirBalance -= 5000; 347 | 348 | if (ourBalance > 0) { 349 | output = new bcoin.output(); 350 | output.script = ourDeliveryScript; 351 | output.value = ourBalance; 352 | tx.addOutput(output); 353 | } 354 | 355 | if (theirBalance > 0) { 356 | output = new bcoin.output(); 357 | output.script = theirDeliveryScript; 358 | output.value = theirBalance; 359 | tx.addOutput(output); 360 | } 361 | 362 | tx.sortMembers(); 363 | 364 | return tx; 365 | }; 366 | -------------------------------------------------------------------------------- /lib/wire.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bcoin = require('bcoin'); 4 | var utils = bcoin.utils; 5 | var assert = utils.assert; 6 | var constants = bcoin.constants; 7 | var ZERO_SIG = new Buffer(73); 8 | ZERO_SIG.fill(0); 9 | 10 | // Commands used in lightning message headers which detail the type of message. 11 | var msgType = { 12 | // Commands for opening a channel funded by one party (single funder). 13 | SingleFundingRequest: 100, 14 | SingleFundingResponse: 110, 15 | SingleFundingComplete: 120, 16 | SingleFundingSignComplete: 130, 17 | SingleFundingOpenProof: 140, 18 | 19 | // Commands for the workflow of cooperatively closing an active channel. 20 | CloseRequest: 300, 21 | CloseComplete: 310, 22 | 23 | // Commands for negotiating HTLCs. 24 | HTLCAddRequest: 1000, 25 | HTLCAddAccept: 1010, 26 | HTLCAddReject: 1020, 27 | HTLCSettleRequest: 1100, 28 | HTLCTimeoutRequest: 1300, 29 | 30 | // Commands for modifying commitment transactions. 31 | CommitSignature: 2000, 32 | CommitRevocation: 2010, 33 | 34 | // Commands for routing 35 | NeighborHello: 3000, 36 | NeighborUpd: 3010, 37 | NeighborAck: 3020, 38 | NeighborRst: 3030, 39 | RoutingTableRequest: 3040, 40 | RoutingTableTransfer: 3050, 41 | 42 | // Commands for reporting protocol errors. 43 | ErrorGeneric: 4000 44 | }; 45 | 46 | function fromRaw(cmd, data) { 47 | switch (cmd) { 48 | case msgType.SingleFundingRequest: 49 | return SingleFundingRequest.fromRaw(data); 50 | case msgType.SingleFundingResponse: 51 | return SingleFundingResponse.fromRaw(data); 52 | case msgType.SingleFundingComplete: 53 | return SingleFundingComplete.fromRaw(data); 54 | case msgType.SingleFundingSignComplete: 55 | return SingleFundingSignComplete.fromRaw(data); 56 | case msgType.SingleFundingOpenProof: 57 | return SingleFundingOpenProof.fromRaw(data); 58 | 59 | // Commands for the workflow of cooperatively closing an active channel. 60 | case msgType.CloseRequest: 61 | return CloseRequest.fromRaw(data); 62 | case msgType.CloseComplete: 63 | return CloseComplete.fromRaw(data); 64 | 65 | // Commands for negotiating HTLCs. 66 | case msgType.HTLCAddRequest: 67 | return HTLCAddRequest.fromRaw(data); 68 | // case msgType.HTLCAddAccept: 69 | // return HTLCAddAccept.fromRaw(data); 70 | case msgType.HTLCAddReject: 71 | return HTLCAddReject.fromRaw(data); 72 | case msgType.HTLCSettleRequest: 73 | return HTLCSettleRequest.fromRaw(data); 74 | case msgType.HTLCTimeoutRequest: 75 | return HTLCTimeoutRequest.fromRaw(data); 76 | 77 | // Commands for modifying commitment transactions. 78 | case msgType.CommitSignature: 79 | return CommitSignature.fromRaw(data); 80 | case msgType.CommitRevocation: 81 | return CommitRevocation.fromRaw(data); 82 | 83 | // Commands for routing 84 | case msgType.NeighborHello: 85 | return NeighborHello.fromRaw(data); 86 | case msgType.NeighborUpd: 87 | return NeighborUpd.fromRaw(data); 88 | case msgType.NeighborAck: 89 | return NeighborAck.fromRaw(data); 90 | case msgType.NeighborRst: 91 | return NeighborRst.fromRaw(data); 92 | case msgType.RoutingTableRequest: 93 | return RoutingTableRequest.fromRaw(data); 94 | case msgType.RoutingTableTransfer: 95 | return RoutingTableTransfer.fromRaw(data); 96 | 97 | // Commands for reporting protocol errors. 98 | case msgType.ErrorGeneric: 99 | return ErrorGeneric.fromRaw(data); 100 | default: 101 | throw new Error('Unknown cmd.'); 102 | } 103 | } 104 | 105 | function CloseComplete() { 106 | this.channelPoint = new bcoin.outpoint(); 107 | this.responderCloseSig = ZERO_SIG; 108 | } 109 | 110 | CloseComplete.prototype.cmd = msgType.CloseComplete; 111 | 112 | CloseComplete.prototype.fromRaw = function fromRaw(data) { 113 | var p = new bcoin.reader(data); 114 | this.channelPoint.fromRaw(p); 115 | this.responderCloseSig = p.readBytes(73); 116 | return this; 117 | }; 118 | 119 | CloseComplete.fromRaw = function fromRaw(data) { 120 | return new CloseComplete().fromRaw(data); 121 | }; 122 | 123 | CloseComplete.prototype.toRaw = function toRaw() { 124 | var p = new bcoin.writer(); 125 | this.channelPoint.toRaw(p); 126 | p.writeBytes(this.responderCloseSig); 127 | return p.render(); 128 | }; 129 | 130 | function CloseRequest() { 131 | this.channelPoint = new bcoin.outpoint(); 132 | this.requesterCloseSig = ZERO_SIG; 133 | this.fee = 0; 134 | } 135 | 136 | CloseRequest.prototype.cmd = msgType.CloseRequest; 137 | 138 | CloseRequest.prototype.fromRaw = function fromRaw(data) { 139 | var p = new bcoin.reader(data); 140 | this.channelPoint.fromRaw(p); 141 | this.requesterCloseSig = p.readBytes(73); 142 | this.fee = p.read64NBE(); 143 | return this; 144 | }; 145 | 146 | CloseRequest.fromRaw = function fromRaw(data) { 147 | return new CloseRequest().fromRaw(data); 148 | }; 149 | 150 | CloseRequest.prototype.toRaw = function toRaw() { 151 | var p = new bcoin.writer(); 152 | this.channelPoint.toRaw(p); 153 | p.writeBytes(this.requesterCloseSig); 154 | p.write64BE(this.fee); 155 | return p.render(); 156 | }; 157 | 158 | function CommitRevocation() { 159 | this.channelPoint = new bcoin.outpoint(); 160 | this.revocation = constants.ZERO_HASH; 161 | this.nextRevKey = constants.ZERO_KEY; 162 | this.nextRevHash = constants.ZERO_HASH; 163 | } 164 | 165 | CommitRevocation.prototype.cmd = msgType.CommitRevocation; 166 | 167 | CommitRevocation.prototype.fromRaw = function fromRaw(data) { 168 | var p = new bcoin.reader(data); 169 | this.channelPoint.fromRaw(p); 170 | this.revocation = p.readBytes(32); 171 | this.nextRevKey = p.readBytes(33); 172 | this.nextRevHash = p.readBytes(32); 173 | return this; 174 | }; 175 | 176 | CommitRevocation.fromRaw = function fromRaw(data) { 177 | return new CommitRevocation().fromRaw(data); 178 | }; 179 | 180 | CommitRevocation.prototype.toRaw = function toRaw() { 181 | var p = new bcoin.writer(); 182 | this.channelPoint.toRaw(p); 183 | p.writeBytes(this.revocation); 184 | p.writeBytes(this.nextRevKey); 185 | p.writeBytes(this.nextRevHash); 186 | return p.render(); 187 | }; 188 | 189 | function CommitSignature() { 190 | this.channelPoint = new bcoin.outpoint(); 191 | this.logIndex = 0; 192 | this.fee = 0; 193 | this.commitSig = ZERO_SIG; 194 | } 195 | 196 | CommitSignature.prototype.cmd = msgType.CommitSignature; 197 | 198 | CommitSignature.prototype.fromRaw = function fromRaw(data) { 199 | var p = new bcoin.reader(data); 200 | this.channelPoint.fromRaw(p); 201 | this.logIndex = p.readU64NBE(); 202 | this.fee = p.read64NBE(); 203 | this.commitSig = p.readBytes(73); 204 | return this; 205 | }; 206 | 207 | CommitSignature.fromRaw = function fromRaw(data) { 208 | return new CommitSignature().fromRaw(data); 209 | }; 210 | 211 | CommitSignature.prototype.toRaw = function toRaw() { 212 | var p = new bcoin.writer(); 213 | this.channelPoint.toRaw(p); 214 | p.writeU64BE(this.logIndex); 215 | p.write64BE(this.fee); 216 | p.writeBytes(this.commitSig); 217 | return p.render(); 218 | }; 219 | 220 | function ErrorGeneric() { 221 | this.channelPoint = new bcoin.outpoint(); 222 | this.errorID = 0; 223 | this.problem = ''; 224 | } 225 | 226 | ErrorGeneric.prototype.cmd = msgType.ErrorGeneric; 227 | 228 | ErrorGeneric.prototype.fromRaw = function fromRaw(data) { 229 | var p = new bcoin.reader(data); 230 | this.channelPoint.fromRaw(p); 231 | this.errorID = p.readU16BE(); 232 | this.problem = p.readVarString('utf8'); 233 | return this; 234 | }; 235 | 236 | ErrorGeneric.fromRaw = function fromRaw(data) { 237 | return new ErrorGeneric().fromRaw(data); 238 | }; 239 | 240 | ErrorGeneric.prototype.toRaw = function toRaw() { 241 | var p = new bcoin.writer(); 242 | this.channelPoint.toRaw(p); 243 | p.writeU16BE(this.errorID); 244 | p.writeString(this.problem, 'utf8'); 245 | return p.render(); 246 | }; 247 | 248 | function HTLCAddReject() { 249 | this.channelPoint = new bcoin.outpoint(); 250 | this.htlcKey = 0; 251 | } 252 | 253 | HTLCAddReject.prototype.cmd = msgType.HTLCAddReject; 254 | 255 | HTLCAddReject.prototype.fromRaw = function fromRaw(data) { 256 | var p = new bcoin.reader(data); 257 | this.channelPoint.fromRaw(p); 258 | this.htlcKey = p.readU64NBE(); 259 | return this; 260 | }; 261 | 262 | HTLCAddReject.fromRaw = function fromRaw(data) { 263 | return new HTLCAddReject().fromRaw(data); 264 | }; 265 | 266 | HTLCAddReject.prototype.toRaw = function toRaw() { 267 | var p = new bcoin.writer(); 268 | this.channelPoint.toRaw(p); 269 | p.writeU64BE(this.htlcKey); 270 | return p.render(); 271 | }; 272 | 273 | function HTLCAddRequest() { 274 | this.channelPoint = new bcoin.outpoint(); 275 | this.expiry = 0; 276 | this.value = 0; 277 | this.refundContext = null; // not currently used 278 | this.contractType = 0; // bitfield for m of n 279 | this.redemptionHashes = []; 280 | this.onionBlob = new Buffer(0); 281 | } 282 | 283 | HTLCAddRequest.prototype.cmd = msgType.HTLCAddRequest; 284 | 285 | HTLCAddRequest.prototype.fromRaw = function fromRaw(data) { 286 | var p = new bcoin.reader(data); 287 | var i, count; 288 | 289 | this.channelPoint.fromRaw(p); 290 | this.expiry = p.readU32BE(); 291 | this.value = p.readU32BE(); 292 | this.contractType = p.readU8(); 293 | 294 | count = p.readU16BE(); 295 | 296 | for (i = 0; i < count; i++) 297 | this.redemptionHashes.push(p.readBytes(32)); 298 | 299 | this.onionBlob = p.readVarBytes(); 300 | 301 | return this; 302 | }; 303 | 304 | HTLCAddRequest.fromRaw = function fromRaw(data) { 305 | return new HTLCAddRequest().fromRaw(data); 306 | }; 307 | 308 | HTLCAddRequest.prototype.toRaw = function toRaw() { 309 | var p = new bcoin.writer(); 310 | var i; 311 | 312 | this.channelPoint.toRaw(p); 313 | 314 | p.writeU32BE(this.expiry); 315 | p.writeU32BE(this.value); 316 | p.writeU8(this.contractType); 317 | p.writeU16BE(this.redemptionHashes.length); 318 | 319 | for (i = 0; i < this.redemptionHashes.length; i++) 320 | p.writeBytes(this.redemptionHashes[i]); 321 | 322 | p.writeVarBytes(this.onionBlob); 323 | 324 | return p.render(); 325 | }; 326 | 327 | function HTLCSettleRequest() { 328 | this.channelPoint = new bcoin.outpoint(); 329 | this.htlcKey = 0; 330 | this.redemptionProofs = []; 331 | } 332 | 333 | HTLCSettleRequest.prototype.cmd = msgType.HTLCSettleRequest; 334 | 335 | HTLCSettleRequest.prototype.fromRaw = function fromRaw(data) { 336 | var p = new bcoin.reader(data); 337 | var i, count; 338 | 339 | this.channelPoint.fromRaw(p); 340 | this.htlcKey = p.readU64NBE(); 341 | 342 | count = p.readU16BE(); 343 | 344 | for (i = 0; i < count; i++) 345 | this.redemptionProofs.push(p.readBytes(32)); 346 | 347 | return this; 348 | }; 349 | 350 | HTLCSettleRequest.fromRaw = function fromRaw(data) { 351 | return new HTLCSettleRequest().fromRaw(data); 352 | }; 353 | 354 | HTLCSettleRequest.prototype.toRaw = function toRaw() { 355 | var p = new bcoin.writer(); 356 | var i; 357 | 358 | this.channelPoint.toRaw(p); 359 | 360 | p.writeU64BE(this.htlcKey); 361 | p.writeU16BE(this.redemptionProofs.length); 362 | 363 | for (i = 0; i < this.redemptionProofs.length; i++) 364 | p.writeBytes(this.redemptionProofs[i]); 365 | 366 | return p.render(); 367 | }; 368 | 369 | function HTLCTimeoutRequest() { 370 | this.channelPoint = new bcoin.outpoint(); 371 | this.htlcKey = 0; 372 | } 373 | 374 | HTLCTimeoutRequest.prototype.cmd = msgType.HTLCTimeoutRequest; 375 | 376 | HTLCTimeoutRequest.prototype.fromRaw = function fromRaw(data) { 377 | var p = new bcoin.reader(data); 378 | this.channelPoint.fromRaw(p); 379 | this.htlcKey = p.readU64NBE(); 380 | return this; 381 | }; 382 | 383 | HTLCTimeoutRequest.fromRaw = function fromRaw(data) { 384 | return new HTLCTimeoutRequest().fromRaw(data); 385 | }; 386 | 387 | HTLCTimeoutRequest.prototype.toRaw = function toRaw() { 388 | var p = new bcoin.writer(); 389 | this.channelPoint.toRaw(p); 390 | p.writeU64BE(this.htlcKey); 391 | return p.render(); 392 | }; 393 | 394 | function NeighborAck() { 395 | } 396 | 397 | NeighborAck.prototype.cmd = msgType.NeighborAck; 398 | 399 | NeighborAck.prototype.fromRaw = function fromRaw(data) { 400 | var p = new bcoin.reader(data); 401 | assert(p.readString(18, 'ascii') === 'NeighborAckMessage'); 402 | return this; 403 | }; 404 | 405 | NeighborAck.fromRaw = function fromRaw(data) { 406 | return new NeighborAck().fromRaw(data); 407 | }; 408 | 409 | NeighborAck.prototype.toRaw = function toRaw() { 410 | var p = new bcoin.writer(); 411 | p.writeString('NeighborAckMessage', 'ascii'); 412 | return p.render(); 413 | }; 414 | 415 | function NeighborHello() { 416 | this.rt = null; // TODO: Routing table. 417 | } 418 | 419 | NeighborHello.prototype.fromRaw = function fromRaw(data) { 420 | // var p = new bcoin.reader(data); 421 | // this.rt.fromRaw(data); 422 | return this; 423 | }; 424 | 425 | NeighborHello.fromRaw = function fromRaw(data) { 426 | return new NeighborAck().fromRaw(data); 427 | }; 428 | 429 | NeighborHello.prototype.toRaw = function toRaw() { 430 | // var p = new bcoin.writer(); 431 | // this.rt.toRaw(p); 432 | // return p.render(); 433 | return new Buffer(0); 434 | }; 435 | 436 | function NeighborRst() { 437 | } 438 | 439 | NeighborRst.prototype.cmd = msgType.NeighborRst; 440 | 441 | NeighborRst.prototype.fromRaw = function fromRaw(data) { 442 | return this; 443 | }; 444 | 445 | NeighborRst.fromRaw = function fromRaw(data) { 446 | return new NeighborRst().fromRaw(data); 447 | }; 448 | 449 | NeighborRst.prototype.toRaw = function toRaw() { 450 | return new Buffer(0); 451 | }; 452 | 453 | function NeighborUpd() { 454 | this.diffBuff = null; // TODO: routing table diff buff 455 | } 456 | 457 | NeighborUpd.prototype.fromRaw = function fromRaw(data) { 458 | return this; 459 | }; 460 | 461 | NeighborUpd.fromRaw = function fromRaw(data) { 462 | return new NeighborUpd().fromRaw(data); 463 | }; 464 | 465 | NeighborUpd.prototype.toRaw = function toRaw() { 466 | return new Buffer(0); 467 | }; 468 | 469 | function RoutingTableRequest() { 470 | this.rt = null; // TODO 471 | } 472 | 473 | RoutingTableRequest.prototype.cmd = msgType.RoutingTableRequest; 474 | 475 | RoutingTableRequest.prototype.fromRaw = function fromRaw(data) { 476 | return this; 477 | }; 478 | 479 | RoutingTableRequest.fromRaw = function fromRaw(data) { 480 | return new RoutingTableRequest().fromRaw(data); 481 | }; 482 | 483 | RoutingTableRequest.prototype.toRaw = function toRaw() { 484 | return new Buffer(0); 485 | }; 486 | 487 | function RoutingTableTransfer() { 488 | } 489 | 490 | RoutingTableTransfer.prototype.cmd = msgType.RoutingTableTransfer; 491 | 492 | RoutingTableTransfer.prototype.fromRaw = function fromRaw(data) { 493 | return this; 494 | }; 495 | 496 | RoutingTableTransfer.fromRaw = function fromRaw(data) { 497 | return new RoutingTableTransfer().fromRaw(data); 498 | }; 499 | 500 | RoutingTableTransfer.prototype.toRaw = function toRaw() { 501 | return new Buffer(0); 502 | }; 503 | 504 | function SingleFundingComplete() { 505 | this.channelID = 0; 506 | this.fundingOutpoint = new bcoin.outpoint(); 507 | this.commitSignature = ZERO_SIG; 508 | this.revocationKey = constants.ZERO_KEY; 509 | } 510 | 511 | SingleFundingComplete.prototype.cmd = msgType.SingleFundingComplete; 512 | 513 | SingleFundingComplete.prototype.fromRaw = function fromRaw(data) { 514 | var p = new bcoin.reader(data); 515 | this.channelID = p.readU64NBE(); 516 | this.fundingOutpoint.fromRaw(p); 517 | this.commitSignature = p.readBytes(73); 518 | this.revocationKey = p.readBytes(33); 519 | assert(this.fundingOutpoint.hash !== constants.NULL_HASH); 520 | return this; 521 | }; 522 | 523 | SingleFundingComplete.fromRaw = function fromRaw(data) { 524 | return new SingleFundingComplete().fromRaw(data); 525 | }; 526 | 527 | SingleFundingComplete.prototype.toRaw = function toRaw() { 528 | var p = new bcoin.writer(); 529 | p.writeU64BE(this.channelID); 530 | this.fundingOutpoint.toRaw(p); 531 | p.writeBytes(this.commitSignature); 532 | p.writeBytes(this.revocationKey); 533 | return p.render(); 534 | }; 535 | 536 | function SingleFundingOpenProof() { 537 | this.channelID = 0; 538 | this.spvProof = new Buffer(0); 539 | } 540 | 541 | SingleFundingOpenProof.prototype.cmd = msgType.SingleFundingOpenProof; 542 | 543 | SingleFundingOpenProof.prototype.fromRaw = function fromRaw(data) { 544 | var p = new bcoin.reader(data); 545 | this.channelID = p.readU64NBE(); 546 | this.spvProof = p.readVarBytes(); 547 | return this; 548 | }; 549 | 550 | SingleFundingOpenProof.fromRaw = function fromRaw(data) { 551 | return new SingleFundingOpenProof().fromRaw(data); 552 | }; 553 | 554 | SingleFundingOpenProof.prototype.toRaw = function toRaw() { 555 | var p = new bcoin.writer(); 556 | p.writeU64BE(this.channelID); 557 | p.writeVarBytes(this.spvProof); 558 | return p.render(); 559 | }; 560 | 561 | function SingleFundingRequest() { 562 | this.channelID = 0; 563 | this.channelType = 0; 564 | this.coinType = 0; 565 | this.feeRate = 0; 566 | this.fundingValue = 0; 567 | this.csvDelay = 0; 568 | this.commitKey = constants.ZERO_KEY; 569 | this.channelDerivationPoint = constants.ZERO_KEY; 570 | this.deliveryScript = new bcoin.script(); 571 | } 572 | 573 | SingleFundingRequest.prototype.cmd = msgType.SingleFundingRequest; 574 | 575 | SingleFundingRequest.prototype.fromRaw = function fromRaw(data) { 576 | var p = new bcoin.reader(data); 577 | this.channelID = p.readU64NBE(); 578 | this.channelType = p.readU8(); 579 | this.coinType = p.readU64NBE(); 580 | this.feeRate = p.readU64NBE(); 581 | this.fundingValue = p.readU64NBE(); 582 | this.csvDelay = p.readU32BE(); 583 | this.commitKey = p.readBytes(33); 584 | this.channelDerivationPoint = p.readBytes(33); 585 | this.deliveryScript.fromRaw(p.readVarBytes()); 586 | return this; 587 | }; 588 | 589 | SingleFundingRequest.fromRaw = function fromRaw(data) { 590 | return new SingleFundingRequest().fromRaw(data); 591 | }; 592 | 593 | SingleFundingRequest.prototype.toRaw = function toRaw() { 594 | var p = new bcoin.writer(); 595 | p.writeU64BE(this.channelID); 596 | p.writeU8(this.channelType); 597 | p.writeU64BE(this.coinType); 598 | p.writeU64BE(this.feeRate); 599 | p.writeU64BE(this.fundingValue); 600 | p.writeU32BE(this.csvDelay); 601 | p.writeBytes(this.commitKey); 602 | p.writeBytes(this.channelDerivationPoint); 603 | p.writeVarBytes(this.deliveryScript.toRaw()); 604 | return p.render(); 605 | }; 606 | 607 | function SingleFundingResponse() { 608 | this.channelID = 0; 609 | this.channelDerivationPoint = constants.ZERO_KEY; 610 | this.commitKey = constants.ZERO_KEY; 611 | this.revocationKey = constants.ZERO_KEY; 612 | this.csvDelay = 0; 613 | this.deliveryScript = new bcoin.script(); 614 | } 615 | 616 | SingleFundingResponse.prototype.cmd = msgType.SingleFundingResponse; 617 | 618 | SingleFundingResponse.prototype.fromRaw = function fromRaw(data) { 619 | var p = new bcoin.reader(data); 620 | this.channelID = p.readU64NBE(); 621 | this.channelDerivationPoint = p.readBytes(33); 622 | this.commitKey = p.readBytes(33); 623 | this.revocationKey = p.readBytes(33); 624 | this.csvDelay = p.readU32BE(); 625 | this.deliveryScript.fromRaw(p.readVarBytes()); 626 | return this; 627 | }; 628 | 629 | SingleFundingResponse.fromRaw = function fromRaw(data) { 630 | return new SingleFundingResponse().fromRaw(data); 631 | }; 632 | 633 | SingleFundingResponse.prototype.toRaw = function toRaw() { 634 | var p = new bcoin.writer(); 635 | p.writeU64BE(this.channelID); 636 | p.writeBytes(this.channelDerivationPoint); 637 | p.writeBytes(this.commitKey); 638 | p.writeBytes(this.revocationKey); 639 | p.writeU32BE(this.csvDelay); 640 | p.writeVarBytes(this.deliveryScript.toRaw()); 641 | return p.render(); 642 | }; 643 | 644 | function SingleFundingSignComplete() { 645 | this.channelID = 0; 646 | this.commitSignature = ZERO_SIG; 647 | } 648 | 649 | SingleFundingSignComplete.prototype.cmd = msgType.SingleFundingSignComplete; 650 | 651 | SingleFundingSignComplete.prototype.fromRaw = function fromRaw(data) { 652 | var p = new bcoin.reader(data); 653 | this.channelID = p.readU64NBE(); 654 | this.commitSignature = p.readBytes(73); 655 | return this; 656 | }; 657 | 658 | SingleFundingSignComplete.fromRaw = function fromRaw(data) { 659 | return new SingleFundingSignComplete().fromRaw(data); 660 | }; 661 | 662 | SingleFundingSignComplete.prototype.toRaw = function toRaw() { 663 | var p = new bcoin.writer(); 664 | p.writeU64BE(this.channelID); 665 | p.writeBytes(this.commitSignature); 666 | return p.render(); 667 | }; 668 | 669 | exports.CloseComplete = CloseComplete; 670 | exports.CloseRequest = CloseRequest; 671 | exports.CommitRevocation = CommitRevocation; 672 | exports.CommitSignature = CommitSignature; 673 | exports.ErrorGeneric = ErrorGeneric; 674 | exports.HTLCAddReject = HTLCAddReject; 675 | exports.HTLCAddRequest = HTLCAddRequest; 676 | exports.HTLCSettleRequest = HTLCSettleRequest; 677 | exports.HTLCTimeoutRequest = HTLCTimeoutRequest; 678 | exports.NeighborAck = NeighborAck; 679 | exports.NeighborHello = NeighborHello; 680 | exports.NeighborRst = NeighborRst; 681 | exports.NeighborUpd = NeighborUpd; 682 | exports.RoutingTableRequest = RoutingTableRequest; 683 | exports.RoutingTableTransfer = RoutingTableTransfer; 684 | exports.SingleFundingComplete = SingleFundingComplete; 685 | exports.SingleFundingOpenProof = SingleFundingOpenProof; 686 | exports.SingleFundingRequest = SingleFundingRequest; 687 | exports.SingleFundingResponse = SingleFundingResponse; 688 | exports.SingleFundingSignComplete = SingleFundingSignComplete; 689 | exports.msgType = msgType; 690 | exports.fromRaw = fromRaw; 691 | -------------------------------------------------------------------------------- /lib/channel.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * channel.js 3 | * https://github.com/bcoin-org/plasma 4 | * 5 | * References: 6 | * https://github.com/lightningnetwork/lnd/blob/master/lnwallet/channel.go 7 | * https://github.com/lightningnetwork/lnd/blob/master/lnwallet/script_utils.go 8 | * https://github.com/lightningnetwork/lnd/blob/master/elkrem/elkrem.go 9 | * https://github.com/lightningnetwork/lnd/blob/master/channeldb/channel.go 10 | * https://github.com/lightningnetwork/lnd/blob/master/lnwire/htlc_addrequest.go 11 | * https://github.com/lightningnetwork/lnd/blob/master/lnwallet/script_utils.go 12 | * https://github.com/lightningnetwork/lnd/blob/master/lnwallet/script_utils_test.go 13 | * https://github.com/lightningnetwork/lnd/blob/master/lnwallet/channel_test.go 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var bcoin = require('bcoin'); 19 | var constants = bcoin.constants; 20 | var utils = require('bcoin/lib/utils/util'); 21 | var assert = require('assert'); 22 | var hashType = constants.hashType; 23 | var util = require('./scriptutil'); 24 | var crypto = bcoin.crypto; 25 | var wire = require('./wire'); 26 | var CommitRevocation = wire.CommitRevocation; 27 | var List = require('./list'); 28 | 29 | /** 30 | * Channel 31 | */ 32 | 33 | function Channel(options) { 34 | this.wallet = options.wallet || null; 35 | this.chain = options.chain || null; 36 | this.ourLogCounter = 0; 37 | this.theirLogCounter = 0; 38 | this.status = Channel.states.PENDING; 39 | this.currentHeight = options.state.numUpdates || 0; 40 | this.revocationWindowEdge = options.state.numUpdates || 0; 41 | this.usedRevocations = []; 42 | this.revocationWindow = []; 43 | this.remoteCommitChain = new CommitmentChain(); 44 | this.localCommitChain = new CommitmentChain(); 45 | this.state = options.state; 46 | this.ourUpdateLog = new List(); 47 | this.theirUpdateLog = new List(); 48 | this.ourLogIndex = {}; 49 | this.theirLogIndex = {}; 50 | this.fundingInput = new bcoin.coin(); 51 | this.fundingInput.version = 1; 52 | this.fundingP2WSH = null; 53 | this.db = options.db || null; 54 | this.started = 0; 55 | this.shutdown = 0; 56 | 57 | this._init(); 58 | } 59 | 60 | Channel.states = { 61 | PENDING: 0, 62 | OPEN: 1, 63 | CLOSING: 2, 64 | CLOSED: 3, 65 | DISPUTE: 4, 66 | PENDINGPAYMENT: 5 67 | }; 68 | 69 | Channel.updateType = { 70 | ADD: 0, 71 | TIMEOUT: 1, 72 | SETTLE: 2 73 | }; 74 | 75 | Channel.maxPendingPayments = 100; 76 | 77 | Channel.initialRevocationWindow = 4; 78 | 79 | Channel.prototype._init = function _init() { 80 | var initialCommit = new Commitment(); 81 | var fundingScript; 82 | 83 | initialCommit.height = this.currentHeight; 84 | initialCommit.ourBalance = this.state.ourBalance; 85 | initialCommit.theirBalance = this.state.theirBalance; 86 | 87 | this.localCommitChain.add(initialCommit); 88 | this.remoteCommitChain.add(initialCommit); 89 | 90 | fundingScript = util.toWitnessScripthash(this.state.fundingScript); 91 | 92 | this.fundingInput.hash = this.state.fundingInput.hash; 93 | this.fundingInput.index = this.state.fundingInput.index; 94 | this.fundingInput.script = fundingScript; 95 | this.fundingP2WSH = fundingScript; 96 | }; 97 | 98 | Channel.prototype.getCommitmentView = getCommitmentView; 99 | 100 | function getCommitmentView(ourLogIndex, theirLogIndex, revKey, revHash, remoteChain) { 101 | var commitChain, ourBalance, theirBalance, nextHeight; 102 | var view, filtered; 103 | var selfKey, remoteKey, delay, delayBalance, p2wpkhBalance; 104 | var i, ourCommit, commit, htlc, commitment; 105 | 106 | if (remoteChain) 107 | commitChain = this.remoteCommitChain; 108 | else 109 | commitChain = this.localCommitChain; 110 | 111 | if (!commitChain.tip()) { 112 | ourBalance = this.state.ourBalance; 113 | theirBalance = this.state.theirBalance; 114 | nextHeight = 1; 115 | } else { 116 | ourBalance = commitChain.tip().ourBalance; 117 | theirBalance = commitChain.tip().theirBalance; 118 | nextHeight = commitChain.tip().height + 1; 119 | } 120 | 121 | view = this.getHTLCView(theirLogIndex, ourLogIndex); 122 | 123 | filtered = this.evalHTLCView( 124 | view, ourBalance, theirBalance, 125 | nextHeight, remoteChain); 126 | 127 | if (remoteChain) { 128 | selfKey = this.state.theirCommitKey; 129 | remoteKey = this.state.ourCommitPub; 130 | delay = this.state.remoteCSVDelay; 131 | delayBalance = filtered.theirBalance; 132 | p2wpkhBalance = filtered.ourBalance; 133 | } else { 134 | selfKey = this.state.ourCommitPub; 135 | remoteKey = this.state.theirCommitKey; 136 | delay = this.state.localCSVDelay; 137 | delayBalance = filtered.ourBalance; 138 | p2wpkhBalance = filtered.theirBalance; 139 | } 140 | 141 | ourCommit = !remoteChain; 142 | 143 | commit = util.createCommitTX( 144 | this.fundingInput, selfKey, remoteKey, 145 | revKey, delay, delayBalance, p2wpkhBalance); 146 | 147 | for (i = 0; i < filtered.ourUpdates.length; i++) { 148 | htlc = filtered.ourUpdates[i]; 149 | this.pushHTLC(commit, ourCommit, htlc, revHash, delay, false); 150 | } 151 | 152 | for (i = 0; i < filtered.theirUpdates.length; i++) { 153 | htlc = filtered.theirUpdates[i]; 154 | this.pushHTLC(commit, ourCommit, htlc, revHash, delay, true); 155 | } 156 | 157 | commit.sortMembers(); 158 | 159 | commitment = new Commitment(); 160 | commitment.tx = commit; 161 | commitment.height = nextHeight; 162 | commitment.ourBalance = filtered.ourBalance; 163 | commitment.ourMessageIndex = ourLogIndex; 164 | commitment.theirMessageIndex = theirLogIndex; 165 | commitment.theirBalance = filtered.theirBalance; 166 | 167 | return commitment; 168 | } 169 | 170 | Channel.prototype.getHTLCView = function getHTLCView(theirLogIndex, ourLogIndex) { 171 | var ours = []; 172 | var theirs = []; 173 | var item, htlc; 174 | 175 | for (item = this.ourUpdateLog.head; item; item = item.next) { 176 | htlc = item.value; 177 | if (htlc.index < ourLogIndex) 178 | ours.push(htlc); 179 | } 180 | 181 | for (item = this.theirUpdateLog.head; item; item = item.next) { 182 | htlc = item.value; 183 | if (htlc.index < theirLogIndex) 184 | theirs.push(htlc); 185 | } 186 | 187 | return new HTLCView(ours, theirs); 188 | }; 189 | 190 | Channel.prototype.evalHTLCView = evalHTLCView; 191 | 192 | function evalHTLCView(view, ourBalance, theirBalance, nextHeight, remoteChain) { 193 | var filtered = new HTLCView(); 194 | var skipUs = {}; 195 | var skipThem = {}; 196 | var i, entry, addEntry, isAdd; 197 | 198 | filtered.ourBalance = ourBalance; 199 | filtered.theirBalance = theirBalance; 200 | 201 | for (i = 0; i < view.ourUpdates.length; i++) { 202 | entry = view.ourUpdates[i]; 203 | if (entry.entryType === Channel.updateType.ADD) 204 | continue; 205 | addEntry = this.theirLogIndex[entry.parentIndex]; 206 | skipThem[addEntry.value.index] = true; 207 | processRemoveEntry(entry, filtered, nextHeight, remoteChain, true); 208 | } 209 | 210 | for (i = 0; i < view.theirUpdates.length; i++) { 211 | entry = view.theirUpdates[i]; 212 | if (entry.entryType === Channel.updateType.ADD) 213 | continue; 214 | addEntry = this.ourLogIndex[entry.parentIndex]; 215 | skipUs[addEntry.value.index] = true; 216 | processRemoveEntry(entry, filtered, nextHeight, remoteChain, false); 217 | } 218 | 219 | for (i = 0; i < view.ourUpdates.length; i++) { 220 | entry = view.ourUpdates[i]; 221 | isAdd = entry.entryType === Channel.updateType.ADD; 222 | if (!isAdd || skipUs[entry.index]) 223 | continue; 224 | processAddEntry(entry, filtered, nextHeight, remoteChain, false); 225 | filtered.ourUpdates.push(entry); 226 | } 227 | 228 | for (i = 0; i < view.theirUpdates.length; i++) { 229 | entry = view.theirUpdates[i]; 230 | isAdd = entry.entryType === Channel.updateType.ADD; 231 | if (!isAdd || skipThem[entry.index]) 232 | continue; 233 | processAddEntry(entry, filtered, nextHeight, remoteChain, true); 234 | filtered.theirUpdates.push(entry); 235 | } 236 | 237 | return filtered; 238 | } 239 | 240 | function processAddEntry(htlc, filtered, nextHeight, remoteChain, isIncoming) { 241 | var addHeight; 242 | 243 | if (remoteChain) 244 | addHeight = htlc.addCommitHeightRemote; 245 | else 246 | addHeight = htlc.addCommitHeightLocal; 247 | 248 | if (addHeight !== 0) 249 | return; 250 | 251 | if (isIncoming) 252 | filtered.theirBalance -= htlc.value; 253 | else 254 | filtered.ourBalance -= htlc.value; 255 | 256 | if (remoteChain) 257 | htlc.addCommitHeightRemote = nextHeight; 258 | else 259 | htlc.addCommitHeightLocal = nextHeight; 260 | } 261 | 262 | function processRemoveEntry(htlc, filtered, nextHeight, remoteChain, isIncoming) { 263 | var removeHeight; 264 | 265 | if (remoteChain) 266 | removeHeight = htlc.removeCommitHeightRemote; 267 | else 268 | removeHeight = htlc.removeCommitHeightLocal; 269 | 270 | if (removeHeight !== 0) 271 | return; 272 | 273 | if (isIncoming) { 274 | if (htlc.entryType === Channel.updateType.SETTLE) 275 | filtered.ourBalance += htlc.value; 276 | else if (htlc.entryType === Channel.updateType.TIMEOUT) 277 | filtered.theirBalance += htlc.value; 278 | } else { 279 | if (htlc.entryType === Channel.updateType.SETTLE) 280 | filtered.theirBalance += htlc.value; 281 | else if (htlc.entryType === Channel.updateType.TIMEOUT) 282 | filtered.ourBalance += htlc.value; 283 | } 284 | 285 | if (remoteChain) 286 | htlc.removeCommitHeightRemote = nextHeight; 287 | else 288 | htlc.removeCommitHeightLocal = nextHeight; 289 | } 290 | 291 | Channel.prototype.signNextCommitment = function signNextCommitment() { 292 | var nextRev, remoteRevKey, remoteRevHash, view, sig; 293 | 294 | if (this.revocationWindow.length === 0 295 | || this.usedRevocations.length === Channel.initialRevocationWindow) { 296 | throw new Error('No revocation window.'); 297 | } 298 | 299 | nextRev = this.revocationWindow[0]; 300 | remoteRevKey = nextRev.nextRevKey; 301 | remoteRevHash = nextRev.nextRevHash; 302 | 303 | view = this.getCommitmentView( 304 | this.ourLogCounter, this.theirLogCounter, 305 | remoteRevKey, remoteRevHash, true); 306 | 307 | view.tx.inputs[0].coin.value = this.state.capacity; 308 | 309 | sig = view.tx.signature(0, 310 | this.state.fundingScript, 311 | this.state.ourMultisigKey, 312 | hashType.ALL, 313 | 1); 314 | 315 | this.remoteCommitChain.add(view); 316 | 317 | this.usedRevocations.push(nextRev); 318 | this.revocationWindow.shift(); 319 | 320 | return { 321 | sig: sig.slice(0, -1), 322 | index: this.theirLogCounter 323 | }; 324 | }; 325 | 326 | Channel.prototype.receiveNewCommitment = function receiveNewCommitment(sig, ourLogIndex) { 327 | var theirCommitKey = this.state.theirCommitKey; 328 | var theirMultisigKey = this.state.theirMultisigKey; 329 | var nextHeight = this.currentHeight + 1; 330 | var revocation = this.state.localElkrem.getIndex(nextHeight); 331 | var revKey = util.deriveRevPub(theirCommitKey, revocation); 332 | var revHash = crypto.sha256(revocation); 333 | var view, localCommit, multisigScript; 334 | var msg, result; 335 | 336 | view = this.getCommitmentView( 337 | ourLogIndex, this.theirLogCounter, 338 | revKey, revHash, false); 339 | 340 | localCommit = view.tx; 341 | multisigScript = this.state.fundingScript; 342 | 343 | localCommit.inputs[0].coin.value = this.state.capacity; 344 | 345 | msg = localCommit.signatureHash(0, multisigScript, hashType.ALL, 1); 346 | result = bcoin.ec.verify(msg, sig, theirMultisigKey); 347 | 348 | if (!result) 349 | throw new Error('Invalid commitment signature.'); 350 | 351 | view.sig = sig; 352 | 353 | this.localCommitChain.add(view); 354 | }; 355 | 356 | Channel.prototype.pendingUpdates = function pendingUpdates() { 357 | var localTip = this.localCommitChain.tip(); 358 | var remoteTip = this.remoteCommitChain.tip(); 359 | return localTip.ourMessageIndex !== remoteTip.ourMessageIndex; 360 | }; 361 | 362 | Channel.prototype.revokeCurrentCommitment = function revokeCurrentCommitment() { 363 | var theirCommitKey = this.state.theirCommitKey; 364 | var revMsg = new CommitRevocation(); 365 | var currentRev, revEdge, tail; 366 | 367 | revMsg.channelPoint = this.state.id; 368 | 369 | currentRev = this.state.localElkrem.getIndex(this.currentHeight); 370 | revMsg.revocation = currentRev; 371 | 372 | this.revocationWindowEdge++; 373 | 374 | revEdge = this.state.localElkrem.getIndex(this.revocationWindowEdge); 375 | revMsg.nextRevKey = util.deriveRevPub(theirCommitKey, revEdge); 376 | revMsg.nextRevHash = crypto.sha256(revEdge); 377 | 378 | this.localCommitChain.advanceTail(); 379 | this.currentHeight++; 380 | 381 | tail = this.localCommitChain.tail(); 382 | this.state.ourCommitTX = tail.tx; 383 | this.state.ourBalance = tail.ourBalance; 384 | this.state.theirBalance = tail.theirBalance; 385 | this.state.ourCommitSig = tail.sig; 386 | this.state.numUpdates++; 387 | 388 | this.state.fullSync(); 389 | 390 | return revMsg; 391 | }; 392 | 393 | Channel.prototype.receiveRevocation = function receiveRevocation(revMsg) { 394 | var ourCommitKey, currentRevKey, pendingRev; 395 | var revPriv, revPub, revHash, nextRev; 396 | var remoteChainTail, localChainTail; 397 | var item, htlcsToForward, htlc, uncommitted; 398 | 399 | if (utils.equal(revMsg.revocation, constants.ZERO_HASH)) { 400 | this.revocationWindow.push(revMsg); 401 | return; 402 | } 403 | 404 | ourCommitKey = this.state.ourCommitKey; 405 | currentRevKey = this.state.theirCurrentRevocation; 406 | pendingRev = revMsg.revocation; 407 | 408 | this.state.remoteElkrem.addNext(pendingRev); 409 | 410 | revPriv = util.deriveRevPriv(ourCommitKey, pendingRev); 411 | revPub = bcoin.ec.publicKeyCreate(revPriv, true); 412 | 413 | if (!utils.equal(revPub, currentRevKey)) 414 | throw new Error('Revocation key mistmatch.'); 415 | 416 | if (!utils.equal(this.state.theirCurrentRevHash, constants.ZERO_HASH)) { 417 | revHash = crypto.sha256(pendingRev); 418 | if (!utils.equal(this.state.theirCurrentRevHash, revHash)) 419 | throw new Error('Revocation hash mismatch.'); 420 | } 421 | 422 | nextRev = this.usedRevocations[0]; 423 | 424 | this.state.theirCurrentRevocation = nextRev.nextRevKey; 425 | this.state.theirCurrentRevHash = nextRev.nextRevHash; 426 | this.usedRevocations.shift(); 427 | this.revocationWindow.push(revMsg); 428 | 429 | this.state.syncRevocation(); 430 | this.remoteCommitChain.advanceTail(); 431 | 432 | remoteChainTail = this.remoteCommitChain.tail().height; 433 | localChainTail = this.localCommitChain.tail().height; 434 | 435 | htlcsToForward = []; 436 | 437 | for (item = this.theirUpdateLog.head; item; item = item.next) { 438 | htlc = item.value; 439 | 440 | if (htlc.isForwarded) 441 | continue; 442 | 443 | uncommitted = htlc.addCommitHeightRemote === 0 444 | || htlc.addCommitHeightLocal === 0; 445 | 446 | if (htlc.entryType === Channel.updateType.ADD && uncommitted) 447 | continue; 448 | 449 | if (htlc.entryType === Channel.updateType.ADD 450 | && remoteChainTail >= htlc.addCommitHeightRemote 451 | && localChainTail >= htlc.addCommitHeightLocal) { 452 | htlc.isForwarded = true; 453 | htlcsToForward.push(htlc); 454 | continue; 455 | } 456 | 457 | if (htlc.entryType !== Channel.updateType.ADD 458 | && remoteChainTail >= htlc.removeCommitHeightRemote 459 | && localChainTail >= htlc.removeCommitHeightLocal) { 460 | htlc.isForwarded = true; 461 | htlcsToForward.push(htlc); 462 | } 463 | } 464 | 465 | this.compactLogs( 466 | this.ourUpdateLog, this.theirUpdateLog, 467 | localChainTail, remoteChainTail); 468 | 469 | return htlcsToForward; 470 | }; 471 | 472 | Channel.prototype.compactLogs = compactLogs; 473 | 474 | function compactLogs(ourLog, theirLog, localChainTail, remoteChainTail) { 475 | function compact(logA, logB, indexB, indexA) { 476 | var item, next, htlc, parentLink, parentIndex; 477 | 478 | for (item = logA.head; item; item = next) { 479 | htlc = item.value; 480 | next = item.next; 481 | 482 | if (htlc.entryType === Channel.updateType.ADD) 483 | continue; 484 | 485 | if (htlc.removeCommitHeightRemote === 0 486 | || htlc.removeCommitHeightLocal === 0) { 487 | continue; 488 | } 489 | 490 | if (remoteChainTail >= htlc.removeCommitHeightRemote 491 | && localChainTail >= htlc.removeCommitHeightLocal) { 492 | parentLink = indexB[htlc.parentIndex]; 493 | assert(htlc.parentIndex === parentLink.value.index); 494 | parentIndex = parentLink.value.index; 495 | logB.removeItem(parentLink); 496 | logA.removeItem(item); 497 | delete indexB[parentIndex]; 498 | delete indexA[htlc.index]; 499 | } 500 | } 501 | } 502 | 503 | compact(ourLog, theirLog, this.theirLogIndex, this.ourLogIndex); 504 | compact(theirLog, ourLog, this.ourLogIndex, this.theirLogIndex); 505 | }; 506 | 507 | Channel.prototype.extendRevocationWindow = function extendRevocationWindow() { 508 | var revMsg = new CommitRevocation(); 509 | var nextHeight = this.revocationWindowEdge + 1; 510 | var revocation = this.state.localElkrem.getIndex(nextHeight); 511 | var theirCommitKey = this.state.theirCommitKey; 512 | 513 | revMsg.channelPoint = this.state.id; 514 | revMsg.nextRevKey = util.deriveRevPub(theirCommitKey, revocation); 515 | revMsg.nextRevHash = crypto.sha256(revocation); 516 | 517 | this.revocationWindowEdge++; 518 | 519 | return revMsg; 520 | }; 521 | 522 | Channel.prototype.addHTLC = function addHTLC(htlc) { 523 | var pd = new PaymentDescriptor(); 524 | var item; 525 | 526 | pd.entryType = Channel.updateType.ADD; 527 | pd.paymentHash = htlc.redemptionHashes[0]; 528 | pd.timeout = htlc.expiry; 529 | pd.value = htlc.value; 530 | pd.index = this.ourLogCounter; 531 | 532 | item = this.ourUpdateLog.push(pd); 533 | this.ourLogIndex[pd.index] = item; 534 | this.ourLogCounter++; 535 | 536 | return pd.index; 537 | }; 538 | 539 | Channel.prototype.receiveHTLC = function receiveHTLC(htlc) { 540 | var pd = new PaymentDescriptor(); 541 | var item; 542 | 543 | pd.entryType = Channel.updateType.ADD; 544 | pd.paymentHash = htlc.redemptionHashes[0]; 545 | pd.timeout = htlc.expiry; 546 | pd.value = htlc.value; 547 | pd.index = this.theirLogCounter; 548 | 549 | item = this.theirUpdateLog.push(pd); 550 | this.theirLogIndex[pd.index] = item; 551 | this.theirLogCounter++; 552 | 553 | return pd.index; 554 | }; 555 | 556 | Channel.prototype.settleHTLC = function settleHTLC(preimage) { 557 | var paymentHash = crypto.sha256(preimage); 558 | var item, htlc, target, pd; 559 | 560 | for (item = this.theirUpdateLog.head; item; item = item.next) { 561 | htlc = item.value; 562 | 563 | if (htlc.entryType !== Channel.updateType.ADD) 564 | continue; 565 | 566 | if (htlc.settled) 567 | continue; 568 | 569 | if (utils.equal(htlc.paymentHash, paymentHash)) { 570 | htlc.settled = true; 571 | target = htlc; 572 | break; 573 | } 574 | } 575 | 576 | if (!target) 577 | throw new Error('Invalid payment hash.'); 578 | 579 | pd = new PaymentDescriptor(); 580 | pd.value = target.value; 581 | pd.index = this.ourLogCounter; 582 | pd.parentIndex = target.index; 583 | pd.entryType = Channel.updateType.SETTLE; 584 | 585 | this.ourUpdateLog.push(pd); 586 | this.ourLogCounter++; 587 | 588 | return target.index; 589 | }; 590 | 591 | Channel.prototype.receiveHTLCSettle = function receiveHTLCSettle(preimage, logIndex) { 592 | var paymentHash = crypto.sha256(preimage); 593 | var addEntry = this.ourLogIndex[logIndex]; 594 | var htlc, pd; 595 | 596 | if (!addEntry) 597 | throw new Error('Non existent log entry.'); 598 | 599 | htlc = addEntry.value; 600 | 601 | if (!utils.equal(htlc.paymentHash, paymentHash)) 602 | throw new Error('Invalid payment hash.'); 603 | 604 | pd = new PaymentDescriptor(); 605 | pd.value = htlc.value; 606 | pd.parentIndex = htlc.index; 607 | pd.index = this.theirLogCounter; 608 | pd.entryType = Channel.updateType.SETTLE; 609 | 610 | this.theirUpdateLog.push(pd); 611 | this.theirLogCounter++; 612 | }; 613 | 614 | Channel.prototype.channelPoint = function channelPoint() { 615 | return this.state.id; 616 | }; 617 | 618 | Channel.prototype.pushHTLC = pushHTLC; 619 | 620 | function pushHTLC(commitTX, ourCommit, pd, revocation, delay, isIncoming) { 621 | var localKey = this.state.ourCommitPub; 622 | var remoteKey = this.state.theirCommitKey; 623 | var timeout = pd.timeout; 624 | var payHash = pd.paymentHash; 625 | var redeem, script, pending, output; 626 | 627 | if (isIncoming) { 628 | if (ourCommit) { 629 | redeem = util.createReceiverHTLC( 630 | timeout, delay, remoteKey, 631 | localKey, revocation, payHash); 632 | } else { 633 | redeem = util.createSenderHTLC( 634 | timeout, delay, remoteKey, 635 | localKey, revocation, payHash); 636 | } 637 | } else { 638 | if (ourCommit) { 639 | redeem = util.createSenderHTLC( 640 | timeout, delay, localKey, 641 | remoteKey, revocation, payHash); 642 | } else { 643 | redeem = util.createReceiverHTLC( 644 | timeout, delay, localKey, 645 | remoteKey, revocation, payHash); 646 | } 647 | } 648 | 649 | script = util.toWitnessScripthash(redeem); 650 | pending = pd.value; 651 | 652 | output = new bcoin.output(); 653 | output.script = script; 654 | output.value = pending; 655 | 656 | commitTX.addOutput(output); 657 | } 658 | 659 | Channel.prototype.forceClose = function forceClose() { 660 | }; 661 | 662 | Channel.prototype.initCooperativeClose = function initCooperativeClose() { 663 | var closeTX, sig; 664 | 665 | if (this.status === Channel.states.CLOSING 666 | || this.status === Channel.states.CLOSED) { 667 | throw new Error('Channel is already closed.'); 668 | } 669 | 670 | this.status = Channel.states.CLOSING; 671 | 672 | closeTX = util.createCooperativeClose( 673 | this.fundingInput, 674 | this.state.ourBalance, 675 | this.state.theirBalance, 676 | this.state.ourDeliveryScript, 677 | this.state.theirDeliveryScript, 678 | true); 679 | 680 | closeTX.inputs[0].coin.value = this.state.capacity; 681 | 682 | sig = closeTX.signature(0, 683 | this.state.fundingScript, 684 | this.state.ourMultisigKey, 685 | hashType.ALL, 1); 686 | 687 | return { 688 | sig: sig, 689 | hash: closeTX.hash() 690 | }; 691 | }; 692 | 693 | Channel.prototype.completeCooperativeClose = function completeCooperativeClose(remoteSig) { 694 | var closeTX, redeem, sig, ourKey, theirKey, witness; 695 | 696 | if (this.status === Channel.states.CLOSING 697 | || this.status === Channel.states.CLOSED) { 698 | throw new Error('Channel is already closed.'); 699 | } 700 | 701 | this.status = Channel.states.CLOSED; 702 | 703 | closeTX = util.createCooperativeClose( 704 | this.fundingInput, 705 | this.state.ourBalance, 706 | this.state.theirBalance, 707 | this.state.ourDeliveryScript, 708 | this.state.theirDeliveryScript, 709 | false); 710 | 711 | closeTX.inputs[0].coin.value = this.state.capacity; 712 | 713 | redeem = this.state.fundingScript; 714 | 715 | sig = closeTX.signature(0, 716 | redeem, 717 | this.state.ourMultisigKey, 718 | hashType.ALL, 1); 719 | 720 | ourKey = this.state.ourMultisigPub; 721 | theirKey = this.state.theirMultisigKey; 722 | witness = util.spendMultisig(redeem, ourKey, sig, theirKey, remoteSig); 723 | 724 | closeTX.inputs[0].witness = witness; 725 | 726 | if (!closeTX.verify()) 727 | throw new Error('TX did not verify.'); 728 | 729 | return closeTX; 730 | }; 731 | 732 | /** 733 | * Payment Descriptor 734 | */ 735 | 736 | function PaymentDescriptor() { 737 | this.paymentHash = constants.ZERO_HASH; 738 | this.timeout = 0; 739 | this.value = 0; 740 | this.index = 0; 741 | this.parentIndex = 0; 742 | this.payload = null; 743 | this.entryType = Channel.updateType.ADD; 744 | this.addCommitHeightRemote = 0; 745 | this.addCommitHeightLocal = 0; 746 | this.removeCommitHeightRemote = 0; 747 | this.removeCommitHeightLocal = 0; 748 | this.isForwarded = false; 749 | this.settled = false; 750 | } 751 | 752 | /** 753 | * Commitment 754 | */ 755 | 756 | function Commitment() { 757 | this.height = 0; 758 | this.ourMessageIndex = 0; 759 | this.theirMessageIndex = 0; 760 | this.tx = new bcoin.mtx(); 761 | this.sig = constants.ZERO_SIG; 762 | this.ourBalance = 0; 763 | this.theirBalance = 0; 764 | } 765 | 766 | /** 767 | * Commitment Chain 768 | */ 769 | 770 | function CommitmentChain(height) { 771 | this.list = new List(); 772 | this.startingHeight = height || 0; 773 | } 774 | 775 | CommitmentChain.prototype.add = function(c) { 776 | this.list.push(c); 777 | }; 778 | 779 | CommitmentChain.prototype.advanceTail = function() { 780 | this.list.shift(); 781 | }; 782 | 783 | CommitmentChain.prototype.tip = function() { 784 | if (!this.list.tail) 785 | return; 786 | return this.list.tail.value; 787 | }; 788 | 789 | CommitmentChain.prototype.tail = function() { 790 | if (!this.list.head) 791 | return; 792 | return this.list.head.value; 793 | }; 794 | 795 | /** 796 | * HTLC View 797 | */ 798 | 799 | function HTLCView(ourUpdates, theirUpdates) { 800 | this.ourUpdates = ourUpdates || []; 801 | this.theirUpdates = theirUpdates || []; 802 | this.ourBalance = 0; 803 | this.theirBalance = 0; 804 | } 805 | 806 | /* 807 | * Expose 808 | */ 809 | 810 | module.exports = Channel; 811 | --------------------------------------------------------------------------------