Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

Commit

Permalink
Merge pull request #48 from Vesta-wallet/p2sh-multisig
Browse files Browse the repository at this point in the history
P2SH Multisig Input Signing and Output Address Generation
  • Loading branch information
willyfromtheblock authored Dec 19, 2022
2 parents aad2645 + 1066137 commit 1db66b7
Show file tree
Hide file tree
Showing 13 changed files with 290 additions and 92 deletions.
2 changes: 1 addition & 1 deletion lib/src/address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class Address {
}

if (prefix == network.scriptHash) {
return createP2shOutputScript(data);
return P2SH.fromScriptHash(data).outputScript;
}

throw ArgumentError('Invalid version or Network mismatch');
Expand Down
2 changes: 2 additions & 0 deletions lib/src/classify.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:typed_data';
import '../src/utils/script.dart' as bscript;
import 'templates/pubkeyhash.dart' as pubkeyhash;
import 'templates/pay_to_script_hash.dart' as pay_to_script_hash;
import 'templates/pubkey.dart' as pubkey;
import 'templates/witnesspubkeyhash.dart' as witness_pubkey_hash;
import 'templates/witness_script_hash.dart' as witness_script_hash;
Expand Down Expand Up @@ -29,6 +30,7 @@ String? classifyInput(Uint8List script) {
final chunks = bscript.decompile(script);
if (chunks == null) throw ArgumentError('Invalid script');
if (pubkeyhash.inputCheck(chunks)) return scriptTypes['P2PKH'];
if (pay_to_script_hash.inputCheck(chunks)) return scriptTypes['P2SH'];
if (pubkey.inputCheck(chunks)) return scriptTypes['P2PK'];
return scriptTypes['NONSTANDARD'];
}
Expand Down
32 changes: 26 additions & 6 deletions lib/src/payments/p2sh.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import 'dart:typed_data';
import '../utils/script.dart' as bscript;
import '../utils/constants/op.dart';
import "../crypto.dart" show hash160;
import './multisig.dart';
import '../models/networks.dart';
import 'package:bs58check/bs58check.dart' as bs58check;

