Skip to content

Commit

Permalink
Add TaprootKeyInput and key-path spend logic
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewLM committed Oct 18, 2023
1 parent 335827c commit d8c51b2
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 3 deletions.
4 changes: 4 additions & 0 deletions coinlib/lib/src/coinlib_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ export 'package:coinlib/src/tx/output.dart';

export 'package:coinlib/src/tx/inputs/input.dart';
export 'package:coinlib/src/tx/inputs/input_signature.dart';
export 'package:coinlib/src/tx/inputs/legacy_input.dart';
export 'package:coinlib/src/tx/inputs/legacy_witness_input.dart';
export 'package:coinlib/src/tx/inputs/p2pkh_input.dart';
export 'package:coinlib/src/tx/inputs/p2sh_multisig_input.dart';
export 'package:coinlib/src/tx/inputs/p2wpkh_input.dart';
export 'package:coinlib/src/tx/inputs/pkh_input.dart';
export 'package:coinlib/src/tx/inputs/raw_input.dart';
export 'package:coinlib/src/tx/inputs/taproot_input.dart';
export 'package:coinlib/src/tx/inputs/taproot_key_input.dart';
export 'package:coinlib/src/tx/inputs/witness_input.dart';

export 'package:coinlib/src/tx/sighash/legacy_signature_hasher.dart';
Expand Down
58 changes: 58 additions & 0 deletions coinlib/lib/src/tx/inputs/taproot_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 'input.dart';
import 'input_signature.dart';
import 'witness_input.dart';

/// Represents v1 Taproot program inputs
abstract class TaprootInput extends WitnessInput {

TaprootInput({
required super.prevOut,
required super.witness,
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<Output> prevOuts,
hashType = const SigHashType.all(),
}) => 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<Output> prevOuts,
hashType = const SigHashType.all(),
Uint8List? leafHash,
int codeSeperatorPos = 0xFFFFFFFF,
}) => SchnorrInputSignature(
SchnorrSignature.sign(
key,
TaprootSignatureHasher(
tx: tx,
inputN: inputN,
prevOuts: prevOuts,
hashType: hashType,
leafHash: leafHash,
codeSeperatorPos: codeSeperatorPos,
).hash,
),
hashType,
);

}
106 changes: 106 additions & 0 deletions coinlib/lib/src/tx/inputs/taproot_key_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'dart:typed_data';
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/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 'input.dart';
import 'input_signature.dart';
import 'raw_input.dart';

/// A [TaprootInput] which spends using the key-path
class TaprootKeyInput extends TaprootInput {

final SchnorrInputSignature? insig;

TaprootKeyInput({
required OutPoint prevOut,
this.insig,
int sequence = Input.sequenceFinal,
}) : super(
prevOut: prevOut,
sequence: sequence,
witness: [if (insig != null) insig.bytes],
);

/// Checks if the [raw] input and [witness] data match the expected format for
/// a [TaprootKeyInput], with a signature. If it does it returns a
/// [TaprootKeyInput] for the input or else it returns null.
static TaprootKeyInput? match(RawInput raw, List<Uint8List> witness) {

if (raw.scriptSig.isNotEmpty) return null;
if (witness.length != 1) return null;

try {
return TaprootKeyInput(
prevOut: raw.prevOut,
insig: SchnorrInputSignature.fromBytes(witness[0]),
sequence: raw.sequence,
);
} on InvalidInputSignature {
return null;
}

}

@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 ECPrivateKey key,
required List<Output> prevOuts,
hashType = const SigHashType.all(),
}) {

if (inputN >= prevOuts.length) {
throw CannotSignInput(
"Input is out of range of the previous outputs provided",
);
}

// Check key corresponds to matching prevOut
final program = prevOuts[inputN].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,
),
);

}

/// Returns a new [TaprootKeyInput] with the [SchnorrInputSignature] added.
/// Any existing signature is replaced.
TaprootKeyInput addSignature(SchnorrInputSignature insig) => TaprootKeyInput(
prevOut: prevOut,
insig: insig,
sequence: sequence,
);

@override
TaprootKeyInput filterSignatures(
bool Function(InputSignature insig) predicate,
) => insig == null || predicate(insig!) ? this : TaprootKeyInput(
prevOut: prevOut,
insig: null,
sequence: sequence,
);

