UNPKG

hsd

Version:
2,097 lines (1,642 loc) 49.3 kB
/*! * tx.js - transaction object for hsd * Copyright (c) 2017-2018, Christopher Jeffrey (MIT License). * https://github.com/handshake-org/hsd */ 'use strict'; const assert = require('bsert'); const bio = require('bufio'); const blake2b = require('bcrypto/lib/blake2b'); const secp256k1 = require('bcrypto/lib/secp256k1'); const {BufferSet} = require('buffer-map'); const util = require('../utils/util'); const Amount = require('../ui/amount'); const Network = require('../protocol/network'); const Script = require('../script/script'); const Input = require('./input'); const Output = require('./output'); const Outpoint = require('./outpoint'); const rules = require('../covenants/rules'); const InvItem = require('./invitem'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const ScriptError = require('../script/scripterror'); const {OwnershipProof} = require('../covenants/ownership'); const AirdropProof = require('../primitives/airdropproof'); const {encoding} = bio; const {hashType} = Script; /** @typedef {import('@handshake-org/bfilter').BloomFilter} BloomFilter */ /** @typedef {import('../types').SighashType} SighashType */ /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').Amount} AmountValue */ /** @typedef {import('../types').Rate} Rate */ /** @typedef {import('../types').VerifyFlags} VerifyFlags */ /** @typedef {import('../types').HexHash} HexHash */ /** @typedef {import('../types').BufioWriter} BufioWriter */ /** @typedef {import('../blockchain/chainentry')} ChainEntry */ /** @typedef {import('../covenants/ownership').OwnershipProof} OwnershipProof */ /** @typedef {import('../workers/workerpool')} WorkerPool */ /** @typedef {import('../coins/coinview')} CoinView */ /** @typedef {import('./covenant')} Covenant */ /** @typedef {import('./coin')} Coin */ /** @typedef {import('./address')} Address */ /** * TX * A static transaction object. * @alias module:primitives.TX * @property {Number} version * @property {Input[]} inputs * @property {Output[]} outputs * @property {Number} locktime */ class TX extends bio.Struct { /** * Create a transaction. * @constructor * @param {Object?} [options] */ constructor(options) { super(); this.version = 0; /** @type {Input[]} */ this.inputs = []; /** @type {Output[]} */ this.outputs = []; this.locktime = 0; this.mutable = false; /** @type {Hash?} */ this._hash = null; /** @type {Hash?} */ this._wdhash = null; /** @type {Hash?} */ this._whash = null; /** @type {Buffer?} */ this._raw = null; /** @type {Sizes?} */ this._sizes = null; /** @type {Hash?} */ this._hashPrevouts = null; /** @type {Hash?} */ this._hashSequence = null; /** @type {Hash?} */ this._hashOutputs = null; if (options) this.fromOptions(options); } /** * Inject properties from options object. * @param {Object} options */ fromOptions(options) { assert(options, 'TX data is required.'); if (options.version != null) { assert((options.version >>> 0) === options.version, 'Version must be a uint32.'); this.version = options.version; } if (options.inputs) { assert(Array.isArray(options.inputs), 'Inputs must be an array.'); for (const input of options.inputs) this.inputs.push(new Input(input)); } if (options.outputs) { assert(Array.isArray(options.outputs), 'Outputs must be an array.'); for (const output of options.outputs) this.outputs.push(new Output(output)); } if (options.locktime != null) { assert((options.locktime >>> 0) === options.locktime, 'Locktime must be a uint32.'); this.locktime = options.locktime; } return this; } /** * Inject properties from tx. * Used for cloning. * @param {this} tx * @returns {this} */ inject(tx) { this.version = tx.version; for (const input of tx.inputs) this.inputs.push(input.clone()); for (const output of tx.outputs) this.outputs.push(output.clone()); this.locktime = tx.locktime; return this; } /** * Clear any cached values. */ refresh() { this._hash = null; this._wdhash = null; this._whash = null; this._raw = null; this._sizes = null; this._hashPrevouts = null; this._hashSequence = null; this._hashOutputs = null; return this; } /** * Hash the transaction with the non-witness serialization. * @returns {Hash} */ hash() { if (this.mutable) return this.left(); if (!this._hash) this._hash = this.left(); return this._hash; } /** * Hash the transaction with the witness * serialization, return the wtxid (normal * hash if no witness is present, all zeroes * if coinbase). * @returns {Hash} hash */ witnessHash() { if (this.mutable) return this.root(); if (!this._whash) this._whash = this.root(); return this._whash; } /** * Calculate the virtual size of the transaction. * Note that this is cached. * @returns {Number} vsize */ getVirtualSize() { const scale = consensus.WITNESS_SCALE_FACTOR; return (this.getWeight() + scale - 1) / scale | 0; } /** * Calculate the virtual size of the transaction * (weighted against bytes per sigop cost). * @param {Number} sigops - Sigops cost. * @returns {Number} vsize */ getSigopsSize(sigops) { const scale = consensus.WITNESS_SCALE_FACTOR; const bytes = policy.BYTES_PER_SIGOP; const weight = Math.max(this.getWeight(), sigops * bytes); return (weight + scale - 1) / scale | 0; } /** * Calculate the weight of the transaction. * Note that this is cached. * @returns {Number} weight */ getWeight() { const sizes = this.getSizes(); return sizes.getWeight(); } /** * Calculate the real size of the transaction * with the witness included. * @returns {Number} size */ getSize() { const {base, witness} = this.getSizes(); return base + witness; } /** * Calculate the size of the transaction * without the witness. * with the witness included. * @returns {Number} size */ getBaseSize() { const {base} = this.getSizes(); return base; } /** * Test whether the transaction has a non-empty witness. * @returns {Boolean} */ hasWitness() { for (const {witness} of this.inputs) { if (witness.items.length > 0) return true; } return false; } /** * Get the signature hash of the transaction for signing verifying. * @param {Number} index - Index of input being signed/verified. * @param {Script} prev - Previous output script or redeem script * (in the case of witnesspubkeyhash, this should be the generated * p2pkh script). * @param {AmountValue} value - Previous output value. * @param {SighashType} type - Sighash type. * @returns {Buffer} Signature hash. */ signatureHash(index, prev, value, type) { assert(index >= 0 && index < this.inputs.length); assert(prev instanceof Script); assert(typeof value === 'number'); assert(typeof type === 'number'); let input = this.inputs[index]; let prevouts = consensus.ZERO_HASH; let sequences = consensus.ZERO_HASH; let outputs = consensus.ZERO_HASH; // SIGHASH_NOINPUT does not commit to "this input's data", meaning the // outpoint and sequence of this input (only this input) is malleable. // However, our implementation neglects to also nullify the `prevouts` // and `sequences` values as specified in the original SIGHASH_NOINPUT BIP // https://blockstream.com/eltoo.pdf (appendix A). // This means that our NOINPUT is not useful by itself, because even though // it removes this input's outpoint from one part of the signed data, it // leaves it in place in a different part, so it is still committed to by // the signature. // Note that eltoo's NOINPUT and it's successor BIP-118 also make committing // to the witness script optional (in our code that value is called `prev`). if (type & hashType.NOINPUT) input = new Input(); if (!(type & hashType.ANYONECANPAY)) { if (this._hashPrevouts) { prevouts = this._hashPrevouts; } else { const bw = bio.pool(this.inputs.length * 36); for (const input of this.inputs) input.prevout.write(bw); prevouts = blake2b.digest(bw.render()); if (!this.mutable) this._hashPrevouts = prevouts; } } if (!(type & hashType.ANYONECANPAY) && (type & 0x1f) !== hashType.SINGLE && (type & 0x1f) !== hashType.SINGLEREVERSE && (type & 0x1f) !== hashType.NONE) { if (this._hashSequence) { sequences = this._hashSequence; } else { const bw = bio.pool(this.inputs.length * 4); for (const input of this.inputs) bw.writeU32(input.sequence); sequences = blake2b.digest(bw.render()); if (!this.mutable) this._hashSequence = sequences; } } if ((type & 0x1f) !== hashType.SINGLE && (type & 0x1f) !== hashType.SINGLEREVERSE && (type & 0x1f) !== hashType.NONE) { if (this._hashOutputs) { outputs = this._hashOutputs; } else { let size = 0; for (const output of this.outputs) size += output.getSize(); const bw = bio.pool(size); for (const output of this.outputs) output.write(bw); outputs = blake2b.digest(bw.render()); if (!this.mutable) this._hashOutputs = outputs; } } else if ((type & 0x1f) === hashType.SINGLE) { if (index < this.outputs.length) { const output = this.outputs[index]; outputs = blake2b.digest(output.encode()); } } else if ((type & 0x1f) === hashType.SINGLEREVERSE) { if (index < this.outputs.length) { const output = this.outputs[(this.outputs.length - 1) - index]; outputs = blake2b.digest(output.encode()); } } const size = 156 + prev.getVarSize(); const bw = bio.pool(size); bw.writeU32(this.version); bw.writeBytes(prevouts); bw.writeBytes(sequences); bw.writeHash(input.prevout.hash); bw.writeU32(input.prevout.index); bw.writeVarBytes(prev.encode()); bw.writeU64(value); bw.writeU32(input.sequence); bw.writeBytes(outputs); bw.writeU32(this.locktime); bw.writeU32(type); return blake2b.digest(bw.render()); } /** * Verify signature. * @param {Number} index * @param {Script} prev * @param {AmountValue} value * @param {Buffer} sig * @param {Buffer} key * @returns {Boolean} */ checksig(index, prev, value, sig, key) { if (sig.length === 0) return false; const type = sig[sig.length - 1]; const hash = this.signatureHash(index, prev, value, type); return secp256k1.verify(hash, sig.slice(0, -1), key); } /** * Create a signature suitable for inserting into scriptSigs/witnesses. * @param {Number} index - Index of input being signed. * @param {Script} prev - Previous output script or redeem script * (in the case of witnesspubkeyhash, this should be the generated * p2pkh script). * @param {AmountValue} value - Previous output value. * @param {Buffer} key * @param {SighashType} type * @returns {Buffer} Signature in DER format. */ signature(index, prev, value, key, type) { if (type == null) type = hashType.ALL; const hash = this.signatureHash(index, prev, value, type); const sig = secp256k1.sign(hash, key); const bw = bio.write(65); bw.writeBytes(sig); bw.writeU8(type); return bw.render(); } /** * Verify all transaction inputs. * @param {CoinView} view * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] * @throws {ScriptError} on invalid inputs */ check(view, flags) { if (this.inputs.length === 0) throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); if (this.isCoinbase()) { for (let i = 1; i < this.inputs.length; i++) { const {witness} = this.inputs[i]; if (witness.items.length !== 1) throw new ScriptError('UNKNOWN_ERROR', 'Invalid claim proof.'); if (i >= this.outputs.length) throw new ScriptError('UNKNOWN_ERROR', 'Invalid claim proof.'); const output = this.outputs[i]; if (!output.covenant.isClaim()) { assert(output.covenant.isNone()); let proof; try { proof = AirdropProof.decode(witness.items[0]); } catch (e) { throw new ScriptError('UNKNOWN_ERROR', 'Invalid airdrop proof.'); } if (!proof.isSane()) throw new ScriptError('UNKNOWN_ERROR', 'Non-sane airdrop proof.'); if (!proof.verify()) { throw new ScriptError('UNKNOWN_ERROR', 'Invalid airdrop signature.'); } if (output.value !== proof.getValue() - proof.fee || output.address.version !== proof.version || !output.address.hash.equals(proof.address)) { throw new ScriptError('UNKNOWN_ERROR', 'Invalid airdrop output.'); } continue; } /** @type {OwnershipProof} */ let proof; try { proof = OwnershipProof.decode(witness.items[0]); } catch (e) { throw new ScriptError('UNKNOWN_ERROR', 'Invalid claim proof.'); } if (!proof.isSane()) throw new ScriptError('UNKNOWN_ERROR', 'Non-sane claim proof.'); if (!proof.verifySignatures()) throw new ScriptError('UNKNOWN_ERROR', 'Invalid claim signatures.'); } return; } for (let i = 0; i < this.inputs.length; i++) { const {prevout} = this.inputs[i]; const coin = view.getOutput(prevout); if (!coin) throw new ScriptError('UNKNOWN_ERROR', 'No coin available.'); this.checkInput(i, coin, flags); } } /** * Verify a transaction input. * @param {Number} index - Index of output being * verified. * @param {Coin|Output} coin - Previous output. * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] * @throws {ScriptError} on invalid input */ checkInput(index, coin, flags) { const input = this.inputs[index]; assert(input, 'Input does not exist.'); assert(coin, 'No coin passed.'); Script.verify( input.witness, coin.address, this, index, coin.value, flags ); } /** * Verify the transaction inputs on the worker pool * (if workers are enabled). * @param {CoinView} view * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] * @param {WorkerPool?} [pool] * @returns {Promise} */ async checkAsync(view, flags, pool) { if (this.inputs.length === 0) throw new ScriptError('UNKNOWN_ERROR', 'No inputs.'); if (!pool) { this.check(view, flags); return undefined; } return pool.check(this, view, flags); } /** * Verify a transaction input asynchronously. * @param {Number} index - Index of output being * verified. * @param {Coin|Output} coin - Previous output. * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] * @param {WorkerPool?} [pool] * @returns {Promise} */ async checkInputAsync(index, coin, flags, pool) { const input = this.inputs[index]; assert(input, 'Input does not exist.'); assert(coin, 'No coin passed.'); if (!pool) { this.checkInput(index, coin, flags); return undefined; } return pool.checkInput(this, index, coin, flags); } /** * Verify all transaction inputs. * @param {CoinView} view * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] * @returns {Boolean} Whether the inputs are valid. */ verify(view, flags) { try { this.check(view, flags); } catch (e) { if (e.type === 'ScriptError') return false; throw e; } return true; } /** * Verify a transaction input. * @param {Number} index - Index of output being * verified. * @param {Coin|Output} coin - Previous output. * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] * @returns {Boolean} Whether the input is valid. */ verifyInput(index, coin, flags) { try { this.checkInput(index, coin, flags); } catch (e) { if (e.type === 'ScriptError') return false; throw e; } return true; } /** * Verify the transaction inputs on the worker pool * (if workers are enabled). * @param {CoinView} view * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] * @param {WorkerPool?} [pool] * @returns {Promise} */ async verifyAsync(view, flags, pool) { try { await this.checkAsync(view, flags, pool); } catch (e) { if (e.type === 'ScriptError') return false; throw e; } return true; } /** * Verify a transaction input asynchronously. * @param {Number} index - Index of output being * verified. * @param {Coin|Output} coin - Previous output. * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] * @param {WorkerPool?} [pool] * @returns {Promise} */ async verifyInputAsync(index, coin, flags, pool) { try { await this.checkInputAsync(index, coin, flags, pool); } catch (e) { if (e.type === 'ScriptError') return false; throw e; } return true; } /** * Test whether the transaction is a coinbase * by examining the inputs. * @returns {Boolean} */ isCoinbase() { return this.inputs.length > 0 && this.inputs[0].prevout.isNull(); } /** * Calculate the fee for the transaction. * @param {CoinView} view * @returns {AmountValue} fee (zero if not all coins are available). */ getFee(view) { if (!this.hasCoins(view)) return 0; return this.getInputValue(view) - this.getOutputValue(); } /** * Calculate the total input value. * @param {CoinView} view * @returns {AmountValue} value */ getInputValue(view) { let total = 0; for (const {prevout} of this.inputs) { const coin = view.getOutput(prevout); if (!coin) return 0; total += coin.value; } return total; } /** * Calculate the total output value. * @returns {AmountValue} value */ getOutputValue() { let total = 0; for (const output of this.outputs) total += output.value; return total; } /** * Get all input addresses. * @private * @param {CoinView} view * @returns {Array} [addrs, table] */ _getInputAddresses(view) { const table = new BufferSet(); const addrs = []; if (this.isCoinbase()) return [addrs, table]; for (const input of this.inputs) { const coin = view ? view.getOutputFor(input) : null; const addr = input.getAddress(coin); if (!addr) continue; const hash = addr.getHash(); if (!table.has(hash)) { table.add(hash); addrs.push(addr); } } return [addrs, table]; } /** * Get all output addresses. * @private * @returns {Array} [addrs, table] */ _getOutputAddresses() { const table = new BufferSet(); const addrs = []; for (const output of this.outputs) { const addr = output.getAddress(); if (!addr) continue; const hash = addr.getHash(); if (!table.has(hash)) { table.add(hash); addrs.push(addr); } } return [addrs, table]; } /** * Get all addresses. * @private * @param {CoinView} view * @returns {Array} [addrs, table] */ _getAddresses(view) { const [addrs, table] = this._getInputAddresses(view); const output = this.getOutputAddresses(); for (const addr of output) { const hash = addr.getHash(); if (!table.has(hash)) { table.add(hash); addrs.push(addr); } } return [addrs, table]; } /** * Get all input addresses. * @param {CoinView|null} view * @returns {Address[]} addresses */ getInputAddresses(view) { const [addrs] = this._getInputAddresses(view); return addrs; } /** * Get all output addresses. * @returns {Address[]} addresses */ getOutputAddresses() { const [addrs] = this._getOutputAddresses(); return addrs; } /** * Get all addresses. * @param {CoinView|null} view * @returns {Address[]} addresses */ getAddresses(view) { const [addrs] = this._getAddresses(view); return addrs; } /** * Get all input address hashes. * @param {CoinView|null} view * @returns {Hash[]} hashes */ getInputHashes(view) { const [, table] = this._getInputAddresses(view); return table.toArray(); } /** * Get all output address hashes. * @returns {Hash[]} hashes */ getOutputHashes() { const [, table] = this._getOutputAddresses(); return table.toArray(); } /** * Get all address hashes. * @param {CoinView|null} view * @returns {Hash[]} hashes */ getHashes(view) { const [, table] = this._getAddresses(view); return table.toArray(); } /** * Test whether the transaction has * all coins available. * @param {CoinView} view * @returns {Boolean} */ hasCoins(view) { if (this.inputs.length === 0) return false; for (const {prevout} of this.inputs) { if (!view.hasEntry(prevout)) return false; } return true; } /** * Check finality of transaction by examining * nLocktime and nSequence values. * @example * tx.isFinal(chain.height + 1, network.now()); * @param {Number} height - Height at which to test. This * is usually the chain height, or the chain height + 1 * when the transaction entered the mempool. * @param {Number} time - Time at which to test. This is * usually the chain tip's parent's median time, or the * time at which the transaction entered the mempool. If * MEDIAN_TIME_PAST is enabled this will be the median * time of the chain tip's previous entry's median time. * @returns {Boolean} */ isFinal(height, time) { const FLAG = consensus.LOCKTIME_FLAG; const MASK = consensus.LOCKTIME_MASK; const MULT = consensus.LOCKTIME_MULT; if (this.locktime === 0) return true; if (this.locktime & FLAG) { const locktime = this.locktime & MASK; if ((locktime * MULT) < time) return true; } else { if (this.locktime < height) return true; } for (const input of this.inputs) { if (input.sequence !== 0xffffffff) return false; } return true; } /** * Verify the absolute locktime of a transaction. * Called by OP_CHECKLOCKTIMEVERIFY. * @param {Number} index - Index of input being verified. * @param {Number} predicate - Locktime to verify against. * @returns {Boolean} */ verifyLocktime(index, predicate) { const FLAG = consensus.LOCKTIME_FLAG; const MASK = consensus.LOCKTIME_MASK; const input = this.inputs[index]; assert(input, 'Input does not exist.'); assert(predicate >= 0, 'Locktime must be non-negative.'); // Locktimes must be of the same type (blocks or seconds). if ((this.locktime & FLAG) !== (predicate & FLAG)) return false; if ((predicate & MASK) > (this.locktime & MASK)) return false; if (input.sequence === 0xffffffff) return false; return true; } /** * Verify the relative locktime of an input. * Called by OP_CHECKSEQUENCEVERIFY. * @param {Number} index - Index of input being verified. * @param {Number} predicate - Relative locktime to verify against. * @returns {Boolean} */ verifySequence(index, predicate) { const DISABLE_FLAG = consensus.SEQUENCE_DISABLE_FLAG; const TYPE_FLAG = consensus.SEQUENCE_TYPE_FLAG; const MASK = consensus.SEQUENCE_MASK; const input = this.inputs[index]; assert(input, 'Input does not exist.'); assert(predicate >= 0, 'Locktime must be non-negative.'); // For future softfork capability. if (predicate & DISABLE_FLAG) return true; // Cannot use the disable flag without // the predicate also having the disable // flag (for future softfork capability). if (input.sequence & DISABLE_FLAG) return false; // Locktimes must be of the same type (blocks or seconds). if ((input.sequence & TYPE_FLAG) !== (predicate & TYPE_FLAG)) return false; if ((predicate & MASK) > (input.sequence & MASK)) return false; return true; } /** * Calculate sigops. * @param {CoinView} view * @returns {Number} */ getSigops(view) { if (this.isCoinbase()) return 0; let total = 0; for (const input of this.inputs) { const coin = view.getOutputFor(input); if (!coin) continue; total += coin.address.getSigops(input.witness); } return total; } /** * Non-contextual sanity checks for the transaction. * Will mostly verify coin and output values. * @see CheckTransaction() * @returns {Array} [result, reason, score] */ isSane() { const [valid] = this.checkSanity(); return valid; } /** * Non-contextual sanity checks for the transaction. * Will mostly verify coin and output values. * @see CheckTransaction() * @returns {Array} [valid, reason, score] */ checkSanity() { if (this.inputs.length === 0) return [false, 'bad-txns-vin-empty', 100]; if (this.outputs.length === 0) return [false, 'bad-txns-vout-empty', 100]; if (this.getBaseSize() > consensus.MAX_TX_SIZE) return [false, 'bad-txns-oversize', 100]; if (this.getWeight() > consensus.MAX_TX_WEIGHT) return [false, 'bad-txns-overweight', 100]; // Single TX should not exceed consensus block limits. if (rules.countOpens(this) > consensus.MAX_BLOCK_OPENS) return [false, 'bad-txns-opens', 100]; if (rules.countUpdates(this) > consensus.MAX_BLOCK_UPDATES) return [false, 'bad-txns-updates', 100]; if (rules.countRenewals(this) > consensus.MAX_BLOCK_RENEWALS) return [false, 'bad-txns-renewals', 100]; let total = 0; for (const output of this.outputs) { if (output.value < 0) return [false, 'bad-txns-vout-negative', 100]; if (output.value > consensus.MAX_MONEY) return [false, 'bad-txns-vout-toolarge', 100]; total += output.value; if (total < 0 || total > consensus.MAX_MONEY) return [false, 'bad-txns-txouttotal-toolarge', 100]; if (!output.address.isValid()) return [false, 'bad-txns-address-size', 100]; } if (this.isCoinbase()) { if (!this.inputs[0].prevout.isNull()) return [false, 'bad-cb-outpoint', 100]; const size = this.inputs[0].witness.getSize(); if (size > 1000) return [false, 'bad-cb-length', 100]; for (let i = 1; i < this.inputs.length; i++) { const input = this.inputs[i]; if (!input.prevout.isNull()) return [false, 'bad-cb-outpoint', 100]; if (input.witness.items.length !== 1) return [false, 'bad-cb-witness', 100]; const size = input.witness.items[0].length; if (size > 10000) return [false, 'bad-cb-length', 100]; } } else { const prevout = new BufferSet(); for (const input of this.inputs) { const key = input.prevout.toKey(); if (prevout.has(key)) return [false, 'bad-txns-inputs-duplicate', 100]; prevout.add(key); } for (const input of this.inputs) { if (input.prevout.isNull()) return [false, 'bad-txns-prevout-null', 10]; } } if (!this.hasSaneCovenants()) return [false, 'bad-txns-covenants', 100]; return [true, 'valid', 0]; } /** * Test whether the transaction violates * any basic covenants rules. * @returns {Boolean} */ hasSaneCovenants() { return rules.hasSaneCovenants(this); } /** * Non-contextual checks to determine whether the * transaction has all standard output script * types and standard input script size with only * pushdatas in the code. * Will mostly verify coin and output values. * @see IsStandardTx() * @returns {Array} [valid, reason, score] */ isStandard() { const [valid] = this.checkStandard(); return valid; } /** * Non-contextual checks to determine whether the * transaction has all standard output script * types and standard input script size with only * pushdatas in the code. * Will mostly verify coin and output values. * @see IsStandardTx() * @returns {Array} [valid, reason, score] */ checkStandard() { if (this.version > policy.MAX_TX_VERSION) return [false, 'version', 0]; if (this.getWeight() > policy.MAX_TX_WEIGHT) return [false, 'tx-size', 0]; let nulldata = 0; for (const output of this.outputs) { if (output.address.isUnknown()) return [false, 'address', 0]; if (output.address.isNulldata()) { nulldata += 1; continue; } if (output.covenant.isUnknown()) return [false, 'covenant', 0]; if (output.isDust(policy.MIN_RELAY)) return [false, 'dust', 0]; } if (nulldata > 1) return [false, 'multi-op-return', 0]; return [true, 'valid', 0]; } /** * Perform contextual checks to verify coin and input * script standardness (including the redeem script). * @see AreInputsStandard() * @param {CoinView} view * @returns {Boolean} */ hasStandardInputs(view) { if (this.isCoinbase()) return true; for (const input of this.inputs) { const witness = input.witness; const coin = view.getOutputFor(input); if (!coin) continue; if (witness.items.length === 0) continue; const addr = coin.address; if (addr.isPubkeyhash()) { if (witness.items.length !== 2) return false; if (witness.items[0].length !== 65) return false; if (witness.items[1].length !== 33) return false; continue; } if (addr.isScripthash()) { if (witness.items.length - 1 > policy.MAX_P2WSH_STACK) return false; for (let i = 0; i < witness.items.length - 1; i++) { const item = witness.items[i]; if (item.length > policy.MAX_P2WSH_PUSH) return false; } const raw = witness.items[witness.items.length - 1]; if (raw.length > policy.MAX_P2WSH_SIZE) return false; const redeem = Script.decode(raw); if (redeem.isPubkey()) { if (witness.items.length - 1 !== 1) return false; if (witness.items[0].length !== 65) return false; continue; } if (redeem.isPubkeyhash()) { if (input.witness.items.length - 1 !== 2) return false; if (witness.items[0].length !== 65) return false; if (witness.items[1].length !== 33) return false; continue; } const [m] = redeem.getMultisig(); if (m !== -1) { if (witness.items.length - 1 !== m + 1) return false; if (witness.items[0].length !== 0) return false; for (let i = 1; i < witness.items.length - 1; i++) { const item = witness.items[i]; if (item.length !== 65) return false; } } continue; } if (witness.items.length > policy.MAX_P2WSH_STACK) return false; for (const item of witness.items) { if (item.length > policy.MAX_P2WSH_PUSH) return false; } } return true; } /** * Perform contextual checks to verify input, output, * and fee values, as well as coinbase spend maturity * (coinbases can only be spent 100 blocks or more * after they're created). Note that this function is * consensus critical. * @param {CoinView} view * @param {Number} height - Height at which the * transaction is being spent. In the mempool this is * the chain height plus one at the time it entered the pool. * @param {Network} network * @returns {Boolean} */ verifyInputs(view, height, network) { const [fee] = this.checkInputs(view, height, network); return fee !== -1; } /** * Perform contextual checks to verify input, output, * and fee values, as well as coinbase spend maturity * (coinbases can only be spent 100 blocks or more * after they're created). Note that this function is * consensus critical. * @param {CoinView} view * @param {Number} height - Height at which the * transaction is being spent. In the mempool this is * the chain height plus one at the time it entered the pool. * @param {Network} network * @returns {Array} [fee, reason, score] */ checkInputs(view, height, network) { assert(typeof height === 'number'); assert(network instanceof Network); if (this.isCoinbase()) { const conjured = this.verifyCovenants(view, height, network); if (conjured === -1) return [-1, 'bad-txns-claims', 100]; return [conjured, 'valid', 0]; } let total = 0; for (const {prevout} of this.inputs) { const entry = view.getEntry(prevout); if (!entry) return [-1, 'bad-txns-inputs-missingorspent', 0]; if (entry.coinbase) { if (height - entry.height < network.coinbaseMaturity) return [-1, 'bad-txns-premature-spend-of-coinbase', 0]; } const coin = view.getOutput(prevout); assert(coin); if (coin.value < 0 || coin.value > consensus.MAX_MONEY) return [-1, 'bad-txns-inputvalues-outofrange', 100]; total += coin.value; if (total < 0 || total > consensus.MAX_MONEY) return [-1, 'bad-txns-inputvalues-outofrange', 100]; } // Overflows already checked in `isSane()`. const value = this.getOutputValue(); if (total < value) return [-1, 'bad-txns-in-belowout', 100]; const fee = total - value; if (fee < 0) return [-1, 'bad-txns-fee-negative', 100]; if (fee > consensus.MAX_MONEY) return [-1, 'bad-txns-fee-outofrange', 100]; const conjured = this.verifyCovenants(view, height, network); if (conjured === -1) return [-1, 'bad-txns-covenants', 100]; assert(conjured === 0); return [fee, 'valid', 0]; } /** * Test whether the transaction violates * any contextual covenants rules. * @param {CoinView} view * @param {Number} height * @param {Network} network * @returns {Number} */ verifyCovenants(view, height, network) { return rules.verifyCovenants(this, view, height, network); } /** * Calculate the modified size of the transaction. This * is used in the mempool for calculating priority. * @param {Number?} size - The size to modify. If not present, * virtual size will be used. * @returns {Number} Modified size. */ getModifiedSize(size) { if (size == null) size = this.getVirtualSize(); for (const input of this.inputs) { const offset = 45 + Math.min(100, input.witness.getSize()); if (size > offset) size -= offset; } return size; } /** * Calculate the transaction priority. * @param {CoinView} view * @param {Number} height * @param {Number?} [size] - Size to calculate priority * based on. If not present, virtual size will be used. * @returns {Number} */ getPriority(view, height, size) { assert(typeof height === 'number', 'Must pass in height.'); if (this.isCoinbase()) return 0; if (size == null) size = this.getVirtualSize(); let sum = 0; for (const {prevout} of this.inputs) { const coin = view.getOutput(prevout); if (!coin) continue; const coinHeight = view.getHeight(prevout); if (coinHeight === -1) continue; if (coinHeight <= height) { const age = height - coinHeight; sum += coin.value * age; } } return Math.floor(sum / size); } /** * Calculate the transaction's on-chain value. * @param {CoinView} view * @returns {Number} */ getChainValue(view) { if (this.isCoinbase()) return 0; let value = 0; for (const {prevout} of this.inputs) { const coin = view.getOutput(prevout); if (!coin) continue; const height = view.getHeight(prevout); if (height === -1) continue; value += coin.value; } return value; } /** * Determine whether the transaction is above the * free threshold in priority. A transaction which * passed this test is most likely relayable * without a fee. * @param {CoinView} view * @param {Number} height - If not present, tx * height or network height will be used. * @param {Number?} [size] - If not present, modified * size will be calculated and used. * @returns {Boolean} */ isFree(view, height, size) { const priority = this.getPriority(view, height, size); return priority > policy.FREE_THRESHOLD; } /** * Calculate minimum fee in order for the transaction * to be relayable (not the constant min relay fee). * @param {Number?} [size] - If not present, max size * estimation will be calculated and used. * @param {Rate?} [rate] - Rate of dollarydoo per kB. * @returns {AmountValue} fee */ getMinFee(size, rate) { if (size == null) size = this.getVirtualSize(); return policy.getMinFee(size, rate); } /** * Calculate the minimum fee in order for the transaction * to be relayable, but _round to the nearest kilobyte * when taking into account size. * @param {Number?} [size] - If not present, max size * estimation will be calculated and used. * @param {Rate?} [rate] - Rate of dollarydoo per kB. * @returns {AmountValue} fee */ getRoundFee(size, rate) { if (size == null) size = this.getVirtualSize(); return policy.getRoundFee(size, rate); } /** * Calculate the transaction's rate based on size * and fees. Size will be calculated if not present. * @param {CoinView} view * @param {Number?} [size] * @returns {Rate} */ getRate(view, size) { const fee = this.getFee(view); if (fee < 0) return 0; if (size == null) size = this.getVirtualSize(); return policy.getRate(size, fee); } /** * Get all unique outpoint hashes. * @returns {Hash[]} Outpoint hashes. */ getPrevout() { if (this.isCoinbase()) return []; const prevout = new BufferSet(); for (const input of this.inputs) prevout.add(input.prevout.hash); return prevout.toArray(); } /** * Test a transaction against a bloom filter. * @param {BloomFilter} filter * @returns {Boolean} */ test(filter) { if (filter.test(this.hash())) return true; for (let i = 0; i < this.outputs.length; i++) { const {address, covenant} = this.outputs[i]; if (filter.test(address.hash) || covenant.test(filter)) return true; } for (const {prevout} of this.inputs) { if (filter.test(prevout.encode())) return true; } return false; } /** * Test a transaction against a bloom filter. * @param {BloomFilter} filter * @returns {Boolean} */ testAndMaybeUpdate(filter) { let found = false; if (filter.test(this.hash())) found = true; for (let i = 0; i < this.outputs.length; i++) { const {address, covenant} = this.outputs[i]; if (filter.test(address.hash) || covenant.test(filter)) { const prevout = Outpoint.fromTX(this, i); filter.add(prevout.encode()); found = true; } } if (found) return found; for (const {prevout} of this.inputs) { if (filter.test(prevout.encode())) return true; } return false; } /** * Get little-endian tx hash. * @returns {HexHash} */ txid() { return this.hash().toString('hex'); } /** * Get little-endian wtx hash. * @returns {HexHash} */ wtxid() { return this.witnessHash().toString('hex'); } /** * Create outpoint from output index. * @param {Number} index * @returns {Outpoint} */ outpoint(index) { return new Outpoint(this.hash(), index); } /** * Get input from index. * @param {Number} index * @returns {Input|null} */ input(index) { if (index >= this.inputs.length) return null; return this.inputs[index]; } /** * Get output from index. * @param {Number} index * @returns {Output|null} */ output(index) { if (index >= this.outputs.length) return null; return this.outputs[index]; } /** * Get covenant from index. * @param {Number} index * @returns {Covenant|null} */ covenant(index) { if (index >= this.outputs.length) return null; return this.outputs[index].covenant; } /** * Convert the tx to an inv item. * @returns {InvItem} */ toInv() { return new InvItem(InvItem.types.TX, this.hash()); } /** * Inspect the transaction and return a more * user-friendly representation of the data. * @param {CoinView?} [view] * @param {ChainEntry?} [entry] * @param {Number?} [index] * @returns {Object} */ format(view, entry, index) { let rate = 0; let fee = 0; let height = -1; let block = null; let time = 0; let date = null; if (view) { fee = this.getFee(view); rate = this.getRate(view); // Rate can exceed 53 bits in testing. if (!Number.isSafeInteger(rate)) rate = 0; } if (entry) { height = entry.height; block = entry.hash.toString('hex'); time = entry.time; date = util.date(time); } if (index == null) index = -1; return { hash: this.txid(), witnessHash: this.wtxid(), size: this.getSize(), virtualSize: this.getVirtualSize(), value: Amount.coin(this.getOutputValue()), fee: Amount.coin(fee), rate: Amount.coin(rate), minFee: Amount.coin(this.getMinFee()), height: height, block: block, time: time, date: date, index: index, version: this.version, inputs: this.inputs.map((input) => { const coin = view ? view.getOutputFor(input) : null; return input.format(coin); }), outputs: this.outputs, locktime: this.locktime }; } /** * Convert the transaction to an object suitable * for JSON serialization. * @param {Network} [network] * @param {CoinView} [view] * @param {ChainEntry} [entry] * @param {Number} [index] * @returns {Object} */ getJSON(network, view, entry, index) { let rate, fee, height, block, time, date; if (view) { fee = this.getFee(view); rate = this.getRate(view); // Rate can exceed 53 bits in testing. if (!Number.isSafeInteger(rate)) rate = 0; } if (entry) { height = entry.height; block = entry.hash.toString('hex'); time = entry.time; date = util.date(time); } network = Network.get(network); return { hash: this.txid(), witnessHash: this.wtxid(), fee: fee, rate: rate, mtime: util.now(), height: height, block: block, time: time, date: date, index: index, version: this.version, inputs: this.inputs.map((input) => { const coin = view ? view.getCoinFor(input) : null; const path = view ? view.getPathFor(input) : null; return input.getJSON(network, coin, path); }), outputs: this.outputs.map((output) => { return output.getJSON(network); }), locktime: this.locktime, hex: this.toHex() }; } /** * Inject properties from a json object. * @param {Object} json */ fromJSON(json) { assert(json, 'TX data is required.'); assert((json.version >>> 0) === json.version, 'Version must be a uint32.'); assert(Array.isArray(json.inputs), 'Inputs must be an array.'); assert(Array.isArray(json.outputs), 'Outputs must be an array.'); assert((json.locktime >>> 0) === json.locktime, 'Locktime must be a uint32.'); this.version = json.version; for (const input of json.inputs) this.inputs.push(Input.fromJSON(input)); for (const output of json.outputs) this.outputs.push(Output.fromJSON(output)); this.locktime = json.locktime; return this; } /** * Inject properties from serialized * buffer reader (witness serialization). * @param {bio.BufferReader} br */ read(br) { br.start(); this.version = br.readU32(); const inCount = br.readVarint(); for (let i = 0; i < inCount; i++) this.inputs.push(Input.read(br)); const outCount = br.readVarint(); for (let i = 0; i < outCount; i++) this.outputs.push(Output.read(br)); this.locktime = br.readU32(); const start = br.offset; for (let i = 0; i < inCount; i++) { const input = this.inputs[i]; input.witness.read(br); } const witness = br.offset - start; if (!this.mutable) { const raw = br.endData(); const base = raw.length - witness; this._raw = raw; this._sizes = new Sizes(base, witness); } else { br.end(); } return this; } /** * Calculate the real size of the transaction * with the witness included. * @returns {Sizes} */ getSizes() { if (this._sizes) return this._sizes; let base = 0; let witness = 0; base += 4; base += encoding.sizeVarint(this.inputs.length); for (const input of this.inputs) { base += 40; witness += input.witness.getVarSize(); } base += encoding.sizeVarint(this.outputs.length); for (const output of this.outputs) base += output.getSize(); base += 4; const sizes = new Sizes(base, witness); if (!this.mutable) this._sizes = sizes; return sizes; } /** * Serialize transaction with witness. Calculates the witness * size as it is framing (exposed on return value as `witness`). * @param {BufioWriter} bw * @returns {BufioWriter} */ write(bw) { if (this._raw) { bw.writeBytes(this._raw); return bw; } bw.writeU32(this.version); bw.writeVarint(this.inputs.length); for (const input of this.inputs) input.write(bw); bw.writeVarint(this.outputs.length); for (const output of this.outputs) output.write(bw); bw.writeU32(this.locktime); for (const input of this.inputs) input.witness.write(bw); return bw; } /** * Serialize transaction. * @returns {Buffer} */ encode() { if (this.mutable) return super.encode(); if (!this._raw) this._raw = super.encode(); return this._raw; } /** * Calculate left hash. * @returns {Buffer} */ left() { return this.hashes()[0]; } /** * Calculate right hash. * @returns {Buffer} */ right() { return this.hashes()[1]; } /** * Calculate root hash. * @returns {Buffer} */ root() { return this.hashes()[2]; } /** * Calculate all three transaction hashes. * @private * @returns {Buffer[]} */ hashes() { if (this._hash && this._wdhash && this._whash) return [this._hash, this._wdhash, this._whash]; const {base, witness} = this.getSizes(); const raw = this.encode(); assert(raw.length === base + witness); // Normal data. const ndata = raw.slice(0, base); // Witness data. const wdata = raw.slice(base, base + witness); // Left = HASH(normal-data) = normal txid const hash = blake2b.digest(ndata); // Right = HASH(witness-data) const wdhash = blake2b.digest(wdata); // WTXID = HASH(normal-txid || witness-data-hash) const whash = blake2b.root(hash, wdhash); if (!this.mutable) { this._hash = hash; this._wdhash = wdhash; this._whash = whash; } return [hash, wdhash, whash]; } /** * Test whether an object is a TX. * @param {Object} obj * @returns {Boolean} */ static isTX(obj) { return obj instanceof TX; } } /* * Helpers */ class Sizes { constructor(base, witness) { this.base = base; this.witness = witness; } getWeight() { const total = this.base + this.witness; return this.base * (consensus.WITNESS_SCALE_FACTOR - 1) + total; } } /* * Expose */ module.exports = TX;