-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add TaprootKeyInput and key-path spend logic
- Loading branch information
Showing
7 changed files
with
361 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
||
}); | ||
|
||
} |
Oops, something went wrong.