diff --git a/coinlib/example/coinlib_example.dart b/coinlib/example/coinlib_example.dart index 86936ed..ea553ab 100644 --- a/coinlib/example/coinlib_example.dart +++ b/coinlib/example/coinlib_example.dart @@ -76,7 +76,7 @@ void main() async { // Sign the input with the private key. The signed transaction is returned as // a new object as most objects in the library are immutable. - final signedTx = tx.sign(inputN: 0, key: key1.privateKey); + final signedTx = tx.signLegacy(inputN: 0, key: key1.privateKey); if (signedTx.complete) { print("Signed transaction is complete"); @@ -106,7 +106,7 @@ void main() async { final trTx = Transaction( inputs: [TaprootKeyInput(prevOut: OutPoint(prevHash, 1))], outputs: [trOutput], - ).sign( + ).signTaproot( inputN: 0, // Private keys must be tweaked by the Taproot object key: taproot.tweakPrivateKey(key1.privateKey), diff --git a/coinlib/lib/src/coinlib_base.dart b/coinlib/lib/src/coinlib_base.dart index bc5c12e..6b3cc56 100644 --- a/coinlib/lib/src/coinlib_base.dart +++ b/coinlib/lib/src/coinlib_base.dart @@ -35,6 +35,7 @@ export 'package:coinlib/src/scripts/programs/p2wsh.dart'; export 'package:coinlib/src/tx/coin_selection.dart'; export 'package:coinlib/src/tx/transaction.dart'; +export 'package:coinlib/src/tx/sign_details.dart'; export 'package:coinlib/src/tx/outpoint.dart'; export 'package:coinlib/src/tx/output.dart'; diff --git a/coinlib/lib/src/tx/inputs/legacy_input.dart b/coinlib/lib/src/tx/inputs/legacy_input.dart index 0f14a21..7253765 100644 --- a/coinlib/lib/src/tx/inputs/legacy_input.dart +++ b/coinlib/lib/src/tx/inputs/legacy_input.dart @@ -1,9 +1,7 @@ import 'package:coinlib/src/crypto/ec_private_key.dart'; import 'package:coinlib/src/crypto/ecdsa_signature.dart'; -import 'package:coinlib/src/scripts/script.dart'; import 'package:coinlib/src/tx/sighash/legacy_signature_hasher.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'input.dart'; import 'input_signature.dart'; import 'p2pkh_input.dart'; @@ -19,35 +17,21 @@ abstract class LegacyInput extends RawInput { super.sequence = Input.sequenceFinal, }); - /// Signs the input given the [tx], input number ([inputN]) and a private - /// [key] using the specifified [hashType]. + /// Signs the input given the sign [details] and [key]. /// Implemented by specific subclasses. LegacyInput sign({ - required Transaction tx, - required int inputN, + required LegacySignDetails details, required ECPrivateKey key, - SigHashType hashType = const SigHashType.all(), }); /// Creates a signature for the input. Used by subclasses to implement /// signing. ECDSAInputSignature createInputSignature({ - required Transaction tx, - required int inputN, + required LegacySignDetailsWithScript details, required ECPrivateKey key, - required Script scriptCode, - SigHashType hashType = const SigHashType.all(), }) => ECDSAInputSignature( - ECDSASignature.sign( - key, - LegacySignatureHasher( - tx: tx, - inputN: inputN, - scriptCode: scriptCode, - hashType: RawInput.checkHashTypeNotSchnorr(hashType), - ).hash, - ), - hashType, + ECDSASignature.sign(key, LegacySignatureHasher(details).hash), + details.hashType, ); } diff --git a/coinlib/lib/src/tx/inputs/legacy_witness_input.dart b/coinlib/lib/src/tx/inputs/legacy_witness_input.dart index 242c0b7..1f32516 100644 --- a/coinlib/lib/src/tx/inputs/legacy_witness_input.dart +++ b/coinlib/lib/src/tx/inputs/legacy_witness_input.dart @@ -1,9 +1,7 @@ import 'package:coinlib/src/crypto/ec_private_key.dart'; import 'package:coinlib/src/crypto/ecdsa_signature.dart'; -import 'package:coinlib/src/scripts/script.dart'; -import 'package:coinlib/src/tx/inputs/raw_input.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; import 'package:coinlib/src/tx/sighash/witness_signature_hasher.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'package:coinlib/src/tx/transaction.dart'; import 'input.dart'; import 'input_signature.dart'; @@ -18,39 +16,22 @@ abstract class LegacyWitnessInput extends WitnessInput { super.sequence = Input.sequenceFinal, }); - /// Signs the input given the [tx], input number ([inputN]), private - /// [key] and input [value] using the specifified [hashType]. Should throw + /// Signs the input given the [details] and [key]. Should throw /// [CannotSignInput] if the key cannot sign the input. /// Implemented by specific subclasses. LegacyWitnessInput sign({ - required Transaction tx, - required int inputN, + required LegacyWitnessSignDetails details, required ECPrivateKey key, - required BigInt value, - SigHashType hashType = const SigHashType.all(), }); /// Creates a signature for the input. Used by subclasses to implement /// signing. ECDSAInputSignature createInputSignature({ - required Transaction tx, - required int inputN, + required LegacyWitnessSignDetailsWithScript details, required ECPrivateKey key, - required Script scriptCode, - required BigInt value, - SigHashType hashType = const SigHashType.all(), }) => ECDSAInputSignature( - ECDSASignature.sign( - key, - WitnessSignatureHasher( - tx: tx, - inputN: inputN, - scriptCode: scriptCode, - value: value, - hashType: RawInput.checkHashTypeNotSchnorr(hashType), - ).hash, - ), - hashType, + ECDSASignature.sign(key, WitnessSignatureHasher(details).hash), + details.hashType, ); } diff --git a/coinlib/lib/src/tx/inputs/p2pkh_input.dart b/coinlib/lib/src/tx/inputs/p2pkh_input.dart index a961306..936e76d 100644 --- a/coinlib/lib/src/tx/inputs/p2pkh_input.dart +++ b/coinlib/lib/src/tx/inputs/p2pkh_input.dart @@ -3,8 +3,7 @@ import 'package:coinlib/src/crypto/ec_public_key.dart'; import 'package:coinlib/src/scripts/operations.dart'; import 'package:coinlib/src/scripts/programs/p2pkh.dart'; import 'package:coinlib/src/scripts/script.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'input.dart'; import 'input_signature.dart'; import 'legacy_input.dart'; @@ -64,17 +63,12 @@ class P2PKHInput extends LegacyInput with PKHInput { @override P2PKHInput sign({ - required Transaction tx, - required int inputN, + required LegacySignDetails details, required ECPrivateKey key, - hashType = const SigHashType.all(), }) => addSignature( createInputSignature( - tx: tx, - inputN: inputN, key: checkKey(key), - scriptCode: scriptCode, - hashType: hashType, + details: details.addScript(scriptCode), ), ); diff --git a/coinlib/lib/src/tx/inputs/p2sh_multisig_input.dart b/coinlib/lib/src/tx/inputs/p2sh_multisig_input.dart index b788636..c980efe 100644 --- a/coinlib/lib/src/tx/inputs/p2sh_multisig_input.dart +++ b/coinlib/lib/src/tx/inputs/p2sh_multisig_input.dart @@ -9,6 +9,7 @@ import 'package:coinlib/src/scripts/programs/p2sh.dart'; import 'package:coinlib/src/scripts/script.dart'; import 'package:coinlib/src/tx/sighash/legacy_signature_hasher.dart'; import 'package:coinlib/src/tx/sighash/sighash_type.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'package:coinlib/src/tx/transaction.dart'; import 'input.dart'; import 'input_signature.dart'; @@ -95,10 +96,8 @@ class P2SHMultisigInput extends LegacyInput { @override LegacyInput sign({ - required Transaction tx, - required int inputN, + required LegacySignDetails details, required ECPrivateKey key, - hashType = const SigHashType.all(), }) { if (!program.pubkeys.contains(key.pubkey)) { @@ -107,18 +106,17 @@ class P2SHMultisigInput extends LegacyInput { return insertSignature( createInputSignature( - tx: tx, - inputN: inputN, key: key, - scriptCode: program.script, - hashType: hashType, + details: details.addScript(program.script), ), key.pubkey, (hashType) => LegacySignatureHasher( - tx: tx, - inputN: inputN, - scriptCode: program.script, - hashType: hashType, + LegacySignDetailsWithScript( + tx: details.tx, + inputN: details.inputN, + scriptCode: program.script, + hashType: hashType, + ), ).hash, ); @@ -128,8 +126,10 @@ class P2SHMultisigInput extends LegacyInput { /// The [pubkey] should be the public key for the signature to ensure that it /// matches. [getSigHash] obtains the signature hash for a given type so that /// existing signatures can be checked. + /// /// If existing signatures are not in-order then they may not be fully matched /// and included in the resulting input. + /// /// If there are more signatures than the required threshold, the last /// signature will be removed. P2SHMultisigInput insertSignature( diff --git a/coinlib/lib/src/tx/inputs/p2wpkh_input.dart b/coinlib/lib/src/tx/inputs/p2wpkh_input.dart index 4dcc037..70f5c46 100644 --- a/coinlib/lib/src/tx/inputs/p2wpkh_input.dart +++ b/coinlib/lib/src/tx/inputs/p2wpkh_input.dart @@ -2,8 +2,7 @@ import 'dart:typed_data'; import 'package:coinlib/src/crypto/ec_private_key.dart'; import 'package:coinlib/src/crypto/ec_public_key.dart'; import 'package:coinlib/src/scripts/programs/p2wpkh.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'input.dart'; import 'input_signature.dart'; import 'pkh_input.dart'; @@ -69,19 +68,12 @@ class P2WPKHInput extends LegacyWitnessInput with PKHInput { @override LegacyWitnessInput sign({ - required Transaction tx, - required int inputN, + required LegacyWitnessSignDetails details, required ECPrivateKey key, - required BigInt value, - hashType = const SigHashType.all(), }) => addSignature( createInputSignature( - tx: tx, - inputN: inputN, key: checkKey(key), - scriptCode: scriptCode, - value: value, - hashType: hashType, + details: details.addScript(scriptCode), ), ); diff --git a/coinlib/lib/src/tx/inputs/taproot_input.dart b/coinlib/lib/src/tx/inputs/taproot_input.dart index ed4cdbe..d943257 100644 --- a/coinlib/lib/src/tx/inputs/taproot_input.dart +++ b/coinlib/lib/src/tx/inputs/taproot_input.dart @@ -1,10 +1,7 @@ -import 'dart:typed_data'; import 'package:coinlib/src/crypto/ec_private_key.dart'; import 'package:coinlib/src/crypto/schnorr_signature.dart'; -import 'package:coinlib/src/tx/output.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; import 'package:coinlib/src/tx/sighash/taproot_signature_hasher.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'input.dart'; import 'input_signature.dart'; import 'witness_input.dart'; @@ -18,41 +15,14 @@ abstract class TaprootInput extends WitnessInput { super.sequence = Input.sequenceFinal, }); - /// Signs the input given the [tx], input number ([inputN]), private [key] and - /// [prevOuts] using the specifified [hashType]. Should throw - /// [CannotSignInput] if the key cannot sign the input. Implemented by - /// specific subclasses. - TaprootInput sign({ - required Transaction tx, - required int inputN, - required ECPrivateKey key, - required List prevOuts, - SigHashType hashType = const SigHashType.schnorrDefault(), - }) => throw CannotSignInput("Unimplemented sign() for {this.runtimeType}"); - /// Creates a signature for the input. Used by subclasses to implement /// signing. SchnorrInputSignature createInputSignature({ - required Transaction tx, - required int inputN, required ECPrivateKey key, - required List prevOuts, - SigHashType hashType = const SigHashType.schnorrDefault(), - Uint8List? leafHash, - int codeSeperatorPos = 0xFFFFFFFF, + required TaprootSignDetails details, }) => SchnorrInputSignature( - SchnorrSignature.sign( - key, - TaprootSignatureHasher( - tx: tx, - inputN: inputN, - prevOuts: prevOuts, - hashType: hashType, - leafHash: leafHash, - codeSeperatorPos: codeSeperatorPos, - ).hash, - ), - hashType, + SchnorrSignature.sign(key, TaprootSignatureHasher(details).hash), + details.hashType, ); } diff --git a/coinlib/lib/src/tx/inputs/taproot_key_input.dart b/coinlib/lib/src/tx/inputs/taproot_key_input.dart index 7589290..9bf974c 100644 --- a/coinlib/lib/src/tx/inputs/taproot_key_input.dart +++ b/coinlib/lib/src/tx/inputs/taproot_key_input.dart @@ -3,8 +3,7 @@ import 'package:coinlib/src/crypto/ec_private_key.dart'; import 'package:coinlib/src/scripts/programs/p2tr.dart'; import 'package:coinlib/src/taproot.dart'; import 'package:coinlib/src/tx/inputs/taproot_input.dart'; -import 'package:coinlib/src/tx/output.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'package:coinlib/src/tx/transaction.dart'; import 'input.dart'; import 'input_signature.dart'; @@ -45,40 +44,26 @@ class TaprootKeyInput extends TaprootInput { } - @override /// Return a signed Taproot input using tweaked private key for the key-path /// spend. The [key] should be tweaked by [Taproot.tweakScalar]. TaprootKeyInput sign({ - required Transaction tx, - required int inputN, + required TaprootKeySignDetails details, required ECPrivateKey key, - required List prevOuts, - SigHashType hashType = const SigHashType.schnorrDefault(), }) { - if (inputN >= prevOuts.length) { - throw CannotSignInput( - "Input is out of range of the previous outputs provided", - ); + if (details.hashType.requiresApo) { + throw CannotSignInput("A Taproot key-spend doesn't support APO"); } // Check key corresponds to matching prevOut - final program = prevOuts[inputN].program; + final program = details.program; if (program is! P2TR || key.pubkey.xonly != program.tweakedKey) { throw CannotSignInput( "Key cannot sign for Taproot input's tweaked key", ); } - return addSignature( - createInputSignature( - tx: tx, - inputN: inputN, - key: key, - prevOuts: prevOuts, - hashType: hashType, - ), - ); + return addSignature(createInputSignature(key: key, details: details)); } diff --git a/coinlib/lib/src/tx/inputs/taproot_script_input.dart b/coinlib/lib/src/tx/inputs/taproot_script_input.dart index 7902eb9..8135f29 100644 --- a/coinlib/lib/src/tx/inputs/taproot_script_input.dart +++ b/coinlib/lib/src/tx/inputs/taproot_script_input.dart @@ -6,9 +6,8 @@ import 'package:coinlib/src/scripts/script.dart'; import 'package:coinlib/src/taproot.dart'; import 'package:coinlib/src/tx/inputs/taproot_input.dart'; import 'package:coinlib/src/tx/outpoint.dart'; -import 'package:coinlib/src/tx/output.dart'; import 'package:coinlib/src/tx/sighash/sighash_type.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'input.dart'; import 'input_signature.dart'; import 'raw_input.dart'; @@ -101,22 +100,26 @@ class TaprootScriptInput extends TaprootInput { ); /// Creates a [SchnorrInputSignature] to be used for the input's script data. - /// Provides the leaf hash to an underlying call to [createInputSignature]. + /// Uses the [details] plus an optional [codeSeperatorPos] for the last + /// executed position of the script. + /// + /// [InputSigHashOption.anyPrevOut] or + /// [InputSigHashOption.anyPrevOutAnyScript] can be used, but it must be + /// assured that the tapscript has a signature operation for a BIP118 APO key + /// as this is not checked by this method. SchnorrInputSignature createScriptSignature({ - required Transaction tx, - required int inputN, + required TaprootKeySignDetails details, required ECPrivateKey key, - required List prevOuts, - SigHashType hashType = const SigHashType.schnorrDefault(), int codeSeperatorPos = 0xFFFFFFFF, }) => createInputSignature( - tx: tx, - inputN: inputN, + details: TaprootScriptSignDetails( + tx: details.tx, + inputN: details.inputN, + prevOuts: details.prevOuts, + leafHash: TapLeaf(tapscript).hash, + codeSeperatorPos: codeSeperatorPos, + ), key: key, - prevOuts: prevOuts, - hashType: hashType, - leafHash: TapLeaf(tapscript).hash, - codeSeperatorPos: codeSeperatorPos, ); Uint8List get controlBlock => witness.last; diff --git a/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart b/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart index b13b722..c64e807 100644 --- a/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart +++ b/coinlib/lib/src/tx/sighash/legacy_signature_hasher.dart @@ -4,41 +4,29 @@ import 'package:coinlib/src/crypto/hash.dart'; import 'package:coinlib/src/scripts/operations.dart'; import 'package:coinlib/src/scripts/script.dart'; import 'package:coinlib/src/tx/inputs/raw_input.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; import 'package:coinlib/src/tx/output.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'package:coinlib/src/tx/transaction.dart'; import 'signature_hasher.dart'; /// Produces signature hashes for legacy non-witness inputs. -final class LegacySignatureHasher implements SignatureHasher { +final class LegacySignatureHasher extends SignatureHasher { static final ScriptOp _codeseperator = ScriptOpCode.fromName("CODESEPARATOR"); static final _hashOne = Uint8List(32)..last = 1; - final Transaction tx; - final int inputN; - final Script scriptCode; - final SigHashType hashType; + @override + final LegacySignDetailsWithScript details; - /// Produces the hash of 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. - LegacySignatureHasher({ - required this.tx, - required this.inputN, - required this.scriptCode, - required this.hashType, - }) { - SignatureHasher.checkInputN(tx, inputN); - SignatureHasher.checkLegacySigHashType(hashType); - } + /// Produces the hash of an input signature for a non-witness input. + LegacySignatureHasher(this.details); @override Uint8List get hash { // Remove OP_CODESEPERATOR from the script code final correctedScriptSig = Script( - scriptCode.ops.where((op) => !op.match(_codeseperator)), + details.scriptCode.ops.where((op) => !op.match(_codeseperator)), ).compiled; // If there is no matching output for SIGHASH_SINGLE, then return all null @@ -48,7 +36,7 @@ final class LegacySignatureHasher implements SignatureHasher { // Create modified transaction for obtaining a signature hash final modifiedInputs = ( - hashType.anyOneCanPay ? [tx.inputs[inputN]] : tx.inputs + hashType.anyOneCanPay ? [thisInput] : tx.inputs ).asMap().map( (index, input) { final isThisInput = hashType.anyOneCanPay || index == inputN; diff --git a/coinlib/lib/src/tx/sighash/sighash_type.dart b/coinlib/lib/src/tx/sighash/sighash_type.dart index 539e01a..b88eb07 100644 --- a/coinlib/lib/src/tx/sighash/sighash_type.dart +++ b/coinlib/lib/src/tx/sighash/sighash_type.dart @@ -173,6 +173,8 @@ class SigHashType { /// Only the output with the same index as the input shall be signed bool get single => outputs == OutputSigHashOption.single; + /// All inputs shall be signed + bool get allInputs => inputs == InputSigHashOption.all; /// Only the input receiving the signature shall be signed bool get anyOneCanPay => inputs == InputSigHashOption.anyOneCanPay; /// Only the input receiving the signature shall be signed without the output diff --git a/coinlib/lib/src/tx/sighash/signature_hasher.dart b/coinlib/lib/src/tx/sighash/signature_hasher.dart index bd3682c..8e33867 100644 --- a/coinlib/lib/src/tx/sighash/signature_hasher.dart +++ b/coinlib/lib/src/tx/sighash/signature_hasher.dart @@ -1,24 +1,14 @@ import 'dart:typed_data'; +import 'package:coinlib/src/tx/inputs/input.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'package:coinlib/src/tx/transaction.dart'; import 'sighash_type.dart'; -abstract interface class SignatureHasher { - - static void checkInputN(Transaction tx, int inputN) { - if (inputN < 0 || inputN >= tx.inputs.length) { - throw RangeError.index(inputN, tx.inputs, "inputN"); - } - } - - static void checkLegacySigHashType(SigHashType type) { - if (!type.supportsLegacy) { - throw ArgumentError.value( - type, "type", - "hash type is not supported for legacy signature hashes", - ); - } - } - +abstract class SignatureHasher { Uint8List get hash; - + SignDetails get details; + Transaction get tx => details.tx; + int get inputN => details.inputN; + SigHashType get hashType => details.hashType; + Input get thisInput => tx.inputs[inputN]; } diff --git a/coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart b/coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart index 704d3a7..009810f 100644 --- a/coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart +++ b/coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart @@ -1,64 +1,31 @@ import 'dart:typed_data'; import 'package:coinlib/src/common/serial.dart'; import 'package:coinlib/src/crypto/hash.dart'; -import 'package:coinlib/src/tx/output.dart'; -import 'package:coinlib/src/tx/sighash/sighash_type.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'precomputed_signature_hashes.dart'; import 'signature_hasher.dart'; /// Produces signature hashes for taproot inputs -final class TaprootSignatureHasher with Writable implements SignatureHasher { +final class TaprootSignatureHasher extends SignatureHasher with Writable { static final tapSigHash = getTaggedHasher("TapSighash"); - final Transaction tx; + @override + final TaprootSignDetails details; final TransactionSignatureHashes txHashes; final PrevOutSignatureHashes? prevOutHashes; - final int inputN; - final List prevOuts; - final SigHashType hashType; - final Uint8List? leafHash; - final int codeSeperatorPos; - - /// Produces the hash for a Taproot input signature at [inputN]. - /// Unless [SigHashType.anyOneCanPay] is true, [prevOuts] must contain the - /// full list of previous outputs being spent. - /// The [hashType] controls what data is included. If ommitted it will be - /// treated as SIGHASH_DEFAULT which includes the same data as SIGHASH_ALL but - /// produces distinct signatures. - /// If an input is being signed for a tapscript, the [leafHash] must be - /// provided. [codeSeperatorPos] must be provided with the position of the - /// last executed CODESEPARATOR unless none have been executed in the script - TaprootSignatureHasher({ - required this.tx, - required this.inputN, - required this.prevOuts, - this.hashType = const SigHashType.schnorrDefault(), - this.leafHash, - this.codeSeperatorPos = 0xFFFFFFFF, - }) : txHashes = TransactionSignatureHashes(tx), - prevOutHashes = PrevOutSignatureHashes(prevOuts) { - - SignatureHasher.checkInputN(tx, inputN); - - if (hashType.single && inputN >= tx.outputs.length) { - throw ArgumentError.value( - inputN, "inputN", "has no corresponing output for SIGHASH_SINGLE", - ); - } - if (prevOuts.length != tx.inputs.length) { - throw ArgumentError.value( - prevOuts.length, "prevOuts.length", "must be same length as inputs", - ); - } - - } + /// Produces the hash for a Taproot input signature. + TaprootSignatureHasher(this.details) + : txHashes = TransactionSignatureHashes(details.tx), + prevOutHashes = details.hashType.allInputs + ? PrevOutSignatureHashes(details.prevOuts) + : null; @override void write(Writer writer) { + final leafHash = details.leafHash; final extFlag = leafHash == null ? 0 : 1; writer.writeUInt8(0); // "Epoch" @@ -68,7 +35,7 @@ final class TaprootSignatureHasher with Writable implements SignatureHasher { writer.writeUInt32(tx.version); writer.writeUInt32(tx.locktime); - if (!hashType.anyOneCanPay) { + if (hashType.allInputs) { writer.writeSlice(txHashes.prevouts.singleHash); writer.writeSlice(prevOutHashes!.amounts.singleHash); writer.writeSlice(prevOutHashes!.scripts.singleHash); @@ -83,9 +50,9 @@ final class TaprootSignatureHasher with Writable implements SignatureHasher { writer.writeUInt8(extFlag << 1); if (hashType.anyOneCanPay) { - tx.inputs[inputN].prevOut.write(writer); - prevOuts[inputN].write(writer); - writer.writeUInt32(tx.inputs[inputN].sequence); + thisInput.prevOut.write(writer); + details.prevOuts.first.write(writer); + writer.writeUInt32(thisInput.sequence); } else { writer.writeUInt32(inputN); } @@ -99,9 +66,9 @@ final class TaprootSignatureHasher with Writable implements SignatureHasher { // Data specific to the script if (leafHash != null) { - writer.writeSlice(leafHash!); + writer.writeSlice(leafHash); writer.writeUInt8(0); // Key version = 0 - writer.writeUInt32(codeSeperatorPos); + writer.writeUInt32(details.codeSeperatorPos); } } diff --git a/coinlib/lib/src/tx/sighash/witness_signature_hasher.dart b/coinlib/lib/src/tx/sighash/witness_signature_hasher.dart index 649238f..e5ab3d6 100644 --- a/coinlib/lib/src/tx/sighash/witness_signature_hasher.dart +++ b/coinlib/lib/src/tx/sighash/witness_signature_hasher.dart @@ -1,44 +1,26 @@ import 'dart:typed_data'; import 'package:coinlib/src/common/serial.dart'; import 'package:coinlib/src/crypto/hash.dart'; -import 'package:coinlib/src/scripts/script.dart'; -import 'package:coinlib/src/tx/transaction.dart'; +import 'package:coinlib/src/tx/sign_details.dart'; import 'precomputed_signature_hashes.dart'; -import 'sighash_type.dart'; import 'signature_hasher.dart'; /// Produces signature hashes for non-taproot witness inputs -final class WitnessSignatureHasher with Writable implements SignatureHasher { +final class WitnessSignatureHasher extends SignatureHasher with Writable { static final hashZero = Uint8List(32); - final Transaction tx; + @override + final LegacyWitnessSignDetailsWithScript details; final TransactionSignatureHashes hashes; - final int inputN; - final Script scriptCode; - final BigInt value; - final SigHashType hashType; - /// Produces the hash of an input signature for a non-taproot witness input - /// at [inputN]. The [scriptCode] of the redeem script is necessary and the - /// [value] of the previous output is required. - /// [hashType] controls what data is included in the signature. - WitnessSignatureHasher({ - required this.tx, - required this.inputN, - required this.scriptCode, - required this.value, - required this.hashType, - }) : hashes = TransactionSignatureHashes(tx) { - SignatureHasher.checkInputN(tx, inputN); - SignatureHasher.checkLegacySigHashType(hashType); - } + /// Produces the hash of an input signature for a non-taproot witness input. + WitnessSignatureHasher(this.details) + : hashes = TransactionSignatureHashes(details.tx); @override void write(Writer writer) { - final thisIn = tx.inputs[inputN]; - final hashPrevouts = !hashType.anyOneCanPay ? hashes.prevouts.doubleHash : hashZero; @@ -57,10 +39,10 @@ final class WitnessSignatureHasher with Writable implements SignatureHasher { writer.writeUInt32(tx.version); writer.writeSlice(hashPrevouts); writer.writeSlice(hashSequences); - thisIn.prevOut.write(writer); - writer.writeVarSlice(scriptCode.compiled); - writer.writeUInt64(value); - writer.writeUInt32(thisIn.sequence); + thisInput.prevOut.write(writer); + writer.writeVarSlice(details.scriptCode.compiled); + writer.writeUInt64(details.value); + writer.writeUInt32(thisInput.sequence); writer.writeSlice(hashOutputs); writer.writeUInt32(tx.locktime); writer.writeUInt32(hashType.value); diff --git a/coinlib/lib/src/tx/sign_details.dart b/coinlib/lib/src/tx/sign_details.dart new file mode 100644 index 0000000..f24c727 --- /dev/null +++ b/coinlib/lib/src/tx/sign_details.dart @@ -0,0 +1,231 @@ +import 'dart:typed_data'; +import 'package:coinlib/src/scripts/program.dart'; +import 'package:coinlib/src/scripts/script.dart'; +import 'output.dart'; +import 'sighash/sighash_type.dart'; +import 'transaction.dart'; + +/// Sign details that are shared for all types of signature +abstract base class SignDetails { + + /// The transaction to sign + final Transaction tx; + /// The input index to sign + final int inputN; + /// The signature hash type + final SigHashType hashType; + + SignDetails({ + required this.tx, + required this.inputN, + required this.hashType, + }) { + + if (inputN < 0 || inputN >= tx.inputs.length) { + throw ArgumentError.value(inputN, "inputN", "outside range of inputs"); + } + + if (!hashType.none && tx.outputs.isEmpty) { + throw CannotSignInput("Cannot sign input without any outputs"); + } + + } + +} + +abstract base class LegacyOrWitnessSignDetails extends SignDetails { + LegacyOrWitnessSignDetails({ + required super.tx, + required super.inputN, + required super.hashType, + }) : super() { + if (!hashType.supportsLegacy) { + throw CannotSignInput( + "$hashType is not supported for legacy signature hashes", + ); + } + } +} + +abstract base class LegacyOrWitnessSignDetailsWithScript +extends LegacyOrWitnessSignDetails { + + /// The redeem script for the input being signed + final Script scriptCode; + + LegacyOrWitnessSignDetailsWithScript({ + required super.tx, + required super.inputN, + required super.hashType, + required this.scriptCode, + }) : super(); + +} + +/// Details for signing a legacy transaction input +final class LegacySignDetails extends LegacyOrWitnessSignDetails { + + /// By default, SIGHASH_ALL will be used + LegacySignDetails({ + required super.tx, + required super.inputN, + super.hashType = const SigHashType.all(), + }) : super(); + + LegacySignDetailsWithScript addScript(Script script) + => LegacySignDetailsWithScript( + tx: tx, + inputN: inputN, + hashType: hashType, + scriptCode: script, + ); + +} + +/// Details for signing a legacy transaction input with the redeem script +final class LegacySignDetailsWithScript +extends LegacyOrWitnessSignDetailsWithScript { + + /// By default, SIGHASH_ALL will be used + LegacySignDetailsWithScript({ + required super.tx, + required super.inputN, + required super.scriptCode, + super.hashType = const SigHashType.all(), + }) : super(); + +} + +/// Details for signing a legacy witness transaction input +final class LegacyWitnessSignDetails extends LegacyOrWitnessSignDetails { + + /// The value of the previous output + final BigInt value; + + /// By default, SIGHASH_ALL will be used + LegacyWitnessSignDetails({ + required super.tx, + required super.inputN, + required this.value, + super.hashType = const SigHashType.all(), + }) : super(); + + LegacyWitnessSignDetailsWithScript addScript(Script script) + => LegacyWitnessSignDetailsWithScript( + tx: tx, + inputN: inputN, + value: value, + scriptCode: script, + hashType: hashType, + ); + +} + +/// Details for signing a legacy witness transaction input with the redeem +/// script +final class LegacyWitnessSignDetailsWithScript +extends LegacyOrWitnessSignDetailsWithScript { + + /// The value of the previous output + final BigInt value; + + /// By default, SIGHASH_ALL will be used + LegacyWitnessSignDetailsWithScript({ + required super.tx, + required super.inputN, + required this.value, + required super.scriptCode, + super.hashType = const SigHashType.all(), + }) : super(); + +} + +/// Details for signing a Taproot transaction input. Use [TaprootKeySignDetails] +/// or [TaprootScriptSignDetails]. +base class TaprootSignDetails extends SignDetails { + + /// Details of previous outputs. This should carry only the previous output of + /// the input to sign when using ANYONECANPAY or ANYPREVOUT. This should be + /// empty for ANYPREVOUTANYSCRIPT. + final List prevOuts; + + /// The leafhash to sign, or null for key-spends + final Uint8List? leafHash; + /// The last executed CODESEPARATOR position in the script + final int codeSeperatorPos; + + /// The [hashType] controls what data is included. If ommitted it will be + /// treated as SIGHASH_DEFAULT which includes the same data as SIGHASH_ALL but + /// produces distinct signatures. + /// + /// [prevOuts] must contain all previous outputs if the input option is + /// [InputSigHashOption.all] which is the default. If ANYONECANPAY + /// ([InputSigHashOption.anyOneCanPay]) or ANYPREVOUT + /// ([InputSigHashOption.anyPrevOut]) is used, only a single output for the + /// input being signed must be provided. If ANYPREVOUTANYSCRIPT + /// ([InputSigHashOption.anyPrevOutAnyScript]) is used, this must be empty. + TaprootSignDetails({ + required super.tx, + required super.inputN, + required this.prevOuts, + super.hashType = const SigHashType.schnorrDefault(), + this.leafHash, + this.codeSeperatorPos = 0xFFFFFFFF, + }) : super() { + + if (hashType.single && inputN >= tx.outputs.length) { + throw CannotSignInput("No corresponing output for SIGHASH_SINGLE"); + } + + final expPrevOutLen = switch (hashType.inputs) { + InputSigHashOption.all => tx.inputs.length, + InputSigHashOption.anyOneCanPay || InputSigHashOption.anyPrevOut => 1, + InputSigHashOption.anyPrevOutAnyScript => 0, + }; + + if (prevOuts.length != expPrevOutLen) { + throw CannotSignInput( + "prevOut length should be $expPrevOutLen for $hashType", + ); + } + + } + +} + +/// Details for a Taproot key-spend +final class TaprootKeySignDetails extends TaprootSignDetails { + + /// See [TaprootSignDetails()]. + TaprootKeySignDetails({ + required super.tx, + required super.inputN, + required super.prevOuts, + super.hashType, + }) : super(); + + Program? get program => switch (hashType.inputs) { + InputSigHashOption.all => prevOuts[inputN].program, + InputSigHashOption.anyOneCanPay || InputSigHashOption.anyPrevOut + => prevOuts.first.program, + InputSigHashOption.anyPrevOutAnyScript => null, + }; + +} + +/// Details for a Taproot script-spend, containing the leaf hash +final class TaprootScriptSignDetails extends TaprootSignDetails { + /// See [TaprootSignDetails()]. + /// + /// The [leafHash] must be provided. [codeSeperatorPos] can be provided with + /// the position of the last executed CODESEPARATOR unless none have been + /// executed in the script. + TaprootScriptSignDetails({ + required super.tx, + required super.inputN, + required super.prevOuts, + required Uint8List leafHash, + super.codeSeperatorPos, + super.hashType, + }) : super(leafHash: leafHash); +} diff --git a/coinlib/lib/src/tx/transaction.dart b/coinlib/lib/src/tx/transaction.dart index 12c5ef6..6b19d13 100644 --- a/coinlib/lib/src/tx/transaction.dart +++ b/coinlib/lib/src/tx/transaction.dart @@ -9,13 +9,11 @@ import 'inputs/input.dart'; import 'inputs/input_signature.dart'; import 'inputs/legacy_input.dart'; import 'inputs/legacy_witness_input.dart'; -import 'inputs/p2pkh_input.dart'; -import 'inputs/p2sh_multisig_input.dart'; -import 'inputs/p2wpkh_input.dart'; import 'inputs/raw_input.dart'; import 'inputs/witness_input.dart'; import 'sighash/sighash_type.dart'; import 'output.dart'; +import 'sign_details.dart'; class TransactionTooLarge implements Exception {} class InvalidTransaction implements Exception {} @@ -186,102 +184,88 @@ class Transaction with Writable { } - /// 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 - /// [P2PKHInput], [P2WPKHInput], [P2SHMultisigInput] or [TaprootKeyInput]. - /// Otherwise [CannotSignInput] will be thrown. Other inputs may be signed - /// seperately and inserted back into the transaction via [replaceInput]. - /// [value] is only required for P2WPKH. - /// [prevOuts] is only required for Taproot inputs. - Transaction sign({ - required int inputN, - required ECPrivateKey key, - SigHashType hashType = const SigHashType.all(), - BigInt? value, - List? prevOuts, - }) { - - if (inputN >= inputs.length) { - throw ArgumentError.value(inputN, "inputN", "outside range of inputs"); - } + Transaction _newInputs(List newInputs) => Transaction( + version: version, + inputs: newInputs, + outputs: outputs, + locktime: locktime, + ); - if (!hashType.none && outputs.isEmpty) { - throw CannotSignInput("Cannot sign input without any outputs"); + T _requireInputOfType(int inputN) { + if (inputN < 0 || inputN >= inputs.length) { + throw RangeError.range(inputN, 0, inputs.length-1, "inputN"); } - final input = inputs[inputN]; + if (input is! T) throw CannotSignInput("Input to sign is not a $T"); + return input as T; + } - // Sign input - late Input signedIn; - - if (input is LegacyInput) { - signedIn = input.sign( - tx: this, - inputN: inputN, - key: key, - hashType: hashType, - ); - } else if (input is LegacyWitnessInput) { + Transaction _replaceNewlySigned(int n, Input input) => _newInputs( + [...inputs.take(n), input, ...inputs.skip(n+1)], + ); - if (value == null) { - throw CannotSignInput("Prevout values are required for witness inputs"); - } + /// Sign a [LegacyInput] at [inputN] with the [key]. The signature hash is + /// SIGHASH_ALL by default but can be changed via [hashType]. + Transaction signLegacy({ + required int inputN, + required ECPrivateKey key, + SigHashType hashType = const SigHashType.all(), + }) => _replaceNewlySigned( + inputN, + _requireInputOfType(inputN).sign( + details: LegacySignDetails(tx: this, inputN: inputN, hashType: hashType), + key: key, + ), + ); - signedIn = input.sign( + /// Sign a [LegacyWitnessInput] at [inputN] with the [key]. Must contain the + /// [value] being spent. The signature hash is SIGHASH_ALL by default but can + /// be changed via [hashType]. + Transaction signLegacyWitness({ + required int inputN, + required ECPrivateKey key, + required BigInt value, + SigHashType hashType = const SigHashType.all(), + }) => _replaceNewlySigned( + inputN, + _requireInputOfType(inputN).sign( + details: LegacyWitnessSignDetails( tx: this, inputN: inputN, - key: key, value: value, hashType: hashType, - ); - - } else if (input is TaprootKeyInput) { - - if (prevOuts == null) { - throw CannotSignInput( - "Previous outputs are required when signing a taproot input", - ); - } - - if (prevOuts.length != inputs.length) { - throw CannotSignInput( - "The number of previous outputs must match the number of inputs", - ); - } + ), + key: key, + ), + ); - signedIn = input.sign( + /// Sign a [TaprootKeyInput] at [inputN] with the [key]. If all inputs are + /// being signed, all previous outputs must be provided to [prevOuts]. If + /// ANYONECANPAY is used, only the output of the input should be included in + /// [prevOuts]. The signature hash is SIGHASH_DEFAULT by default but can be + /// changed via [hashType]. + Transaction signTaproot({ + required int inputN, + required ECPrivateKey key, + required List prevOuts, + SigHashType hashType = const SigHashType.schnorrDefault(), + }) => _replaceNewlySigned( + inputN, + _requireInputOfType(inputN).sign( + details: TaprootKeySignDetails( tx: this, inputN: inputN, - key: key, prevOuts: prevOuts, hashType: hashType, - ); - - } else { - throw CannotSignInput("${input.runtimeType} not a signable input"); - } - - // Replace input in input list - final newInputs = inputs.asMap().map( - (index, input) => MapEntry( - index, index == inputN ? signedIn : input, ), - ).values; - - return Transaction( - version: version, - inputs: newInputs, - outputs: outputs, - locktime: locktime, - ); - - } - + key: key, + ), + ); /// Replaces the input at [n] with the new [input] and invalidates other /// input signatures that have standard sighash types accordingly. This is /// useful for signing or otherwise updating inputs that cannot be signed with - /// the [sign] method. + /// the [signLegacy], [signLegacyWitness] or [signTaproot] methods. Transaction replaceInput(Input input, int n) { final oldInput = inputs[n]; @@ -294,8 +278,8 @@ class Transaction with Writable { final filtered = inputs.map( (input) => input.filterSignatures( (insig) - // Allow ANYONECANPAY - => insig.hashType.anyOneCanPay + // Allow ANYONECANPAY, ANYPREVOUT or ANYPREVOUTANYSCRIPT + => insig.hashType.inputs != InputSigHashOption.all // Allow signature if previous output hasn't changed and the sequence // has not changed for taproot inputs or when using SIGHASH_ALL. || !( @@ -307,12 +291,7 @@ class Transaction with Writable { ), ).toList(); - return Transaction( - version: version, - inputs: [...filtered.take(n), input, ...filtered.sublist(n+1)], - outputs: outputs, - locktime: locktime, - ); + return _newInputs([...filtered.take(n), input, ...filtered.skip(n+1)]); } @@ -321,10 +300,11 @@ class Transaction with Writable { Transaction addInput(Input input) => Transaction( version: version, inputs: [ - // Only keep ANYONECANPAY signatures when adding a new input + // Only keep ANYONECANPAY, ANYPREVOUT and ANYPREVOUTANYSCRIPT signatures + // when adding a new input ...inputs.map( (input) => input.filterSignatures( - (insig) => insig.hashType.anyOneCanPay, + (insig) => insig.hashType.inputs != InputSigHashOption.all, ), ), input, diff --git a/coinlib/test/tx/coin_selection_test.dart b/coinlib/test/tx/coin_selection_test.dart index 65fb140..c22fe00 100644 --- a/coinlib/test/tx/coin_selection_test.dart +++ b/coinlib/test/tx/coin_selection_test.dart @@ -154,7 +154,7 @@ void main() { var tx = selection.transaction; for (int i = 0; i < vector.inputValues.length; i++) { - tx = tx.sign(inputN: i, key: keyPairVectors[0].privateObj); + tx = tx.signLegacy(inputN: i, key: keyPairVectors[0].privateObj); } expect( diff --git a/coinlib/test/tx/inputs/taproot_key_input_test.dart b/coinlib/test/tx/inputs/taproot_key_input_test.dart index bad3d91..bc444a5 100644 --- a/coinlib/test/tx/inputs/taproot_key_input_test.dart +++ b/coinlib/test/tx/inputs/taproot_key_input_test.dart @@ -108,18 +108,20 @@ void main() { test(".sign() should sign as SIGHASH_DEFAULT by default", () { final input = TaprootKeyInput(prevOut: prevOut); final signedInput = input.sign( - tx: Transaction( - inputs: [input], - outputs: [exampleOutput], + details: TaprootKeySignDetails( + tx: Transaction( + inputs: [input], + outputs: [exampleOutput], + ), + inputN: 0, + prevOuts: [ + Output.fromProgram( + BigInt.from(10000), + P2TR.fromTweakedKey(keyPairVectors[0].publicObj), + ), + ], ), - inputN: 0, key: keyPairVectors[0].privateObj, - prevOuts: [ - Output.fromProgram( - BigInt.from(10000), - P2TR.fromTweakedKey(keyPairVectors[0].publicObj), - ), - ], ); expect(signedInput.insig!.hashType.schnorrDefault, true); }); diff --git a/coinlib/test/tx/inputs/taproot_script_input_test.dart b/coinlib/test/tx/inputs/taproot_script_input_test.dart index eb43143..933d040 100644 --- a/coinlib/test/tx/inputs/taproot_script_input_test.dart +++ b/coinlib/test/tx/inputs/taproot_script_input_test.dart @@ -128,20 +128,6 @@ void main() { expectNoMatch("", [hexToBytes("0101"), controlBlock]); }); - test("sign() not implemented", () => expect( - () => TaprootScriptInput.fromTaprootLeaf( - prevOut: prevOut, - taproot: taprootVec.object, - leaf: taprootVec.object.leaves[0], - ).sign( - tx: Transaction(inputs: [], outputs: []), - inputN: 0, - key: ECPrivateKey.generate(), - prevOuts: [], - ), - throwsA(isA()), - ),); - }); } diff --git a/coinlib/test/tx/sighash/legacy_signature_hasher_test.dart b/coinlib/test/tx/sighash/legacy_signature_hasher_test.dart index c235c28..0d8f003 100644 --- a/coinlib/test/tx/sighash/legacy_signature_hasher_test.dart +++ b/coinlib/test/tx/sighash/legacy_signature_hasher_test.dart @@ -7,10 +7,12 @@ void main() { signatureHasherTester( "LegacySignatureHasher", (Transaction tx, int inputN, SigHashVector vec) => LegacySignatureHasher( - tx: tx, - inputN: inputN, - scriptCode: Script.fromAsm(vec.scriptCodeAsm), - hashType: vec.type, + LegacySignDetailsWithScript( + tx: tx, + inputN: inputN, + scriptCode: Script.fromAsm(vec.scriptCodeAsm), + hashType: vec.type, + ), ).hash, (SigHashVector vec) => vec.hash, ); diff --git a/coinlib/test/tx/sighash/signature_hasher_tester.dart b/coinlib/test/tx/sighash/signature_hasher_tester.dart index 659b9f7..5ef4f4d 100644 --- a/coinlib/test/tx/sighash/signature_hasher_tester.dart +++ b/coinlib/test/tx/sighash/signature_hasher_tester.dart @@ -49,7 +49,7 @@ void signatureHasherTester( witnessHash: "", ), ), - throwsArgumentError, + throwsA(isA()), ); } }); diff --git a/coinlib/test/tx/sighash/taproot_signature_hasher_test.dart b/coinlib/test/tx/sighash/taproot_signature_hasher_test.dart index 1278ad7..164403e 100644 --- a/coinlib/test/tx/sighash/taproot_signature_hasher_test.dart +++ b/coinlib/test/tx/sighash/taproot_signature_hasher_test.dart @@ -114,13 +114,17 @@ void main() { expect( bytesToHex( TaprootSignatureHasher( - tx: tx, - inputN: vec.inputN, - prevOuts: prevOuts, - leafHash: vec.leafHashHex == null - ? null - : hexToBytes(vec.leafHashHex!), - hashType: vec.hashType, + TaprootSignDetails( + tx: tx, + inputN: vec.inputN, + prevOuts: vec.hashType.anyOneCanPay + ? [prevOuts[vec.inputN]] + : prevOuts, + leafHash: vec.leafHashHex == null + ? null + : hexToBytes(vec.leafHashHex!), + hashType: vec.hashType, + ), ).hash, ), vec.sigHashHex, @@ -130,22 +134,41 @@ void main() { test("input out of range", () => expect( () => TaprootSignatureHasher( - tx: tx, - inputN: 9, - prevOuts: prevOuts, - hashType: SigHashType.all(), + TaprootKeySignDetails( + tx: tx, + inputN: 9, + prevOuts: prevOuts, + hashType: SigHashType.all(), + ), ), throwsArgumentError, ),); - test("prevOuts length incorrect", () => expect( - () => TaprootSignatureHasher( - tx: tx, - inputN: 0, - prevOuts: prevOuts.sublist(0, prevOuts.length-1), - hashType: SigHashType.all(), - ), - throwsArgumentError, - ),); + test("prevOuts length incorrect", () { + for(final (hashType, length) in [ + (SigHashType.all(), prevOuts.length-1), + (SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), prevOuts.length), + (SigHashType.all(inputs: InputSigHashOption.anyPrevOut), prevOuts.length), + ( + SigHashType.all(inputs: InputSigHashOption.anyPrevOutAnyScript), + prevOuts.length, + ), + (SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), 2), + (SigHashType.all(inputs: InputSigHashOption.anyPrevOut), 2), + (SigHashType.all(inputs: InputSigHashOption.anyPrevOutAnyScript), 1), + ]) { + () => expect( + () => TaprootSignatureHasher( + TaprootKeySignDetails( + tx: tx, + inputN: 0, + prevOuts: prevOuts.sublist(0, length), + hashType: hashType, + ), + ), + throwsArgumentError, + ); + } + }); } diff --git a/coinlib/test/tx/sighash/witness_signature_hasher_test.dart b/coinlib/test/tx/sighash/witness_signature_hasher_test.dart index d90bd08..55ddd54 100644 --- a/coinlib/test/tx/sighash/witness_signature_hasher_test.dart +++ b/coinlib/test/tx/sighash/witness_signature_hasher_test.dart @@ -7,11 +7,13 @@ void main() { signatureHasherTester( "WitnessSignatureHasher", (Transaction tx, int inputN, SigHashVector vec) => WitnessSignatureHasher( - tx: tx, - inputN: inputN, - scriptCode: Script.fromAsm(vec.scriptCodeAsm), - value: witnessValue, - hashType: vec.type, + LegacyWitnessSignDetailsWithScript( + tx: tx, + inputN: inputN, + scriptCode: Script.fromAsm(vec.scriptCodeAsm), + value: witnessValue, + hashType: vec.type, + ), ).hash, (SigHashVector vec) => vec.witnessHash, ); diff --git a/coinlib/test/tx/transaction_test.dart b/coinlib/test/tx/transaction_test.dart index b87871a..97901de 100644 --- a/coinlib/test/tx/transaction_test.dart +++ b/coinlib/test/tx/transaction_test.dart @@ -175,11 +175,16 @@ void main() { }); - test("sign() failure", () { + void expectCannotSign(void Function() doSign) => expect( + doSign, throwsA(isA()), + ); + + test("sign failures", () { final privkey = ECPrivateKey.generate(); final pubkey = privkey.pubkey; final wrongkey = ECPrivateKey.generate(); + final value = BigInt.parse("10000"); final txNoOutput = Transaction( inputs: [ @@ -196,59 +201,47 @@ void main() { // SIGHASH_NONE OK with no outputs expect( - txNoOutput.sign(inputN: 1, key: privkey, hashType: SigHashType.none()), + txNoOutput.signLegacy( + inputN: 1, key: privkey, hashType: SigHashType.none(), + ), isA(), ); // No outputs - expect( - () => txNoOutput.sign(inputN: 1, key: privkey), - throwsA(isA()), - ); + expectCannotSign(() => txNoOutput.signLegacy(inputN: 1, key: privkey)); final tx = txNoOutput.addOutput(exampleOutput); // OK - expect(tx.sign(inputN: 1, key: privkey), isA()); + expect(tx.signLegacy(inputN: 1, key: privkey), isA()); // Input out of range - expect(() => tx.sign(inputN: 4, key: privkey), throwsArgumentError); + expect(() => tx.signLegacy(inputN: 4, key: privkey), throwsRangeError); // Wrong key for P2PKH - expect( - () => tx.sign(inputN: 1, key: wrongkey), - throwsA(isA()), - ); + expectCannotSign(() => tx.signLegacy(inputN: 1, key: wrongkey)); - // Cannot sign witness input without value - expect( - () => tx.sign(inputN: 0, key: privkey), - throwsA(isA()), - ); + // Cannot sign witness input as legacy + expectCannotSign(() => tx.signLegacy(inputN: 0, key: privkey)); // Cannot sign raw unmatched input - expect( - () => tx.sign(inputN: 3, key: privkey), - throwsA(isA()), - ); + expectCannotSign(() => tx.signLegacy(inputN: 3, key: privkey)); // Cannot use schnorrDefault to sign legacy inputs - expect( - () => tx.sign( + expectCannotSign( + () => tx.signLegacy( inputN: 1, key: privkey, hashType: SigHashType.schnorrDefault(), ), - throwsA(isA()), ); - expect( - () => tx.sign( + expectCannotSign( + () => tx.signLegacyWitness( inputN: 0, key: privkey, hashType: SigHashType.schnorrDefault(), - value: BigInt.parse("10000"), + value: value, ), - throwsA(isA()), ); // Taproot tests @@ -263,41 +256,76 @@ void main() { Output.blank(), ]; - // Require prev outs for TR - expect( - () => tx.sign( + // Cannot sign taproot as legacy + expectCannotSign(() => tx.signLegacy(inputN: 2, key: tweakedKey)); + expectCannotSign( + () => tx.signLegacyWitness(inputN: 2, key: tweakedKey, value: value), + ); + + // Require prev out number to match number of inputs when signing all + // inputs + expectCannotSign( + () => tx.signTaproot( inputN: 2, key: tweakedKey, + prevOuts: prevOuts.take(3).toList(), ), - throwsA(isA()), ); - // Require prev out number to match number of inputs - expect( - () => tx.sign( + /// Should have only one prevOut for ANYONECANPAY + expectCannotSign( + () => tx.signTaproot( inputN: 2, key: tweakedKey, - prevOuts: prevOuts.sublist(0, 3), + prevOuts: prevOuts, + hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), ), - throwsA(isA()), ); // Wrong (untweaked) key for TR - expect( - () => tx.sign( + expectCannotSign( + () => tx.signTaproot(inputN: 2, key: privkey, prevOuts: prevOuts), + ); + + // Require matching output for SIGHASH_SINGLE + expectCannotSign( + () => tx.signTaproot( inputN: 2, - key: privkey, + key: tweakedKey, prevOuts: prevOuts, + hashType: SigHashType.single(), ), - throwsA(isA()), ); // Ensure it does work with correct key expect( - tx.sign(inputN: 2, key: tweakedKey, prevOuts: prevOuts), + tx.signTaproot(inputN: 2, key: tweakedKey, prevOuts: prevOuts), isA(), ); + // Disallow APO + for (final apoType in [ + SigHashType.all(inputs: InputSigHashOption.anyPrevOut), + SigHashType.all(inputs: InputSigHashOption.anyPrevOutAnyScript), + ]) { + expectCannotSign( + () => tx.signLegacy(inputN: 1, key: privkey, hashType: apoType), + ); + expectCannotSign( + () => tx.signLegacyWitness( + inputN: 0, key: privkey, value: value, hashType: apoType, + ), + ); + expectCannotSign( + () => tx.signTaproot( + inputN: 2, + key: privkey, + prevOuts: apoType.anyPrevOutAnyScript ? [] : [prevOuts[2]], + hashType: apoType, + ), + ); + } + }); test("immutable inputs/outputs", () { @@ -330,7 +358,7 @@ void main() { var signed = tx; for (int i = 0; i < tx.inputs.length; i++) { - signed = signed.sign( + signed = signed.signLegacy( inputN: i, key: keyVec.privateObj, hashType: hashType, @@ -424,10 +452,10 @@ void main() { ); expect(tx.complete, false); - final partSigned = tx.sign(inputN: 0, key: keyVec.privateObj); + final partSigned = tx.signLegacy(inputN: 0, key: keyVec.privateObj); expect(partSigned.complete, false); - final signed = partSigned.sign( + final signed = partSigned.signLegacyWitness( inputN: 1, key: keyVec.privateObj, value: BigInt.from(3000000), ); expect(signed.complete, true); @@ -473,14 +501,14 @@ void main() { ]; final tweakedPriv = taproot.tweakPrivateKey(keyVec.privateObj); - final signed = tx.sign( + final signed = tx.signTaproot( inputN: 0, key: tweakedPriv, prevOuts: prevOuts, - ).sign( + ).signTaproot( inputN: 1, key: tweakedPriv, - prevOuts: prevOuts, + prevOuts: [prevOuts[1]], hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), ); @@ -491,7 +519,7 @@ void main() { expect( signed.toHex(), - "03000000000102c17074e66379635bdab769340f8acc2feea02f9d6c177cb8969c59a97fcf68ec0100000000ffffffff1e218726e8c81578cd9ed1b5568f5eff482d70f59571fed851514656071ddaca0100000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac0141525f7f8e98fe4a116131c1d51aba29bbe15852260faa5308d23768d4d99251004b56306396f5f07018e7a94b41594a485455107b818d1c8899a18afbc98618b0010141399278f778c70fc3a7eee1997cb53acbef4a86ab65d85c12d40286f26cb50b189656df26d86e9be4f16bfe0b574d3f632e2b537d19d0e04d545e61c3d76b07058100000000", + "03000000000102c17074e66379635bdab769340f8acc2feea02f9d6c177cb8969c59a97fcf68ec0100000000ffffffff1e218726e8c81578cd9ed1b5568f5eff482d70f59571fed851514656071ddaca0100000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac0140b9fa12df36bdab8b63eb647986da6b66d8f64210cd73f5c4a6a1ce0d772590da21aece30df4cd3bd5174206d46cc837b71af3fbd0f54de27e44c6abfbb170d1f0141399278f778c70fc3a7eee1997cb53acbef4a86ab65d85c12d40286f26cb50b189656df26d86e9be4f16bfe0b574d3f632e2b537d19d0e04d545e61c3d76b07058100000000", ); // Invalidates first input and keeps ANYONECANPAY when adding new input @@ -562,10 +590,12 @@ void main() { final solvedInput = inputToSign.updateStack([ inputToSign.createScriptSignature( - tx: tx, - inputN: 0, + details: TaprootKeySignDetails( + tx: tx, + inputN: 0, + prevOuts: [prevOut], + ), key: keyPairVectors[2].privateObj, - prevOuts: [prevOut], ).bytes, ]); final solvedTx = tx.replaceInput(solvedInput, 0); @@ -619,11 +649,11 @@ void main() { final signedSizeFromUnsigned = tx.inputs[1].signedSize; // Sign first P2PKH input - var signed = tx.sign(inputN: 0, key: privkeys[0]); + var signed = tx.signLegacy(inputN: 0, key: privkeys[0]); expect(signed.complete, false); // Sign 3 with SIGHASH_ALL and ANYONECANPAY - signed = signed.sign( + signed = signed.signLegacy( inputN: 1, key: privkeys[3], hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), @@ -631,7 +661,7 @@ void main() { expect(signed.complete, false); // Sign 1 with SIGHASH_SINGLE - signed = signed.sign( + signed = signed.signLegacy( inputN: 1, key: privkeys[1], hashType: SigHashType.single(), @@ -639,7 +669,7 @@ void main() { expect(signed.complete, false); // Sign 2 with SIGHASH_NONE - signed = signed.sign( + signed = signed.signLegacy( inputN: 1, key: privkeys[2], hashType: SigHashType.none(), @@ -699,7 +729,11 @@ void main() { for (int i = 0; i < 2; i++) { expect(tx.complete, false); - tx = tx.sign(inputN: i, key: privkey, hashType: SigHashType.single()); + tx = tx.signLegacy( + inputN: i, + key: privkey, + hashType: SigHashType.single(), + ); } expect(tx.complete, true); @@ -754,40 +788,40 @@ void main() { outputs: [exampleOutput], ) // Sign legacy - .sign(inputN: 0, key: keyVec.privateObj) - .sign( + .signLegacy(inputN: 0, key: keyVec.privateObj) + .signLegacy( inputN: 2, key: keyVec.privateObj, hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), ) - .sign( + .signLegacy( inputN: 3, key: keyVec.privateObj, hashType: SigHashType.single(), ) // Sign witness - .sign(inputN: 4, key: keyVec.privateObj, value: value) - .sign( + .signLegacyWitness(inputN: 4, key: keyVec.privateObj, value: value) + .signLegacyWitness( inputN: 5, key: keyVec.privateObj, hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), value: value, ) - .sign( + .signLegacyWitness( inputN: 6, key: keyVec.privateObj, hashType: SigHashType.none(), value: value, ) // Sign taproot - .sign(inputN: 7, key: keyVec.privateObj, prevOuts: taprootPrevOuts) - .sign( + .signTaproot(inputN: 7, key: keyVec.privateObj, prevOuts: taprootPrevOuts) + .signTaproot( inputN: 8, key: keyVec.privateObj, hashType: SigHashType.all(inputs: InputSigHashOption.anyOneCanPay), - prevOuts: taprootPrevOuts, + prevOuts: [taprootPrevOuts[8]], ) - .sign( + .signTaproot( inputN: 9, key: keyVec.privateObj, hashType: SigHashType.none(), @@ -855,8 +889,7 @@ void main() { // Sign second input outside tx and check it is OK final signedIn = (tx.inputs[1] as P2PKHInput).sign( - tx: tx, - inputN: 1, + details: LegacySignDetails(tx: tx, inputN: 1), key: keyVec.privateObj, ); final signedTx = tx.replaceInput(signedIn, 1); @@ -865,7 +898,7 @@ void main() { expect( signedTx.toHex(), - "0300000000010af1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a473044022079f777b6059975ce333332bb3ecde653be038dcbddefc7920072124b1ffe43fc022030926798d6440ea69aab4e28a3ebf84fa46f3f31ae2c1c38b04c73120abea2cf01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402201e8ab341d37d9cdd8563d649e710eee973ae601fe61b57da6e1d7ae10ba21a7a022023a9d5b20a43df3c60697c876ba2030d754dda0e07804a699222616ffce3cf3801210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402202c2f712bbef221026214ae9e54e817eb73df6c73aca4d35b5190caf28c454cd102207dc3750935a86b4431e55476a1e049d3d8ee7cf13162f264ce9964afa4b09c7181210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402205a0d9a76a926bce5a74db8fa127d8c8779d868de26c1422f4b8acd3f8735ba580220381287903eeb349023c8d6a0d77ddcdf9fcbc01ce968627183f124ecd5e860c203210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000024730440220487c6b12556adc75a199a8b390d38b928bd4efbb831f2010250389995fee821302204dfa74a4e7711a8b96249a6ec7836e4cc374d6dbf3864fdd142a25a67663ebfe01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980247304402206e47c35235a3f5dab7420ad3e2ecdc395cb402b9fc29e8ada89ff6e380ac4df3022010812c5beacf5ef47ac380511989d204804b33a664ffe9019ea8be23ccf56f2981210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980247304402202b4600fe4e9823210f36074f9e3d1fa442d940e00970b707014630f2a2f695b402203e2468c4c2501959a92fafe6475cff1ba1497b5e50c77701188923399654d1d602210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179801416dc042d6c7ada370318f0d33e16421154964ad39c8b7ee275fcaa681a2ebfe48d9b53033672b232821e94aafa0772174d20c99a97e8b44b53937a0cd9caf904d010141af1a113a9cd6f83655cb1444e8f8e3bf07751381a942a3a35b40bac08f61c56a8736a49d8998380305e488e156ecd0516b40474db401ba359ef46cd49d96743d810141f2edfd966b88a16e30c840bf8a93e05bd2bc2bed954a01712bc0ff2fbfcdff654f6262054ba965b09c08e03c8e29c2bd19fd6a2294e5464fa45f58908fab0c8e0200000000", + "0300000000010af1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a473044022079f777b6059975ce333332bb3ecde653be038dcbddefc7920072124b1ffe43fc022030926798d6440ea69aab4e28a3ebf84fa46f3f31ae2c1c38b04c73120abea2cf01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402201e8ab341d37d9cdd8563d649e710eee973ae601fe61b57da6e1d7ae10ba21a7a022023a9d5b20a43df3c60697c876ba2030d754dda0e07804a699222616ffce3cf3801210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402202c2f712bbef221026214ae9e54e817eb73df6c73aca4d35b5190caf28c454cd102207dc3750935a86b4431e55476a1e049d3d8ee7cf13162f264ce9964afa4b09c7181210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe000000006a47304402205a0d9a76a926bce5a74db8fa127d8c8779d868de26c1422f4b8acd3f8735ba580220381287903eeb349023c8d6a0d77ddcdf9fcbc01ce968627183f124ecd5e860c203210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000fffffffff1fefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe0000000000ffffffff01a0860100000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000024730440220487c6b12556adc75a199a8b390d38b928bd4efbb831f2010250389995fee821302204dfa74a4e7711a8b96249a6ec7836e4cc374d6dbf3864fdd142a25a67663ebfe01210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980247304402206e47c35235a3f5dab7420ad3e2ecdc395cb402b9fc29e8ada89ff6e380ac4df3022010812c5beacf5ef47ac380511989d204804b33a664ffe9019ea8be23ccf56f2981210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980247304402202b4600fe4e9823210f36074f9e3d1fa442d940e00970b707014630f2a2f695b402203e2468c4c2501959a92fafe6475cff1ba1497b5e50c77701188923399654d1d602210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817980140622c919fec9e9eeb8e7561bb768c6b7559e9aae170d2269c06f74b57cca6ce447dc9dc3789cd96d304faa7b759a6cfcc9287d965f34a911960c29f0eac9a79ad0141af1a113a9cd6f83655cb1444e8f8e3bf07751381a942a3a35b40bac08f61c56a8736a49d8998380305e488e156ecd0516b40474db401ba359ef46cd49d96743d810141f2edfd966b88a16e30c840bf8a93e05bd2bc2bed954a01712bc0ff2fbfcdff654f6262054ba965b09c08e03c8e29c2bd19fd6a2294e5464fa45f58908fab0c8e0200000000", ); });