@override
bool get complete => insig != null;

}
2 changes: 2 additions & 0 deletions coinlib/lib/src/tx/inputs/witness_input.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:typed_data';
import 'package:coinlib/src/tx/inputs/taproot_key_input.dart';
import 'package:coinlib/src/tx/outpoint.dart';
import 'input.dart';
import 'raw_input.dart';
Expand Down Expand Up @@ -27,6 +28,7 @@ class WitnessInput extends RawInput {
? (
// Is a witness input, so match with the specific input type
P2WPKHInput.match(raw, witness)
?? TaprootKeyInput.match(raw, witness)
?? WitnessInput(
prevOut: raw.prevOut,
witness: witness,
Expand Down
25 changes: 25 additions & 0 deletions coinlib/lib/src/tx/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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/hash.dart';
import 'package:coinlib/src/tx/inputs/taproot_key_input.dart';
import 'inputs/input.dart';
import 'inputs/legacy_input.dart';
import 'inputs/legacy_witness_input.dart';
Expand Down Expand Up @@ -185,11 +186,13 @@ class Transaction with Writable {
/// [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.
/// [prevOuts] is only required for Taproot inputs.
Transaction sign({
required int inputN,
required ECPrivateKey key,
hashType = const SigHashType.all(),
BigInt? value,
List<Output>? prevOuts,
}) {

if (inputN >= inputs.length) {
Expand Down Expand Up @@ -226,6 +229,28 @@ class Transaction with Writable {
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",
);
}

signedIn = input.sign(
tx: this,
inputN: inputN,
key: key,
prevOuts: prevOuts,
hashType: hashType,
);

} else {
throw CannotSignInput("${input.runtimeType} not a signable input");
}
Expand Down
115 changes: 115 additions & 0 deletions coinlib/test/tx/inputs/taproot_key_input_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import 'dart:typed_data';
import 'package:coinlib/coinlib.dart';
import 'package:test/test.dart';
import '../../vectors/signatures.dart';
import '../../vectors/inputs.dart';

void main() {

group("TaprootKeyInput", () {

late SchnorrInputSignature insig;

setUpAll(() async {
await loadCoinlib();
insig = SchnorrInputSignature(
SchnorrSignature.fromHex(validSchnorrSig),
SigHashType.none(),
);
});

getWitness(bool hasSig) => [if (hasSig) insig.bytes];

test("valid key-path taproot inputs inc. addSignature", () {

final rawBytes = Uint8List.fromList([
...prevOutHash,
0xef, 0xbe, 0xed, 0xfe,
0,
0xed, 0xfe, 0xef, 0xbe,
]);

expectTaprootKeyInput(TaprootKeyInput input, bool hasSig) {

expectInput(input);

expect(input.complete, hasSig);
expect(input.insig, hasSig ? isNotNull : null);
expect(input.scriptSig.isEmpty, true);
expect(input.script!.length, 0);

if (hasSig) {
expect(bytesToHex(input.insig!.signature.data), validSchnorrSig);
expect(input.insig!.hashType.none, true);
}

expect(input.witness, getWitness(hasSig));
expect(input.size, rawBytes.length);
expect(input.toBytes(), rawBytes);

}

final noSig = TaprootKeyInput(prevOut: prevOut, sequence: sequence);

final withSig = TaprootKeyInput(
prevOut: prevOut,
sequence: sequence,
insig: insig,
);

expectTaprootKeyInput(noSig, false);
expectTaprootKeyInput(withSig, true);
expectTaprootKeyInput(noSig.addSignature(insig), true);

// Expect match only when there is a Schnorr signature present, as there
// is no way to distinguish otherwise
final matched = Input.match(
RawInput.fromReader(BytesReader(rawBytes)),
getWitness(true),
);
expect(matched, isA<TaprootKeyInput>());
expectTaprootKeyInput(matched as TaprootKeyInput, true);

});

test("doesn't match non key-spend inputs", () {

expectNoMatch(String asm, List<Uint8List> witness) => expect(
TaprootKeyInput.match(
RawInput(
prevOut: prevOut,
scriptSig: Script.fromAsm(asm).compiled,
sequence: 0,
),
witness,
),
null,
);

expectNoMatch("0", getWitness(true));
// Doesn't match without signature
expectNoMatch("", getWitness(false));
expectNoMatch("", [...getWitness(true), ...getWitness(true)]);
// Not allowing annex
expectNoMatch("", [...getWitness(true), hexToBytes("5001020304")]);
expectNoMatch(
"",
[
Uint8List.fromList([
...hexToBytes(validDerSigs[0]),
SigHashType.noneValue,
]),
],
);

});

test("filterSignatures", () {
final input = TaprootKeyInput(prevOut: prevOut, insig: insig);
expect(input.filterSignatures((insig) => false).insig, isNull);
expect(input.filterSignatures((insig) => true).insig, isNotNull);
});

});

}
Loading

0 comments on commit d8c51b2

Please sign in to comment.