Skip to content

Commit

Permalink
Complete APO single-sig signing
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewLM committed Feb 24, 2025
1 parent b5cd99e commit 6cb8598
Show file tree
Hide file tree
Showing 25 changed files with 815 additions and 90 deletions.
2 changes: 1 addition & 1 deletion coinlib/lib/src/address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:coinlib/src/scripts/programs/p2witness.dart';
import 'package:coinlib/src/scripts/programs/p2wpkh.dart';
import 'package:coinlib/src/scripts/programs/p2wsh.dart';
import 'package:coinlib/src/scripts/script.dart';
import 'package:coinlib/src/taproot.dart';
import 'package:coinlib/src/taproot/taproot.dart';

class InvalidAddress implements Exception {}
class InvalidAddressNetwork implements Exception {}
Expand Down
5 changes: 4 additions & 1 deletion coinlib/lib/src/coinlib_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export 'package:coinlib/src/scripts/programs/p2witness.dart';
export 'package:coinlib/src/scripts/programs/p2wpkh.dart';
export 'package:coinlib/src/scripts/programs/p2wsh.dart';

export 'package:coinlib/src/taproot/leaves.dart';
export 'package:coinlib/src/taproot/taproot.dart';

export 'package:coinlib/src/tx/coin_selection.dart';
export 'package:coinlib/src/tx/transaction.dart';
export 'package:coinlib/src/tx/sign_details.dart';
Expand All @@ -51,6 +54,7 @@ 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/taproot_single_script_sig_input.dart';
export 'package:coinlib/src/tx/inputs/witness_input.dart';

export 'package:coinlib/src/tx/sighash/legacy_signature_hasher.dart';
Expand All @@ -61,6 +65,5 @@ export 'package:coinlib/src/tx/sighash/witness_signature_hasher.dart';
export 'package:coinlib/src/address.dart';
export 'package:coinlib/src/coin_unit.dart';
export 'package:coinlib/src/network.dart';
export 'package:coinlib/src/taproot.dart';

