Skip to content

Commit

Permalink
Add TaprootScriptInput
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewLM committed Oct 24, 2023
1 parent 04cba60 commit 72aea61
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 29 deletions.
1 change: 1 addition & 0 deletions coinlib/lib/src/coinlib_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ 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/taproot_script_input.dart';
export 'package:coinlib/src/tx/inputs/witness_input.dart';

export 'package:coinlib/src/tx/sighash/legacy_signature_hasher.dart';
Expand Down
3 changes: 2 additions & 1 deletion coinlib/lib/src/taproot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,11 @@ class TapBranch implements TapNode {
class TapLeaf with Writable implements TapNode {

static final leafHash = getTaggedHasher("TapLeaf");
static const int tapscriptVersion = 0xc0;

/// The Tapscript version is fixed as 0xc0 as this is the only implemented and
/// enforced version
final int version = 0xc0;
final int version = tapscriptVersion;
final Script script;

TapLeaf(this.script);
Expand Down
90 changes: 90 additions & 0 deletions coinlib/lib/src/tx/inputs/taproot_script_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'dart:typed_data';
import 'package:coinlib/src/common/serial.dart';
import 'package:coinlib/src/scripts/operations.dart';
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 'input.dart';
import 'raw_input.dart';

/// A [TaprootInput] which spends using the script-path for 0xc0 version
/// Tapscripts. There is no signing logic and sign() is not implemented.
/// Subclasses should handle signing. [createInputSignature] can be used to
/// create signatures for insertion as necessary.
class TaprootScriptInput extends TaprootInput {

/// The tapscript embedded in the witness data, not to be confused with the
/// empty [script].
final Script tapscript;

TaprootScriptInput({
required OutPoint prevOut,
required Uint8List controlBlock,
required this.tapscript,
List<Uint8List>? stack,
int sequence = Input.sequenceFinal,
}) : super(
prevOut: prevOut,
sequence: sequence,
witness: [if (stack != null) ...stack, tapscript.compiled, controlBlock],
);

TaprootScriptInput.fromTaprootLeaf({
required OutPoint prevOut,
required Taproot taproot,
required TapLeaf leaf,
List<Uint8List>? stack,
int sequence = Input.sequenceFinal,
}) : this(
prevOut: prevOut,
controlBlock: taproot.controlBlockForLeaf(leaf),
tapscript: leaf.script,
stack: stack,
sequence: sequence,
);

/// Checks if the [raw] input and [witness] data match the expected format for
/// a [TaprootScriptInput] with the control block and script. If it matches
/// this returns a [TaprootScriptInput] for the input or else it returns null.
/// The script must be valid with minimal push data. The control block must be
/// the correct size and contain the correct 0xc0 tapscript version but the
/// internal key and parity bit is not validated.
static TaprootScriptInput? match(RawInput raw, List<Uint8List> witness) {

if (raw.scriptSig.isNotEmpty) return null;
if (witness.length < 2) return null;

final controlBlock = witness.last;
final lengthAfterKey = controlBlock.length - 33;

if (
controlBlock.length < 33
|| lengthAfterKey % 32 != 0
|| lengthAfterKey / 32 > 128
|| controlBlock[0] & 0xfe != TapLeaf.tapscriptVersion
) {
return null;
}

try {

return TaprootScriptInput(
prevOut: raw.prevOut,
controlBlock: controlBlock,
tapscript: Script.decompile(witness[witness.length-2]),
stack: witness.sublist(0, witness.length-2),
sequence: raw.sequence,
);

} on OutOfData {
return null;
} on PushDataNotMinimal {
return null;
}

}

Uint8List get controlBlock => witness.last;

}
4 changes: 3 additions & 1 deletion coinlib/lib/src/tx/inputs/witness_input.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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';
import 'p2wpkh_input.dart';
import 'taproot_key_input.dart';
import 'taproot_script_input.dart';

/// The base-class for all witness inputs
class WitnessInput extends RawInput {
Expand All @@ -29,6 +30,7 @@ class WitnessInput extends RawInput {
// Is a witness input, so match with the specific input type
P2WPKHInput.match(raw, witness)
?? TaprootKeyInput.match(raw, witness)
?? TaprootScriptInput.match(raw, witness)
?? WitnessInput(
prevOut: raw.prevOut,
witness: witness,
Expand Down
13 changes: 3 additions & 10 deletions coinlib/test/tx/inputs/p2wpkh_input_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,6 @@ void main() {

test("valid p2wpkh inputs inc. addSignature", () {

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

expectP2WPKHInput(P2WPKHInput input, bool hasSig) {

expectInput(input);
Expand All @@ -56,8 +49,8 @@ void main() {
}

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

}

Expand All @@ -80,7 +73,7 @@ void main() {

expectMatched(bool hasSig) {
final matched = Input.match(
RawInput.fromReader(BytesReader(rawBytes)),
RawInput.fromReader(BytesReader(rawWitnessInputBytes)),
getWitness(hasSig),
);
expect(matched, isA<P2WPKHInput>());
Expand Down
13 changes: 3 additions & 10 deletions coinlib/test/tx/inputs/taproot_key_input_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,6 @@ void main() {

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);
Expand All @@ -44,8 +37,8 @@ void main() {
}

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

}

Expand All @@ -64,7 +57,7 @@ void main() {
// 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)),
RawInput.fromReader(BytesReader(rawWitnessInputBytes)),
getWitness(true),
);
expect(matched, isA<TaprootKeyInput>());
Expand Down
133 changes: 133 additions & 0 deletions coinlib/test/tx/inputs/taproot_script_input_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import 'dart:typed_data';
import 'package:coinlib/coinlib.dart';
import 'package:test/test.dart';
import '../../vectors/inputs.dart';
import '../../vectors/taproot.dart';

// Placed in global for lazy initialisation after loadCoinlib
final taprootVec = taprootVectors[3];
final controlBlockHex = taprootVec.controlBlocks[0];
final controlBlock = hexToBytes(controlBlockHex);
final script = taprootVec.object.leaves[0].script;
final witness = [script.compiled, controlBlock];
final stack = [hexToBytes("0102030405"), hexToBytes("01020304")];

void main() {

group("TaprootScriptInput", () {

setUpAll(loadCoinlib);

test("valid script-path taproot inputs", () {

expectTaprootScriptInput(TaprootScriptInput input, bool withStack) {

expectInput(input);

expect(input.complete, true);
expect(input.scriptSig.isEmpty, true);
expect(input.script!.length, 0);

expect(input.tapscript.asm, script.asm);
expect(bytesToHex(input.controlBlock), controlBlockHex);

expect(input.witness, [if (withStack) ...stack, ...witness]);
expect(input.size, rawWitnessInputBytes.length);
expect(input.toBytes(), rawWitnessInputBytes);

}

for (final withStack in [false, true]) {
expectTaprootScriptInput(
TaprootScriptInput(
prevOut: prevOut,
controlBlock: controlBlock,
tapscript: script,
sequence: sequence,
stack: withStack ? stack : null,
),
withStack,
);
expectTaprootScriptInput(
TaprootScriptInput.fromTaprootLeaf(
prevOut: prevOut,
taproot: taprootVec.object,
leaf: taprootVec.object.leaves[0],
sequence: sequence,
stack: withStack ? stack : null,
),
withStack,
);
expectTaprootScriptInput(
Input.match(
RawInput.fromReader(BytesReader(rawWitnessInputBytes)),
[if (withStack) ...stack, ...witness],
) as TaprootScriptInput,
withStack,
);
}

});

test("control blocks up-to 128 hashes accepted", () {
expect(
TaprootScriptInput(
prevOut: prevOut,
controlBlock: Uint8List.fromList(
[...controlBlock.take(33), ...Uint8List(32*128)],
),
tapscript: script,
),
isA<TaprootScriptInput>(),
);
});

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

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

expectNoMatch("0", witness);
// Requires script
expectNoMatch("", witness.skip(1).toList());
// Not allowing annex
expectNoMatch("", [...witness, hexToBytes("5001020304")]);
// Control block must be correct size
expectNoMatch("", [script.compiled, hexToBytes("c1")]);
expectNoMatch("", [script.compiled, Uint8List(0)]);
expectNoMatch(
"",
[script.compiled, controlBlock.sublist(0, controlBlock.length-1)],
);
expectNoMatch(
"",
[
script.compiled,
Uint8List.fromList([...controlBlock.take(33), ...Uint8List(32*129)]),
],
);
// Control block must have valid tapscript version.
expectNoMatch(
"",
[
script.compiled,
Uint8List.fromList([0xc2, ...controlBlock.sublist(1)]),
],
);
// Script must be valid and minimal
expectNoMatch("", [hexToBytes("0201"), controlBlock]);
expectNoMatch("", [hexToBytes("0101"), controlBlock]);
});

});

}
9 changes: 2 additions & 7 deletions coinlib/test/tx/inputs/witness_input_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:coinlib/coinlib.dart';
import 'package:test/test.dart';
import '../../vectors/inputs.dart';

void main() {

Expand All @@ -10,13 +11,7 @@ void main() {
final prevOutN = 0xfeedbeef;
final sequence = 0xbeeffeed;

final rawBytes = Uint8List.fromList([
...prevOutHash,
0xef, 0xbe, 0xed, 0xfe,
0,
0xed, 0xfe, 0xef, 0xbe,
]);
final raw = RawInput.fromReader(BytesReader(rawBytes));
final raw = RawInput.fromReader(BytesReader(rawWitnessInputBytes));
final witness = [Uint8List.fromList([0, 1, 0xff])];

test("matches witness inputs", () {
Expand Down
7 changes: 7 additions & 0 deletions coinlib/test/vectors/inputs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ final prevOutN = 0xfeedbeef;
final prevOut = OutPoint(prevOutHash, prevOutN);
final sequence = 0xbeeffeed;

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

expectInput(Input input) {
expect(input.prevOut.hash, prevOutHash);
expect(input.prevOut.n, prevOutN);
Expand Down

0 comments on commit 72aea61

Please sign in to comment.