/// Takes the hash160 ([scriptHash]) of a P2SH redeemScript and returns the
/// output script (scriptPubKey)
Uint8List createP2shOutputScript(Uint8List scriptHash) {
if (scriptHash.length != 20) {
throw ArgumentError('Invalid script hash length');
class P2SH {
Uint8List scriptHash;

P2SH.fromScriptHash(Uint8List hash) : scriptHash = hash {
if (scriptHash.length != 20) {
throw ArgumentError('Invalid P2SH script hash length');
}
}

return bscript.compile([ops['OP_HASH160'], scriptHash, ops['OP_EQUAL']]);
P2SH.fromScriptBytes(Uint8List bytes) : this.fromScriptHash(hash160(bytes));
P2SH.fromMultisig(MultisigScript script)
: this.fromScriptBytes(script.scriptBytes);

/// Returns the outputScript (scriptPubKey)
Uint8List get outputScript =>
bscript.compile([ops["OP_HASH160"], scriptHash, ops["OP_EQUAL"]]);

/// Returns the base58 address for a given network
String address(NetworkType network) {
final payload = Uint8List(21);
payload.buffer.asByteData().setUint8(0, network.scriptHash);
payload.setRange(1, payload.length, scriptHash);
return bs58check.encode(payload);
}
}
2 changes: 1 addition & 1 deletion lib/src/payments/p2wsh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class P2WSH {

P2WSH.fromScriptHash(Uint8List hash) : scriptHash = hash {
if (scriptHash.length != 32) {
throw ArgumentError('Invalid script hash length');
throw ArgumentError('Invalid P2WSH script hash length');
}
}
P2WSH.fromScriptBytes(Uint8List bytes)
Expand Down
40 changes: 40 additions & 0 deletions lib/src/templates/pay_to_script_hash.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:typed_data';
import '../utils/script.dart' as bscript;
import '../utils/constants/op.dart';
import 'package:coinslib/src/payments/multisig.dart';

// This is similar to the inputCheck for P2WSH and a candidate for abstraction.
/// Only allows multisig P2SH at the moment
bool inputCheck(List<dynamic> chunks) {
if (chunks.length < 2) return false;

// Check that the first item is 0 which is necessary for CHECKMULTISIG
if (chunks[0] != 0) return false;

// Last push needs to be the redeemScript
if (chunks.last is! Uint8List) return false;

// Check redeemScript is multisig
try {
final multisig = MultisigScript.fromScriptBytes(chunks.last);
// Can only have upto threshold sigs plus OP_0 and redeemScript
if (chunks.length > 2 + multisig.threshold) return false;
} on ArgumentError {
return false;
}

// Check signatures
for (final sig in chunks.getRange(1, chunks.length - 1)) {
if (!bscript.isCanonicalScriptSignature(sig)) return false;
}

return true;
}

bool outputCheck(Uint8List script) {
final buffer = bscript.compile(script);
return buffer.length == 23 &&
buffer[0] == ops['OP_HASH160'] &&
buffer[1] == 0x14 &&
buffer[22] == ops['OP_EQUAL'];
}
2 changes: 1 addition & 1 deletion lib/src/templates/witness_script_hash.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../utils/script.dart' as bscript;
/// multisig. It checks if there are 0 to threshold signatures and therefore allows
/// incomplete signatures
bool inputCheck(List<Uint8List> witness) {
if (witness.isEmpty) return false;
if (witness.length < 2) return false;

// Check that the first argument is an empty array (BIP 147)
if (witness.first.isNotEmpty) return false;
Expand Down
27 changes: 21 additions & 6 deletions lib/src/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,12 @@ class Input {
type = ssType ?? wsType;
}

List<InputSignature> decodeSigs(List<dynamic> encodedSigs) {
return encodedSigs
.map((encoded) => InputSignature.decode(encoded))
.toList();
}

if (type == scriptTypes['P2WPKH']) {
P2WPKH p2wpkh = P2WPKH(data: PaymentData(witness: witness));
return Input(
Expand All @@ -498,20 +504,29 @@ class Input {
} else if (type == scriptTypes['P2WSH']) {
// Having witness data handled in a class would be nicer, but I'm
// sticking reasonably close to the library interface as-is
final signatures = witness
.sublist(1, witness.length - 1)
.map((encoded) => InputSignature.decode(encoded))
.toList();
final signatures = decodeSigs(witness.sublist(1, witness.length - 1));
final multisig = MultisigScript.fromScriptBytes(witness.last);
final threshold = multisig.threshold;

return Input(
prevOutType: type,
pubkeys: multisig.pubkeys,
signatures: signatures,
threshold: threshold,
threshold: multisig.threshold,
witness: witness,
);
} else if (type == scriptTypes['P2SH']) {
final scriptChunks = bscript.decompile(scriptSig)!;
final signatures = decodeSigs(
scriptChunks.sublist(1, scriptChunks.length - 1),
);
final multisig = MultisigScript.fromScriptBytes(scriptChunks.last);

return Input(
prevOutType: type,
pubkeys: multisig.pubkeys,
signatures: signatures,
threshold: multisig.threshold,
);
} else if (type == scriptTypes['P2PKH']) {
P2PKH p2pkh = P2PKH(data: PaymentData(input: scriptSig));
return Input(
Expand Down
48 changes: 33 additions & 15 deletions lib/src/transaction_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class TransactionBuilder {
required ECPair keyPair,
BigInt? witnessValue,
Uint8List? witnessScript,
Uint8List? redeemScript,
int? hashType,
}) {
hashType ??= sigHashAll;
Expand Down Expand Up @@ -198,15 +199,24 @@ class TransactionBuilder {
// Extract public keys from the witnessScript
input.pubkeys = multisig.pubkeys;
input.threshold = multisig.threshold;
} else if (redeemScript != null) {
// P2SH input when redeemScript is provided

final multisig = MultisigScript.fromScriptBytes(redeemScript);

input.prevOutType = scriptTypes['P2SH'];
input.signScript = redeemScript;
input.pubkeys = multisig.pubkeys;
input.threshold = multisig.threshold;
} else if (input.prevOutScript != null &&
classifyOutput(input.prevOutScript!) == scriptTypes['P2WPKH']) {
input.prevOutType = scriptTypes['P2WPKH'];
input.witness = [];
input.pubkeys = [ourPubKey];
input.signScript =
P2PKH(data: PaymentData(pubkey: ourPubKey), network: network)
.data
.output;
input.signScript = P2PKH(
data: PaymentData(pubkey: ourPubKey),
network: network,
).data.output;
} else {
Uint8List prevOutScript = pubkeyToOutputScript(ourPubKey);
input.prevOutType = scriptTypes['P2PKH'];
Expand Down Expand Up @@ -243,17 +253,16 @@ class TransactionBuilder {
return _build(true);
}

Iterable<InputSignature> _orderSigsForPubkeys({
Iterable<Uint8List> _orderedEncodedSigs({
required int inIndex,
required Input input,
required Iterable<InputSignature> signatures,
required List<Uint8List> pubkeys,
}) {
// Ensure signatures are matched to public keys in the correct order

final pubkeys = input.pubkeys!;
List<InputSignature?> positionedSigs = List.filled(pubkeys.length, null);

for (final sig in signatures) {
for (final sig in input.signatures) {
var matched = false;
for (var i = 0; i < pubkeys.length; i++) {
// Check if the signature matches the public key
Expand All @@ -276,7 +285,9 @@ class TransactionBuilder {
}

// Remove nulls
return positionedSigs.whereType<InputSignature>();
return positionedSigs.whereType<InputSignature>().map(
(sig) => sig.encode(),
);
}

Transaction _build(bool allowIncomplete) {
Expand Down Expand Up @@ -312,17 +323,24 @@ class TransactionBuilder {
input.witness = [
Uint8List.fromList([]),
// Ensure signatures are in the correct order for multisig
..._orderSigsForPubkeys(
inIndex: i,
input: input,
signatures: input.signatures,
pubkeys: input.pubkeys!,
).map((sig) => sig.encode()),
..._orderedEncodedSigs(inIndex: i, input: input),
input.signScript!
];
}

tx.setWitness(i, input.witness);
} else if (input.prevOutType == scriptTypes['P2SH']) {
// Build P2SH input script even if incomplete

if (input.hasNewSignatures) {
final script = bscript.compile([
0,
..._orderedEncodedSigs(inIndex: i, input: input),
input.signScript!
]);

tx.setInputScript(i, script);
}
} else if (input.isComplete()) {
// Build the following types of input only when complete

Expand Down
27 changes: 18 additions & 9 deletions test/integration/addresses_test.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'dart:typed_data';

import 'package:coinslib/src/models/networks.dart' as networks;
import 'package:coinslib/src/ecpair.dart' show ECPair;
import 'package:coinslib/src/payments/index.dart' show PaymentData;
import 'package:coinslib/src/payments/p2pkh.dart' show P2PKH;
import 'package:coinslib/src/payments/p2sh.dart';
import 'package:coinslib/src/payments/p2wpkh.dart' show P2WPKH;
import 'package:coinslib/src/payments/p2wsh.dart' show P2WSH;
import 'package:coinslib/src/bip32_base.dart' show Bip32Type;
Expand Down Expand Up @@ -94,19 +94,28 @@ main() {
expect(address, 'tb1qgmp0h7lvexdxx9y05pmdukx09xcteu9sx2h4ya');
});

final multisig = MultisigScript(
pubkeys: [aliceKey, bobKey, carolKey, davidKey]
.map((key) => key.publicKey!)
.toList(),
threshold: 3,
);

test('can generate multisig P2WSH address', () {
final p2wsh = P2WSH.fromMultisig(
MultisigScript(
pubkeys: [aliceKey, bobKey, carolKey, davidKey]
.map((key) => key.publicKey!)
.toList(),
threshold: 3,
),
);
final p2wsh = P2WSH.fromMultisig(multisig);

expect(
p2wsh.address(networks.peercoin),
"pc1qk7z8s30kzdn9zwuxxrdmga3txymeljpsc42cdm7khww9xqa8w2gq4js5tx",
);
});

test('can generate a P2SH address', () {
final p2sh = P2SH.fromMultisig(multisig);

expect(
p2sh.address(networks.bitcoin),
"32QQmWZAbqBr837PE5dir6EgXcxFByojx1",
);
});
}
Loading

0 comments on commit 1db66b7

Please sign in to comment.