diff --git a/src/block.ts b/src/block.ts index b2602dd..92c4fb1 100644 --- a/src/block.ts +++ b/src/block.ts @@ -1,3 +1,4 @@ +import { Transaction } from './transaction' const Buffer = require('safe-buffer').Buffer const bcrypto = require('./crypto') const fastMerkleRoot = require('merkle-lib/fastRoot') @@ -9,7 +10,7 @@ const errorMerkleNoTxes = new TypeError('Cannot compute merkle root for zero tra 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 { +function txesHaveWitness (transactions: Array): boolean { return transactions !== undefined && transactions instanceof Array && transactions[0] && @@ -21,8 +22,6 @@ function txesHaveWitness (transactions: Array): boolean { transactions[0].ins[0].witness.length > 0 } -const Transaction = require('./transaction') - export class Block { version: number prevHash: Buffer @@ -31,7 +30,7 @@ export class Block { witnessCommit: Buffer bits: number nonce: number - transactions: Array + transactions: Array constructor () { this.version = 1 @@ -120,12 +119,12 @@ export class Block { return target } - static calculateMerkleRoot (transactions: Array, forWitness: boolean | void): Buffer { + 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 - const hashes = transactions.map(transaction => transaction.getHash(forWitness)) + const hashes = transactions.map(transaction => transaction.getHash((forWitness))) const rootHash = fastMerkleRoot(hashes, bcrypto.hash256) diff --git a/src/index.ts b/src/index.ts index 4d840a9..389e2b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ const opcodes = require('bitcoin-ops') import { Block } from './block' import * as ECPair from './ecpair' -import * as Transaction from './transaction' +import { Transaction } from './transaction' import * as TransactionBuilder from './transaction_builder' import * as address from './address' import * as bip32 from 'bip32' diff --git a/src/transaction.ts b/src/transaction.ts index 4ee08d3..88e4280 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1,5 +1,5 @@ +import * as bcrypto from './crypto' const Buffer = require('safe-buffer').Buffer -const bcrypto = require('./crypto') const bscript = require('./script') const bufferutils = require('./bufferutils') const opcodes = require('bitcoin-ops') @@ -7,487 +7,563 @@ const typeforce = require('typeforce') const types = require('./types') const varuint = require('varuint-bitcoin') -function varSliceSize (someScript) { +function varSliceSize (someScript: Buffer): number { const length = someScript.length return varuint.encodingLength(length) + length } -function vectorSize (someVector) { +function vectorSize (someVector: Array): number { const length = someVector.length - return varuint.encodingLength(length) + someVector.reduce(function (sum, witness) { + return varuint.encodingLength(length) + someVector.reduce((sum, witness) => { return sum + varSliceSize(witness) }, 0) } -function Transaction () { - this.version = 1 - this.locktime = 0 - this.ins = [] - this.outs = [] -} - -Transaction.DEFAULT_SEQUENCE = 0xffffffff -Transaction.SIGHASH_ALL = 0x01 -Transaction.SIGHASH_NONE = 0x02 -Transaction.SIGHASH_SINGLE = 0x03 -Transaction.SIGHASH_ANYONECANPAY = 0x80 -Transaction.ADVANCED_TRANSACTION_MARKER = 0x00 -Transaction.ADVANCED_TRANSACTION_FLAG = 0x01 - -const EMPTY_SCRIPT = Buffer.allocUnsafe(0) -const EMPTY_WITNESS = [] -const ZERO = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex') -const ONE = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex') -const VALUE_UINT64_MAX = Buffer.from('ffffffffffffffff', 'hex') -const BLANK_OUTPUT = { +const EMPTY_SCRIPT: Buffer = Buffer.allocUnsafe(0) +const EMPTY_WITNESS: Array = [] +const ZERO: Buffer = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex') +const ONE: Buffer = Buffer.from('0000000000000000000000000000000000000000000000000000000000000001', 'hex') +const VALUE_UINT64_MAX: Buffer = Buffer.from('ffffffffffffffff', 'hex') +const BLANK_OUTPUT: BlankOutput = { script: EMPTY_SCRIPT, valueBuffer: VALUE_UINT64_MAX } -Transaction.fromBuffer = function (buffer, __noStrict) { - let offset = 0 - function readSlice (n) { - offset += n - return buffer.slice(offset - n, offset) +function isOutput(out: Output | BlankOutput): out is Output { + return (out).value !== undefined +} + +export type BlankOutput = { + script: Buffer + valueBuffer: Buffer +} + +export type Output = { + script: Buffer + value: number +} + +export type Input = { + hash: Buffer + index: number + script: Buffer + sequence: number + witness: Array +} + +export class Transaction { + version: number + locktime: number + ins: Array + outs: Array + + static readonly DEFAULT_SEQUENCE = 0xffffffff + static readonly SIGHASH_ALL = 0x01 + static readonly SIGHASH_NONE = 0x02 + static readonly SIGHASH_SINGLE = 0x03 + static readonly SIGHASH_ANYONECANPAY = 0x80 + static readonly ADVANCED_TRANSACTION_MARKER = 0x00 + static readonly ADVANCED_TRANSACTION_FLAG = 0x01 + + constructor () { + this.version = 1 + this.locktime = 0 + this.ins = [] + this.outs = [] } + + static fromBuffer (buffer: Buffer, __noStrict: boolean): Transaction { + let offset: number = 0 - function readUInt32 () { - const i = buffer.readUInt32LE(offset) - offset += 4 - return i - } + function readSlice (n: number): Buffer { + offset += n + return buffer.slice(offset - n, offset) + } - function readInt32 () { - const i = buffer.readInt32LE(offset) - offset += 4 - return i - } + function readUInt32 (): number { + const i = buffer.readUInt32LE(offset) + offset += 4 + return i + } - function readUInt64 () { - const i = bufferutils.readUInt64LE(buffer, offset) - offset += 8 - return i - } + function readInt32 (): number { + const i = buffer.readInt32LE(offset) + offset += 4 + return i + } - function readVarInt () { - const vi = varuint.decode(buffer, offset) - offset += varuint.decode.bytes - return vi - } + function readUInt64 (): number { + const i = bufferutils.readUInt64LE(buffer, offset) + offset += 8 + return i + } - function readVarSlice () { - return readSlice(readVarInt()) - } + function readVarInt (): number { + const vi = varuint.decode(buffer, offset) + offset += varuint.decode.bytes + return vi + } - function readVector () { - const count = readVarInt() - const vector = [] - for (var i = 0; i < count; i++) vector.push(readVarSlice()) - return vector - } + function readVarSlice (): Buffer { + return readSlice(readVarInt()) + } - const tx = new Transaction() - tx.version = readInt32() + function readVector (): Array { + const count = readVarInt() + const vector: Array = [] + for (var i = 0; i < count; i++) vector.push(readVarSlice()) + return vector + } - const marker = buffer.readUInt8(offset) - const flag = buffer.readUInt8(offset + 1) + const tx = new Transaction() + tx.version = readInt32() - let hasWitnesses = false - if (marker === Transaction.ADVANCED_TRANSACTION_MARKER && + const marker = buffer.readUInt8(offset) + const flag = buffer.readUInt8(offset + 1) + + let hasWitnesses = false + if (marker === Transaction.ADVANCED_TRANSACTION_MARKER && flag === Transaction.ADVANCED_TRANSACTION_FLAG) { - offset += 2 - hasWitnesses = true + offset += 2 + hasWitnesses = true + } + + const vinLen = readVarInt() + for (var i = 0; i < vinLen; ++i) { + tx.ins.push({ + hash: readSlice(32), + index: readUInt32(), + script: readVarSlice(), + sequence: readUInt32(), + witness: EMPTY_WITNESS + }) + } + + const voutLen = readVarInt() + for (i = 0; i < voutLen; ++i) { + tx.outs.push({ + value: readUInt64(), + script: readVarSlice() + }) + } + + if (hasWitnesses) { + for (i = 0; i < vinLen; ++i) { + tx.ins[i].witness = readVector() + } + + // was this pointless? + if (!tx.hasWitnesses()) throw new Error('Transaction has superfluous witness data') + } + + tx.locktime = readUInt32() + + if (__noStrict) return tx + if (offset !== buffer.length) throw new Error('Transaction has unexpected data') + + return tx } - const vinLen = readVarInt() - for (var i = 0; i < vinLen; ++i) { - tx.ins.push({ - hash: readSlice(32), - index: readUInt32(), - script: readVarSlice(), - sequence: readUInt32(), + static fromHex (hex: string): Transaction { + return Transaction.fromBuffer(Buffer.from(hex, 'hex'), false) + } + + static isCoinbaseHash (buffer: Buffer): boolean { + typeforce(types.Hash256bit, buffer) + for (var i = 0; i < 32; ++i) { + if (buffer[i] !== 0) return false + } + return true + } + + isCoinbase (): boolean { + return this.ins.length === 1 && Transaction.isCoinbaseHash(this.ins[0].hash) + } + + addInput (hash: Buffer, index: number, sequence: number, scriptSig: Buffer): number { + typeforce(types.tuple( + types.Hash256bit, + types.UInt32, + types.maybe(types.UInt32), + types.maybe(types.Buffer) + ), arguments) + + if (types.Null(sequence)) { + sequence = Transaction.DEFAULT_SEQUENCE + } + + // Add the input and return the input's index + return (this.ins.push({ + hash: hash, + index: index, + script: scriptSig || EMPTY_SCRIPT, + sequence: sequence, witness: EMPTY_WITNESS + }) - 1) + } + + addOutput (scriptPubKey: Buffer, value: number): number { + typeforce(types.tuple(types.Buffer, types.Satoshi), arguments) + + // Add the output and return the output's index + return (this.outs.push({ + script: scriptPubKey, + value: value + }) - 1) + } + + hasWitnesses (): boolean { + return this.ins.some((x) => { + return x.witness.length !== 0 }) } - const voutLen = readVarInt() - for (i = 0; i < voutLen; ++i) { - tx.outs.push({ - value: readUInt64(), - script: readVarSlice() - }) + weight (): number { + const base = this.__byteLength(false) + const total = this.__byteLength(true) + return base * 3 + total } - if (hasWitnesses) { - for (i = 0; i < vinLen; ++i) { - tx.ins[i].witness = readVector() - } - - // was this pointless? - if (!tx.hasWitnesses()) throw new Error('Transaction has superfluous witness data') + virtualSize (): number { + return Math.ceil(this.weight() / 4) } - tx.locktime = readUInt32() - - if (__noStrict) return tx - if (offset !== buffer.length) throw new Error('Transaction has unexpected data') - - return tx -} - -Transaction.fromHex = function (hex) { - return Transaction.fromBuffer(Buffer.from(hex, 'hex'), undefined) -} - -Transaction.isCoinbaseHash = function (buffer) { - typeforce(types.Hash256bit, buffer) - for (var i = 0; i < 32; ++i) { - if (buffer[i] !== 0) return false - } - return true -} - -Transaction.prototype.isCoinbase = function () { - return this.ins.length === 1 && Transaction.isCoinbaseHash(this.ins[0].hash) -} - -Transaction.prototype.addInput = function (hash, index, sequence, scriptSig) { - typeforce(types.tuple( - types.Hash256bit, - types.UInt32, - types.maybe(types.UInt32), - types.maybe(types.Buffer) - ), arguments) - - if (types.Null(sequence)) { - sequence = Transaction.DEFAULT_SEQUENCE + byteLength (): number { + return this.__byteLength(true) } - // Add the input and return the input's index - return (this.ins.push({ - hash: hash, - index: index, - script: scriptSig || EMPTY_SCRIPT, - sequence: sequence, - witness: EMPTY_WITNESS - }) - 1) -} + __byteLength (__allowWitness: boolean): number { + const hasWitnesses = __allowWitness && this.hasWitnesses() -Transaction.prototype.addOutput = function (scriptPubKey, value) { - typeforce(types.tuple(types.Buffer, types.Satoshi), arguments) + return ( + (hasWitnesses ? 10 : 8) + + varuint.encodingLength(this.ins.length) + + varuint.encodingLength(this.outs.length) + + this.ins.reduce((sum, input) => { + return sum + 40 + varSliceSize(input.script) + }, 0) + + this.outs.reduce((sum, output) => { + return sum + 8 + varSliceSize(output.script) + }, 0) + + (hasWitnesses ? this.ins.reduce((sum, input) => { + return sum + vectorSize(input.witness) + }, 0) : 0) + ) + } - // Add the output and return the output's index - return (this.outs.push({ - script: scriptPubKey, - value: value - }) - 1) -} + clone (): Transaction { + const newTx = new Transaction() + newTx.version = this.version + newTx.locktime = this.locktime -Transaction.prototype.hasWitnesses = function () { - return this.ins.some(function (x) { - return x.witness.length !== 0 - }) -} - -Transaction.prototype.weight = function () { - const base = this.__byteLength(false) - const total = this.__byteLength(true) - return base * 3 + total -} - -Transaction.prototype.virtualSize = function () { - return Math.ceil(this.weight() / 4) -} - -Transaction.prototype.byteLength = function () { - return this.__byteLength(true) -} - -Transaction.prototype.__byteLength = function (__allowWitness) { - const hasWitnesses = __allowWitness && this.hasWitnesses() - - return ( - (hasWitnesses ? 10 : 8) + - varuint.encodingLength(this.ins.length) + - varuint.encodingLength(this.outs.length) + - this.ins.reduce(function (sum, input) { return sum + 40 + varSliceSize(input.script) }, 0) + - this.outs.reduce(function (sum, output) { return sum + 8 + varSliceSize(output.script) }, 0) + - (hasWitnesses ? this.ins.reduce(function (sum, input) { return sum + vectorSize(input.witness) }, 0) : 0) - ) -} - -Transaction.prototype.clone = function () { - const newTx = new Transaction() - newTx.version = this.version - newTx.locktime = this.locktime - - newTx.ins = this.ins.map(function (txIn) { - return { - hash: txIn.hash, - index: txIn.index, - script: txIn.script, - sequence: txIn.sequence, - witness: txIn.witness - } - }) - - newTx.outs = this.outs.map(function (txOut) { - return { - script: txOut.script, - value: txOut.value - } - }) - - return newTx -} - -/** - * Hash transaction for signing a specific input. - * - * Bitcoin uses a different hash for each signed transaction input. - * This method copies the transaction, makes the necessary changes based on the - * hashType, and then hashes the result. - * This hash can then be used to sign the provided transaction input. - */ -Transaction.prototype.hashForSignature = function (inIndex, prevOutScript, hashType) { - typeforce(types.tuple(types.UInt32, types.Buffer, /* types.UInt8 */ types.Number), arguments) - - // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L29 - if (inIndex >= this.ins.length) return ONE - - // ignore OP_CODESEPARATOR - const ourScript = bscript.compile(bscript.decompile(prevOutScript).filter(function (x) { - return x !== opcodes.OP_CODESEPARATOR - })) - - const txTmp = this.clone() - - // SIGHASH_NONE: ignore all outputs? (wildcard payee) - if ((hashType & 0x1f) === Transaction.SIGHASH_NONE) { - txTmp.outs = [] - - // ignore sequence numbers (except at inIndex) - txTmp.ins.forEach(function (input, i) { - if (i === inIndex) return - - input.sequence = 0 + newTx.ins = this.ins.map((txIn) => { + return { + hash: txIn.hash, + index: txIn.index, + script: txIn.script, + sequence: txIn.sequence, + witness: txIn.witness + } }) - // SIGHASH_SINGLE: ignore all outputs, except at the same index? - } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE) { - // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60 - if (inIndex >= this.outs.length) return ONE + newTx.outs = this.outs.map((txOut) => { + return { + script: txOut.script, + value: (txOut).value + } + }) - // truncate outputs after - txTmp.outs.length = inIndex + 1 + return newTx + } - // "blank" outputs before - for (var i = 0; i < inIndex; i++) { - txTmp.outs[i] = BLANK_OUTPUT + /** + * Hash transaction for signing a specific input. + * + * Bitcoin uses a different hash for each signed transaction input. + * This method copies the transaction, makes the necessary changes based on the + * hashType, and then hashes the result. + * This hash can then be used to sign the provided transaction input. + */ + hashForSignature (inIndex: number, prevOutScript: Buffer, hashType: number): Buffer { + typeforce(types.tuple(types.UInt32, types.Buffer, /* types.UInt8 */ types.Number), arguments) + + // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L29 + if (inIndex >= this.ins.length) return ONE + + // ignore OP_CODESEPARATOR + const ourScript = bscript.compile(bscript.decompile(prevOutScript).filter((x) => { + return x !== opcodes.OP_CODESEPARATOR + })) + + const txTmp = this.clone() + + // SIGHASH_NONE: ignore all outputs? (wildcard payee) + if ((hashType & 0x1f) === Transaction.SIGHASH_NONE) { + txTmp.outs = [] + + // ignore sequence numbers (except at inIndex) + txTmp.ins.forEach((input, i) => { + if (i === inIndex) return + + input.sequence = 0 + }) + + // SIGHASH_SINGLE: ignore all outputs, except at the same index? + } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE) { + // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60 + if (inIndex >= this.outs.length) return ONE + + // truncate outputs after + txTmp.outs.length = inIndex + 1 + + // "blank" outputs before + for (var i = 0; i < inIndex; i++) { + txTmp.outs[i] = BLANK_OUTPUT + } + + // ignore sequence numbers (except at inIndex) + txTmp.ins.forEach((input, y) => { + if (y === inIndex) return + + input.sequence = 0 + }) } - // ignore sequence numbers (except at inIndex) - txTmp.ins.forEach(function (input, y) { - if (y === inIndex) return + // SIGHASH_ANYONECANPAY: ignore inputs entirely? + if (hashType & Transaction.SIGHASH_ANYONECANPAY) { + txTmp.ins = [txTmp.ins[inIndex]] + txTmp.ins[0].script = ourScript - input.sequence = 0 - }) + // SIGHASH_ALL: only ignore input scripts + } else { + // "blank" others input scripts + txTmp.ins.forEach((input) => { + input.script = EMPTY_SCRIPT + }) + txTmp.ins[inIndex].script = ourScript + } + + // serialize and hash + const buffer: Buffer = Buffer.allocUnsafe(txTmp.__byteLength(false) + 4) + buffer.writeInt32LE(hashType, buffer.length - 4) + txTmp.__toBuffer(buffer, 0, false) + + return bcrypto.hash256(buffer) } - // SIGHASH_ANYONECANPAY: ignore inputs entirely? - if (hashType & Transaction.SIGHASH_ANYONECANPAY) { - txTmp.ins = [txTmp.ins[inIndex]] - txTmp.ins[0].script = ourScript + hashForWitnessV0 (inIndex: number, prevOutScript: Buffer, value: number, hashType: number): Buffer { + typeforce(types.tuple(types.UInt32, types.Buffer, types.Satoshi, types.UInt32), arguments) - // SIGHASH_ALL: only ignore input scripts - } else { - // "blank" others input scripts - txTmp.ins.forEach(function (input) { input.script = EMPTY_SCRIPT }) - txTmp.ins[inIndex].script = ourScript - } + let tbuffer: Buffer = Buffer.from([]) + let toffset: number = 0 - // serialize and hash - const buffer = Buffer.allocUnsafe(txTmp.__byteLength(false) + 4) - buffer.writeInt32LE(hashType, buffer.length - 4) - txTmp.__toBuffer(buffer, 0, false) + function writeSlice (slice: Buffer): void { + toffset += slice.copy(tbuffer, toffset) + } - return bcrypto.hash256(buffer) -} + function writeUInt32 (i: number): void { + toffset = tbuffer.writeUInt32LE(i, toffset) + } -Transaction.prototype.hashForWitnessV0 = function (inIndex, prevOutScript, value, hashType) { - typeforce(types.tuple(types.UInt32, types.Buffer, types.Satoshi, types.UInt32), arguments) + function writeUInt64 (i: number): void { + toffset = bufferutils.writeUInt64LE(tbuffer, i, toffset) + } - let tbuffer, toffset - function writeSlice (slice) { toffset += slice.copy(tbuffer, toffset) } - function writeUInt32 (i) { toffset = tbuffer.writeUInt32LE(i, toffset) } - function writeUInt64 (i) { toffset = bufferutils.writeUInt64LE(tbuffer, i, toffset) } - function writeVarInt (i) { - varuint.encode(i, tbuffer, toffset) - toffset += varuint.encode.bytes - } - function writeVarSlice (slice) { writeVarInt(slice.length); writeSlice(slice) } + function writeVarInt (i: number): void { + varuint.encode(i, tbuffer, toffset) + toffset += varuint.encode.bytes + } - let hashOutputs = ZERO - let hashPrevouts = ZERO - let hashSequence = ZERO + function writeVarSlice (slice: Buffer): void { + writeVarInt(slice.length) + writeSlice(slice) + } - if (!(hashType & Transaction.SIGHASH_ANYONECANPAY)) { - tbuffer = Buffer.allocUnsafe(36 * this.ins.length) + let hashOutputs = ZERO + let hashPrevouts = ZERO + let hashSequence = ZERO + + if (!(hashType & Transaction.SIGHASH_ANYONECANPAY)) { + tbuffer = Buffer.allocUnsafe(36 * this.ins.length) + toffset = 0 + + this.ins.forEach((txIn) => { + writeSlice(txIn.hash) + writeUInt32(txIn.index) + }) + + hashPrevouts = bcrypto.hash256(tbuffer) + } + + if (!(hashType & Transaction.SIGHASH_ANYONECANPAY) && + (hashType & 0x1f) !== Transaction.SIGHASH_SINGLE && + (hashType & 0x1f) !== Transaction.SIGHASH_NONE) { + tbuffer = Buffer.allocUnsafe(4 * this.ins.length) + toffset = 0 + + this.ins.forEach((txIn) => { + writeUInt32(txIn.sequence) + }) + + hashSequence = bcrypto.hash256(tbuffer) + } + + if ((hashType & 0x1f) !== Transaction.SIGHASH_SINGLE && + (hashType & 0x1f) !== Transaction.SIGHASH_NONE) { + const txOutsSize = this.outs.reduce((sum, output) => { + return sum + 8 + varSliceSize(output.script) + }, 0) + + tbuffer = Buffer.allocUnsafe(txOutsSize) + toffset = 0 + + this.outs.forEach((out) => { + writeUInt64((out).value) + writeVarSlice(out.script) + }) + + hashOutputs = bcrypto.hash256(tbuffer) + } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE && inIndex < this.outs.length) { + const output = this.outs[inIndex] + + tbuffer = Buffer.allocUnsafe(8 + varSliceSize(output.script)) + toffset = 0 + writeUInt64((output).value) + writeVarSlice(output.script) + + hashOutputs = bcrypto.hash256(tbuffer) + } + + tbuffer = Buffer.allocUnsafe(156 + varSliceSize(prevOutScript)) toffset = 0 - this.ins.forEach(function (txIn) { + const input = this.ins[inIndex] + writeUInt32(this.version) + writeSlice(hashPrevouts) + writeSlice(hashSequence) + writeSlice(input.hash) + writeUInt32(input.index) + writeVarSlice(prevOutScript) + writeUInt64(value) + writeUInt32(input.sequence) + writeSlice(hashOutputs) + writeUInt32(this.locktime) + writeUInt32(hashType) + return bcrypto.hash256(tbuffer) + } + + getHash (forWitness: boolean): Buffer { + // wtxid for coinbase is always 32 bytes of 0x00 + if (forWitness && this.isCoinbase()) return Buffer.alloc(32, 0) + return bcrypto.hash256(this.__toBuffer(undefined, undefined, forWitness)) + } + + getId (): string { + // transaction hash's are displayed in reverse order + return Buffer.from(this.getHash(false).reverse()).toString('hex') + } + + toBuffer (buffer: Buffer | void, initialOffset: number | void): Buffer { + return this.__toBuffer(buffer, initialOffset, true) + } + + __toBuffer (buffer: Buffer | void, initialOffset: number | void, __allowWitness: boolean | void): Buffer { + if (!buffer) buffer = Buffer.allocUnsafe(this.__byteLength((__allowWitness))) + + let offset = initialOffset || 0 + + function writeSlice (slice) { + offset += slice.copy(buffer, offset) + } + + function writeUInt8 (i) { + offset = (buffer).writeUInt8(i, offset) + } + + function writeUInt32 (i) { + offset = (buffer).writeUInt32LE(i, offset) + } + + function writeInt32 (i) { + offset = (buffer).writeInt32LE(i, offset) + } + + function writeUInt64 (i) { + offset = bufferutils.writeUInt64LE(buffer, i, offset) + } + + function writeVarInt (i) { + varuint.encode(i, buffer, offset) + offset += varuint.encode.bytes + } + + function writeVarSlice (slice) { + writeVarInt(slice.length) + writeSlice(slice) + } + + function writeVector (vector) { + writeVarInt(vector.length) + vector.forEach(writeVarSlice) + } + + writeInt32(this.version) + + const hasWitnesses = __allowWitness && this.hasWitnesses() + + if (hasWitnesses) { + writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER) + writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG) + } + + writeVarInt(this.ins.length) + + this.ins.forEach((txIn) => { writeSlice(txIn.hash) writeUInt32(txIn.index) - }) - - hashPrevouts = bcrypto.hash256(tbuffer) - } - - if (!(hashType & Transaction.SIGHASH_ANYONECANPAY) && - (hashType & 0x1f) !== Transaction.SIGHASH_SINGLE && - (hashType & 0x1f) !== Transaction.SIGHASH_NONE) { - tbuffer = Buffer.allocUnsafe(4 * this.ins.length) - toffset = 0 - - this.ins.forEach(function (txIn) { + writeVarSlice(txIn.script) writeUInt32(txIn.sequence) }) - hashSequence = bcrypto.hash256(tbuffer) - } + writeVarInt(this.outs.length) + this.outs.forEach((txOut) => { + if (isOutput(txOut)) { + writeUInt64(txOut.value) + } else { + writeSlice(txOut.valueBuffer) + } - if ((hashType & 0x1f) !== Transaction.SIGHASH_SINGLE && - (hashType & 0x1f) !== Transaction.SIGHASH_NONE) { - const txOutsSize = this.outs.reduce(function (sum, output) { - return sum + 8 + varSliceSize(output.script) - }, 0) - - tbuffer = Buffer.allocUnsafe(txOutsSize) - toffset = 0 - - this.outs.forEach(function (out) { - writeUInt64(out.value) - writeVarSlice(out.script) + writeVarSlice(txOut.script) }) - hashOutputs = bcrypto.hash256(tbuffer) - } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE && inIndex < this.outs.length) { - const output = this.outs[inIndex] - - tbuffer = Buffer.allocUnsafe(8 + varSliceSize(output.script)) - toffset = 0 - writeUInt64(output.value) - writeVarSlice(output.script) - - hashOutputs = bcrypto.hash256(tbuffer) - } - - tbuffer = Buffer.allocUnsafe(156 + varSliceSize(prevOutScript)) - toffset = 0 - - const input = this.ins[inIndex] - writeUInt32(this.version) - writeSlice(hashPrevouts) - writeSlice(hashSequence) - writeSlice(input.hash) - writeUInt32(input.index) - writeVarSlice(prevOutScript) - writeUInt64(value) - writeUInt32(input.sequence) - writeSlice(hashOutputs) - writeUInt32(this.locktime) - writeUInt32(hashType) - return bcrypto.hash256(tbuffer) -} - -Transaction.prototype.getHash = function () { - return bcrypto.hash256(this.__toBuffer(undefined, undefined, false)) -} - -Transaction.prototype.getId = function () { - // transaction hash's are displayed in reverse order - return this.getHash().reverse().toString('hex') -} - -Transaction.prototype.toBuffer = function (buffer, initialOffset) { - return this.__toBuffer(buffer, initialOffset, true) -} - -Transaction.prototype.__toBuffer = function (buffer, initialOffset, __allowWitness) { - if (!buffer) buffer = Buffer.allocUnsafe(this.__byteLength(__allowWitness)) - - let offset = initialOffset || 0 - function writeSlice (slice) { offset += slice.copy(buffer, offset) } - function writeUInt8 (i) { offset = buffer.writeUInt8(i, offset) } - function writeUInt32 (i) { offset = buffer.writeUInt32LE(i, offset) } - function writeInt32 (i) { offset = buffer.writeInt32LE(i, offset) } - function writeUInt64 (i) { offset = bufferutils.writeUInt64LE(buffer, i, offset) } - function writeVarInt (i) { - varuint.encode(i, buffer, offset) - offset += varuint.encode.bytes - } - function writeVarSlice (slice) { writeVarInt(slice.length); writeSlice(slice) } - function writeVector (vector) { writeVarInt(vector.length); vector.forEach(writeVarSlice) } - - writeInt32(this.version) - - const hasWitnesses = __allowWitness && this.hasWitnesses() - - if (hasWitnesses) { - writeUInt8(Transaction.ADVANCED_TRANSACTION_MARKER) - writeUInt8(Transaction.ADVANCED_TRANSACTION_FLAG) - } - - writeVarInt(this.ins.length) - - this.ins.forEach(function (txIn) { - writeSlice(txIn.hash) - writeUInt32(txIn.index) - writeVarSlice(txIn.script) - writeUInt32(txIn.sequence) - }) - - writeVarInt(this.outs.length) - this.outs.forEach(function (txOut) { - if (!txOut.valueBuffer) { - writeUInt64(txOut.value) - } else { - writeSlice(txOut.valueBuffer) + if (hasWitnesses) { + this.ins.forEach((input) => { + writeVector(input.witness) + }) } - writeVarSlice(txOut.script) - }) + writeUInt32(this.locktime) - if (hasWitnesses) { - this.ins.forEach(function (input) { - writeVector(input.witness) - }) + // avoid slicing unless necessary + if (initialOffset !== undefined) return buffer.slice((initialOffset), offset) + return buffer } - writeUInt32(this.locktime) + toHex () { + return this.toBuffer(undefined, undefined).toString('hex') + } - // avoid slicing unless necessary - if (initialOffset !== undefined) return buffer.slice(initialOffset, offset) - return buffer + setInputScript (index, scriptSig) { + typeforce(types.tuple(types.Number, types.Buffer), arguments) + + this.ins[index].script = scriptSig + } + + setWitness (index, witness) { + typeforce(types.tuple(types.Number, [types.Buffer]), arguments) + + this.ins[index].witness = witness + } } - -Transaction.prototype.toHex = function () { - return this.toBuffer().toString('hex') -} - -Transaction.prototype.setInputScript = function (index, scriptSig) { - typeforce(types.tuple(types.Number, types.Buffer), arguments) - - this.ins[index].script = scriptSig -} - -Transaction.prototype.setWitness = function (index, witness) { - typeforce(types.tuple(types.Number, [types.Buffer]), arguments) - - this.ins[index].witness = witness -} - -module.exports = Transaction -export {} diff --git a/src/transaction_builder.ts b/src/transaction_builder.ts index 56368bf..83fbe2b 100644 --- a/src/transaction_builder.ts +++ b/src/transaction_builder.ts @@ -1,3 +1,5 @@ +import { Transaction, Output } from './transaction' + const Buffer = require('safe-buffer').Buffer const baddress = require('./address') const bcrypto = require('./crypto') @@ -11,7 +13,6 @@ const classify = require('./classify') const SCRIPT_TYPES = classify.types const ECPair = require('./ecpair') -const Transaction = require('./transaction') function expandInput (scriptSig, witnessStack, type, scriptPubKey) { if (scriptSig.length === 0 && witnessStack.length === 0) return {} @@ -517,9 +518,9 @@ TransactionBuilder.prototype.addInput = function (txHash, vout, sequence, prevOu } else if (txHash instanceof Transaction) { const txOut = txHash.outs[vout] prevOutScript = txOut.script - value = txOut.value + value = (txOut).value - txHash = txHash.getHash() + txHash = txHash.getHash(false) } return this.__addInputUnsafe(txHash, vout, { diff --git a/test/block.js b/test/block.js index 07d0741..76b043d 100644 --- a/test/block.js +++ b/test/block.js @@ -32,6 +32,9 @@ describe('Block', function () { assert.strictEqual(block.version, f.version) assert.strictEqual(block.prevHash.toString('hex'), f.prevHash) assert.strictEqual(block.merkleRoot.toString('hex'), f.merkleRoot) + if (block.witnessCommit) { + assert.strictEqual(block.witnessCommit.toString('hex'), f.witnessCommit) + } assert.strictEqual(block.timestamp, f.timestamp) assert.strictEqual(block.bits, f.bits) assert.strictEqual(block.nonce, f.nonce) @@ -113,6 +116,12 @@ describe('Block', function () { it('returns ' + f.merkleRoot + ' for ' + f.id, function () { assert.strictEqual(Block.calculateMerkleRoot(block.transactions).toString('hex'), f.merkleRoot) }) + + if (f.witnessCommit) { + it('returns witness commit ' + f.witnessCommit + ' for ' + f.id, function () { + assert.strictEqual(Block.calculateMerkleRoot(block.transactions, true).toString('hex'), f.witnessCommit) + }) + } }) }) @@ -129,6 +138,12 @@ describe('Block', function () { it('returns ' + f.valid + ' for ' + f.id, function () { assert.strictEqual(block.checkMerkleRoot(), true) }) + + if (f.witnessCommit) { + it('validates witness commit for ' + f.id, function () { + assert.strictEqual(block.checkWitnessCommit(), true) + }) + } }) }) diff --git a/test/fixtures/block.json b/test/fixtures/block.json index b4a1cfe..c60685e 100644 --- a/test/fixtures/block.json +++ b/test/fixtures/block.json @@ -119,6 +119,21 @@ "timestamp": 1231006505, "valid": true, "version": 1 + }, + { + "description": "Block with witness commit", + "bits": 388503969, + "hash": "ec61d8d62a4945034a1df40dd3dfda364221c0562c3a14000000000000000000", + "height": 542213, + "hex": "000000208980ebb11236bacc66c447d5ad961bc546c0f9cc385a08000000000000000000135e9b653aee9a70028aff2db53d4f43f4484ad52558c3ae300410b79cb6a864df50a35ba11928171396e30104010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5003054608174d696e656420627920416e74506f6f6c393b205ba350dffabe6d6d3a3e92d9efff857664de632e89fa3182f1e793d00be2e71a117b273145945a810400000000000000db250000acba0500ffffffff028a8f814a000000001976a914edf10a7fac6b32e24daa5305c723f3de58db1bc888ac0000000000000000266a24aa21a9ed4a657fcaa2149342376247e2e283a55a6b92dcc35d4d89e4ac7b74488cb63be201200000000000000000000000000000000000000000000000000000000000000000000000000100000001b898273f98d49399ecb5194ffdb1ed15c2fb37cf6d7696b4389bc7d1b76b63db010000006b483045022100e2e9bc1f6bae2deed086e935bb49fd6ac1e13dc3a44c36cd8b9a6f4257efb70d022076537c7021f12d761e1202796029f13798503bc22ab8c2ee8cb98207cbfeb414012102071c2c88e4560b47a03c033c736149a2ddd6071aea54ab85c5169cee156712f8ffffffff02b0710b00000000001976a914849a95fc65eeaa2ac47b6b6fc1f1883edb2c6c9788ace6b62501000000001976a9142964198f7ae9f7b920a2ab7c0b96b90e4ec9b14d88ac000000000100000000010152405e2660055b3540f63424a1b0b3b7bb9bbef10ceec970cd18f6f86f84a7880000000017160014dde2f1a9a4bfda011ba9ec4062990c7e1a531585ffffffff0266d418000000000017a914adc5ec550548f087371e645047170864d5fbdc03877e0400000000000016001441f6746110cc0fec102e83053d4c0ae56fab1bdd02483045022100f93bd5d3529418f60dddd477f169c32ea5ab340c99573438ee7c40d705db9ba20220323f1b4d8840098b1271c95365cdc7e8f81d0bf1fcc568c68fc7de8b871b4bee012103211d047d92547bca4aa116bc51ec4b09188e5991f69f0432fe1b5dfd8947859700000000010000000ac1a19854e5b92792ae96d08eb9f7fc016fb57c51a9047161c342e6b8de8ce721000000006b483045022100fa00a6651015ac807b03d8d54559831db40d80329f46a886b93ca6b3996daf9b02201d0fafccc88c4654a9c3d205ef0828e32376ecb42f79587615203f23fc8f2d42012102a2cda42f6954605e40cbc5601f65673621c057f8c12f16149fbbf632e8be8ed8ffffffffda16ff4a7324f78f16d5e5d4a740168de9df426cf53df569c9a33d1b94e2c25f000000006a4730440220167b4777a23db304f50ac75febfae5db0b1578c90d85769bc76e0f5458484319022023f763e95ea7771c15ea0df91c17519014b2223cd726a3b0a8b1a6f67f99b2bf012103b9492a823f03b70a1750e0fac44f58f5c81e09f4c18d0b28755b44c911ffab5ffffffffff1533f2ea34b859d2be05bbb6859d6105ad4389c911545487187437acae21b70000000006b483045022100be0b8dc174f4136e3fb4f3ccf1a2be67a44d2086179c5726c61a1af010d5232f02205c0fb4cdd7c9cb8698ceed05e688e10a0cbdd0ee5db1a0a2ef92f322e1a60e9f0121020b9f404317cc6ab5a699f607a9bb0acd0bf5588777672f8f7f4c1a13304e9f76ffffffff1395191837aa5b1cfa4cc2a79d6d6bda7d198e8a795a0f7a85f556c446119572000000006b483045022100ef3c61b5ba155b94fd02a96bf788e9a9be2de0ee53e3fe1fa6c24dd9d9aaee12022044c07be54a9c59fed96dfb778da0079f6753ab634d1920bf0fac25ebf7ac49d0012103f93e29be8b393773228f704151964662a91df4c1a3e15364e4cfb38a8cacbda9ffffffff55120f684d5fbb845fd0198cd734e46fc29f40dc8f906bf36743d7f740f5fd7c000000006a4730440220421aef290bbd39a18d1281ecfbb420b43daf2cc1c315b6bb44c895f88cb3cfcc022007a512c65b7768b1505f789b6d78479063d04ffb243072f2061eeb66fb1ade81012102712eea19c72fd644b2698c5480ebe77ce6face0bed64238a34a32085567f2f8efffffffff3553d19ce3156464e9cfa06a5260f9d9d01b16ccad6e2ebaab233ba10472ba6000000006b483045022100a29aea775d2c46028f40dceb1707b23e720bb314cef455854313569200c83162022009d0cdcef2e1f0a9217794991fa838fe6ce2bdc6e3c04ad490343c7e4a0ee7d3012102ed59ec6d98f9c2a4dd1324d46d74c56ff7e15935925f69799e547969a523ea98ffffffff530ab6d95f0d83669bfb12cbe983febe6ca638255139ce2ba0e35887f2fe3db6000000006b483045022100fad9f10989ab4ab019da4f6e430f9fe9ebe76941a9200d7346447358e93b1dce0220756c864b029a64ed9d6eedc13cf8b7d602572fcd7a3d3cb528350bb032d7e3ec012102e3e0c78d034627b1616cf196aa69bfaf20009ba9ebf09cf069453cc0423239e2ffffffff4b8e70824941d965df99703cacadfabcf79bc040029e528ff931e9ba4a7ee4d8000000006b48304502210095f37fb2700c9f96d5e5c02d4b043a7cd804f79dd071d8b221b7ae781f0f5b4e02200ae3135b8bebf813449956ae19e7c02db5eff2472da023afa8b8d4e9baba0cdd01210226cc53dfc0a41cc0cf7117dfd406db2b87161e89e2bab9908a2382173fc6dbe1ffffffff5262129e9881722b217f7e4882ed2b83ee53d9e23b9bc647494c9487ae9fabdf000000006a473044022032361f724fe006079cf37b3df61cb6c51cb0c5fd77f29b61a318134ca4948d0e02202fbe6d484a78730899230244f3662fd5578b87e9eaaccdf9bf8f05d23917de8301210254e84223b3d7f7cfd14315be8fa0c7d7eb1a1a34a672f08e2b8ec134472a66d4ffffffff067040e03288282ebe5db7b705130849c9984458b13ea7ca3c755f07b9d1b9f4000000006b483045022100b78b9c24f5f3e950aba637b827d5b11615c17ae2f43105941d172cc4a8b73c80022064998c17becd06abbcfde47c91b9d87da5ac67873ed9a412010b08b954e0b5cd01210329a0acc2b0d60dd243eef46073a672ed0caf467c92f63ef7293f2036a3851a1effffffff02027f0000000000001976a914c6a396ae979670eeaa6929df3dd1c2d8fba31c3d88ac85a19b020000000017a914409dbd0e9a1ab27853186367130e6aab2509e47f8700000000", + "id": "000000000000000000143a2c56c0214236dadfd30df41d4a0345492ad6d861ec", + "merkleRoot": "135e9b653aee9a70028aff2db53d4f43f4484ad52558c3ae300410b79cb6a864", + "witnessCommit": "4a657fcaa2149342376247e2e283a55a6b92dcc35d4d89e4ac7b74488cb63be2", + "nonce": 31692307, + "prevHash": "8980ebb11236bacc66c447d5ad961bc546c0f9cc385a08000000000000000000", + "timestamp": 1537429727, + "valid": true, + "version": 536870912 } ], "invalid": [ diff --git a/test/transaction.js b/test/transaction.js index f315a7d..74afc8f 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -2,7 +2,7 @@ const { describe, it, beforeEach } = require('mocha') const assert = require('assert') const bscript = require('../dist/src/script') const fixtures = require('./fixtures/transaction') -const Transaction = require('../dist/src/transaction') +const Transaction = require('..').Transaction describe('Transaction', function () { function fromRaw (raw, noWitness) { diff --git a/test/transaction_builder.js b/test/transaction_builder.js index a4b309a..c416846 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -5,7 +5,7 @@ const bscript = require('../dist/src/script') const payments = require('../dist/src/payments') const ECPair = require('../dist/src/ecpair') -const Transaction = require('../dist/src/transaction') +const Transaction = require('..').Transaction const TransactionBuilder = require('../dist/src/transaction_builder') const NETWORKS = require('../dist/src/networks')