diff --git a/modules/ecash-lib/src/index.ts b/modules/ecash-lib/src/index.ts --- a/modules/ecash-lib/src/index.ts +++ b/modules/ecash-lib/src/index.ts @@ -7,7 +7,9 @@ export * from './op.js'; export * from './opcode.js'; export * from './script.js'; +export * from './sigHashType.js'; export * from './tx.js'; +export * from './unsignedTx.js'; export * from './io/bytes.js'; export * from './io/hex.js'; export * from './io/int.js'; diff --git a/modules/ecash-lib/src/unsignedTx.test.ts b/modules/ecash-lib/src/unsignedTx.test.ts new file mode 100644 --- /dev/null +++ b/modules/ecash-lib/src/unsignedTx.test.ts @@ -0,0 +1,507 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { expect } from 'chai'; +import fs from 'node:fs/promises'; + +import { fromHex, toHex } from './io/hex.js'; +import { Script } from './script.js'; +import { SignData, Tx, TxInput } from './tx.js'; +import { SighashPreimage, UnsignedTx } from './unsignedTx.js'; +import { sha256d } from './hash.js'; +import { + ALL_ANYONECANPAY_BIP143, + ALL_BIP143, + ALL_LEGACY, + NONE_ANYONECANPAY_BIP143, + NONE_BIP143, + SINGLE_ANYONECANPAY_BIP143, + SINGLE_BIP143, +} from './sigHashType.js'; +import { initWasm } from './ecc.js'; + +const TX = new Tx({ + version: 0xfacefeed, + inputs: [ + { + prevOut: { + txid: '0123456789abcdef99887766554433220000000000000000f1e2d3c4b5a69788', + outIdx: 0xdeadbeef, + }, + script: new Script(), + sequence: 0x87654321, + signData: { + value: 0x123456789, + outputScript: new Script(fromHex('abacadaeafb0abac')), + }, + }, + { + prevOut: { + txid: new Uint8Array([...Array(32).keys()]), + outIdx: 0x76757473, + }, + script: new Script(), + sequence: 0x10605, + signData: { + value: 0x9876, + redeemScript: new Script(fromHex('ab778899ac55')), + }, + }, + ], + outputs: [ + { + value: 0x2134, + script: new Script(fromHex('1133557799')), + }, + { + value: 0x8079685746352413n, + script: new Script(fromHex('564738291092837465')), + }, + { + value: 0, + script: new Script(fromHex('6a68656c6c6f')), + }, + ], + locktime: 0xf00dbabe, +}); + +const VERSION_HEX = 'edfecefa'; + +const PREVOUT0_HEX = + '8897a6b5c4d3e2f100000000000000002233445566778899efcdab8967452301' + + 'efbeadde'; +const PREVOUT1_HEX = + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' + + '73747576'; +const SEQUENCE0_HEX = '21436587'; +const SEQUENCE1_HEX = '05060100'; +const VALUE0_HEX = '8967452301000000'; +const VALUE1_HEX = '7698000000000000'; + +const OUTPUT0_HEX = '3421000000000000' + '051133557799'; +const OUTPUT1_HEX = '1324354657687980' + '09564738291092837465'; +const OUTPUT2_HEX = '0000000000000000' + '066a68656c6c6f'; +const OUTPUTS_HEX = OUTPUT0_HEX + OUTPUT1_HEX + OUTPUT2_HEX; + +const LOCKTIME_HEX = 'beba0df0'; + +describe('UnsignedTx', async () => { + // Can't use `fetch` for local file so we have to read it using `fs` + await initWasm(fs.readFile('./src/ffi/ecash_lib_wasm_bg.wasm')); + + it('UnsignedTx.dummyFromTx', () => { + const dummy = UnsignedTx.dummyFromTx(new Tx()); + expect(dummy.prevoutsHash).to.deep.equal(new Uint8Array(32)); + expect(dummy.sequencesHash).to.deep.equal(new Uint8Array(32)); + expect(dummy.outputsHash).to.deep.equal(new Uint8Array(32)); + }); + it('UnsignedTx.fromTx empty', () => { + const unsigned = UnsignedTx.fromTx(new Tx()); + const emptyHash = sha256d(new Uint8Array()); + expect(unsigned.prevoutsHash).to.deep.equal(emptyHash); + expect(unsigned.sequencesHash).to.deep.equal(emptyHash); + expect(unsigned.outputsHash).to.deep.equal(emptyHash); + }); + it('UnsignedTx.fromTx', () => { + const unsigned = UnsignedTx.fromTx(TX); + expect(unsigned.prevoutsHash).to.deep.equal( + sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX)), + ); + expect(unsigned.sequencesHash).to.deep.equal( + sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX)), + ); + expect(unsigned.outputsHash).to.deep.equal( + sha256d(fromHex(OUTPUTS_HEX)), + ); + }); + + it('UnsignedTxInput.sigHashPreimage ALL', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect(unsigned.inputAt(0).sigHashPreimage(ALL_BIP143)).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + toHex(sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX))) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + '41000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect(unsigned.inputAt(1).sigHashPreimage(ALL_BIP143)).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + toHex(sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX))) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + '41000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage ALL|ANYONECANPAY', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect( + unsigned.inputAt(0).sigHashPreimage(ALL_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + 'c1000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(1).sigHashPreimage(ALL_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + 'c1000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage NONE', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect(unsigned.inputAt(0).sigHashPreimage(NONE_BIP143)).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + '00'.repeat(32) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + '00'.repeat(32) + + LOCKTIME_HEX + + '42000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect(unsigned.inputAt(1).sigHashPreimage(NONE_BIP143)).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + '00'.repeat(32) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + '00'.repeat(32) + + LOCKTIME_HEX + + '42000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage NONE|ANYONECANPAY', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect( + unsigned.inputAt(0).sigHashPreimage(NONE_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + '00'.repeat(32) + + LOCKTIME_HEX + + 'c2000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(1).sigHashPreimage(NONE_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + '00'.repeat(32) + + LOCKTIME_HEX + + 'c2000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage SINGLE', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect( + unsigned.inputAt(0).sigHashPreimage(SINGLE_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + '00'.repeat(32) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUT0_HEX))) + + LOCKTIME_HEX + + '43000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(1).sigHashPreimage(SINGLE_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + '00'.repeat(32) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + toHex(sha256d(fromHex(OUTPUT1_HEX))) + + LOCKTIME_HEX + + '43000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + + // If there's no corresponding output, hashOutputs will be 000...000 + const unsignedTooFewOutputs = UnsignedTx.fromTx( + new Tx({ + ...TX, + outputs: [], + }), + ); + expect( + unsignedTooFewOutputs.inputAt(0).sigHashPreimage(SINGLE_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + '00'.repeat(32) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + '00'.repeat(32) + + LOCKTIME_HEX + + '43000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage SINGLE|ANYONECANPAY', () => { + const unsigned = UnsignedTx.fromTx(TX); + + expect( + unsigned.inputAt(0).sigHashPreimage(SINGLE_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT0_HEX + + '08abacadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUT0_HEX))) + + LOCKTIME_HEX + + 'c3000000', + ), + scriptCode: TX.inputs[0].signData!.outputScript, + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(1).sigHashPreimage(SINGLE_ANYONECANPAY_BIP143), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + '00'.repeat(64) + + PREVOUT1_HEX + + '06ab778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + toHex(sha256d(fromHex(OUTPUT1_HEX))) + + LOCKTIME_HEX + + 'c3000000', + ), + scriptCode: TX.inputs[1].signData!.redeemScript, + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage OP_CODESEPARATOR', () => { + const unsigned = UnsignedTx.fromTx(TX); + expect( + unsigned.inputAt(0).sigHashPreimage(ALL_BIP143, 0), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + toHex(sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX))) + + PREVOUT0_HEX + + '07acadaeafb0abac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + '41000000', + ), + scriptCode: new Script(fromHex('acadaeafb0abac')), + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(0).sigHashPreimage(ALL_BIP143, 1), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + toHex(sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX))) + + PREVOUT0_HEX + + '01ac' + + VALUE0_HEX + + SEQUENCE0_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + '41000000', + ), + scriptCode: new Script(fromHex('ac')), + redeemScript: TX.inputs[0].signData!.outputScript, + } as SighashPreimage); + + expect( + unsigned.inputAt(1).sigHashPreimage(ALL_BIP143, 0), + ).to.deep.equal({ + bytes: fromHex( + VERSION_HEX + + toHex(sha256d(fromHex(PREVOUT0_HEX + PREVOUT1_HEX))) + + toHex(sha256d(fromHex(SEQUENCE0_HEX + SEQUENCE1_HEX))) + + PREVOUT1_HEX + + '05778899ac55' + + VALUE1_HEX + + SEQUENCE1_HEX + + toHex(sha256d(fromHex(OUTPUTS_HEX))) + + LOCKTIME_HEX + + '41000000', + ), + scriptCode: new Script(fromHex('778899ac55')), + redeemScript: TX.inputs[1].signData!.redeemScript, + } as SighashPreimage); + }); + + it('UnsignedTxInput.sigHashPreimage failure', () => { + expect(() => + UnsignedTx.fromTx(TX).inputAt(0).sigHashPreimage(ALL_LEGACY), + ).to.throw('Legacy sighash type not implemented'); + + expect(() => + UnsignedTx.fromTx( + new Tx({ + inputs: [ + { + prevOut: TX.inputs[0].prevOut, + script: new Script(), + sequence: 0, + }, + ], + }), + ) + .inputAt(0) + .sigHashPreimage(ALL_BIP143), + ).to.throw('Input must have signData set'); + + expect(() => + UnsignedTx.fromTx( + new Tx({ + inputs: [ + { + prevOut: TX.inputs[0].prevOut, + script: new Script(), + sequence: 0, + signData: { value: 0 }, + }, + ], + }), + ) + .inputAt(0) + .sigHashPreimage(ALL_BIP143), + ).to.throw('Must either set outputScript or redeemScript'); + + expect(() => + UnsignedTx.fromTx( + new Tx({ + inputs: [ + { + prevOut: TX.inputs[0].prevOut, + script: new Script(), + sequence: 0, + signData: { + value: 0, + outputScript: Script.p2sh(new Uint8Array(20)), + }, + }, + ], + }), + ) + .inputAt(0) + .sigHashPreimage(ALL_BIP143), + ).to.throw('P2SH requires redeemScript to be set, not outputScript'); + }); + + it('UnsignedTxInput.txInput', () => { + const unsigned = UnsignedTx.fromTx(TX); + expect(unsigned.inputAt(0).txInput()).to.deep.equal(TX.inputs[0]); + expect(unsigned.inputAt(1).txInput()).to.deep.equal(TX.inputs[1]); + }); +}); diff --git a/modules/ecash-lib/src/unsignedTx.ts b/modules/ecash-lib/src/unsignedTx.ts new file mode 100644 --- /dev/null +++ b/modules/ecash-lib/src/unsignedTx.ts @@ -0,0 +1,224 @@ +// Copyright (c) 2024 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import { sha256d } from './hash.js'; +import { Writer } from './io/writer.js'; +import { WriterBytes } from './io/writerbytes.js'; +import { WriterLength } from './io/writerlength.js'; +import { Script } from './script.js'; +import { + SigHashType, + SigHashTypeInputs, + SigHashTypeOutputs, + SigHashTypeVariant, +} from './sigHashType.js'; +import { SignData, Tx, TxInput, writeOutPoint, writeTxOutput } from './tx.js'; + +/** An unsigned tx, which helps us build the sighash preimage we need to sign */ +export class UnsignedTx { + tx: Tx; + prevoutsHash: Uint8Array; + sequencesHash: Uint8Array; + outputsHash: Uint8Array; + + private constructor(params: { + tx: Tx; + prevoutsHash: Uint8Array; + sequencesHash: Uint8Array; + outputsHash: Uint8Array; + }) { + this.tx = params.tx; + this.prevoutsHash = params.prevoutsHash; + this.sequencesHash = params.sequencesHash; + this.outputsHash = params.outputsHash; + } + + /** + * Make an UnsignedTx from a Tx, will precompute the fields required to + * sign the tx + **/ + public static fromTx(tx: Tx): UnsignedTx { + return new UnsignedTx({ + tx, + prevoutsHash: txWriterHash(tx, writePrevouts), + sequencesHash: txWriterHash(tx, writeSequences), + outputsHash: txWriterHash(tx, writeOutputs), + }); + } + + /** + * Make a dummy UnsignedTx from a Tx, will set dummy values for the fields + * required to sign the tx. Useful for tx size estimation. + **/ + public static dummyFromTx(tx: Tx): UnsignedTx { + return new UnsignedTx({ + tx, + prevoutsHash: new Uint8Array(32), + sequencesHash: new Uint8Array(32), + outputsHash: new Uint8Array(32), + }); + } + + /** Return the unsigned tx input at the given input index */ + public inputAt(inputIdx: number): UnsignedTxInput { + return new UnsignedTxInput({ inputIdx, unsignedTx: this }); + } +} + +/** A preimage of a sighash for an input's scriptSig ready to be signed */ +export interface SighashPreimage { + /** Bytes of the serialized sighash preimage */ + bytes: Uint8Array; + /** Script code of the preimage, with OP_CODESEPARATOR cut out */ + scriptCode: Script; + /** Redeem script, with no modifications */ + redeemScript: Script; +} + +/** + * An unsigned tx input, can be used to build a sighash preimage ready to be + * signed + **/ +export class UnsignedTxInput { + inputIdx: number; + unsignedTx: UnsignedTx; + + public constructor(params: { inputIdx: number; unsignedTx: UnsignedTx }) { + this.inputIdx = params.inputIdx; + this.unsignedTx = params.unsignedTx; + } + + /** + * Build the sigHashPreimage for this input, with the given sigHashType + * and OP_CODESEPARATOR index + **/ + public sigHashPreimage( + sigHashType: SigHashType, + nCodesep?: number, + ): SighashPreimage { + if (sigHashType.variant == SigHashTypeVariant.LEGACY) { + throw new Error('Legacy sighash type not implemented'); + } + const tx = this.unsignedTx.tx; + const input = tx.inputs[this.inputIdx]; + if (input.signData === undefined) { + throw new Error('Input must have signData set'); + } + const signData = input.signData; + const redeemScript = signDataScriptCode(input.signData); + const scriptCode = + nCodesep === undefined + ? redeemScript + : redeemScript.cutOutCodesep(nCodesep); + + let hashOutputs: Uint8Array; + switch (sigHashType.outputType) { + case SigHashTypeOutputs.ALL: + hashOutputs = this.unsignedTx.outputsHash; + break; + case SigHashTypeOutputs.NONE: + hashOutputs = new Uint8Array(32); + break; + case SigHashTypeOutputs.SINGLE: + if (this.inputIdx < tx.outputs.length) { + const output = tx.outputs[this.inputIdx]; + const writerOutputLength = new WriterLength(); + writeTxOutput(output, writerOutputLength); + const writerOutput = new WriterBytes( + writerOutputLength.length, + ); + writeTxOutput(output, writerOutput); + hashOutputs = sha256d(writerOutput.data); + } else { + hashOutputs = new Uint8Array(32); + } + break; + } + + const writePreimage = (writer: Writer) => { + writer.putU32(tx.version); + if (sigHashType.inputType == SigHashTypeInputs.FIXED) { + writer.putBytes(this.unsignedTx.prevoutsHash); + } else { + writer.putBytes(new Uint8Array(32)); + } + if ( + sigHashType.inputType == SigHashTypeInputs.FIXED && + sigHashType.outputType == SigHashTypeOutputs.ALL + ) { + writer.putBytes(this.unsignedTx.sequencesHash); + } else { + writer.putBytes(new Uint8Array(32)); + } + writeOutPoint(input.prevOut, writer); + scriptCode.writeWithSize(writer); + writer.putU64(signData.value); + writer.putU32(input.sequence); + writer.putBytes(hashOutputs); + writer.putU32(tx.locktime); + writer.putU32(sigHashType.toInt()); + }; + + const preimageWriterLen = new WriterLength(); + writePreimage(preimageWriterLen); + const preimageWriter = new WriterBytes(preimageWriterLen.length); + writePreimage(preimageWriter); + + return { + bytes: preimageWriter.data, + scriptCode, + redeemScript, + }; + } + + /** Return the TxInput of this UnsignedTxInput */ + public txInput(): TxInput { + return this.unsignedTx.tx.inputs[this.inputIdx]; + } +} + +/** Find the scriptCode that should be signed */ +function signDataScriptCode(signData: SignData): Script { + if (signData.outputScript !== undefined) { + if (signData.outputScript.isP2sh()) { + throw new Error( + 'P2SH requires redeemScript to be set, not outputScript', + ); + } + return signData.outputScript; + } + if (signData.redeemScript === undefined) { + throw new Error('Must either set outputScript or redeemScript'); + } + return signData.redeemScript; +} + +function txWriterHash( + tx: Tx, + fn: (tx: Tx, writer: Writer) => void, +): Uint8Array { + const writerLength = new WriterLength(); + fn(tx, writerLength); + const writer = new WriterBytes(writerLength.length); + fn(tx, writer); + return sha256d(writer.data); +} + +function writePrevouts(tx: Tx, writer: Writer) { + for (const input of tx.inputs) { + writeOutPoint(input.prevOut, writer); + } +} + +function writeSequences(tx: Tx, writer: Writer) { + for (const input of tx.inputs) { + writer.putU32(input.sequence); + } +} + +function writeOutputs(tx: Tx, writer: Writer) { + for (const output of tx.outputs) { + writeTxOutput(output, writer); + } +}