diff --git a/coinlib/example/coinlib_example.dart b/coinlib/example/coinlib_example.dart index 7a3a0fd..62ede67 100644 --- a/coinlib/example/coinlib_example.dart +++ b/coinlib/example/coinlib_example.dart @@ -59,7 +59,7 @@ void main() async { "32d1f1cf811456c6da4ef9e1cb7f8bb80c4c5e9f2d2c3d743f2b68a9c6857823", ); - final tx = LegacyTransaction( + final tx = Transaction( inputs: [ P2PKHInput(prevOut: OutPoint(prevHash, 1), publicKey: key1.publicKey), ], diff --git a/coinlib/lib/src/coinlib_base.dart b/coinlib/lib/src/coinlib_base.dart index a65cccf..54e953e 100644 --- a/coinlib/lib/src/coinlib_base.dart +++ b/coinlib/lib/src/coinlib_base.dart @@ -31,12 +31,13 @@ export 'package:coinlib/src/scripts/programs/p2wsh.dart'; export 'package:coinlib/src/tx/input.dart'; export 'package:coinlib/src/tx/input_signature.dart'; -export 'package:coinlib/src/tx/legacy_transaction.dart'; +export 'package:coinlib/src/tx/transaction.dart'; export 'package:coinlib/src/tx/outpoint.dart'; export 'package:coinlib/src/tx/output.dart'; export 'package:coinlib/src/tx/p2pkh_input.dart'; export 'package:coinlib/src/tx/p2sh_multisig_input.dart'; export 'package:coinlib/src/tx/p2wpkh_input.dart'; +export 'package:coinlib/src/tx/pkh_input.dart'; export 'package:coinlib/src/tx/raw_input.dart'; export 'package:coinlib/src/tx/sighash_type.dart'; export 'package:coinlib/src/tx/witness_input.dart'; diff --git a/coinlib/lib/src/tx/legacy_transaction.dart b/coinlib/lib/src/tx/legacy_transaction.dart deleted file mode 100644 index c2af6f0..0000000 --- a/coinlib/lib/src/tx/legacy_transaction.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:typed_data'; -import 'package:coinlib/src/common/checks.dart'; -import 'package:coinlib/src/common/hex.dart'; -import 'package:coinlib/src/common/serial.dart'; -import 'package:coinlib/src/crypto/ec_private_key.dart'; -import 'package:coinlib/src/crypto/ec_public_key.dart'; -import 'package:coinlib/src/crypto/ecdsa_signature.dart'; -import 'package:coinlib/src/crypto/hash.dart'; -import 'package:coinlib/src/scripts/operations.dart'; -import 'package:coinlib/src/scripts/programs/p2pkh.dart'; -import 'package:coinlib/src/scripts/script.dart'; -import 'p2pkh_input.dart'; -import 'p2sh_multisig_input.dart'; -import 'sighash_type.dart'; -import 'input.dart'; -import 'input_signature.dart'; -import 'output.dart'; -import 'raw_input.dart'; - -class TransactionTooLarge implements Exception {} -class CannotSignInput implements Exception { - final String message; - CannotSignInput(this.message); - @override - String toString() => "CannotSignInput: $message"; -} - -/// A legacy transaction does not include or consider witness data. Use -/// [WitnessTransaction] to sign and build transactions with witness inputs. -class LegacyTransaction with Writable { - - static const int currentVersion = 3; - static const int maxSize = 1000000; - - static const int minInputSize = 41; - static const int minOutputSize = 9; - static const int minOtherSize = 10; - - static const int maxInputs - = (maxSize - minOtherSize - minOutputSize) ~/ minInputSize; - static const int maxOutputs - = (maxSize - minOtherSize - minInputSize) ~/ minOutputSize; - - final int version; - final List inputs; - final List outputs; - final int locktime; - - /// Constructs a transaction with the given [inputs] and [outputs]. - /// [TransactionTooLarge] will be thrown if the resulting transction exceeds - /// [maxSize] (1MB). - LegacyTransaction({ - this.version = currentVersion, - required Iterable inputs, - required Iterable outputs, - this.locktime = 0, - }) - : inputs = List.unmodifiable(inputs), - outputs = List.unmodifiable(outputs) - { - checkInt32(version); - checkUint32(locktime); - if (size > maxSize) throw TransactionTooLarge(); - } - - static int _readAndCheckVarInt(BytesReader reader, int max) { - final n = reader.readVarInt(); - if (n > BigInt.from(max)) throw TransactionTooLarge(); - return n.toInt(); - } - - /// Reads a transaction from a [BytesReader], which may throw - /// [TransactionTooLarge] or [OutOfData] if the data doesn't represent a - /// complete transaction within [maxSize] (1MB). - factory LegacyTransaction.fromReader(BytesReader reader) { - - final version = reader.readInt32(); - - final inputs = List.generate( - _readAndCheckVarInt(reader, maxInputs), - (i) => Input.match(RawInput.fromReader(reader)), - ); - - final outputs = List.generate( - _readAndCheckVarInt(reader, maxOutputs), - (i) => Output.fromReader(reader), - ); - - final locktime = reader.readUInt32(); - - return LegacyTransaction( - version: version, - inputs: inputs, - outputs: outputs, - locktime: locktime, - ); - - } - - /// Constructs a transaction from serialised bytes. See [fromReader]. - factory LegacyTransaction.fromBytes(Uint8List bytes) - => LegacyTransaction.fromReader(BytesReader(bytes)); - - /// Constructs a transaction from the serialised data encoded as hex. See - /// [fromReader]. - factory LegacyTransaction.fromHex(String hex) - => LegacyTransaction.fromBytes(hexToBytes(hex)); - - @override - void write(Writer writer) { - writer.writeInt32(version); - writer.writeVarInt(BigInt.from(inputs.length)); - for (final input in inputs) { - input.write(writer); - } - writer.writeVarInt(BigInt.from(outputs.length)); - for (final output in outputs) { - output.write(writer); - } - writer.writeUInt32(locktime); - } - - static final ScriptOp _codeseperator = ScriptOpCode.fromName("CODESEPARATOR"); - - /// Obtains the hash for an input signature for the input at [inputN]. The - /// [prevOutScript] from the previous output is necessary. [hashType] controls - /// what data is included in the signature. - Uint8List signatureHash(int inputN, Script prevOutScript, SigHashType hashType) { - - if (inputN < 0 || inputN >= inputs.length) { - throw ArgumentError.value( - inputN, "inputN", "not within input range 0-${inputs.length-1}", - ); - } - - // Remove OP_CODESEPERATOR from previous output script - final correctedScriptSig = Script( - prevOutScript.ops.where((op) => !op.match(_codeseperator)), - ).compiled; - - // If there is no matching output for SIGHASH_SINGLE, then return all null - // bytes apart from the last byte that should be 1 - if (hashType.single && inputN >= outputs.length) { - return Uint8List(32)..last = 1; - } - - // Create modified transaction for obtaining a signature hash - - final modifiedInputs = ( - hashType.anyOneCanPay ? [inputs[inputN]] : inputs - ).asMap().map( - (index, input) { - final isThisInput = hashType.anyOneCanPay || index == inputN; - return MapEntry( - index, - RawInput( - prevOut: input.prevOut, - // Use the corrected previous output script for the input being signed - // and blank scripts for all the others - scriptSig: isThisInput ? correctedScriptSig : Uint8List(0), - // Make sequence 0 for other inputs unless using SIGHASH_ALL - sequence: isThisInput || hashType.all ? input.sequence : 0, - ), - ); - } - ).values; - - final modifiedOutputs = hashType.all ? outputs : ( - hashType.none ? [] : [ - // Single output - // Include blank outputs upto output index - ...Iterable.generate(inputN, (i) => Output.blank()), - outputs[inputN], - ] - ); - - final modifiedTx = LegacyTransaction( - version: version, - inputs: modifiedInputs, - outputs: modifiedOutputs, - locktime: locktime, - ); - - // Add sighash type onto the end - final bytes = Uint8List(modifiedTx.size + 4); - final writer = BytesWriter(bytes); - modifiedTx.write(writer); - writer.writeUInt32(hashType.value); - - // Use sha256d for signature hash - return sha256DoubleHash(bytes); - - } - - /// Sign the input at [inputN] with the [key] and [hashType] and return a new - /// [LegacyTransaction] with the signed input. The input must be a signable - /// P2PKH or P2SH multisig input or [CannotSignInput] will be thrown. - LegacyTransaction sign({ - required int inputN, - required ECPrivateKey key, - hashType = const SigHashType.all(), - }) { - - if (inputN >= inputs.length) { - throw ArgumentError.value(inputN, "inputN", "outside range of inputs"); - } - - if (!hashType.none && outputs.isEmpty) { - throw CannotSignInput("Cannot sign input without any outputs"); - } - - final input = inputs[inputN]; - - // Get data for input - late List pubkeys; - late Script prevOutScript; - - if (input is P2PKHInput) { - pubkeys = [input.publicKey]; - prevOutScript = P2PKH.fromPublicKey(input.publicKey).script; - } else if (input is P2SHMultisigInput) { - pubkeys = input.program.pubkeys; - // For P2SH it is the redeem script - prevOutScript = input.program.script; - } else { - throw CannotSignInput("${input.runtimeType} not a signable input"); - } - - if (!pubkeys.contains(key.pubkey)) { - throw CannotSignInput("Public key not part of input"); - } - - // Create signature - final signHash = signatureHash(inputN, prevOutScript, hashType); - final insig = InputSignature(ECDSASignature.sign(key, signHash), hashType); - - // Get input with new signature - late Input newInput; - if (input is P2PKHInput) { - newInput = input.addSignature(insig); - } else if (input is P2SHMultisigInput) { - // Add signature in the correct order - newInput = input.insertSignature( - insig, - key.pubkey, - (hashType) => signatureHash(inputN, prevOutScript, hashType), - ); - } - - // Replace input in input list - final newInputs = inputs.asMap().map( - (index, input) => MapEntry( - index, index == inputN ? newInput : input, - ), - ).values; - - return LegacyTransaction( - version: version, - inputs: newInputs, - outputs: outputs, - locktime: locktime, - ); - - } - - /// Returns a new [LegacyTransaction] with the [input] added to the end of the - /// input list. - LegacyTransaction addInput(Input input) { - - // For existing inputs, remove any signatures without ANYONECANPAY - final modifiedInputs = inputs.map( - (input) => input.filterSignatures((insig) => insig.hashType.anyOneCanPay), - ); - - // Add new input to end of inputs of new transaction - return LegacyTransaction( - version: version, - inputs: [...modifiedInputs, input], - outputs: outputs, - locktime: locktime, - ); - - } - - /// Returns a new [LegacyTransaction] with the [output] added to the end of the - /// output list. - LegacyTransaction addOutput(Output output) { - - final modifiedInputs = inputs.asMap().map( - (i, input) => MapEntry( - i, input.filterSignatures( - (insig) - // Allow signatures that sign no outpus - => insig.hashType.none - // Allow signatures that sign a single output which isn't the one - // being added - || (insig.hashType.single && i != outputs.length), - ), - ), - ).values; - - return LegacyTransaction( - version: version, - inputs: modifiedInputs, - outputs: [...outputs, output], - locktime: locktime, - ); - - } - - Uint8List? _hashCache; - Uint8List get hash => _hashCache ??= sha256DoubleHash(toBytes()); - /// Get the reversed hash as hex which is usual for Peercoin transactions - String get hashHex => bytesToHex(Uint8List.fromList(hash.reversed.toList())); - /// Alias for [hashHex]. This is the tx hash reversed in hex format. - String get txid => hashHex; - - bool get isCoinBase - => inputs.length == 1 - && inputs.first.prevOut.coinbase - && outputs.isNotEmpty; - - bool get isCoinStake - => inputs.isNotEmpty - && !inputs.first.prevOut.coinbase - && outputs.length >= 2 - && outputs.first.value == BigInt.zero - && outputs.first.scriptPubKey.isEmpty; - - /// Returns true when all of the inputs are fully signed with at least one - /// input and one output. There is no guarentee that the transaction is valid - /// on the blockchain. - bool get complete - => inputs.isNotEmpty && outputs.isNotEmpty - && inputs.every((input) => input.complete); - -} diff --git a/coinlib/lib/src/tx/p2pkh_input.dart b/coinlib/lib/src/tx/p2pkh_input.dart index bdbc45c..ce1013b 100644 --- a/coinlib/lib/src/tx/p2pkh_input.dart +++ b/coinlib/lib/src/tx/p2pkh_input.dart @@ -2,6 +2,7 @@ import 'package:coinlib/src/crypto/ec_public_key.dart'; import 'package:coinlib/src/scripts/operations.dart'; import 'package:coinlib/src/tx/input_signature.dart'; import 'package:coinlib/src/tx/outpoint.dart'; +import 'package:coinlib/src/tx/pkh_input.dart'; import '../scripts/script.dart'; import 'input.dart'; import 'raw_input.dart'; @@ -9,9 +10,11 @@ import 'raw_input.dart'; /// An input for a Pay-to-Public-Key-Hash output ([P2PKH]). This contains the /// public key that should match the hash in the associated output. It is either /// signed or unsigned and the [addSignature] method can be used to add a signature. -class P2PKHInput extends RawInput { +class P2PKHInput extends RawInput with PKHInput { + @override final ECPublicKey publicKey; + @override final InputSignature? insig; P2PKHInput({ @@ -53,6 +56,7 @@ class P2PKHInput extends RawInput { } + @override /// Returns a new [P2PKHInput] with the [InputSignature] added. Any existing /// signature is replaced. P2PKHInput addSignature(InputSignature insig) => P2PKHInput( @@ -71,9 +75,6 @@ class P2PKHInput extends RawInput { sequence: sequence, ); - @override - bool get complete => insig != null; - @override Script get script => super.script!; diff --git a/coinlib/lib/src/tx/p2wpkh_input.dart b/coinlib/lib/src/tx/p2wpkh_input.dart index f04e897..cc5016b 100644 --- a/coinlib/lib/src/tx/p2wpkh_input.dart +++ b/coinlib/lib/src/tx/p2wpkh_input.dart @@ -3,6 +3,7 @@ import 'package:coinlib/src/crypto/ec_public_key.dart'; import 'package:coinlib/src/tx/input.dart'; import 'package:coinlib/src/tx/input_signature.dart'; import 'package:coinlib/src/tx/outpoint.dart'; +import 'pkh_input.dart'; import 'raw_input.dart'; import 'witness_input.dart'; @@ -11,9 +12,11 @@ import 'witness_input.dart'; /// It is either signed or unsigned and the [addSignature] method can be used to /// add a signature. Signature and public key data is stored in the witness /// data. -class P2WPKHInput extends WitnessInput { +class P2WPKHInput extends WitnessInput with PKHInput { + @override final ECPublicKey publicKey; + @override final InputSignature? insig; P2WPKHInput({ @@ -60,6 +63,7 @@ class P2WPKHInput extends WitnessInput { } + @override /// Returns a new [P2WPKHInput] with the [InputSignature] added. Any existing /// signature is replaced. P2WPKHInput addSignature(InputSignature insig) => P2WPKHInput( @@ -78,7 +82,4 @@ class P2WPKHInput extends WitnessInput { sequence: sequence, ); - @override - bool get complete => insig != null; - } diff --git a/coinlib/lib/src/tx/pkh_input.dart b/coinlib/lib/src/tx/pkh_input.dart new file mode 100644 index 0000000..c093742 --- /dev/null +++ b/coinlib/lib/src/tx/pkh_input.dart @@ -0,0 +1,11 @@ +import 'package:coinlib/src/crypto/ec_public_key.dart'; +import 'package:coinlib/src/tx/input_signature.dart'; + +/// A mixin for Public Key Hash input types, providing the [ECPublicKey] and +/// [InputSignature] required in these inputs. +abstract mixin class PKHInput { + ECPublicKey get publicKey; + InputSignature? get insig; + PKHInput addSignature(InputSignature insig); + bool get complete => insig != null; +} diff --git a/coinlib/lib/src/tx/transaction.dart b/coinlib/lib/src/tx/transaction.dart new file mode 100644 index 0000000..aec8abb --- /dev/null +++ b/coinlib/lib/src/tx/transaction.dart @@ -0,0 +1,517 @@ +import 'dart:typed_data'; +import 'package:coinlib/src/common/checks.dart'; +import 'package:coinlib/src/common/hex.dart'; +import 'package:coinlib/src/common/serial.dart'; +import 'package:coinlib/src/crypto/ec_private_key.dart'; +import 'package:coinlib/src/crypto/ec_public_key.dart'; +import 'package:coinlib/src/crypto/ecdsa_signature.dart'; +import 'package:coinlib/src/crypto/hash.dart'; +import 'package:coinlib/src/scripts/operations.dart'; +import 'package:coinlib/src/scripts/programs/p2pkh.dart'; +import 'package:coinlib/src/scripts/script.dart'; +import 'p2pkh_input.dart'; +import 'p2sh_multisig_input.dart'; +import 'p2wpkh_input.dart'; +import 'pkh_input.dart'; +import 'sighash_type.dart'; +import 'input.dart'; +import 'input_signature.dart'; +import 'output.dart'; +import 'raw_input.dart'; +import 'witness_input.dart'; + +class TransactionTooLarge implements Exception {} +class InvalidTransaction implements Exception {} +class CannotSignInput implements Exception { + final String message; + CannotSignInput(this.message); + @override + String toString() => "CannotSignInput: $message"; +} + +/// Allows construction and signing of Peercoin transactions including those +/// with witness data. +class Transaction with Writable { + + static Uint8List hashZero = Uint8List(32); + static Uint8List hashOne = Uint8List(32)..last = 1; + + static const int currentVersion = 3; + static const int maxSize = 1000000; + + static const int minInputSize = 41; + static const int minOutputSize = 9; + static const int minOtherSize = 10; + + static const int maxInputs + = (maxSize - minOtherSize - minOutputSize) ~/ minInputSize; + static const int maxOutputs + = (maxSize - minOtherSize - minInputSize) ~/ minOutputSize; + + final int version; + final List inputs; + final List outputs; + final int locktime; + + /// Constructs a transaction with the given [inputs] and [outputs]. + /// [TransactionTooLarge] will be thrown if the resulting transction exceeds + /// [maxSize] (1MB). + Transaction({ + this.version = currentVersion, + required Iterable inputs, + required Iterable outputs, + this.locktime = 0, + }) + : inputs = List.unmodifiable(inputs), + outputs = List.unmodifiable(outputs) + { + checkInt32(version); + checkUint32(locktime); + if (size > maxSize) throw TransactionTooLarge(); + } + + static int _readAndCheckVarInt(BytesReader reader, int max) { + final n = reader.readVarInt(); + if (n > BigInt.from(max)) throw TransactionTooLarge(); + return n.toInt(); + } + + static Transaction? _tryRead(BytesReader reader, bool witness) { + + final version = reader.readInt32(); + + if (witness) { + // Check for witness data + final marker = reader.readUInt8(); + final flag = reader.readUInt8(); + if (marker != 0 || flag != 1) return null; + } + + final rawInputs = List.generate( + _readAndCheckVarInt(reader, maxInputs), + (i) => RawInput.fromReader(reader), + ); + + final outputs = List.generate( + _readAndCheckVarInt(reader, maxOutputs), + (i) => Output.fromReader(reader), + ); + + // Match the raw inputs with witness data if this is a witness transaction + final inputs = rawInputs.map( + (raw) => Input.match(raw, witness ? reader.readVector() : []), + // Create list now to ensure we read the witness data before the locktime + ).toList(); + + final locktime = reader.readUInt32(); + + return Transaction( + version: version, + inputs: inputs, + outputs: outputs, + locktime: locktime, + ); + + } + + /// Reads a transaction from a [BytesReader], which may throw + /// [TransactionTooLarge] or [InvalidTransaction] if the data doesn't + /// represent a complete transaction within [maxSize] (1MB). + /// If [expectWitness] is true, the transaction is assumed to be a witness + /// transaction. If it is false, the transction is assumed to be a legacy + /// non-witness transaction. + /// If [expectWitness] is omitted or null, then this method will determine the + /// correct transaction type from the data, starting with a witness type. + factory Transaction.fromReader(BytesReader reader, { bool? expectWitness }) { + + bool tooLarge = false; + final start = reader.offset; + + Transaction? tryReadAndSetTooLarge(bool witness) { + try { + return _tryRead(reader, witness); + } on TransactionTooLarge { + tooLarge = true; + } on Exception catch(_) {} + return null; + } + + if (expectWitness != false) { // Includes null condition + final witnessTx = tryReadAndSetTooLarge(true); + if (witnessTx != null) return witnessTx; + } + + // Reset offset of reader + reader.offset = start; + + if (expectWitness != true) { // Includes null condition + final legacyTx = tryReadAndSetTooLarge(false); + if (legacyTx != null) return legacyTx; + } + + throw tooLarge ? TransactionTooLarge() : InvalidTransaction(); + + } + + /// Constructs a transaction from serialised bytes. See [fromReader]. + factory Transaction.fromBytes(Uint8List bytes, { bool? expectWitness }) + => Transaction.fromReader(BytesReader(bytes), expectWitness: expectWitness); + + /// Constructs a transaction from the serialised data encoded as hex. See + /// [fromReader]. + factory Transaction.fromHex(String hex, { bool? expectWitness }) + => Transaction.fromBytes(hexToBytes(hex), expectWitness: expectWitness); + + @override + void write(Writer writer) { + + writer.writeInt32(version); + + if (isWitness) { + writer.writeUInt8(0); // Marker + writer.writeUInt8(1); // Flag + } + + writer.writeVarInt(BigInt.from(inputs.length)); + for (final input in inputs) { + input.write(writer); + } + + writer.writeVarInt(BigInt.from(outputs.length)); + for (final output in outputs) { + output.write(writer); + } + + if (isWitness) { + for (final input in inputs) { + writer.writeVector(input is WitnessInput ? input.witness : []); + } + } + + writer.writeUInt32(locktime); + + } + + _requireInputRange(int n) { + if (n < 0 || n >= inputs.length) throw RangeError.index(n, inputs, "n"); + } + + static final ScriptOp _codeseperator = ScriptOpCode.fromName("CODESEPARATOR"); + + /// Obtains the hash for an input signature for a non-witness input at + /// [inputN]. The [scriptCode] of the redeem script is necessary. [hashType] + /// controls what data is included in the signature. + Uint8List signatureHash(int inputN, Script scriptCode, SigHashType hashType) { + + _requireInputRange(inputN); + + // Remove OP_CODESEPERATOR from the script code + final correctedScriptSig = Script( + scriptCode.ops.where((op) => !op.match(_codeseperator)), + ).compiled; + + // If there is no matching output for SIGHASH_SINGLE, then return all null + // bytes apart from the last byte that should be 1 + if (hashType.single && inputN >= outputs.length) return hashOne; + + // Create modified transaction for obtaining a signature hash + + final modifiedInputs = ( + hashType.anyOneCanPay ? [inputs[inputN]] : inputs + ).asMap().map( + (index, input) { + final isThisInput = hashType.anyOneCanPay || index == inputN; + return MapEntry( + index, + RawInput( + prevOut: input.prevOut, + // Use the corrected previous output script for the input being signed + // and blank scripts for all the others + scriptSig: isThisInput ? correctedScriptSig : Uint8List(0), + // Make sequence 0 for other inputs unless using SIGHASH_ALL + sequence: isThisInput || hashType.all ? input.sequence : 0, + ), + ); + } + ).values; + + final modifiedOutputs = hashType.all ? outputs : ( + hashType.none ? [] : [ + // Single output + // Include blank outputs upto output index + ...Iterable.generate(inputN, (i) => Output.blank()), + outputs[inputN], + ] + ); + + final modifiedTx = Transaction( + version: version, + inputs: modifiedInputs, + outputs: modifiedOutputs, + locktime: locktime, + ); + + // Add sighash type onto the end + final bytes = Uint8List(modifiedTx.size + 4); + final writer = BytesWriter(bytes); + modifiedTx.write(writer); + writer.writeUInt32(hashType.value); + + // Use sha256d for signature hash + return sha256DoubleHash(bytes); + + } + + // Obtains the hash of the concatenation of each serialized writable object + Uint8List _hashConcatWritable(Iterable list) => sha256DoubleHash( + Uint8List.fromList( + list + .map((e) => e.toBytes().toList()) + .reduce((a, b) => a+b), + ), + ); + + /// Obtains the hash for an input signature for a witness input at [inputN]. + /// The [scriptCode] should be provided as necessary for the given witness + /// program. [hashType] controls what data is included in the signature. + Uint8List signatureHashForWitness( + int inputN, Script scriptCode, BigInt value, SigHashType hashType, + ) { + + _requireInputRange(inputN); + + final hashPrevouts = !hashType.anyOneCanPay + ? _hashConcatWritable(inputs.map((i) => i.prevOut)) + : hashZero; + + late Uint8List hashSequence; + if (!hashType.anyOneCanPay && !hashType.single && !hashType.none) { + final bytes = Uint8List(4*inputs.length); + final writer = BytesWriter(bytes); + for (final input in inputs) { + writer.writeUInt32(input.sequence); + } + hashSequence = sha256DoubleHash(bytes); + } else { + hashSequence = hashZero; + } + + final hashOutputs = !hashType.single && !hashType.none + ? _hashConcatWritable(outputs) + : ( + hashType.single && inputN < outputs.length + // For SIGHASH_SINGLE, only hash the output at the same index + ? sha256DoubleHash(outputs[inputN].toBytes()) + : hashZero + ); + + final compiledScript = scriptCode.compiled; + final thisIn = inputs[inputN]; + + final size = 156 + (MeasureWriter()..writeVarSlice(compiledScript)).size; + final bytes = Uint8List(size); + final writer = BytesWriter(bytes); + writer.writeUInt32(version); + writer.writeSlice(hashPrevouts); + writer.writeSlice(hashSequence); + thisIn.prevOut.write(writer); + writer.writeVarSlice(compiledScript); + writer.writeUInt64(value); + writer.writeUInt32(thisIn.sequence); + writer.writeSlice(hashOutputs); + writer.writeUInt32(locktime); + writer.writeUInt32(hashType.value); + + return sha256DoubleHash(bytes); + + } + + /// Sign the input at [inputN] with the [key] and [hashType] and return a new + /// [Transaction] with the signed input. The input must be a signable + /// P2PKH, P2WPKH or P2SH multisig input or [CannotSignInput] will be thrown. + /// [value] is only required for P2WPKH. + Transaction sign({ + required int inputN, + required ECPrivateKey key, + hashType = const SigHashType.all(), + BigInt? value, + }) { + + if (inputN >= inputs.length) { + throw ArgumentError.value(inputN, "inputN", "outside range of inputs"); + } + + if (!hashType.none && outputs.isEmpty) { + throw CannotSignInput("Cannot sign input without any outputs"); + } + + final input = inputs[inputN]; + + if (input is WitnessInput && value == null) { + throw CannotSignInput("Prevout values are required for witness inputs"); + } + + // Get data for input + late List pubkeys; + late Script scriptCode; + + if (input is PKHInput) { + // Require explicit cast for a mixin + final pk = (input as PKHInput).publicKey; + pubkeys = [pk]; + scriptCode = P2PKH.fromPublicKey(pk).script; + } else if (input is P2SHMultisigInput) { + pubkeys = input.program.pubkeys; + // For P2SH the script code is the redeem script + scriptCode = input.program.script; + } else { + throw CannotSignInput("${input.runtimeType} not a signable input"); + } + + if (!pubkeys.contains(key.pubkey)) { + throw CannotSignInput("Public key not part of input"); + } + + // Create signature + final signHash = input is WitnessInput + ? signatureHashForWitness(inputN, scriptCode, value!, hashType) + : signatureHash(inputN, scriptCode, hashType); + final insig = InputSignature(ECDSASignature.sign(key, signHash), hashType); + + // Get input with new signature + late Input newInput; + if (input is PKHInput) { + newInput = (input as PKHInput).addSignature(insig) as Input; + } else if (input is P2SHMultisigInput) { + // Add signature in the correct order + newInput = input.insertSignature( + insig, + key.pubkey, + (hashType) => signatureHash(inputN, scriptCode, hashType), + ); + } + + // Replace input in input list + final newInputs = inputs.asMap().map( + (index, input) => MapEntry( + index, index == inputN ? newInput : input, + ), + ).values; + + return Transaction( + version: version, + inputs: newInputs, + outputs: outputs, + locktime: locktime, + ); + + } + + /// Returns a new [Transaction] with the [input] added to the end of the input + /// list. + Transaction addInput(Input input) { + + // For existing inputs, remove any signatures without ANYONECANPAY + final modifiedInputs = inputs.map( + (input) => input.filterSignatures((insig) => insig.hashType.anyOneCanPay), + ); + + // Add new input to end of inputs of new transaction + return Transaction( + version: version, + inputs: [...modifiedInputs, input], + outputs: outputs, + locktime: locktime, + ); + + } + + /// Returns a new [Transaction] with the [output] added to the end of the + /// output list. + Transaction addOutput(Output output) { + + final modifiedInputs = inputs.asMap().map( + (i, input) => MapEntry( + i, input.filterSignatures( + (insig) + // Allow signatures that sign no outpus + => insig.hashType.none + // Allow signatures that sign a single output which isn't the one + // being added + || (insig.hashType.single && i != outputs.length), + ), + ), + ).values; + + return Transaction( + version: version, + inputs: modifiedInputs, + outputs: [...outputs, output], + locktime: locktime, + ); + + } + + Transaction? _legacyCache; + /// Returns a non-witness variant of this transaction. Any witness inputs are + /// replaced with their raw equivalents without witness data. If the + /// transaction is already non-witness, then it shall be returned as-is. + Transaction get legacy => isWitness + ? _legacyCache ??= Transaction( + version: version, + inputs: inputs.map( + // Raw inputs remove all witness data and are serialized as legacy + // inputs. Don't waste creating a new object for non-witness inputs. + (input) => input is WitnessInput + ? RawInput( + prevOut: input.prevOut, + scriptSig: input.scriptSig, + sequence: input.sequence, + ) + : input, + ), + outputs: outputs, + locktime: locktime, + ) + : this; + + Uint8List? _hashCache; + /// The serialized tx data hashed with sha256d + Uint8List get hash => _hashCache ??= sha256DoubleHash(toBytes()); + + Uint8List? _legacyHashCache; + /// The serialized tx data without witness data hashed with sha256d + Uint8List get legacyHash => _legacyHashCache ??= legacy.hash; + + /// Get the reversed hash as hex which is usual for Peercoin transactions + /// This provides the witness txid. See [legacyHash] for the legacy type of + /// hash. + String get hashHex => bytesToHex(Uint8List.fromList(hash.reversed.toList())); + + /// Gets the legacy reversed hash as hex without witness data. + String get txid + => bytesToHex(Uint8List.fromList(legacyHash.reversed.toList())); + + /// If the transaction has any witness inputs. + bool get isWitness => inputs.any((input) => input is WitnessInput); + + bool get isCoinBase + => inputs.length == 1 + && inputs.first.prevOut.coinbase + && outputs.isNotEmpty; + + bool get isCoinStake + => inputs.isNotEmpty + && !inputs.first.prevOut.coinbase + && outputs.length >= 2 + && outputs.first.value == BigInt.zero + && outputs.first.scriptPubKey.isEmpty; + + /// Returns true when all of the inputs are fully signed with at least one + /// input and one output. There is no guarentee that the transaction is valid + /// on the blockchain. + bool get complete + => inputs.isNotEmpty && outputs.isNotEmpty + && inputs.every((input) => input.complete); + +} diff --git a/coinlib/test/tx/legacy_transaction_test.dart b/coinlib/test/tx/transaction_test.dart similarity index 87% rename from coinlib/test/tx/legacy_transaction_test.dart rename to coinlib/test/tx/transaction_test.dart index 0a29309..82050ac 100644 --- a/coinlib/test/tx/legacy_transaction_test.dart +++ b/coinlib/test/tx/transaction_test.dart @@ -6,15 +6,20 @@ import '../vectors/keys.dart'; void main() { - group("LegacyTransaction", () { + group("Transaction", () { setUpAll(loadCoinlib); - expectVectorWithoutObj(LegacyTransaction tx, TxVector vec) { + expectVectorWithoutObj(Transaction tx, TxVector vec) { expect(tx.toHex(), vec.hex); + expect(tx.hashHex, vec.hashHex); - expect(tx.txid, vec.hashHex); expect(tx.hash, hexToBytes(vec.hashHex).reversed); + + expect(tx.txid, vec.txidHex); + expect(tx.legacyHash, hexToBytes(vec.txidHex).reversed); + + expect(tx.isWitness, vec.isWitness); expect(tx.isCoinBase, vec.isCoinBase); expect(tx.isCoinStake, vec.isCoinStake); expect(tx.complete, vec.complete); @@ -24,7 +29,7 @@ void main() { mapToHex(List list) => list.map((e) => e.toHex()); - expectFullVector(LegacyTransaction tx, TxVector vec) { + expectFullVector(Transaction tx, TxVector vec) { expectVectorWithoutObj(tx, vec); expect(tx.version, vec.obj.version); expect(tx.locktime, vec.obj.locktime); @@ -35,8 +40,23 @@ void main() { test("valid txs", () { for (final vec in validTxVecs) { + expectVectorWithoutObj(vec.obj, vec); - expectFullVector(LegacyTransaction.fromHex(vec.hex), vec); + expectFullVector(Transaction.fromHex(vec.hex), vec); + expectFullVector( + Transaction.fromHex(vec.hex, expectWitness: vec.isWitness), vec, + ); + expect(vec.obj == vec.obj.legacy, !vec.isWitness); + + if (vec.isWitness) { + final legacy = vec.obj.legacy; + expect(legacy.isWitness, false); + expect(legacy.inputs, everyElement(isA())); + expect(legacy.toHex(), vec.legacyHex); + expect(legacy.size, vec.legacyHex!.length/2); + expect(legacy.hashHex, vec.txidHex); + } + } }); @@ -47,7 +67,10 @@ void main() { "03000000000100000000", "030000000001a0860100000000001a76a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac04030201", ]) { - expect(() => LegacyTransaction.fromHex(vec), throwsA(isA())); + expect( + () => Transaction.fromHex(vec), + throwsA(isA()), + ); } }); @@ -74,7 +97,7 @@ void main() { } - LegacyTransaction txOfSize(int size) => LegacyTransaction( + Transaction txOfSize(int size) => Transaction( inputs: [ RawInput( prevOut: examplePrevOut, @@ -84,11 +107,11 @@ void main() { outputs: [], ); - expect(LegacyTransaction.fromBytes(dataOfSize(1000000)).size, 1000000); + expect(Transaction.fromBytes(dataOfSize(1000000)).size, 1000000); expect(txOfSize(1000000).size, 1000000); expect( - () => LegacyTransaction.fromBytes(dataOfSize(1000001)), + () => Transaction.fromBytes(dataOfSize(1000001)), throwsA(isA()), ); expect(() => txOfSize(1000001), throwsA(isA())); @@ -97,39 +120,58 @@ void main() { test("signatureHash", () { - final tx = LegacyTransaction.fromHex(sigHashTxHex); + final tx = Transaction.fromHex(sigHashTxHex); for (final vec in sighashVectors) { + expect( bytesToHex( tx.signatureHash( vec.inputN, - Script.fromAsm(vec.prevScriptAsm), + Script.fromAsm(vec.scriptCodeAsm), vec.type, ), ), vec.hash, ); + + expect( + bytesToHex( + tx.signatureHashForWitness( + vec.inputN, + Script.fromAsm(vec.scriptCodeAsm), + witnessValue, + vec.type, + ), + ), + vec.witnessHash, + ); + } }); test("signatureHash input out of range", () { + final tx = Transaction.fromHex(sigHashTxHex); + expect( + () => tx.signatureHash(2, Script([]), SigHashType.all()), + throwsArgumentError, + ); expect( - () => LegacyTransaction.fromHex(sigHashTxHex) - .signatureHash(2, Script([]), SigHashType.all()), + () => tx.signatureHashForWitness( + 2, Script([]), witnessValue, SigHashType.all(), + ), throwsArgumentError, ); }); - test("sign() failure", () { final privkey = ECPrivateKey.generate(); final pubkey = privkey.pubkey; final wrongkey = ECPrivateKey.generate(); - final txNoOutput = LegacyTransaction( + final txNoOutput = Transaction( inputs: [ P2WPKHInput(prevOut: examplePrevOut, publicKey: pubkey), P2PKHInput( @@ -143,7 +185,7 @@ void main() { // SIGHASH_NONE OK with no outputs expect( txNoOutput.sign(inputN: 1, key: privkey, hashType: SigHashType.none()), - isA(), + isA(), ); // No outputs @@ -155,7 +197,7 @@ void main() { final tx = txNoOutput.addOutput(exampleOutput); // OK - expect(tx.sign(inputN: 1, key: privkey), isA()); + expect(tx.sign(inputN: 1, key: privkey), isA()); // Input out of range expect(() => tx.sign(inputN: 2, key: privkey), throwsArgumentError); @@ -196,7 +238,7 @@ void main() { required String hex, }) { - final tx = LegacyTransaction( + final tx = Transaction( inputs: prevTxIds.map((txid) => P2PKHInput( prevOut: OutPoint.fromHex(txid, 1), publicKey: keyVec.publicObj, ), @@ -290,7 +332,7 @@ void main() { final pubkeys = privkeys.map((k) => k.pubkey).toList(); final multisig = MultisigProgram(3, pubkeys); - final tx = LegacyTransaction( + final tx = Transaction( inputs: [ P2PKHInput( prevOut: OutPoint.fromHex( @@ -338,7 +380,7 @@ void main() { ); // Check insigs by reference to their hash types - expectMultisigSigs(LegacyTransaction tx, List types) { + expectMultisigSigs(Transaction tx, List types) { final sigs = (tx.inputs[1] as P2SHMultisigInput).sigs; expect(sigs.length, types.length); expect(sigs.map((s) => s.hashType), types); @@ -381,7 +423,7 @@ void main() { final privkey = ECPrivateKey.generate(); final pubkey = privkey.pubkey; - var tx = LegacyTransaction( + var tx = Transaction( inputs: [ P2PKHInput(prevOut: examplePrevOut, publicKey: pubkey), P2PKHInput(prevOut: examplePrevOut, publicKey: pubkey), diff --git a/coinlib/test/vectors/tx.dart b/coinlib/test/vectors/tx.dart index 6dd66b3..6acc723 100644 --- a/coinlib/test/vectors/tx.dart +++ b/coinlib/test/vectors/tx.dart @@ -2,24 +2,30 @@ import 'dart:typed_data'; import 'package:coinlib/coinlib.dart'; class TxVector { - LegacyTransaction obj; + Transaction obj; String hashHex; + String txidHex; + bool isWitness; bool isCoinBase; bool isCoinStake; bool complete; int size; List inputTypes; String hex; + String? legacyHex; TxVector({ required this.obj, required this.hashHex, + String? txidHex, + required this.isWitness, required this.isCoinBase, required this.isCoinStake, required this.complete, required this.size, required this.inputTypes, required this.hex, - }); + this.legacyHex, + }): txidHex = txidHex ?? hashHex; } final examplePrevOut = OutPoint.fromHex( @@ -62,7 +68,7 @@ final validTxVecs = [ // P2PKH with 1 input and 1 output TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2PKHInput( @@ -74,6 +80,7 @@ final validTxVecs = [ outputs: [exampleOutput], ), hashHex: "422440b9b5f046d03de2ebcb848d64d76ce88170555dcb73a8faea7a10d08572", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: true, @@ -84,7 +91,7 @@ final validTxVecs = [ // Transaction with 2 inputs and 2 outputs TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2PKHInput( @@ -134,6 +141,7 @@ final validTxVecs = [ ], ), hashHex: "6b1944794c215482f9b4532ccd0f982a3be80f0a349394cc2de2c95014c563be", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: true, @@ -144,17 +152,13 @@ final validTxVecs = [ // Transaction with P2PKH input missing signature TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, - inputs: [ - P2PKHInput( - prevOut: examplePrevOut, - publicKey: examplePubkey, - ), - ], + inputs: [P2PKHInput(prevOut: examplePrevOut, publicKey: examplePubkey)], outputs: [exampleOutput], ), hashHex: "38f460ea55b2676b0e5cc1483d63f97d66fb1648bdc508eee79a402a75b97f5b", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: false, @@ -165,7 +169,7 @@ final validTxVecs = [ // Transaction with P2PKH input missing output TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2PKHInput( @@ -177,6 +181,7 @@ final validTxVecs = [ outputs: [], ), hashHex: "404c8e738ae31d63d8b26b7b056632779f8b1d27dfe9ef314d2649f005971910", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: false, @@ -188,7 +193,7 @@ final validTxVecs = [ // Transaction with complete P2SH 2-of-3 multisig TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2SHMultisigInput( @@ -211,6 +216,7 @@ final validTxVecs = [ outputs: [exampleOutput], ), hashHex: "1dcb0d65b0e5938b430ad49a197648d04b4f7d076d23870aba047f4884c785d7", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: true, @@ -221,7 +227,7 @@ final validTxVecs = [ // Transaction with P2SH 2-of-3 multisig and only 1 signature TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2SHMultisigInput( @@ -239,6 +245,7 @@ final validTxVecs = [ outputs: [exampleOutput], ), hashHex: "23fdd138ea95b7154017dc8e8b9e43c009aced55d0bc4b3d3f2eb2214223b378", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: false, @@ -247,15 +254,17 @@ final validTxVecs = [ hex: "0300000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe00000000b40047304402207567ea17703e2df7993ce70ead3f9f051e3bf7b8dfcdc6e9edc7547c0c0c4ef302204332066de953f267db9c31ca934052f1cfabd4281fd2649f928a66b1deb604e7014c69522103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a2892103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baac2103aea0dfd576151cb399347aa6732f8fdf027b9ea3ea2e65fb754803f776e0a50953aeffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", ), - // Transaction with no inputs and alternative locktime + // Transaction with no inputs, one output and alternative locktime + // This transaction ambiguously looks like a witness transaction TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [], outputs: [exampleOutput], locktime: 0x01020304, ), hashHex: "586cf7ffc988e620d69c4164f0eeff7a9ff89a04f6a13a7b9297d1819f4d1730", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: false, @@ -267,12 +276,13 @@ final validTxVecs = [ // Transaction with no inputs or outputs TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [], outputs: [], ), hashHex: "d668682214aa0e8827974155a7c76562205c046ebaeb41e163457d03e9f02822", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: false, @@ -284,7 +294,7 @@ final validTxVecs = [ // Bad output scripts TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2PKHInput( @@ -314,6 +324,7 @@ final validTxVecs = [ ], ), hashHex: "8a219c494ae2fb64cb052c7ff5fbf3d89b4c8a7d4f9cf1aa9e3ba5274cb7dd72", + isWitness: false, isCoinBase: false, isCoinStake: false, complete: true, @@ -324,7 +335,7 @@ final validTxVecs = [ // Coinbase TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ RawInput( @@ -345,6 +356,7 @@ final validTxVecs = [ ], ), hashHex: "c018ee785bea0ce31228bb60afa049341bb52e018d1b59046432e9ab0016fb57", + isWitness: false, isCoinBase: true, isCoinStake: false, complete: true, @@ -355,7 +367,7 @@ final validTxVecs = [ // Coinstake TxVector( - obj: LegacyTransaction( + obj: Transaction( version: 3, inputs: [ P2PKHInput( @@ -370,6 +382,7 @@ final validTxVecs = [ ], ), hashHex: "aec4ced88705b0eec2efa176ccdf91d78d6ac8a54a010c2ed42d5b7314a729b4", + isWitness: false, isCoinBase: false, isCoinStake: true, complete: true, @@ -378,77 +391,152 @@ final validTxVecs = [ hex: "0300000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006b4830450221008732a460737d956fd94d49a31890b2908f7ed7025a9c1d0f25e43290f1841716022004fa7d608a291d44ebbbebbadaac18f943031e7de39ef3bf9920998c43e60c0401210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ffffffff02000000000000000000a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", ), + // P2WPKH witness missing signature + TxVector( + obj: Transaction( + version: 3, + inputs: [P2WPKHInput(prevOut: examplePrevOut, publicKey: examplePubkey)], + outputs: [exampleOutput], + ), + hashHex: "e6bc97ca65d94b014a73fbc66e7829e60eb174fab0ad67c765cbcb74b6f5b36c", + txidHex: "e9c9d11d7cf15ed903e44374103938aba52d1a0dced03b1af08ac640f2cd1554", + isWitness: true, + isCoinBase: false, + isCoinStake: false, + complete: false, + size: 122, + inputTypes: [P2WPKHInput], + hex: "03000000000101f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", + legacyHex: "0300000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", + ), + + // P2WPKH witness with signature + TxVector( + obj: Transaction( + version: 3, + inputs: [P2WPKHInput( + prevOut: examplePrevOut, + publicKey: examplePubkey, + insig: exampleInsig, + sequence: 0x01020304, + )], + outputs: [exampleOutput], + ), + hashHex: "f1cd5422c06f2f1a59bfb45307ab56f1a1720da5af766c06a2317ba43dfefafc", + txidHex: "366710a884b21e2e3f93738dea6f5b2f1eef559c2a4b66bdbbeec2a9c968244e", + isWitness: true, + isCoinBase: false, + isCoinStake: false, + complete: true, + size: 195, + inputTypes: [P2WPKHInput], + hex: "03000000000101f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe00000000000403020101a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac024830450221008732a460737d956fd94d49a31890b2908f7ed7025a9c1d0f25e43290f1841716022004fa7d608a291d44ebbbebbadaac18f943031e7de39ef3bf9920998c43e60c0401210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", + legacyHex: "0300000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe00000000000403020101a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", + ), + + // Unknown witness + TxVector( + obj: Transaction( + version: 3, + inputs: [WitnessInput( + prevOut: examplePrevOut, + witness: [Uint8List(0), Uint8List.fromList([0xff, 1, 2, 3])], + )], + outputs: [exampleOutput], + ), + hashHex: "ef839df6a0094f5af7a8d4bc704e0ea9ad201c956784e7d7d99350de502028b0", + txidHex: "e9c9d11d7cf15ed903e44374103938aba52d1a0dced03b1af08ac640f2cd1554", + isWitness: true, + isCoinBase: false, + isCoinStake: false, + complete: true, + size: 94, + inputTypes: [WitnessInput], + hex: "03000000000101f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac020004ff01020300000000", + legacyHex: "0300000001f1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000", + ) + ]; class SigHashVector { final int inputN; - final String prevScriptAsm; + final String scriptCodeAsm; final SigHashType type; final String hash; + final String witnessHash; SigHashVector({ required this.inputN, - required this.prevScriptAsm, + required this.scriptCodeAsm, required this.type, required this.hash, + required this.witnessHash, }); } final sigHashTxHex = "010000000200000000000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000000000000000"; +final witnessValue = BigInt.from(0x0102030405); final sighashVectors = [ // SIGHASH_ALL SigHashVector( inputN: 0, - prevScriptAsm: "0 03", + scriptCodeAsm: "0 03", type: SigHashType.all(), hash: "c2360721e97635761cd6d94d9528de894448709ed7d40f59fc68f573320a7d9f", + witnessHash: "3314821edf3f58f15b3763fbbd65353e6414c3494a4dd25e85345dbb4c0a9ac0", ), // SIGHASH_ALL with CODESEPARATOR SigHashVector( inputN: 0, - prevScriptAsm: "0 OP_CODESEPARATOR 03", + scriptCodeAsm: "0 OP_CODESEPARATOR 03", type: SigHashType.all(), hash: "c2360721e97635761cd6d94d9528de894448709ed7d40f59fc68f573320a7d9f", + witnessHash: "afb2d290a938c235743deadd0a77a616811c243dfd17b01dd8b1babc2d85e2ed", ), // SIGHASH_SINGLE SigHashVector( inputN: 0, - prevScriptAsm: "0", + scriptCodeAsm: "0", type: SigHashType.single(), hash: "43597296fa4f2bd356a21aec9dc66b4206f7d696a2d5468b840838be84d12987", + witnessHash: "83ea6b964428e0f83ed692b87ce9138ef208d80bf013c47978cc15be1645773c", ), // No matching output for SIGHASH_SINGLE SigHashVector( inputN: 1, - prevScriptAsm: "0", + scriptCodeAsm: "0", type: SigHashType.single(), hash: "0000000000000000000000000000000000000000000000000000000000000001", + witnessHash: "3c8876a85da2d81408d438b67d24df9bc9414be9b3e43654ba3ece7e24f87787", ), // SIGHASH_NONE SigHashVector( inputN: 0, - prevScriptAsm: "0", + scriptCodeAsm: "0", type: SigHashType.none(), hash: "539456df7e47a886a3e03323fab19881fcc195198bebac3f60d3108e86c0dbc0", + witnessHash: "9741f66ec361eb3f8067fce5e43f22c0cbb4a7fbcb69d1e248dbd40b01fcf0b7", ), // ANYONECANPAY SigHashVector( inputN: 0, - prevScriptAsm: "0", + scriptCodeAsm: "0", type: SigHashType.all(anyOneCanPay: true), hash: "6f432eb5ce9f1a48693bab90f84adc0080e87a4d03abe761d261ca8adffb3002", + witnessHash: "9d39499db354af5517b52ea135091b237d47e5006ad322623e6d5634fabe17a9", ), SigHashVector( inputN: 1, - prevScriptAsm: "0", + scriptCodeAsm: "0", type: SigHashType.all(anyOneCanPay: true), hash: "6f432eb5ce9f1a48693bab90f84adc0080e87a4d03abe761d261ca8adffb3002", + witnessHash: "9d39499db354af5517b52ea135091b237d47e5006ad322623e6d5634fabe17a9", ), ];