From 4c653683b746d76419bd79791746d3269409177e Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Fri, 26 Feb 2016 03:21:39 -0800 Subject: [PATCH] segregated - goddamn - witness... and docs update. --- README.md | 125 +++++-------- lib/bcoin/address.js | 133 +++++++++----- lib/bcoin/block.js | 68 ++++++- lib/bcoin/chain.js | 36 +++- lib/bcoin/hd.js | 4 +- lib/bcoin/input.js | 33 +++- lib/bcoin/mtx.js | 36 ++-- lib/bcoin/protocol/constants.js | 13 +- lib/bcoin/protocol/framer.js | 105 ++++++++++- lib/bcoin/protocol/network.js | 190 ++++++++++++++++++- lib/bcoin/protocol/parser.js | 149 ++++++++++++++- lib/bcoin/script.js | 316 +++++++++++++++++++++++++++++--- lib/bcoin/tx.js | 77 +++++++- lib/bcoin/utils.js | 14 +- lib/bcoin/wallet.js | 4 +- scripts/gen.js | 12 +- test/wallet-test.js | 28 ++- 17 files changed, 1140 insertions(+), 203 deletions(-) diff --git a/README.md b/README.md index 6b5f34a2..e854d513 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,29 @@ # BCoin -**BCoin** is a bitcoin node which can act as an SPV node or a (semi-)fullnode. +**BCoin** is a bitcoin node which can act as an SPV node or a fully validating +fullnode. Bcoin runs in node.js, but it can also be browserified. + +## Features + +- SPV mode +- HD Wallets (using BIP44 (or optionally BIP45) derivation) +- Fully browserifiable +- Full block validation +- Full block database (leveldb + regular file i/o) +- Mempool (still a WIP - not completely accurate to the bitcoind mempool) +- Wallet database (leveldb) +- HTTP server which acts as a wallet server and can also serve: + blocks, txs (by hash/address), and utxos (by id/address). + - Fast UTXO retrieval by address for wallets (10000 utxos from 10000 different + addresses in ~700ms, 50000+ utxos from 10-100 addresses in ~400ms) +- Experimental segregated witness support (witness programs still need to be + implemented for tx signing process and wallet, but bcoin should be able to + validate witness blocks and txs on segnet properly) + +## Todo + +- Pruning +- Improve mempool ## Install @@ -8,6 +31,11 @@ $ npm install bcoin ``` +## NOTE + +__The docs below are out of date. Bcoin is under heavy development. They will be +updated as soon as I get the chance.__ + ## Example Usage ### Doing a full blockchain sync @@ -342,7 +370,7 @@ tx.addOutput({ ``` Opcodes are in the form of their symbolic names, in lowercase, with the `OP_` -prefixes removed. Pushdata ops are represented with Arrays. +prefixes removed. Pushdata ops are represented with Buffers. The above script could be redeemed with: @@ -352,8 +380,8 @@ tx2.addInput({ output: bcoin.coin(tx, 0), sequence: 0xffffffff, script: [ - signature, // Byte Array - publicKey // Byte Array + signature, // Buffer + publicKey // Buffer ] }); ``` @@ -362,11 +390,11 @@ Executing a script by itself is also possible: ``` js var stack = []; -bcoin.script.execute([[1], 'dup'], stack); +bcoin.script.execute([new Buffer([1]), 'dup'], stack); console.log(stack); Output: -[[1], [1]] +[, ] ``` #### Pushdata OPs @@ -375,23 +403,19 @@ Note that with bcoins deserialized script format, you do not get to decide pushdata on ops. Bcoin will always serialize to `minimaldata` format scripts in terms of `OP_PUSHDATA0-OP_PUSHDATA4`. -`OP_0` is represented with an empty array (which is appropriate because this is -what gets pushed onto the stack). While `OP_1-16` are actually represented with -numbers. `OP_1NEGATE` is just '1negate'. - So a script making use of all pushdata ops would look like: ``` js script: [ - [], // OP_0 / OP_FALSE - 1, // OP_1 / OP_TRUE - 2, 3, 4, 5, 6, 7, 8, 9, 10, // OP_2-10 - 11, 12, 13, 14, 15, 16, // OP_11-16 - '1negate', // OP_1NEGATE - new Array(0x4b), // PUSHDATA0 (direct push) - new Array(0xff), // PUSHDATA1 - new Array(0xffff), // PUSHDATA2 - new Array(0xffffffff) // PUSHDATA4 + 0, // OP_0 / OP_FALSE + 1, // OP_1 / OP_TRUE + 2, 3, 4, 5, 6, 7, 8, 9, 10, // OP_2-10 + 11, 12, 13, 14, 15, 16, // OP_11-16 + '1negate', // OP_1NEGATE + new Buffer(0x4b), // PUSHDATA0 (direct push) + new Buffer(0xff), // PUSHDATA1 + new Buffer(0xffff), // PUSHDATA2 + new Buffer(0xffffffff) // PUSHDATA4 ]; ``` @@ -471,14 +495,6 @@ console.log(tx.rhash); console.log(block.rhash); ``` -### Arrays vs. Buffers - -Every piece of binary data in bcoin that is user-facing in bcoin is an Array of -bytes. For example, `block.hash()` with no encoding passed in will return a -byte array. Bcoin does use Buffers behind the scenes to speed up parsing of -blocks and transactions coming in through the network, but every piece of data -a programmer using bcoin will deal with is going to be a byte array. - ### Saving transactions to a wallet Most of the time, you won't need all transactions in the blockchain if you're @@ -486,7 +502,7 @@ only building a wallet. When a transaction comes in pertaining to your wallet, it's best to called `wallet.addTX(tx)` and save the wallet afterwards. ``` js -pool.on('watched', function(tx) { +pool.on('tx', function(tx) { wallet.addTX(tx); }); @@ -497,59 +513,6 @@ pool.on('full', function() { }); ``` -### Saving the blockchain - -At the moment, bcoin does not save any full blocks or make any assumptions -about how the programmer wants to do it. It only saves the blockchain (block -headers and chainwork). The programmer simply needs to hook into block events -and save the blocks. - -``` js -pool.on('block', function(block) { - // A simple key-value store: - db.save(block.hash('hex'), utils.toHex(block.render()), function(err) { - if (err) - return console.error(err.message); - console.log('Block %s saved.', block.rhash); - // Could also save transactions individually here for quick lookups - }); -}); -``` - -#### Handling Blockchain Forks - -Bcoin handles blockchain forks like an SPV client. If it sees an alternate tip, -it will reset to the last non-forked block and kill the current peer while -emitting a `fork` event (see Pool events). It will repeat this process until -the network eventually chooses the best chain. - -Bcoin essentially backs off and waits to see which fork wins. This means bcoin -plays no part in protecting the network by helping choose the best chain -according to the chainwork. - -Note that this may _still_ cause an issue with transactions that are already -saved and considered confirmed. It's best to hook into the fork event and -remove all confirmed transactions you may have saved in your database. - -``` js -pool.on('fork', function(tip1, tip2) { - // Keep deleting everything until - // the fork is resolved: - db.get(tip1, function(err, block) { - block.txs.forEach(function(tx) { - db.del(tx.hash('hex')); - }); - db.del(tip1); - }); - db.get(tip2, function(err, block) { - block.txs.forEach(function(tx) { - db.del(tx.hash('hex')); - }); - db.del(tip2); - }); -}); -``` - ### Transaction Building Transaction building happens in 4 stages: diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index a80932e0..0f74008b 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -50,7 +50,7 @@ function Address(options) { assert(this.type === 'pubkeyhash' || this.type === 'multisig'); this.prefixType = this.type === 'multisig' ? 'scripthash' : 'pubkeyhash'; - if (network.prefixes[this.prefixType] == null) + if (network.address.prefixes[this.prefixType] == null) throw new Error('Unknown prefix: ' + this.prefixType); if (this.m < 1 || this.m > this.n) @@ -221,7 +221,7 @@ Address.prototype.getScriptAddress = function getScriptAddress() { if (this._scriptAddress) return this._scriptAddress; - this._scriptAddress = Address.toAddress(this.getScriptHash(), this.prefixType); + this._scriptAddress = Address.compileHash(this.getScriptHash(), this.prefixType); return this._scriptAddress; }; @@ -252,7 +252,7 @@ Address.prototype.getKeyAddress = function getKeyAddress() { if (this._address) return this._address; - this._address = Address.toAddress(this.getKeyHash(), 'pubkeyhash'); + this._address = Address.compileHash(this.getKeyHash(), 'pubkeyhash'); return this._address; }; @@ -468,87 +468,136 @@ Address.hash160 = function hash160(key) { return utils.ripesha(key); }; -Address.toAddress = function toAddress(hash, prefix) { - var addr; +Address.sha256 = function sha256(key) { + key = utils.ensureBuffer(key); + return utils.sha256(key); +}; + +Address.compileHash = function compileHash(hash, prefixType) { + var prefix, version, size, off, addr; if (!Buffer.isBuffer(hash)) hash = new Buffer(hash, 'hex'); - addr = new Buffer(1 + hash.length + 4); + if (!prefixType) + prefixType = 'pubkeyhash'; - prefix = network.prefixes[prefix || 'pubkeyhash']; + prefix = network.address.prefixes[prefixType]; + version = network.address.versions[prefixType]; - utils.writeU8(addr, prefix, 0); - utils.copy(hash, addr, 1); - utils.copy(utils.checksum(addr.slice(0, 21)), addr, 21); + assert(prefix != null); + assert(hash.length === 20 || hash.length === 32); + + size = 1 + hash.length + 4; + + if (version != null) + size += 2; + + addr = new Buffer(size); + + off = 0; + + off += utils.writeU8(addr, prefix, off); + if (version != null) { + off += utils.writeU8(addr, version, off); + off += utils.writeU8(addr, 0, off); + } + off += utils.copy(hash, addr, off); + off += utils.copy(utils.checksum(addr.slice(0, off)), addr, off); return utils.toBase58(addr); }; -Address.compile = function compile(key, prefix) { - return Address.toAddress(Address.hash160(key), prefix); +Address.compileData = function compileData(key, prefix) { + if (prefix === 'witnessscripthash') + key = Address.sha256(key); + else + key = Address.hash160(key); + + return Address.compileHash(key, prefix); }; -Address.toHash = function toHash(addr, prefix) { - var chk; - - if (prefix == null && typeof addr === 'string') - prefix = Address.prefixes[addr[0]]; +Address.parse = function parse(addr, prefixType) { + var chk, prefix, version, size, hash; if (!Buffer.isBuffer(addr)) addr = utils.fromBase58(addr); - prefix = network.prefixes[prefix || 'pubkeyhash']; + if (prefixType == null) + prefixType = network.address.prefixesByVal[addr[0]]; - if (addr.length !== 25) { + if (!prefixType) + prefixType = 'pubkeyhash'; + + prefix = network.address.prefixes[prefixType]; + version = network.address.versions[prefixType]; + + assert(prefix != null); + + // prefix + size = 1; + + // version + nul byte + if (version != null) + size += 2; + + hash = addr.slice(size, -4); + + // hash + if (prefixType === 'witnessscripthash') + size += 32; + else + size += 20; + + if (addr.length !== size + 4) { utils.debug('Address is not the right length.'); - return new Buffer([]); + return; } if (addr[0] !== prefix) { utils.debug('Address is not the right prefix.'); - return new Buffer([]); + return; + } + + if (version != null && (addr[1] !== version || addr[2] !== 0)) { + utils.debug('Address is not the right program version.'); + return; } chk = utils.checksum(addr.slice(0, -4)); - if (utils.readU32(chk, 0) !== utils.readU32(addr, 21)) { + if (utils.readU32(chk, 0) !== utils.readU32(addr, size)) { utils.debug('Address checksum failed.'); - return new Buffer([]); + return; } - return addr.slice(1, -4); + return { + type: prefixType, + hash: hash, + version: version == null ? -1 : version + }; }; -Address.__defineGetter__('prefixes', function() { - if (Address._prefixes) - return Address._prefixes; - - Address._prefixes = ['pubkeyhash', 'scripthash'].reduce(function(out, prefix) { - var ch = Address.compile(new Buffer([]), prefix)[0]; - out[ch] = prefix; - return out; - }, {}); - - return Address._prefixes; -}); - Address.validate = function validate(addr, prefix) { - if (!addr || typeof addr !== 'string') + if (!addr) return false; - var p = Address.toHash(addr, prefix); + if (!Address.parse(addr, prefix)) + return false; - return p.length !== 0; + return true; }; Address.getType = function getType(addr) { var prefix; - if (!addr || typeof addr !== 'string') + if (!addr) return 'unknown'; - prefix = Address.prefixes[addr[0]]; + if (!Buffer.isBuffer(addr)) + addr = utils.fromBase58(addr); + + prefix = network.address.prefixes[addr[0]]; if (!Address.validate(addr, prefix)) return 'unknown'; diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 727ac7bc..13995bfa 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -24,6 +24,7 @@ function Block(data) { bcoin.abstractblock.call(this, data); this.type = 'block'; + this.witness = data.witness || false; this.txs = data.txs || []; @@ -36,29 +37,84 @@ function Block(data) { return bcoin.tx(data, self, i); }); + if (this.witness) { + this._wsize = this._raw.length; + this._wraw = this._raw; + this._raw = null; + this._size = 0; + } + if (!this._raw) this._raw = this.render(); if (!this._size) this._size = this._raw.length; + + this._cost = data._cost || 0; } utils.inherits(Block, bcoin.abstractblock); Block.prototype.render = function render() { - if (this._raw) - return this._raw; - return bcoin.protocol.framer.block(this); + if (!this._raw) { + this._raw = bcoin.protocol.framer.block(this); + this._size = this._raw.length; + } + return this._raw; +}; + +Block.prototype.renderWitness = function renderWitness() { + if (!this._wraw) { + this._wraw = bcoin.protocol.framer.witnessBlock(this); + this._wsize = this._wraw.length; + } + return this._wraw; +}; + +Block.prototype.getBlockSize = function getBlockSize() { + return this.render().length; +}; + +Block.prototype.getBlockCost = function getBlockCost() { + if (!this._cost) + this._cost = this.renderWitness()._cost; + + return this._cost; +}; + +Block.prototype.getSigopsCost = function getSigopCost(scriptHash) { + var cost = 0; + var i; + + for (i = 0; i < this.txs.length; i++) + cost += this.txs[i].getSigopsCost(scriptHash); + + return cost; }; Block.prototype.getMerkleRoot = function getMerkleRoot() { - var hashes = []; + var leaves = []; var i, root; for (i = 0; i < this.txs.length; i++) - hashes.push(this.txs[i].hash()); + leaves.push(this.txs[i].hash()); - root = utils.getMerkleRoot(hashes); + root = utils.getMerkleRoot(leaves); + + if (!root) + return utils.toHex(constants.zeroHash); + + return utils.toHex(root); +}; + +Block.prototype.getWitnessRoot = function getWitnessRoot() { + var leaves = []; + var i, root; + + for (i = 0; i < this.txs.length; i++) + leaves.push(this.txs[i].witnessHash()); + + root = utils.getMerkleRoot(leaves); if (!root) return utils.toHex(constants.zeroHash); diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 92761769..f2d2cc44 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -454,7 +454,8 @@ Chain.prototype._verifyContext = function _verifyContext(block, prev, callback) Chain.prototype._verify = function _verify(block, prev) { var flags = constants.flags.MANDATORY_VERIFY_FLAGS; - var height, ts, i, tx, cb, coinbaseHeight, medianTime, locktimeMedian; + var height, ts, i, tx, cb, coinbaseHeight, medianTime; + var locktimeMedian, coinbase, commitment, segwit, witnessRoot; if (!block.verify()) return flags; @@ -516,6 +517,15 @@ Chain.prototype._verify = function _verify(block, prev) { return false; } + // Only allow version 5 blocks (segwit) + // once the majority of blocks are using it. + if (height >= network.segwitHeight) { + if (block.version < 5 && prev.isOutdated(5)) { + utils.debug('Block is outdated (v5): %s', block.rhash); + return false; + } + } + // Only allow version 8 blocks (locktime median past) // once the majority of blocks are using it. // if (block.version < 8 && prev.isOutdated(8)) { @@ -535,6 +545,14 @@ Chain.prototype._verify = function _verify(block, prev) { if (block.version >= 4 && prev.isUpgraded(4)) flags |= constants.flags.VERIFY_CHECKLOCKTIMEVERIFY; + // Segregrated witness is now usable (the-bip-that-really-needs-to-be-rewritten) + if (height >= network.segwitHeight) { + if (block.witness && block.version >= 5 && prev.isUpgraded(5) ) { + flags |= constants.flags.VERIFY_WITNESS; + segwit = true; + } + } + // Use nLockTime median past (bip113) // https://github.com/btcdrak/bips/blob/d4c9a236ecb947866c61aefb868b284498489c2b/bip-0113.mediawiki // Support version bits: @@ -555,6 +573,22 @@ Chain.prototype._verify = function _verify(block, prev) { } } + // Find the fucking commitment for segregated shitness + if (segwit && block.witness) { + coinbase = block.txs[0]; + for (i = 0; i < coinbase.outputs.length; i++) { + commitment = coinbase.outputs[i].script; + if (bcoin.script.isCommitment(commitment)) { + witnessRoot = bcoin.script.getWitnessRoot(commitment); + if (utils.toHex(witnessRoot) !== block.getWitnessRoot()) { + utils.debug('Block failed witnessroot test: %s', block.rhash); + return false; + } + break; + } + } + } + // Get timestamp for tx.isFinal(). ts = locktimeMedian ? medianTime : block.ts; diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index f5959bc1..93401e1e 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -212,7 +212,7 @@ HDPrivateKey.prototype.scan44 = function scan44(options, txByAddress, callback) // respect the gap limit described below return (function next() { var address = chain.derive(addressIndex++); - var addr = bcoin.address.compile(address.publicKey); + var addr = bcoin.address.compileData(address.publicKey); return txByAddress(addr, function(err, txs) { var result; @@ -363,7 +363,7 @@ HDPrivateKey.prototype.scan45 = function scan45(options, txByAddress, callback) return (function next() { var address = chain.derive(addressIndex++); - var addr = bcoin.address.compile(address.publicKey); + var addr = bcoin.address.compileData(address.publicKey); return txByAddress(addr, function(err, txs) { var result; diff --git a/lib/bcoin/input.js b/lib/bcoin/input.js index f1c52aa3..6b17b5a2 100644 --- a/lib/bcoin/input.js +++ b/lib/bcoin/input.js @@ -23,8 +23,11 @@ function Input(options, tx) { this.prevout = options.prevout; this.script = options.script || []; this.sequence = options.sequence == null ? 0xffffffff : options.sequence; + this.witness = options.witness || []; this._size = options._size || 0; this._offset = options._offset || 0; + this._witnessSize = options._witnessSize || 0; + this._witnessOffset = options._witnessOffset || 0; this._mutable = !tx || (tx instanceof bcoin.mtx); if (options.output) @@ -54,7 +57,11 @@ Input.prototype.getType = function getType() { if (this._type) return this._type; - type = bcoin.script.getInputType(this.script); + if (this.witness.length > 0) + type = bcoin.script.getInputType(this.witness, null, true); + + if (type === 'unknown') + type = bcoin.script.getInputType(this.script); if (!this._mutable) this._type = type; @@ -74,7 +81,11 @@ Input.prototype.getAddress = function getAddress() { if (this._address) return this._address; - address = bcoin.script.getInputAddress(this.script); + if (this.witness.length > 0) + address = bcoin.script.getInputAddress(this.witness, null, true); + + if (!address) + address = bcoin.script.getInputAddress(this.script); if (!this._mutable) this._address = address; @@ -214,6 +225,9 @@ Input.prototype.toJSON = function toJSON() { }, output: this.output ? this.output.toJSON() : null, script: utils.toHex(bcoin.script.encode(this.script)), + witness: this.witness.map(function(chunk) { + return utils.toHex(chunk); + }), sequence: this.sequence }; }; @@ -226,6 +240,9 @@ Input._fromJSON = function _fromJSON(json) { }, output: json.output ? bcoin.coin._fromJSON(json.output) : null, script: bcoin.script.decode(new Buffer(json.script, 'hex')), + witness: json.witness.map(function(chunk) { + return new Buffer(chunk, 'hex'); + }), sequence: json.sequence }; }; @@ -237,18 +254,26 @@ Input.fromJSON = function fromJSON(json) { Input.prototype.toCompact = function toCompact() { return { type: 'input', - input: this.toRaw('hex') + input: this.toRaw('hex'), + witness: this.witness.map(function(chunk) { + return utils.toHex(chunk); + }), }; }; Input._fromCompact = function _fromCompact(json) { - return Input._fromRaw(json.input, 'hex'); + json = Input._fromRaw(json.input, 'hex'); + json.witness = json.witness.map(function(chunk) { + return new Buffer(chunk, 'hex'); + }); + return json; }; Input.fromCompact = function fromCompact(json) { return new Input(Input._fromCompact(json)); }; +// NOTE: We cannot encode the witness here. Input.prototype.toRaw = function toRaw(enc) { var data = bcoin.protocol.framer.input(this); diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js index 788e6886..087b5e7d 100644 --- a/lib/bcoin/mtx.js +++ b/lib/bcoin/mtx.js @@ -81,10 +81,19 @@ MTX.prototype.hash = function hash(enc) { return enc === 'hex' ? utils.toHex(hash) : hash; }; +MTX.prototype.witnessHash = function hash(enc) { + var hash = utils.dsha256(this.renderWitness()); + return enc === 'hex' ? utils.toHex(hash) : hash; +}; + MTX.prototype.render = function render() { return bcoin.protocol.framer.tx(this); }; +MTX.prototype.renderWitness = function renderWitness() { + return bcoin.protocol.framer.witnessTX(this); +}; + MTX.prototype.getSize = function getSize() { return this.render().length; }; @@ -570,7 +579,7 @@ MTX.prototype.addOutput = function addOutput(obj, value) { }; MTX.prototype.scriptOutput = function scriptOutput(index, options) { - var output, script, keys, m, n, hash, flags; + var output, script, keys, m, n, hash, flags, address; if (options instanceof bcoin.output) return; @@ -601,19 +610,20 @@ MTX.prototype.scriptOutput = function scriptOutput(index, options) { return; script = bcoin.script.createMultisig(keys, m, n); - } else if (bcoin.address.getType(options.address) === 'scripthash') { - // P2SH Transaction - // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki - // hash160 [20-byte-redeemscript-hash] equal - script = bcoin.script.createScripthash( - bcoin.address.toHash(options.address, 'scripthash') - ); } else if (options.address) { - // P2PKH Transaction - // dup hash160 [pubkey-hash] equalverify checksig - script = bcoin.script.createPubkeyhash( - bcoin.address.toHash(options.address, 'pubkeyhash') - ); + address = bcoin.address.parse(options.address); + + if (!address) + throw new Error(options.address + ' is not a valid address.'); + + if (address.type === 'pubkeyhash') + script = bcoin.script.createPubkeyhash(address.hash); + else if (address.type === 'scripthash') + script = bcoin.script.createScripthash(address.hash); + else if (address.version !== -1) + script = bcoin.script.createWitnessProgram(address.version, address.hash); + else + throw new Error('Cannot parse address: ' + options.address); } else if (options.key) { // P2PK Transaction // [pubkey] checksig diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 3b0a7f2f..597bae9b 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -21,7 +21,10 @@ exports.inv = { error: 0, tx: 1, block: 2, - filtered: 3 + filtered: 3, + witnesstx: 1 | (1 << 30), + witnessblock: 2 | (1 << 30), + witnessfiltered: 3 | (1 << 30) }; exports.invByVal = { @@ -197,7 +200,9 @@ exports.hashTypeByVal = Object.keys(exports.hashType).reduce(function(out, type) exports.block = { maxSize: 1000000, + maxCost: 4000000, maxSigops: 1000000 / 50, + maxSigopsCost: 4000000 / 50, maxOrphanTx: 1000000 / 100, medianTimeSpan: 11, bip16time: 1333238400 @@ -286,7 +291,9 @@ exports.flags = { VERIFY_MINIMALDATA: (1 << 6), VERIFY_DISCOURAGE_UPGRADABLE_NOPS: (1 << 7), VERIFY_CLEANSTACK: (1 << 8), - VERIFY_CHECKLOCKTIMEVERIFY: (1 << 9) + VERIFY_CHECKLOCKTIMEVERIFY: (1 << 9), + VERIFY_WITNESS: (1 << 10), + VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM: (1 << 9) }; // Block validation @@ -303,3 +310,5 @@ exports.flags.STANDARD_VERIFY_FLAGS = | exports.flags.VERIFY_CLEANSTACK | exports.flags.VERIFY_CHECKLOCKTIMEVERIFY | exports.flags.VERIFY_LOW_S; + // | exports.flags.VERIFY_WITNESS + // | exports.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM; diff --git a/lib/bcoin/protocol/framer.js b/lib/bcoin/protocol/framer.js index ee6b4e00..e070f80d 100644 --- a/lib/bcoin/protocol/framer.js +++ b/lib/bcoin/protocol/framer.js @@ -465,18 +465,119 @@ Framer.tx = function _tx(tx) { off += utils.writeU32(p, tx.locktime, off); + p._cost = p.length * 4; + + return p; +}; + +Framer.witnessTX = function _witnessTX(tx) { + var inputs = []; + var outputs = []; + var witnesses = []; + var inputSize = 0; + var outputSize = 0; + var witnessSize = 0; + var off = 0; + var p, i, input, output, witness; + + for (i = 0; i < tx.inputs.length; i++) { + input = Framer.input(tx.inputs[i]); + inputs.push(input); + inputSize += input.length; + witness = Framer.witness(tx.inputs[i].witness); + witnesses.push(witness); + witnessSize += witness.length; + } + + for (i = 0; i < tx.outputs.length; i++) { + output = Framer.output(tx.outputs[i]); + outputs.push(output); + outputSize += output.length; + } + + p = new Buffer(4 + + 2 + + utils.sizeIntv(tx.inputs.length) + inputSize + + utils.sizeIntv(tx.outputs.length) + outputSize + + 4); + + off += utils.write32(p, tx.version, off); + + // marker + off += utils.writeU8(p, 0, off); + + // flag + off += utils.writeU8(p, tx.flag || 1, off); + + off += utils.writeIntv(p, tx.inputs.length, off); + for (i = 0; i < inputs.length; i++) { + input = inputs[i]; + off += utils.copy(input, p, off); + } + + off += utils.writeIntv(p, tx.outputs.length, off); + for (i = 0; i < outputs.length; i++) { + output = outputs[i]; + off += utils.copy(output, p, off); + } + + p._cost = (off - 2) * 4; + + // NOTE: No varint item count here. + for (i = 0; i < witnesses.length; i++) + off += utils.copy(witnesses[i], p, off); + + off += utils.writeU32(p, tx.locktime, off); + p._cost += 4 * 4; + + p._cost += witnessSize; + + return p; +}; + +Framer.witness = function _witness(witness) { + var off = 0; + var size = 0; + var p, chunk; + + if (!witness) + return new Buffer([0]); + + size += utils.writeIntv(witness.length); + + for (i = 0; i < witness.length; i++) { + chunk = witness[i]; + size += utils.sizeIntv(chunk.length) + chunk.length; + } + + p = new Buffer(size); + + for (i = 0; i < witness.length; i++) { + chunk = witness[i]; + off += utils.writeIntv(p, chunk.length, off); + off += utils.copy(chunk, p, off); + } + return p; }; Framer.block = function _block(block) { + return Framer._block(block, false); +}; + +Framer.witnessBlock = function _witnessBlock(block) { + return Framer._block(block, true); +}; + +Framer._block = function _block(block, witness) { var off = 0; var txSize = 0; var txs = []; var i, tx, p; for (i = 0; i < block.txs.length; i++) { - tx = block.txs[i].render - ? block.txs[i].render() + tx = witness && block.txs[i].witness + ? Framer.witnessTX(block.txs[i]) : Framer.tx(block.txs[i]); txs.push(tx); txSize += tx.length; diff --git a/lib/bcoin/protocol/network.js b/lib/bcoin/protocol/network.js index 56211c3b..32518a97 100644 --- a/lib/bcoin/protocol/network.js +++ b/lib/bcoin/protocol/network.js @@ -14,7 +14,7 @@ var assert = utils.assert; */ var network = exports; -var main, testnet, regtest; +var main, testnet, regtest, segnet; network.set = function set(type) { var net = network[type]; @@ -28,13 +28,34 @@ network.set = function set(type) { main = network.main = {}; main.prefixes = { - pubkeyhash: 0, - scripthash: 5, privkey: 128, xpubkey: 0x0488b21e, xprivkey: 0x0488ade4 }; +main.address = { + prefixes: { + pubkeyhash: 0, + scripthash: 5, + witnesspubkeyhash: 6, + witnessscripthash: 10 + }, + versions: { + witnesspubkeyhash: 0, + witnessscripthash: 0 + } +}; + +main.address.prefixesByVal = Object.keys(main.address.prefixes).reduce(function(out, name) { + out[main.address.prefixes[name]] = name; + return out; +}, {}); + +main.address.versionsByVal = Object.keys(main.address.versions).reduce(function(out, name) { + out[main.address.versions[name]] = name; + return out; +}, {}); + main.type = 'main'; main.seeds = [ @@ -121,6 +142,8 @@ main.block = { majorityWindow: 1000 }; +main.segwitHeight = 2000000000; + main.genesisBlock = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; /** @@ -133,13 +156,34 @@ testnet = network.testnet = {}; testnet.type = 'testnet'; testnet.prefixes = { - pubkeyhash: 111, - scripthash: 196, privkey: 239, xpubkey: 0x043587cf, xprivkey: 0x04358394 }; +testnet.address = { + prefixes: { + pubkeyhash: 111, + scripthash: 196, + witnesspubkeyhash: 3, + witnessscripthash: 40 + }, + versions: { + witnesspubkeyhash: 0, + witnessscripthash: 0 + } +}; + +testnet.address.prefixesByVal = Object.keys(testnet.address.prefixes).reduce(function(out, name) { + out[testnet.address.prefixes[name]] = name; + return out; +}, {}); + +testnet.address.versionsByVal = Object.keys(testnet.address.versions).reduce(function(out, name) { + out[testnet.address.versions[name]] = name; + return out; +}, {}); + testnet.seeds = [ 'testnet-seed.alexykot.me', 'testnet-seed.bitcoin.petertodd.org', @@ -210,6 +254,8 @@ testnet.block = { majorityWindow: 100 }; +testnet.segwitHeight = 2000000000; + testnet.genesisBlock = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; /** @@ -221,13 +267,34 @@ regtest = network.regtest = {}; regtest.type = 'testnet'; regtest.prefixes = { - pubkeyhash: 111, - scripthash: 196, privkey: 239, xpubkey: 0x043587cf, xprivkey: 0x04358394 }; +regtest.address = { + prefixes: { + pubkeyhash: 111, + scripthash: 196, + witnesspubkeyhash: 3, + witnessscripthash: 40 + }, + versions: { + witnesspubkeyhash: 0, + witnessscripthash: 0 + } +}; + +regtest.address.prefixesByVal = Object.keys(regtest.address.prefixes).reduce(function(out, name) { + out[regtest.address.prefixes[name]] = name; + return out; +}, {}); + +regtest.address.versionsByVal = Object.keys(regtest.address.versions).reduce(function(out, name) { + out[regtest.address.versions[name]] = name; + return out; +}, {}); + regtest.seeds = [ '127.0.0.1' ]; @@ -281,4 +348,113 @@ regtest.block = { majorityWindow: 1000 }; +regtest.segwitHeight = 0; + regtest.genesisBlock = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f20020000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; + +/** + * Segnet + */ + +segnet = network.segnet = {}; + +segnet.type = 'segnet'; + +segnet.prefixes = { + privkey: 158, + xpubkey: 0x053587cf, + xprivkey: 0x05358394 +}; + +segnet.address = { + prefixes: { + pubkeyhash: 30, + scripthash: 50, + witnesspubkeyhash: 3, + witnessscripthash: 40 + }, + versions: { + witnesspubkeyhash: 0, + witnessscripthash: 0 + } +}; + +segnet.address.prefixesByVal = Object.keys(segnet.address.prefixes).reduce(function(out, name) { + out[segnet.address.prefixes[name]] = name; + return out; +}, {}); + +segnet.address.versionsByVal = Object.keys(segnet.address.versions).reduce(function(out, name) { + out[segnet.address.versions[name]] = name; + return out; +}, {}); + +segnet.seeds = [ + '104.243.38.34', + '104.155.1.158', + '119.246.245.241', + '46.101.235.82' +]; + +segnet.port = 28333; + +segnet.alertKey = new Buffer('' + + '04302390343f91cc401d56d68b123028bf52e5f' + + 'ca1939df127f63c6467cdf9c8e2c14b61104cf8' + + '17d0b780da337893ecc4aaff1309e536162dabb' + + 'db45200ca2b0a', + 'hex'); + +segnet.checkpoints = []; + +segnet.checkpoints = segnet.checkpoints.reduce(function(out, block) { + out[block.height] = utils.revHex(block.hash); + return block; +}, {}); + +segnet.checkpoints.tsLastCheckpoint = 0; +segnet.checkpoints.txsLastCheckpoint = 0; +segnet.checkpoints.txsPerDay = 300; +segnet.checkpoints.lastHeight = 0; + +segnet.halvingInterval = 210000; + +segnet.genesis = { + version: 1, + hash: utils.revHex( + 'ead13e4b1d0164b21128523156f729373d7a11bc9b6a1ee2e6e883ab9d9a728c' + ), + prevBlock: utils.toHex( + [ 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 ]), + merkleRoot: utils.revHex( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' + ), + ts: 1452368293, + bits: 0x1d00ffff, + nonce: 0 +}; + +segnet.magic = 0x99f57166; + +segnet.powLimit = new bn( + '00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + 'hex' +); +segnet.powTargetTimespan = 14 * 24 * 60 * 60; // two weeks +segnet.powTargetSpacing = 10 * 60; +segnet.powDiffInterval = segnet.powTargetTimespan / segnet.powTargetSpacing | 0; +segnet.powAllowMinDifficultyBlocks = true; +segnet.powNoRetargeting = false; + +segnet.block = { + majorityEnforceUpgrade: 7, + majorityRejectOutdated: 9, + majorityWindow: 10 +}; + +segnet.segwitHeight = 0; + +segnet.genesisBlock = '0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4aa5619156ffff001d000000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000'; diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index 6cfa9c40..df99de0f 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -128,9 +128,6 @@ Parser.prototype.parsePayload = function parsePayload(cmd, p) { if (cmd === 'headers') return Parser.parseHeaders(p); - // if (cmd === 'block') - // return Parser.parseBlock(p); - if (cmd === 'block') return Parser.parseBlockCompact(p); @@ -334,7 +331,9 @@ Parser.parseHeaders = function parseHeaders(p) { Parser.parseBlock = function parseBlock(p) { var txs = []; + var cost = 0; var i, result, off, totalTX, tx; + var witness; if (p.length < 81) throw new Error('Invalid block size'); @@ -343,16 +342,22 @@ Parser.parseBlock = function parseBlock(p) { off = result.off; totalTX = result.r; + cost += off * 4; + for (i = 0; i < totalTX; i++) { tx = Parser.parseTX(p.slice(off)); if (!tx) throw new Error('Invalid tx count for block'); + if (tx.witness) + witness = true; tx._offset = off; off += tx._size; + cost += tx._cost; txs.push(tx); } return { + witness: witness, version: utils.read32(p, 0), prevBlock: p.slice(4, 36), merkleRoot: p.slice(36, 68), @@ -362,7 +367,8 @@ Parser.parseBlock = function parseBlock(p) { totalTX: totalTX, txs: txs, _raw: p, - _size: p.length + _size: p.length, + _cost: cost }; }; @@ -522,6 +528,9 @@ Parser.parseTX = function parseTX(p) { if (p.length < 10) throw new Error('Invalid tx size'); + if (Parser.isWitnessTX(p)) + return Parser.parseWitnessTX(p); + inCount = utils.readIntv(p, 4); off = inCount.off; inCount = inCount.r; @@ -575,11 +584,143 @@ Parser.parseTX = function parseTX(p) { inputs: txIn, outputs: txOut, locktime: utils.readU32(p, off), + _cost: (off + 4) * 4, _raw: p.slice(0, off + 4), _size: off + 4 }; }; +Parser.isWitnessTX = function isWitnessTX(p) { + if (p.length < 12) + return false; + + return p[4] === 0 && p[5] !== 0; +}; + +Parser.parseWitnessTX = function parseWitnessTX(p) { + var cost = 0; + var inCount, off, txIn, tx; + var outCount, txOut; + var marker, flag; + var i; + + if (p.length < 12) + throw new Error('Invalid witness tx size'); + + marker = utils.readU8(p, 4); + flag = utils.readU8(p, 5); + + if (marker !== 0) + throw new Error('Invalid witness tx (marker != 0)'); + + if (flag === 0) + throw new Error('Invalid witness tx (flag == 0)'); + + inCount = utils.readIntv(p, 6); + off = inCount.off; + inCount = inCount.r; + + if (inCount < 0) + throw new Error('Invalid witness tx_in count (negative)'); + + if (off + 41 * inCount + 5 > p.length) + throw new Error('Invalid witness tx_in count (too big)'); + + txIn = new Array(inCount); + for (i = 0; i < inCount; i++) { + tx = Parser.parseInput(p.slice(off)); + + if (!tx) + return; + + txIn[i] = tx; + tx._offset = off; + off += tx._size; + + if (off + 5 > p.length) + throw new Error('Invalid witness tx_in offset'); + } + + outCount = utils.readIntv(p, off); + off = outCount.off; + outCount = outCount.r; + if (outCount < 0) + throw new Error('Invalid witness tx_out count (negative)'); + if (off + 9 * outCount + 4 > p.length) + throw new Error('Invalid witness tx_out count (too big)'); + + txOut = new Array(outCount); + for (i = 0; i < outCount; i++) { + tx = Parser.parseOutput(p.slice(off)); + + if (!tx) + return; + + txOut[i] = tx; + tx._offset = off; + off += tx._size; + + if (off + 4 > p.length) + throw new Error('Invalid tx_out offset'); + } + + cost += (off - 2) * 4; + + for (i = 0; i < inCount; i++) { + tx = Parser.parseWitness(p.slice(off)); + + if (!tx) + return; + + txIn[i].witness = tx.witness; + txIn[i]._witnessSize = tx._size; + txIn[i]._witnessOffset = off; + off += tx._size; + cost += tx._size; + + if (off + 4 > p.length) + throw new Error('Invalid witness offset'); + } + + return { + witness: true, + version: utils.read32(p, 0), + marker: marker, + flag: flag, + inputs: txIn, + outputs: txOut, + locktime: utils.readU32(p, off), + _raw: p.slice(0, off + 4), + _size: off + 4, + _cost: cost + }; +}; + +Parser.parseWitness = function parseWitness(p) { + var witness = []; + var off, chunkCount, chunkSize, item, i; + + chunkCount = utils.readIntv(p, 0); + off = chunkCount.off; + chunkCount = chunkCount.r; + + for (i = 0; i < chunkCount; i++) { + chunkSize = utils.readIntv(p, off); + off = chunkSize.off; + chunkSize = chunkSize.r; + item = p.slice(off, off + chunkSize); + off += chunkSize; + witness.push(item); + if (off > p.length) + throw new Error('Invalid witness offset'); + } + + return { + _size: off, + witness: witness + }; +}; + Parser.parseReject = function parseReject(p) { var messageLen, off, message, ccode, reasonLen, reason, data; diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 79d5bbc0..4400cbb3 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -203,6 +203,49 @@ script.encode = function encode(s) { return res; }; +// Witnesses aren't scripts, but we still +// want to convert [0] to OP_0, [0xff] to 1negate, etc. +script.decodeWitness = function decodeWitness(witness) { + var chunk, i, op; + var script = []; + + for (i = 0; i < witness.length; i++) { + chunk = witness[i]; + op = chunk; + + if (chunk.length === 1) { + if (chunk[0] === 0xff) + op = '1negate'; + else if (chunk[0] >= 0 && chunk <= 16) + op = chunk[0]; + } + + script.push(op); + } + + return script; +}; + +script.encodeWitness = function encodeWitness(script) { + var chunk, i, chunk; + var witness = []; + + for (i = 0; i < script.length; i++) { + chunk = script[i]; + + if (chunk === '1negate') + chunk = new Buffer([0xff]); + else if (chunk >= 0 && chunk <= 16) + chunk = new Buffer([op]); + + assert(Buffer.isBuffer(op)); + + witness.push(op); + } + + return witness; +}; + script.normalize = function normalize(s) { var i, op; @@ -244,7 +287,7 @@ script.normalize = function normalize(s) { return s; }; -script.verify = function verify(input, output, tx, i, flags) { +script.verify = function verify(input, witness, output, tx, i, flags) { var copy, res, redeem; var stack = []; @@ -270,6 +313,11 @@ script.verify = function verify(input, output, tx, i, flags) { if (!res || stack.length === 0 || !script.bool(stack.pop())) return false; + if ((flags & constants.flags.VERIFY_WITNESS) && script.isWitnessProgram(output)) { + if (!script.verifyProgram(input, witness, output, tx, i, flags)) + return false; + } + // If the script is P2SH, execute the real output script if ((flags & constants.flags.VERIFY_P2SH) && script.isScripthash(output)) { // P2SH can only have push ops in the scriptSig @@ -297,10 +345,17 @@ script.verify = function verify(input, output, tx, i, flags) { // Verify the script did not fail as well as the stack values if (!res || stack.length === 0 || !script.bool(stack.pop())) return false; + + if ((flags & constants.flags.VERIFY_WITNESS) && script.isWitnessProgram(redeem)) { + if (!script.verifyProgram(input, witness, redeem, tx, i, flags)) + return false; + } } // Ensure there is nothing left on the stack if (flags & constants.flags.VERIFY_CLEANSTACK) { + assert((flags & constants.flags.VERIFY_P2SH) !== 0); + // assert((flags & constants.flags.VERIFY_WITNESS) !== 0); if (stack.length !== 0) return false; } @@ -308,6 +363,85 @@ script.verify = function verify(input, output, tx, i, flags) { return true; }; +script.verifyProgram = function verifyProgram(input, witness, output, tx, i, flags) { + var program, witnessScript, script, stack, j; + + if (!(flags & constants.flags.VERIFY_WITNESS) || !script.isWitnessProgram(output)) + return true; + + program = script.getWitnessProgram(output); + + // Failure on version=0 (bad program data length) + if (!program.type) { + utils.debug('Malformed witness program.'); + return false; + } + + if (program.version > 0) { + utils.debug('Unknown witness program version: %s', program.version); + // Anyone can spend (we can return true here + // if we want to always relay these transactions). + // Otherwise, if we want to act like an "old" + // implementation and only accept them in blocks, + // we can use the regalar output script which will + // succeed in a block, but fail in the mempool + // due to VERIFY_CLEANSTACK. + if (flags & constants.flags.VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) + return false; + return true; + } + + stack = witness.slice(); + + if (program.type === 'witnesspubkeyhash') { + if (input.length !== 0) + return false; + + if (stack.length !== 2) + return false; + + if (!Buffer.isBuffer(stack[0]) || !Buffer.isBuffer(stack[1])) + return false; + + // Why the fuck are these allowed to be so big? + if (stack[0].length > 520 || stack[1].length > 520) + return false; + + script = ['dup', 'hash160', program.data, 'equalverify', 'checksig']; + } else if (program.type === 'witnessscripthash') { + if (stack.length === 0) + return false; + + witnessScript = stack.pop(); + + if (!Buffer.isBuffer(witnessScript)) + return false; + + if (witnessScript.length > constants.script.maxSize) + return false; + + if (!utils.isEqual(utils.sha256(witnessScript), program.data)) + return false; + + script = script.decode(witnessScript); + } else { + assert(false); + } + + for (j = 0; j < stack.length; j++) { + if (stack[j].length > constants.script.maxSize) + return false; + } + + res = script.execute(output, stack, tx, i, flags); + + // Verify the script did not fail as well as the stack values + if (!res || stack.length === 0 || !script.bool(stack.pop())) + return false; + + return true; +}; + script.getSubscript = function getSubscript(s, lastSep) { var i, res; @@ -1272,6 +1406,19 @@ script.getRedeem = function getRedeem(s) { script.getType = script.getOutputType = function getOutputType(s) { + var program; + + if (script.isCommitment(s)) + return 'commitment'; + + if (script.isWitnessProgram(s)) { + if (script.isWitnessPubkeyhash(s)) + return 'witnesspubkeyhash'; + if (script.isWitnessScripthash(s)) + return 'witnessscripthash'; + return 'unknown'; + } + return (script.isPubkey(s) && 'pubkey') || (script.isPubkeyhash(s) && 'pubkeyhash') || (script.isMultisig(s) && 'multisig') @@ -1443,7 +1590,7 @@ script._getInputData = function _getInputData(s, type) { sig = new Buffer([]); key = s[1]; hash = bcoin.address.hash160(key); - address = bcoin.address.toAddress(hash, 'pubkeyhash'); + address = bcoin.address.compileHash(hash, 'pubkeyhash'); return { type: 'pubkeyhash', side: 'input', @@ -1473,7 +1620,7 @@ script._getInputData = function _getInputData(s, type) { redeem = script.decode(raw); locktime = script.getLocktime(redeem); hash = bcoin.address.hash160(raw); - address = bcoin.address.toAddress(hash, 'scripthash'); + address = bcoin.address.compileHash(hash, 'scripthash'); output = script.getOutputData(redeem, true); input = script._getInputData(s.slice(0, -1), output.type); delete input.none; @@ -1499,7 +1646,7 @@ script.getOutputData = function getOutputData(s, inScriptHash) { key = s[0]; hash = bcoin.address.hash160(key); // Convert p2pk to p2pkh addresses - address = bcoin.address.toAddress(hash, 'pubkeyhash'); + address = bcoin.address.compileHash(hash, 'pubkeyhash'); return { type: 'pubkey', side: 'output', @@ -1512,7 +1659,7 @@ script.getOutputData = function getOutputData(s, inScriptHash) { if (script.isPubkeyhash(s)) { hash = s[2]; - address = bcoin.address.toAddress(hash, 'pubkeyhash'); + address = bcoin.address.compileHash(hash, 'pubkeyhash'); return { type: 'pubkeyhash', side: 'output', @@ -1529,12 +1676,12 @@ script.getOutputData = function getOutputData(s, inScriptHash) { }); // Convert bare multisig to p2pkh addresses address = hash.map(function(hash) { - return bcoin.address.toAddress(hash, 'pubkeyhash'); + return bcoin.address.compileHash(hash, 'pubkeyhash'); }); // Convert bare multisig script to scripthash address if (!inScriptHash) { mhash = bcoin.address.hash160(s._raw || script.encode(s)); - maddress = bcoin.address.toAddress(mhash, 'scripthash'); + maddress = bcoin.address.compileHash(mhash, 'scripthash'); } return { type: 'multisig', @@ -1550,7 +1697,7 @@ script.getOutputData = function getOutputData(s, inScriptHash) { if (script.isScripthash(s)) { hash = s[1]; - address = bcoin.address.toAddress(hash, 'scripthash'); + address = bcoin.address.compileHash(hash, 'scripthash'); return { type: 'scripthash', side: 'output', @@ -1589,7 +1736,7 @@ script.getUnknownData = function getUnknownData(s) { }); address = hash.map(function(hash) { - return bcoin.address.toAddress(hash, 'pubkeyhash'); + return bcoin.address.compileHash(hash, 'pubkeyhash'); }); return { @@ -1681,31 +1828,40 @@ script.getInputAddress = function getInputAddress(s, prev) { return; if (script.isPubkeyhashInput(s)) - return bcoin.address.compile(s[1], 'pubkeyhash'); + return bcoin.address.compileData(s[1], 'pubkeyhash'); if (script.isMultisigInput(s)) return; if (script.isScripthashInput(s)) - return bcoin.address.compile(s[s.length - 1], 'scripthash'); + return bcoin.address.compileData(s[s.length - 1], 'scripthash'); }; script.getOutputAddress = function getOutputAddress(s) { + var program; + + if (script.isWitnessProgram(s)) { + program = script.getWitnessProgram(s); + if (!program.type || program.type === 'witnessunknown') + return; + return bcoin.address.compileHash(program.data, program.type); + } + // Convert p2pk to p2pkh addresses if (script.isPubkey(s)) - return bcoin.address.compile(s[0], 'pubkeyhash'); + return bcoin.address.compileData(s[0], 'pubkeyhash'); if (script.isPubkeyhash(s)) - return bcoin.address.toAddress(s[2], 'pubkeyhash') + return bcoin.address.compileHash(s[2], 'pubkeyhash') // Convert bare multisig to scripthash address if (script.isMultisig(s)) { s = s._raw || script.encode(s); - return bcoin.address.compile(s, 'scripthash'); + return bcoin.address.compileData(s, 'scripthash'); } if (script.isScripthash(s)) - return bcoin.address.toAddress(s[1], 'scripthash'); + return bcoin.address.compileHash(s[1], 'scripthash'); }; script.getInputMN = function getInputMN(s, prev) { @@ -1888,15 +2044,105 @@ script.isNulldata = function isNulldata(s) { return true; }; -script.getInputType = function getInputType(s, prev) { +script.isCommitment = function isCommitment(s) { + return s.length >= 2 + && s[0] === 'return' + && Buffer.isBuffer(s[1]) + && s[1].length === 36 + && utils.readU32(s[1], 0) === 0xeda921aa24; +}; + +script.getWitnessRoot = function getWitnessRoot(s) { + if (!script.isCommitment(s)) + return; + + return s[1].slice(4, 36); +}; + +script.isWitnessProgram = function isWitnessProgram(s) { + if (s.length !== 2) + return false; + + if (typeof s[0] !== 'number') + return false; + + if (!Buffer.isBuffer(s[1])) + return false; + + return s[0] >= 0 && s[0] <= 16 + && s[1].length >= 2 && s[1].length <= 32; +}; + +script.getWitnessProgram = function getWitnessProgram(s) { + var version, data, type; + + if (!script.isWitnessProgram(s)) + return; + + version = s[0]; + data = s[1]; + + if (version > 0) { + // No interpretation of script (anyone can spend) + type = 'witnessunknown'; + } else if (version === 0 && data.length === 20) { + type = 'witnesspubkeyhash'; + } else if (version === 0 && data.length === 32) { + type = 'witnessscripthash'; + } else { + // Fail on bad version=0 + type = null; + } + + return { + version: version, + type: type, + data: data + }; +}; + +script.isWitnessPubkeyhash = function isWitnessPubkeyhash(s) { + if (!script.isWitnessProgram(s)) + return false; + + return s[0] === 0 && s[1].length === 20; +}; + +script.isWitnessScripthash = function isWitnessScripthash(s) { + if (!script.isWitnessProgram(s)) + return false; + + return s[0] === 0 && s[1].length === 32; +}; + +script.createWitnessProgram = function createWitnessProgram(version, data) { + assert(typeof version === 'number' && version >= 0 && version <= 16); + assert(Buffer.isBuffer(data)); + assert(data.length === 20 || data.length === 32); + return [version, data]; +}; + +script.getInputType = function getInputType(s, prev, isWitness) { + var type; + if (prev) return script.getOutputType(prev); - return (script.isPubkeyInput(s) && 'pubkey') + type = (script.isPubkeyInput(s) && 'pubkey') || (script.isPubkeyhashInput(s) && 'pubkeyhash') || (script.isMultisigInput(s) && 'multisig') || (script.isScripthashInput(s) && 'scripthash') - || null; + || 'unknown'; + + if (isWitness) { + if (type === 'pubkeyhash') + return 'witnesspubkeyhash'; + if (type === 'scripthash') + return 'witnessscripthash'; + return 'unknown'; + } + + return type; }; script.isPubkeyInput = function isPubkeyInput(s, key, tx, index) { @@ -1956,7 +2202,7 @@ script.isMultisigInput = function isMultisigInput(s, keys, tx, index) { if (s.length < 3) return false; - if (!Buffer.isBuffer(s[0]) || s[0].length !== 0) + if (s[0] !== 0 && !script.isDummy(s[0])) return false; for (i = 1; i < s.length; i++) { @@ -1976,7 +2222,7 @@ script.isMultisigInput = function isMultisigInput(s, keys, tx, index) { return false; } - // We also also try to recover the keys from the signatures. + // We can also try to recover the keys from the signatures. // if (keys) { // var prev, recovered, j, total; // recovered = []; @@ -2007,8 +2253,9 @@ script.isScripthashInput = function isScripthashInput(s, redeem) { raw = s[s.length - 1]; // Need at least one data element with - // the redeem script. - if (s.length < 2) + // the redeem script. NOTE: NOT THE CASE FOR SEGWIT! + // if (s.length < 2) + if (s.length < 1) return false; // Last data element should be an array @@ -2392,6 +2639,31 @@ script.getScripthashSigops = function getScripthashSigops(s, prev) { s = script.getRedeem(s); + // Need to do this too + // if (script.isWitnessProgram(s)) { + // call witness sigops + // } + + return script.getSigops(s, true); +}; + +script.getWitnessSigops = function getWitnessSigops(s) { + if (script.isWitnessPubkeyhash(s)) + return 1; + return 0; +}; + +script.getWitnessScripthashSigops = function getWitnessScripthashSigops(witness, prev) { + var redeem; + + if (!prev) + return 0; + + if (!script.isWitnessScripthash(prev)) + return 0; + + redeem = script.getRedeem(witness); + return script.getSigops(s, true); }; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 0b553561..4b55127b 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -33,9 +33,12 @@ function TX(data, block, index) { this._hash = null; this._raw = data._raw || null; + this._wraw = data._wraw || null; this._size = data._size || 0; this._offset = data._offset || 0; + this.witness = data.witness || false; + this.height = data.height != null ? data.height : -1; this._chain = data.chain; @@ -60,6 +63,13 @@ function TX(data, block, index) { } } + if (this.witness) { + this._wsize = this._raw.length; + this._wraw = this._raw; + this._raw = null; + this._size = 0; + } + if (!this._raw) this._raw = this.render(); @@ -99,6 +109,12 @@ TX.prototype.clone = function clone() { return tx; }; +TX.prototype.txid = function txid(enc) { + if (this.hasWitness()) + return this.witnessHash(enc); + return this.hash(enc); +}; + TX.prototype.hash = function hash(enc) { if (!this._hash) this._hash = utils.dsha256(this.render()); @@ -106,10 +122,36 @@ TX.prototype.hash = function hash(enc) { return enc === 'hex' ? utils.toHex(this._hash) : this._hash; }; +TX.prototype.witnessHash = function witnessHash(enc) { + if (!this._whash) + this._whash = utils.dsha256(this.renderWitness()); + + return enc === 'hex' ? utils.toHex(this._whash) : this._whash; +}; + +TX.prototype.hasWitness = function hasWitness() { + if (this.inputs.length === 0) + return false; + + return this.inputs.every(function(input) { + return input.witness.length > 0; + }); +}; + TX.prototype.render = function render() { - if (this._raw) - return this._raw; - return bcoin.protocol.framer.tx(this); + if (!this._raw) { + this._raw = bcoin.protocol.framer.tx(this); + this._size = this._raw.length; + } + return this._raw; +}; + +TX.prototype.renderWitness = function renderWitness() { + if (!this._wraw) { + this._wraw = bcoin.protocol.framer.witnessTX(this); + this._wsize = this._wraw.length; + } + return this._wraw; }; TX.prototype.getSize = function getSize() { @@ -217,7 +259,7 @@ TX.prototype.signatureHash = function signatureHash(index, s, type) { return hash; }; -TX.prototype.tbsHash = function tbsHash(enc, force) { +TX.prototype.normalizedHash = function normalizedHash(enc, force) { var copy = this.clone(); var i; @@ -265,7 +307,14 @@ TX.prototype.verify = function verify(index, force, flags) { return false; } - return bcoin.script.verify(input.script, input.output.script, this, i, flags); + return bcoin.script.verify( + input.script, + input.witness, + input.output.script, + this, + i, + flags + ); }, this); }; @@ -489,6 +538,24 @@ TX.prototype.getSigops = function getSigops(scriptHash, accurate) { return n; }; +TX.prototype.getSigopsCost = function getSigopsCost(scriptHash, accurate) { + var n = 0; + this.inputs.forEach(function(input) { + var prev; + n += bcoin.script.getSigops(input.script, accurate) * 4; + if (scriptHash && !this.isCoinbase()) { + prev = input.output ? input.output.script : null; + n += bcoin.script.getScripthashSigops(input.script, prev) * 4; + } + n += bcoin.script.getWitnessScripthashSigops(input.witness, prev); + }, this); + this.outputs.forEach(function(output) { + n += bcoin.script.getSigops(output.script, accurate) * 4; + n += bcoin.script.getWitnessSigops(output.script); + }, this); + return n; +}; + TX.prototype.isStandard = function isStandard(flags) { var i, input, output, type; var nulldata = 0; diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index c052a60d..8b790a77 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -1593,12 +1593,12 @@ utils.once = function once(callback) { return onceFn; }; -utils.buildMerkleTree = function buildMerkleTree(items) { - var tree = items.slice(); +utils.buildMerkleTree = function buildMerkleTree(leaves) { + var tree = leaves.slice(); var i, j, size, i2, hash; j = 0; - size = items.length; + size = leaves.length; for (; size > 1; size = ((size + 1) / 2) | 0) { for (i = 0; i < size; i += 2) { @@ -1620,16 +1620,16 @@ utils.buildMerkleTree = function buildMerkleTree(items) { return tree; }; -utils.getMerkleRoot = function getMerkleRoot(items) { - var tree = utils.buildMerkleTree(items); +utils.getMerkleRoot = function getMerkleRoot(leaves) { + var tree = utils.buildMerkleTree(leaves); if (!tree) return; return tree[tree.length - 1]; }; -utils.getMerkleBranch = function getMerkleBranch(index, hashes) { - var tree = utils.buildMerkleTree(hashes); +utils.getMerkleBranch = function getMerkleBranch(index, leaves) { + var tree = utils.buildMerkleTree(leaves); var branch = []; var size = this.totalTX; var j = 0; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 6b477e82..e0ed5927 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -72,7 +72,7 @@ function Wallet(options) { assert(this.type === 'pubkeyhash' || this.type === 'multisig'); this.prefixType = this.type === 'multisig' ? 'scripthash' : 'pubkeyhash'; - if (network.prefixes[this.prefixType] == null) + if (network.address.prefixes[this.prefixType] == null) throw new Error('Unknown prefix: ' + this.prefixType); if (this.m < 1 || this.m > this.n) @@ -281,7 +281,7 @@ Wallet.prototype.getID = function getID() { if (this.options.id) return this.options.id; - return bcoin.address.compile(this.accountKey.publicKey); + return bcoin.address.compileData(this.accountKey.publicKey); }; Wallet.prototype.createReceive = function createReceive() { diff --git a/scripts/gen.js b/scripts/gen.js index 178bf016..8762c8ef 100644 --- a/scripts/gen.js +++ b/scripts/gen.js @@ -4,7 +4,7 @@ var utils = bcoin.utils; var bn = bcoin.bn; function createGenesisBlock(options) { - var parser = new bcoin.protocol.parser(); + var parser = bcoin.protocol.parser; var tx, block; if (!options.flags) { @@ -98,9 +98,17 @@ var regtest = createGenesisBlock({ nonce: 2 }); +var segnet = createGenesisBlock({ + version: 1, + ts: 1452368293, + bits: 0x1d00ffff, + nonce: 0 +}); + utils.print(main); utils.print(testnet); utils.print(regtest); +utils.print(segnet); utils.print('main hash: %s', main.hash); utils.print('main raw: %s', utils.toHex(main._raw)); utils.print(''); @@ -109,3 +117,5 @@ utils.print('testnet raw: %s', utils.toHex(testnet._raw)); utils.print(''); utils.print('regtest hash: %s', regtest.hash); utils.print('regtest raw: %s', utils.toHex(regtest._raw)); +utils.print('segnet hash: %s', segnet.hash); +utils.print('segnet raw: %s', utils.toHex(segnet._raw)); diff --git a/test/wallet-test.js b/test/wallet-test.js index 53077275..87426abf 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -3,6 +3,24 @@ var bn = require('bn.js'); var bcoin = require('../'); var constants = bcoin.protocol.constants; +var dummyInput = { + prevout: { + hash: constants.zeroHash, + index: 0 + }, + output: { + version: 1, + height: 0, + value: constants.maxMoney.clone(), + script: [], + hash: constants.zeroHash, + index: 0, + spent: false + }, + script: [], + sequence: 0xffffffff +}; + describe('Wallet', function() { it('should generate new key and address', function() { var w = bcoin.wallet(); @@ -29,9 +47,10 @@ describe('Wallet', function() { address: w.getAddress() }, { value: 5460 * 2, - address: w.getAddress() + 'x' + address: bcoin.address.compileData(new Buffer([])) }] }); + src.addInput(dummyInput); assert(w.ownOutput(src)); assert.equal(w.ownOutput(src).reduce(function(acc, out) { return acc.iadd(out.value); @@ -63,9 +82,10 @@ describe('Wallet', function() { keys: [ w.getPublicKey(), k2.derive('m/0/0').publicKey ] }, { value: 5460 * 2, - address: w.getAddress() + 'x' + address: bcoin.address.compileData(new Buffer([])) }] }); + src.addInput(dummyInput); assert(w.ownOutput(src)); assert.equal(w.ownOutput(src).reduce(function(acc, out) { return acc.iadd(out.value); @@ -87,6 +107,7 @@ describe('Wallet', function() { // Coinbase var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); + t1.addInput(dummyInput); // balance: 51000 w.sign(t1); var t2 = bcoin.mtx().addInput(t1, 0) // 50000 @@ -191,10 +212,12 @@ describe('Wallet', function() { // Coinbase var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); + t1.addInput(dummyInput); // Fake TX should temporarly change output w1.addTX(t1); // Coinbase var t2 = bcoin.mtx().addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460); + t2.addInput(dummyInput); // Fake TX should temporarly change output w2.addTX(t2); @@ -290,6 +313,7 @@ describe('Wallet', function() { // Add a shared unspent transaction to our wallets var utx = bcoin.mtx(); utx.addOutput({ address: addr, value: 5460 * 10 }); + utx.addInput(dummyInput); // Simulate a confirmation utx.ps = 0;