diff --git a/src/block.ts b/src/block.ts index 1d69d0b..b2602dd 100644 --- a/src/block.ts +++ b/src/block.ts @@ -5,174 +5,224 @@ const typeforce = require('typeforce') const types = require('./types') const varuint = require('varuint-bitcoin') -const Transaction = require('./transaction') - -function Block () { - this.version = 1 - this.prevHash = null - this.merkleRoot = null - this.timestamp = 0 - this.bits = 0 - this.nonce = 0 +const errorMerkleNoTxes = new TypeError('Cannot compute merkle root for zero transactions') +const errorWitnessNotSegwit = new TypeError('Cannot compute witness commit for non-segwit block') +const errorBufferTooSmall = new Error('Buffer too small (< 80 bytes)') + +function txesHaveWitness (transactions: Array): boolean { + return transactions !== undefined && + transactions instanceof Array && + transactions[0] && + transactions[0].ins && + transactions[0].ins instanceof Array && + transactions[0].ins[0] && + transactions[0].ins[0].witness && + transactions[0].ins[0].witness instanceof Array && + transactions[0].ins[0].witness.length > 0 } -Block.fromBuffer = function (buffer) { - if (buffer.length < 80) throw new Error('Buffer too small (< 80 bytes)') - - let offset = 0 - function readSlice (n) { - offset += n - return buffer.slice(offset - n, offset) - } - - function readUInt32 () { - const i = buffer.readUInt32LE(offset) - offset += 4 - return i - } +const Transaction = require('./transaction') - function readInt32 () { - const i = buffer.readInt32LE(offset) - offset += 4 - return i +export class Block { + version: number + prevHash: Buffer + merkleRoot: Buffer + timestamp: number + witnessCommit: Buffer + bits: number + nonce: number + transactions: Array + + constructor () { + this.version = 1 + this.timestamp = 0 + this.bits = 0 + this.nonce = 0 } - const block = new Block() - block.version = readInt32() - block.prevHash = readSlice(32) - block.merkleRoot = readSlice(32) - block.timestamp = readUInt32() - block.bits = readUInt32() - block.nonce = readUInt32() - - if (buffer.length === 80) return block - - function readVarInt () { - const vi = varuint.decode(buffer, offset) - offset += varuint.decode.bytes - return vi + static fromBuffer (buffer: Buffer): Block { + if (buffer.length < 80) throw errorBufferTooSmall + + let offset: number = 0 + const readSlice = (n: number): Buffer => { + offset += n + return buffer.slice(offset - n, offset) + } + + const readUInt32 = (): number => { + const i = buffer.readUInt32LE(offset) + offset += 4 + return i + } + + const readInt32 = (): number => { + const i = buffer.readInt32LE(offset) + offset += 4 + return i + } + + const block = new Block() + block.version = readInt32() + block.prevHash = readSlice(32) + block.merkleRoot = readSlice(32) + block.timestamp = readUInt32() + block.bits = readUInt32() + block.nonce = readUInt32() + + if (buffer.length === 80) return block + + const readVarInt = (): number => { + const vi = varuint.decode(buffer, offset) + offset += varuint.decode.bytes + return vi + } + + const readTransaction = (): any => { + const tx = Transaction.fromBuffer(buffer.slice(offset), true) + offset += tx.byteLength() + return tx + } + + const nTransactions = readVarInt() + block.transactions = [] + + for (var i = 0; i < nTransactions; ++i) { + const tx = readTransaction() + block.transactions.push(tx) + } + + // This Block contains a witness commit + if (block.hasWitnessCommit()) { + // The merkle root for the witness data is in an OP_RETURN output. + // There is no rule for the index of the output, so use filter to find it. + // The root is prepended with 0xaa21a9ed so check for 0x6a24aa21a9ed + // If multiple commits are found, the output with highest index is assumed. + let witnessCommits = block.transactions[0].outs + .filter(out => out.script.slice(0, 6).equals(Buffer.from('6a24aa21a9ed', 'hex'))) + .map(out => out.script.slice(6, 38)) + + // Use the commit with the highest output (should only be one though) + block.witnessCommit = witnessCommits[witnessCommits.length - 1] + } + + return block } - function readTransaction () { - const tx = Transaction.fromBuffer(buffer.slice(offset), true) - offset += tx.byteLength() - return tx + static fromHex (hex: string): Block { + return Block.fromBuffer(Buffer.from(hex, 'hex')) } - const nTransactions = readVarInt() - block.transactions = [] - - for (var i = 0; i < nTransactions; ++i) { - const tx = readTransaction() - block.transactions.push(tx) + static calculateTarget (bits: number): Buffer { + const exponent = ((bits & 0xff000000) >> 24) - 3 + const mantissa = bits & 0x007fffff + const target = Buffer.alloc(32, 0) + target.writeUIntBE(mantissa, 29 - exponent, 3) + return target } - return block -} - -Block.prototype.byteLength = function (headersOnly) { - if (headersOnly || !this.transactions) return 80 - - return 80 + varuint.encodingLength(this.transactions.length) + this.transactions.reduce(function (a, x) { - return a + x.byteLength() - }, 0) -} - -Block.fromHex = function (hex) { - return Block.fromBuffer(Buffer.from(hex, 'hex')) -} - -Block.prototype.getHash = function () { - return bcrypto.hash256(this.toBuffer(true)) -} - -Block.prototype.getId = function () { - return this.getHash().reverse().toString('hex') -} - -Block.prototype.getUTCDate = function () { - const date = new Date(0) // epoch - date.setUTCSeconds(this.timestamp) + static calculateMerkleRoot (transactions: Array, forWitness: boolean | void): Buffer { + typeforce([{ getHash: types.Function }], transactions) + if (transactions.length === 0) throw errorMerkleNoTxes + if (forWitness && !txesHaveWitness(transactions)) throw errorWitnessNotSegwit - return date -} + const hashes = transactions.map(transaction => transaction.getHash(forWitness)) -// TODO: buffer, offset compatibility -Block.prototype.toBuffer = function (headersOnly) { - const buffer = Buffer.allocUnsafe(this.byteLength(headersOnly)) + const rootHash = fastMerkleRoot(hashes, bcrypto.hash256) - let offset = 0 - function writeSlice (slice) { - slice.copy(buffer, offset) - offset += slice.length + return forWitness + ? bcrypto.hash256(Buffer.concat([rootHash, transactions[0].ins[0].witness[0]])) + : rootHash } - function writeInt32 (i) { - buffer.writeInt32LE(i, offset) - offset += 4 - } - function writeUInt32 (i) { - buffer.writeUInt32LE(i, offset) - offset += 4 + hasWitnessCommit (): boolean { + return txesHaveWitness(this.transactions) } - writeInt32(this.version) - writeSlice(this.prevHash) - writeSlice(this.merkleRoot) - writeUInt32(this.timestamp) - writeUInt32(this.bits) - writeUInt32(this.nonce) + byteLength (headersOnly: boolean): number { + if (headersOnly || !this.transactions) return 80 - if (headersOnly || !this.transactions) return buffer + return 80 + varuint.encodingLength(this.transactions.length) + + this.transactions.reduce((a, x) => a + x.byteLength(), 0) + } - varuint.encode(this.transactions.length, buffer, offset) - offset += varuint.encode.bytes + getHash (): Buffer { + return bcrypto.hash256(this.toBuffer(true)) + } - this.transactions.forEach(function (tx) { - const txSize = tx.byteLength() // TODO: extract from toBuffer? - tx.toBuffer(buffer, offset) - offset += txSize - }) + getId (): string { + return Buffer.from(this.getHash().reverse()).toString('hex') + } - return buffer -} + getUTCDate (): Date { + const date = new Date(0) // epoch + date.setUTCSeconds(this.timestamp) -Block.prototype.toHex = function (headersOnly) { - return this.toBuffer(headersOnly).toString('hex') -} + return date + } -Block.calculateTarget = function (bits) { - const exponent = ((bits & 0xff000000) >> 24) - 3 - const mantissa = bits & 0x007fffff - const target = Buffer.alloc(32, 0) - target.writeUIntBE(mantissa, 29 - exponent, 3) - return target -} + // TODO: buffer, offset compatibility + toBuffer (headersOnly: boolean): Buffer { + const buffer: Buffer = Buffer.allocUnsafe(this.byteLength(headersOnly)) + + let offset: number = 0 + const writeSlice = (slice: Buffer): void => { + slice.copy(buffer, offset) + offset += slice.length + } + + const writeInt32 = (i: number): void => { + buffer.writeInt32LE(i, offset) + offset += 4 + } + const writeUInt32 = (i: number): void => { + buffer.writeUInt32LE(i, offset) + offset += 4 + } + + writeInt32(this.version) + writeSlice(this.prevHash) + writeSlice(this.merkleRoot) + writeUInt32(this.timestamp) + writeUInt32(this.bits) + writeUInt32(this.nonce) + + if (headersOnly || !this.transactions) return buffer + + varuint.encode(this.transactions.length, buffer, offset) + offset += varuint.encode.bytes + + this.transactions.forEach(tx => { + const txSize = tx.byteLength() // TODO: extract from toBuffer? + tx.toBuffer(buffer, offset) + offset += txSize + }) + + return buffer + } -Block.calculateMerkleRoot = function (transactions) { - typeforce([{ getHash: types.Function }], transactions) - if (transactions.length === 0) throw TypeError('Cannot compute merkle root for zero transactions') + toHex (headersOnly: boolean): string { + return this.toBuffer(headersOnly).toString('hex') + } - const hashes = transactions.map(function (transaction) { - return transaction.getHash() - }) + checkMerkleRoot (): boolean { + if (!this.transactions) throw errorMerkleNoTxes - return fastMerkleRoot(hashes, bcrypto.hash256) -} + const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions) + return this.merkleRoot.compare(actualMerkleRoot) === 0 + } -Block.prototype.checkMerkleRoot = function () { - if (!this.transactions) return false + checkWitnessCommit (): boolean { + if (!this.transactions) throw errorMerkleNoTxes + if (!this.hasWitnessCommit()) throw errorWitnessNotSegwit - const actualMerkleRoot = Block.calculateMerkleRoot(this.transactions) - return this.merkleRoot.compare(actualMerkleRoot) === 0 -} + const actualWitnessCommit = Block.calculateMerkleRoot(this.transactions, true) + return this.witnessCommit.compare(actualWitnessCommit) === 0 + } -Block.prototype.checkProofOfWork = function () { - const hash = this.getHash().reverse() - const target = Block.calculateTarget(this.bits) + checkProofOfWork (): boolean { + const hash: Buffer = Buffer.from(this.getHash().reverse()) + const target = Block.calculateTarget(this.bits) - return hash.compare(target) <= 0 + return hash.compare(target) <= 0 + } } - -module.exports = Block -export {} diff --git a/src/index.ts b/src/index.ts index d1b897b..4d840a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ const opcodes = require('bitcoin-ops') -import * as Block from './block' +import { Block } from './block' import * as ECPair from './ecpair' import * as Transaction from './transaction' import * as TransactionBuilder from './transaction_builder' diff --git a/test/block.js b/test/block.js index eb52c0c..07d0741 100644 --- a/test/block.js +++ b/test/block.js @@ -1,6 +1,6 @@ const { describe, it, beforeEach } = require('mocha') const assert = require('assert') -const Block = require('../dist/src/block') +const Block = require('..').Block const fixtures = require('./fixtures/block')