block/crypto: refactor merkle trees.

This commit is contained in:
Christopher Jeffrey 2016-11-28 14:28:28 -08:00
parent 6520cf32e0
commit d1e37582d1
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
11 changed files with 254 additions and 172 deletions

View File

@ -22,7 +22,7 @@ log-file: true
# witness: true
# prune: false
use-checkpoints: true
coin-cache: true
coin-cache: 40000000
index-tx: false
index-address: false

View File

@ -280,7 +280,7 @@ Chain.prototype.isGenesis = function isGenesis(block) {
Chain.prototype.verify = co(function* verify(block, prev) {
var ret = new VerifyResult();
var i, err, height, ts, tx, medianTime;
var commitmentHash, ancestors, state;
var commit, ancestors, state;
// Skip the genesis block.
if (this.isGenesis(block))
@ -358,15 +358,15 @@ Chain.prototype.verify = co(function* verify(block, prev) {
// Check the commitment hash for segwit.
if (state.hasWitness()) {
commitmentHash = block.commitmentHash;
if (commitmentHash) {
commit = block.getCommitmentHash();
if (commit) {
// These are totally malleable. Someone
// may have even accidentally sent us
// the non-witness version of the block.
// We don't want to consider this block
// "invalid" if either of these checks
// fail.
if (!block.witnessNonce) {
if (!block.getWitnessNonce()) {
err = new VerifyError(block,
'invalid',
'bad-witness-merkle-size',
@ -375,7 +375,7 @@ Chain.prototype.verify = co(function* verify(block, prev) {
throw err;
}
if (commitmentHash !== block.getCommitmentHash('hex')) {
if (!util.equal(commit, block.createCommitmentHash())) {
err = new VerifyError(block,
'invalid',
'bad-witness-merkle-match',
@ -388,7 +388,7 @@ Chain.prototype.verify = co(function* verify(block, prev) {
// Blocks that do not commit to
// witness data cannot contain it.
if (!commitmentHash) {
if (!commit) {
if (block.hasWitness()) {
err = new VerifyError(block,
'invalid',

View File

@ -217,59 +217,64 @@ crypto.hkdfExpand = function hkdfExpand(prk, info, len, alg) {
/**
* Build a merkle tree from leaves.
* Note that this will mutate the `leaves` array!
* @param {Buffer[]} leaves
* @returns {Buffer[]} Tree (in rare cases this may return null).
* @returns {MerkleTree}
*/
crypto.buildMerkleTree = function buildMerkleTree(leaves) {
var tree = leaves.slice();
crypto.createMerkleTree = function createMerkleTree(leaves) {
var nodes = leaves;
var size = leaves.length;
var i, j, i2, hash, left, right, buf;
var malleated = false;
var i, j, k, hash, left, right, lr;
if (size > 1)
buf = new Buffer(64);
if (size === 0) {
hash = new Buffer(32);
hash.fill(0);
nodes.push(hash);
return new MerkleTree(nodes, malleated);
}
lr = new Buffer(64);
for (j = 0; size > 1; size = ((size + 1) / 2) | 0) {
for (i = 0; i < size; i += 2) {
i2 = Math.min(i + 1, size - 1);
left = tree[j + i];
right = tree[j + i2];
k = Math.min(i + 1, size - 1);
left = nodes[j + i];
right = nodes[j + k];
if (i2 === i + 1 && i2 + 1 === size
if (k === i + 1 && k + 1 === size
&& left.compare(right) === 0) {
return;
malleated = true;
}
left.copy(buf, 0);
right.copy(buf, 32);
hash = crypto.hash256(buf);
left.copy(lr, 0);
right.copy(lr, 32);
tree.push(hash);
hash = crypto.hash256(lr);
nodes.push(hash);
}
j += size;
}
if (tree.length === 0)
return;
return tree;
return new MerkleTree(nodes, malleated);
};
if (native)
crypto.buildMerkleTree = native.buildMerkleTree;
crypto.createMerkleTree = native.createMerkleTree;
/**
* Calculate merkle root from leaves.
* @param {Buffer[]} leaves
* @returns {Buffer?} Merkle root.
* @returns {MerkleRoot}
*/
crypto.getMerkleRoot = function getMerkleRoot(leaves) {
var tree = crypto.buildMerkleTree(leaves);
if (!tree)
return;
return tree[tree.length - 1];
crypto.createMerkleRoot = function createMerkleRoot(leaves) {
var tree = crypto.createMerkleTree(leaves);
var hash = tree.nodes[tree.nodes.length - 1];
var malleated = tree.malleated;
return new MerkleRoot(hash, malleated);
};
/**
@ -279,16 +284,16 @@ crypto.getMerkleRoot = function getMerkleRoot(leaves) {
* @returns {Buffer[]} branch
*/
crypto.getMerkleBranch = function getMerkleBranch(index, leaves) {
var tree = crypto.buildMerkleTree(leaves);
crypto.createMerkleBranch = function createMerkleBranch(index, leaves) {
var size = leaves.length;
var tree = crypto.createMerkleTree(leaves);
var branch = [];
var j = 0;
var i;
for (; size > 1; size = (size + 1) / 2 | 0) {
i = Math.min(index ^ 1, size - 1);
branch.push(tree[j + i]);
branch.push(tree.nodes[j + i]);
index >>>= 1;
j += size;
}
@ -304,26 +309,26 @@ crypto.getMerkleBranch = function getMerkleBranch(index, leaves) {
* @returns {Buffer} Hash.
*/
crypto.checkMerkleBranch = function checkMerkleBranch(hash, branch, index) {
var i, otherside, buf;
crypto.verifyMerkleBranch = function verifyMerkleBranch(hash, branch, index) {
var i, otherside, lr;
if (branch.length === 0)
return hash;
buf = new Buffer(64);
lr = new Buffer(64);
for (i = 0; i < branch.length; i++) {
otherside = branch[i];
if (index & 1) {
otherside.copy(buf, 0);
hash.copy(buf, 32);
otherside.copy(lr, 0);
hash.copy(lr, 32);
} else {
hash.copy(buf, 0);
otherside.copy(buf, 32);
hash.copy(lr, 0);
otherside.copy(lr, 32);
}
hash = crypto.hash256(buf);
hash = crypto.hash256(lr);
index >>>= 1;
}
@ -331,7 +336,7 @@ crypto.checkMerkleBranch = function checkMerkleBranch(hash, branch, index) {
};
if (native)
crypto.checkMerkleBranch = native.checkMerkleBranch;
crypto.verifyMerkleBranch = native.verifyMerkleBranch;
/**
* Encrypt with aes-256-cbc.
@ -465,3 +470,17 @@ crypto.randomRange = function randomRange(min, max) {
var num = crypto.randomInt();
return Math.floor((num / 0x100000000) * (max - min) + min);
};
/*
* Helpers
*/
function MerkleTree(nodes, malleated) {
this.nodes = nodes;
this.malleated = malleated;
}
function MerkleRoot(hash, malleated) {
this.hash = hash;
this.malleated = malleated;
}

View File

@ -19,10 +19,10 @@ exports.scrypt = crypto.scrypt;
exports.scryptAsync = crypto.scryptAsync;
exports.hkdfExtract = crypto.hkdfExtract;
exports.hkdfExpand = crypto.hkdfExpand;
exports.buildMerkleTree = crypto.buildMerkleTree;
exports.getMerkleRoot = crypto.getMerkleRoot;
exports.getMerkleBranch = crypto.getMerkleBranch;
exports.checkMerkleBranch = crypto.checkMerkleBranch;
exports.createMerkleTree = crypto.createMerkleTree;
exports.createMerkleRoot = crypto.createMerkleRoot;
exports.createMerkleBranch = crypto.createMerkleBranch;
exports.verifyMerkleBranch = crypto.verifyMerkleBranch;
exports.encipher = crypto.encipher;
exports.decipher = crypto.decipher;
exports.ccmp = crypto.ccmp;

View File

@ -107,7 +107,16 @@ MinerBlock.prototype._init = function _init() {
var scale = constants.WITNESS_SCALE_FACTOR;
var block = this.block;
var cb = this.coinbase;
var input, output, hash, witnessNonce;
var input, output, nonce;
// Setup our block.
block.version = this.version;
block.prevBlock = this.tip.hash;
block.merkleRoot = constants.NULL_HASH;
block.ts = Math.max(time.now(), this.tip.ts + 1);
block.bits = this.bits;
block.nonce = 0;
block.height = this.height;
// Coinbase input.
input = new Input();
@ -145,26 +154,16 @@ MinerBlock.prototype._init = function _init() {
if (this.witness) {
// Our witness nonce is the hash256
// of the previous block hash.
hash = new Buffer(this.tip.hash, 'hex');
witnessNonce = crypto.hash256(hash);
nonce = block.createWitnessNonce();
// Set up the witness nonce.
input.witness.set(0, witnessNonce);
input.witness.set(0, nonce);
input.witness.compile();
// Commitment output.
cb.outputs.push(new Output());
}
// Setup our block.
block.version = this.version;
block.prevBlock = this.tip.hash;
block.merkleRoot = constants.NULL_HASH;
block.ts = Math.max(time.now(), this.tip.ts + 1);
block.bits = this.bits;
block.nonce = 0;
block.height = this.height;
block.txs.push(cb);
// Update coinbase since our coinbase was added.
@ -196,7 +195,7 @@ MinerBlock.prototype.updateCommitment = function updateCommitment() {
var hash;
// Recalculate witness merkle root.
hash = this.block.getCommitmentHash();
hash = this.block.createCommitmentHash();
// Update commitment.
output.script.clear();
@ -253,7 +252,7 @@ MinerBlock.prototype.updateMerkle = function updateMerkle() {
this.block.ts = Math.max(time.now(), this.tip.ts + 1);
// Recalculate merkle root.
this.block.merkleRoot = this.block.getMerkleRoot('hex');
this.block.merkleRoot = this.block.createMerkleRoot('hex');
};
/**

View File

@ -146,35 +146,35 @@ Block.prototype.getRaw = function getRaw() {
/**
* Calculate real size and size of the witness bytes.
* @returns {Object} Contains `size` and `witnessSize`.
* @returns {Object} Contains `total` and `witness`.
*/
Block.prototype.getSizes = function getSizes() {
var sizes = new BlockSizes();
var writer;
if (this._size !== -1) {
return {
size: this._size,
witnessSize: this._witnessSize
};
sizes.total = this._size;
sizes.witness = this._witnessSize;
return sizes;
}
if (!this.mutable) {
assert(!this._raw);
this.getRaw();
return {
size: this._size,
witnessSize: this._witnessSize
};
sizes.total = this._size;
sizes.witness = this._witnessSize;
return sizes;
}
writer = new BufferWriter();
this.toRaw(writer);
return {
size: writer.written,
witnessSize: this._lastWitnessSize
};
sizes.total = writer.written;
sizes.witness = this._lastWitnessSize;
return sizes;
};
/**
@ -194,8 +194,8 @@ Block.prototype.getVirtualSize = function getVirtualSize() {
Block.prototype.getWeight = function getWeight() {
var sizes = this.getSizes();
var base = sizes.size - sizes.witnessSize;
return base * (constants.WITNESS_SCALE_FACTOR - 1) + sizes.size;
var base = sizes.total - sizes.witness;
return base * (constants.WITNESS_SCALE_FACTOR - 1) + sizes.total;
};
/**
@ -204,7 +204,7 @@ Block.prototype.getWeight = function getWeight() {
*/
Block.prototype.getSize = function getSize() {
return this.getSizes().size;
return this.getSizes().total;
};
/**
@ -214,7 +214,7 @@ Block.prototype.getSize = function getSize() {
Block.prototype.getBaseSize = function getBaseSize() {
var sizes = this.getSizes();
return sizes.size - sizes.witnessSize;
return sizes.total - sizes.witness;
};
/**
@ -280,106 +280,148 @@ Block.prototype.indexOf = function indexOf(hash) {
};
/**
* Calculate merkle root.
* Calculate merkle root. Returns null
* if merkle tree has been malleated.
* @param {String?} enc - Encoding, can be `'hex'` or null.
* @returns {Buffer|Hash} hash
* @returns {Hash|null}
*/
Block.prototype.getMerkleRoot = function getMerkleRoot(enc) {
Block.prototype.createMerkleRoot = function createMerkleRoot(enc) {
var leaves = [];
var i, root;
for (i = 0; i < this.txs.length; i++)
leaves.push(this.txs[i].hash());
root = crypto.getMerkleRoot(leaves);
root = crypto.createMerkleRoot(leaves);
if (!root)
return;
if (root.malleated)
return null;
return enc === 'hex'
? root.toString('hex')
: root;
? root.hash.toString('hex')
: root.hash;
};
/**
* Create a witness nonce (for mining).
* @returns {Buffer}
*/
Block.prototype.createWitnessNonce = function createWitnessNonce() {
return crypto.hash256(new Buffer(this.prevBlock, 'hex'));
};
/**
* Calculate commitment hash (the root of the
* witness merkle tree hashed with the witnessNonce).
* @param {String?} enc - Encoding, can be `'hex'` or null.
* @returns {Buffer|Hash} hash
* @returns {Hash}
*/
Block.prototype.createCommitmentHash = function createCommitmentHash(enc) {
var nonce = this.getWitnessNonce();
var leaves = [];
var i, root, data, hash;
assert(nonce, 'No witness nonce present.');
leaves.push(constants.ZERO_HASH);
for (i = 1; i < this.txs.length; i++)
leaves.push(this.txs[i].witnessHash());
root = crypto.createMerkleRoot(leaves);
// Note: malleation check ignored here.
// assert(!root.malleated);
data = util.concat(root.hash, nonce);
hash = crypto.hash256(data);
return enc === 'hex'
? hash.toString('hex')
: hash;
};
/**
* Retrieve the merkle root from the block header.
* @param {String?} enc
* @returns {Hash}
*/
Block.prototype.getMerkleRoot = function getMerkleRoot(enc) {
if (enc === 'hex')
return this.merkleRoot;
return new Buffer(this.merkleRoot, 'hex');
};
/**
* Retrieve the witness nonce from the
* coinbase's witness vector (if present).
* @returns {Buffer|null}
*/
Block.prototype.getWitnessNonce = function getWitnessNonce() {
var coinbase = this.txs[0];
var input;
if (!coinbase)
return null;
if (coinbase.inputs.length !== 1)
return null;
input = coinbase.inputs[0];
if (input.witness.items.length !== 1)
return null;
if (input.witness.items[0].length !== 32)
return null;
return input.witness.items[0];
};
/**
* Retrieve the commitment hash
* from the coinbase's outputs.
* @param {String?} enc
* @returns {Hash|null}
*/
Block.prototype.getCommitmentHash = function getCommitmentHash(enc) {
var leaves = [];
var witnessNonce = this.witnessNonce;
var i, buf, witnessRoot, commitmentHash;
var hash = this._commitmentHash;
var i, coinbase, output;
if (!witnessNonce)
return;
if (!hash) {
coinbase = this.txs[0];
for (i = 0; i < this.txs.length; i++)
leaves.push(this.txs[i].witnessHash());
if (!coinbase)
return null;
witnessRoot = crypto.getMerkleRoot(leaves);
for (i = coinbase.outputs.length - 1; i >= 0; i--) {
output = coinbase.outputs[i];
if (!witnessRoot)
return;
if (output.script.isCommitment()) {
hash = output.script.getCommitmentHash();
buf = new Buffer(64);
witnessRoot.copy(buf, 0);
witnessNonce.copy(buf, 32);
if (!this.mutable)
this._commitmentHash = hash;
commitmentHash = crypto.hash256(buf);
return enc === 'hex'
? commitmentHash.toString('hex')
: commitmentHash;
};
Block.prototype.__defineGetter__('witnessNonce', function() {
var coinbase = this.txs[0];
if (!coinbase)
return;
if (coinbase.inputs.length !== 1)
return;
if (coinbase.inputs[0].witness.items.length !== 1)
return;
if (coinbase.inputs[0].witness.items[0].length !== 32)
return;
return coinbase.inputs[0].witness.items[0];
});
Block.prototype.__defineGetter__('commitmentHash', function() {
var i, coinbase, script, commitmentHash;
if (this._commitmentHash)
return this._commitmentHash;
coinbase = this.txs[0];
if (!coinbase)
return;
for (i = coinbase.outputs.length - 1; i >= 0; i--) {
script = coinbase.outputs[i].script;
if (script.isCommitment()) {
commitmentHash = script.getCommitmentHash();
commitmentHash = commitmentHash.toString('hex');
if (!this.mutable)
this._commitmentHash = commitmentHash;
break;
break;
}
}
if (!hash)
return null;
}
return commitmentHash;
});
return enc === 'hex'
? hash.toString('hex')
: hash;
};
/**
* Do non-contextual verification on the block. Including checking the block
@ -441,7 +483,7 @@ Block.prototype._verify = function _verify(ret) {
}
// Check merkle root
merkle = this.getMerkleRoot('hex');
merkle = this.createMerkleRoot('hex');
// If the merkle is mutated,
// we have duplicate txs.
@ -563,6 +605,7 @@ Block.prototype.getPrevout = function getPrevout() {
*/
Block.prototype.inspect = function inspect() {
var commitmentHash = this.getCommitmentHash('hex');
return {
hash: this.rhash,
height: this.height,
@ -572,8 +615,8 @@ Block.prototype.inspect = function inspect() {
version: util.hex32(this.version),
prevBlock: util.revHex(this.prevBlock),
merkleRoot: util.revHex(this.merkleRoot),
commitmentHash: this.commitmentHash
? util.revHex(this.commitmentHash)
commitmentHash: commitmentHash
? util.revHex(commitmentHash)
: null,
ts: this.ts,
bits: this.bits,
@ -783,6 +826,15 @@ Block.isBlock = function isBlock(obj) {
&& typeof obj.getClaimed === 'function';
};
/*
* Helpers
*/
function BlockSizes() {
this.total = 0;
this.witness = 0;
}
/*
* Expose
*/

View File

@ -356,24 +356,24 @@ TX.prototype.getRaw = function getRaw() {
*/
TX.prototype.getSizes = function getSizes() {
var sizes = new TXSizes();
var writer;
if (this.mutable) {
assert(!this._raw);
writer = new BufferWriter();
this.toRaw(writer);
return {
size: writer.written,
witnessSize: this._lastWitnessSize
};
sizes.total = writer.written;
sizes.witness = this._lastWitnessSize;
return sizes;
}
this.getRaw();
return {
size: this._size,
witnessSize: this._witnessSize
};
sizes.total = this._size;
sizes.witness = this._witnessSize;
return sizes;
};
/**
@ -395,8 +395,8 @@ TX.prototype.getVirtualSize = function getVirtualSize() {
TX.prototype.getWeight = function getWeight() {
var sizes = this.getSizes();
var base = sizes.size - sizes.witnessSize;
return base * (constants.WITNESS_SCALE_FACTOR - 1) + sizes.size;
var base = sizes.total - sizes.witness;
return base * (constants.WITNESS_SCALE_FACTOR - 1) + sizes.total;
};
/**
@ -406,7 +406,7 @@ TX.prototype.getWeight = function getWeight() {
*/
TX.prototype.getSize = function getSize() {
return this.getSizes().size;
return this.getSizes().total;
};
/**
@ -418,7 +418,7 @@ TX.prototype.getSize = function getSize() {
TX.prototype.getBaseSize = function getBaseSize() {
var sizes = this.getSizes();
return sizes.size - sizes.witnessSize;
return sizes.total - sizes.witness;
};
/**
@ -2541,6 +2541,15 @@ TX.isTX = function isTX(obj) {
&& typeof obj.witnessHash === 'function';
};
/*
* Helpers
*/
function TXSizes() {
this.total = 0;
this.witness = 0;
}
/*
* Expose
*/

View File

@ -3357,7 +3357,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) {
}
scriptRoot = crypto.hash256(scriptRoot.render());
scriptRoot = crypto.checkMerkleBranch(scriptRoot, path, pos);
scriptRoot = crypto.verifyMerkleBranch(scriptRoot, path, pos);
mastRoot.writeBytes(scriptRoot);
mastRoot = crypto.hash256(mastRoot.render());

View File

@ -186,12 +186,15 @@ util.isHex = function isHex(obj) {
util.equal = function equal(a, b) {
var i;
if (!Buffer.isBuffer(a))
if (a == null)
return false;
if (!Buffer.isBuffer(b))
if (b == null)
return false;
assert(Buffer.isBuffer(a));
assert(Buffer.isBuffer(b));
if (a.compare)
return a.compare(b) === 0;

View File

@ -41,7 +41,7 @@
"elliptic": "6.3.2"
},
"optionalDependencies": {
"bcoin-native": "0.0.11",
"bcoin-native": "0.0.12",
"leveldown": "1.5.0",
"secp256k1": "3.2.0",
"socket.io": "1.4.8",

View File

@ -126,7 +126,7 @@ describe('Block', function() {
'8cc72c02a958de5a8b35a23bb7e3bced8bf840cc0a4e1c820000000000000000');
assert.equal(block.rhash,
'0000000000000000821c4e0acc40f88bedbce3b73ba2358b5ade58a9022cc78c');
assert.equal(block.merkleRoot, block.getMerkleRoot('hex'));
assert.equal(block.merkleRoot, block.createMerkleRoot('hex'));
});
it('should create a merkle block', function() {