Future<void> loadCoinlib() => secp256k1.load();
4 changes: 4 additions & 0 deletions coinlib/lib/src/scripts/operations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ abstract class ScriptOp {
/// Represents a [ScriptOp] that is an op code
class ScriptOpCode implements ScriptOp {

static final checksig = ScriptOpCode.fromName("CHECKSIG");
static final checkmultisig = ScriptOpCode.fromName("CHECKMULTISIG");
static final number1 = ScriptOpCode(ScriptOp.op1);

final int code;
ScriptOpCode(this.code);

Expand Down
7 changes: 4 additions & 3 deletions coinlib/lib/src/scripts/programs/multisig.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:collection/collection.dart';
class MultisigProgram implements Program {

static const maxPubkeys = 20;
static final checkmultisig = ScriptOpCode.fromName("CHECKMULTISIG");

@override
final Script script;
Expand All @@ -25,7 +24,7 @@ class MultisigProgram implements Program {
ScriptOp.fromNumber(threshold),
...pubkeys.map((pk) => ScriptPushData(pk.data)),
ScriptOp.fromNumber(pubkeys.length),
checkmultisig,
ScriptOpCode.checkmultisig,
]) {

if (pubkeys.isEmpty || pubkeys.length > maxPubkeys) {
Expand Down Expand Up @@ -60,7 +59,9 @@ class MultisigProgram implements Program {
throw NoProgramMatch();
}

if (!script.ops.last.match(checkmultisig)) throw NoProgramMatch();
if (!script.ops.last.match(ScriptOpCode.checkmultisig)) {
throw NoProgramMatch();
}

final pknum = script[script.length-2].number;
if (
Expand Down
2 changes: 1 addition & 1 deletion coinlib/lib/src/scripts/programs/p2tr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:coinlib/src/crypto/ec_public_key.dart';
import 'package:coinlib/src/scripts/program.dart';
import 'package:coinlib/src/scripts/programs/p2witness.dart';
import 'package:coinlib/src/scripts/script.dart';
import 'package:coinlib/src/taproot.dart';
import 'package:coinlib/src/taproot/taproot.dart';

/// Pay-to-Taproot program taking a 32-byte Taproot tweaked key.
class P2TR extends P2Witness {
Expand Down
64 changes: 64 additions & 0 deletions coinlib/lib/src/taproot/leaves.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'dart:typed_data';
import 'package:coinlib/src/crypto/ec_public_key.dart';
import 'package:coinlib/src/scripts/script.dart';
import 'package:coinlib/src/scripts/operations.dart';
import 'taproot.dart';

/// A TapLeaf containing a single CHECKSIG operation for a key
class TapLeafChecksig extends TapLeaf {

/// The 1-byte APO key, referring to the internal Taproot key that must be
/// be tweaked prior to signing
static final apoInternal = TapLeafChecksig._(ScriptOpCode.number1);

TapLeafChecksig._(ScriptOp keyPush) : super(
Script([keyPush, ScriptOpCode.checksig]),
);

/// Regular key
TapLeafChecksig(ECPublicKey key) : this._(ScriptPushData(key.x));

/// A specified APO key
TapLeafChecksig.apo(
ECPublicKey key,
) : this._(
ScriptPushData(Uint8List.fromList([1, ...key.x])),
);

static TapLeafChecksig? match(Script script) {

final ops = script.ops;
if (ops.length != 2 || !ops.last.match(ScriptOpCode.checksig)) return null;

final first = ops.first;
if (first.match(ScriptOpCode.number1)) return apoInternal;
if (first is! ScriptPushData) return null;

final data = first.data;
final pubkeyData = switch(data.length) {
32 => data,
33 => data.first != 1 ? null : data.sublist(1),
_ => null,
};
if (pubkeyData == null) return null;

try {
ECPublicKey.fromXOnly(pubkeyData);
} on InvalidPublicKey {
return null;
}

return TapLeafChecksig._(first);

}

bool get isApo {
final push = script.ops.first;
return push.match(ScriptOpCode.number1) || (
push is ScriptPushData
&& push.data.length == 33
&& push.data.first == 1
);
}

}
File renamed without changes.
2 changes: 1 addition & 1 deletion coinlib/lib/src/tx/inputs/taproot_key_input.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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/taproot/taproot.dart';
import 'package:coinlib/src/tx/inputs/taproot_input.dart';
import 'package:coinlib/src/tx/sign_details.dart';
import 'package:coinlib/src/tx/transaction.dart';
Expand Down
2 changes: 1 addition & 1 deletion coinlib/lib/src/tx/inputs/taproot_script_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:coinlib/src/common/serial.dart';
import 'package:coinlib/src/crypto/ec_private_key.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/taproot/taproot.dart';
import 'package:coinlib/src/tx/inputs/taproot_input.dart';
import 'package:coinlib/src/tx/outpoint.dart';
import 'package:coinlib/src/tx/sighash/sighash_type.dart';
Expand Down
172 changes: 172 additions & 0 deletions coinlib/lib/src/tx/inputs/taproot_single_script_sig_input.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import 'dart:typed_data';
import 'package:coinlib/src/crypto/ec_private_key.dart';
import 'package:coinlib/src/taproot/leaves.dart';
import 'package:coinlib/src/taproot/taproot.dart';
import 'package:coinlib/src/tx/outpoint.dart';
import 'package:coinlib/src/tx/sign_details.dart';
import 'package:coinlib/src/tx/transaction.dart';
import 'input.dart';
import 'input_signature.dart';
import 'raw_input.dart';
import 'taproot_input.dart';
import 'taproot_script_input.dart';

/// An input that provides a single signature to satisfy a tapscript [leaf].
class TaprootSingleScriptSigInput extends TaprootInput {

final TapLeafChecksig? leaf;
final SchnorrInputSignature? insig;

TaprootSingleScriptSigInput._({
OutPoint? prevOut,
this.leaf,
Uint8List? controlBlock,
this.insig,
required super.sequence,
}) : super(
prevOut: prevOut ?? OutPoint.nothing,
witness: [
if (insig != null) insig.bytes,
if (leaf != null) leaf.script.compiled,
if (controlBlock != null) controlBlock,
],
);

/// Constructs an input with all the information for signing with any sighash
/// type.
TaprootSingleScriptSigInput({
required OutPoint prevOut,
required Taproot taproot,
required TapLeafChecksig leaf,
SchnorrInputSignature? insig,
int sequence = Input.sequenceFinal,
}) : this._(
prevOut: prevOut,
controlBlock: taproot.controlBlockForLeaf(leaf),
leaf: leaf,
insig: insig,
sequence: sequence,
);

/// Create an APO input with no information that can only be signed with
/// ANYPREVOUTANYSCRIPT.
TaprootSingleScriptSigInput.anyPrevOutAnyScript({
SchnorrInputSignature? insig,
int sequence = Input.sequenceFinal,
}) : this._(insig: insig, sequence: sequence);

/// Create an APO input specifying a [Taproot] and [TapLeaf] that can be
/// signed using ANYPREVOUT or ANYPREVOUTANYSCRIPT. ANYPREVOUTANYSCRIPT may
/// also ommit the taproot information using [anyPrevOutAnyScript()].
TaprootSingleScriptSigInput.anyPrevOut({
required Taproot taproot,
required TapLeafChecksig leaf,
SchnorrInputSignature? insig,
int sequence = Input.sequenceFinal,
}) : this._(
leaf: leaf,
controlBlock: taproot.controlBlockForLeaf(leaf),
insig: insig,
sequence: sequence,
);

/// Matches a [RawInput] as a [TaprootSingleScriptSigInput] if it contains the
/// control block and [TapLeafChecksig] leaf script.
static TaprootSingleScriptSigInput? match(
RawInput raw, List<Uint8List> witness,
) {

// Only match up-to 3 witness items including signature
if (witness.length > 3) return null;

// Try to match as generic script input
final scriptIn = TaprootScriptInput.match(raw, witness);
if (scriptIn == null) return null;

// Check if the script is a match
final leaf = TapLeafChecksig.match(scriptIn.tapscript);
if (leaf == null) return null;

try {
return TaprootSingleScriptSigInput._(
prevOut: raw.prevOut,
leaf: leaf,
controlBlock: scriptIn.controlBlock,
insig: witness.length == 2
? null
: SchnorrInputSignature.fromBytes(witness[0]),
sequence: raw.sequence,
);
} on InvalidInputSignature {
return null;
}

}

/// Add the [Taproot] and [TapLeaf] required to complete the input for
/// ANYPREVOUTANYSCRIPT. The [prevOut] may optionally be added.
///
/// The signature is not invalidated for ANYPREVOUTANYSCRIPT.
TaprootSingleScriptSigInput addTaproot({
required Taproot taproot,
required TapLeafChecksig leaf,
OutPoint? prevOut,
}) => TaprootSingleScriptSigInput._(
prevOut: prevOut ?? this.prevOut,
leaf: leaf,
controlBlock: taproot.controlBlockForLeaf(leaf),
insig: (insig != null && insig!.hashType.anyPrevOutAnyScript)
? insig : null,
sequence: sequence,
);

/// Complete the input by adding (or replacing) the [OutPoint].
///
/// A signature is not invalidated if ANYPREVOUT or ANYPREVOUTANYSCRIPT is
/// used.
TaprootSingleScriptSigInput addPrevOut(
OutPoint prevOut,
) => TaprootSingleScriptSigInput._(
prevOut: prevOut,
leaf: leaf,
controlBlock: leaf == null ? null : witness.last,
insig: (insig != null && insig!.hashType.requiresApo) ? insig : null,
sequence: sequence,
);

/// Add a preprepared input signature.
TaprootSingleScriptSigInput addSignature(
SchnorrInputSignature insig,
) => TaprootSingleScriptSigInput._(
prevOut: prevOut,
leaf: leaf,
controlBlock: leaf == null ? null : witness.last,
insig: insig,
sequence: sequence,
);

/// Sign the input for the tapscript key.
TaprootSingleScriptSigInput sign({
required TaprootScriptSignDetails details,
required ECPrivateKey key,
}) {

if (leaf != null && !leaf!.isApo && details.hashType.requiresApo) {
throw CannotSignInput(
"Cannot sign with ${details.hashType} for non-APO key",
);
}

return addSignature(
createInputSignature(
key: key,
details: leaf == null ? details : details.addLeafHash(leaf!.hash),
),
);

}

@override
bool get complete => witness.length == 3 && !prevOut.isNothing;

}
4 changes: 4 additions & 0 deletions coinlib/lib/src/tx/outpoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import 'output.dart';
/// Reference to an [Output] by transaction hash and index
class OutPoint with Writable {

/// Specify no previous output
static final nothing = OutPoint(Uint8List(32), 0);

final Uint8List _hash;
final int n;

Expand All @@ -32,6 +35,7 @@ class OutPoint with Writable {
Uint8List get hash => Uint8List.fromList(_hash);
/// True if this out point is the type found in a coinbase
bool get coinbase => _hash.every((e) => e == 0) && n == 0xffffffff;
bool get isNothing => _hash.every((e) => e == 0) && n == 0;

@override
bool operator ==(Object other)
Expand Down
14 changes: 9 additions & 5 deletions coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ final class TaprootSignatureHasher extends SignatureHasher with Writable {
prevOutHashes = details.hashType.allInputs
? PrevOutSignatureHashes(details.prevOuts)
: null {
if (details is TaprootScriptSignDetails) {
if (
details.isScript
&& details.leafHash == null
&& !hashType.anyPrevOutAnyScript
) {
throw CannotSignInput("Missing leaf hash for tapscript sign details");
}
}
Expand All @@ -31,7 +35,7 @@ final class TaprootSignatureHasher extends SignatureHasher with Writable {
void write(Writer writer) {

final leafHash = details.leafHash;
final extFlag = leafHash == null ? 0 : 1;
final extFlag = details.isScript ? 1 : 0;

writer.writeUInt8(0); // "Epoch"
writer.writeUInt8(hashType.value);
Expand Down Expand Up @@ -80,10 +84,10 @@ final class TaprootSignatureHasher extends SignatureHasher with Writable {
);
}

// Data specific to the script
if (leafHash != null) {
// Data specific to the tapscript
if (details.isScript) {
if (!hashType.anyPrevOutAnyScript) {
writer.writeSlice(leafHash);
writer.writeSlice(leafHash!);
}
final keyVersion = hashType.requiresApo ? 1 : 0;
writer.writeUInt8(keyVersion);
Expand Down
Loading

0 comments on commit 6cb8598

Please sign in to comment.