Skip to content

Multisig

A bare multisig (P2MS) Output lists n Public Keys in its Locking Script, and needs Signatures from m of them to spend: OP_m <pubkey 1> ... <pubkey n> OP_n OP_CHECKMULTISIG. On BSV this is the usual way to do multisig, since the hash-wrapped P2SH form is legacy. The standard script templates page in the Developer Guide covers how OP_CHECKMULTISIG evaluates.

The examples use three keys, belonging to Alice, Bob and Carol, plus the setup from the Transaction builder.

List<PublicKey> keys = List.of(alice.getPublicKey(), bob.getPublicKey(), carol.getPublicKey());
// 2-of-3. The keys are sorted by their hex encoding unless you pass sortKeys = false.
P2MSLockBuilder locker = new P2MSLockBuilder(keys, 2);

Sorting makes the script independent of the order in which the parties exchanged keys, so that everyone builds the same script. The builder sorts a copy, so the list we pass, mutable or not, is left as it was. We pass false as the third argument to keep our own order.

To find out what an existing multisig Output requires, we parse it.

P2MSLockBuilder parsed = new P2MSLockBuilder(locker.getLockingScript());
int required = parsed.getRequiredSigs();
List<PublicKey> orderInScript = parsed.getPublicKeys();

The Unlocking Script is OP_0 followed by m Signatures, in the same order as their Public Keys appear in the Locking Script. OP_CHECKMULTISIG pops one item more than it uses, which is what the OP_0 is for.

P2MSUnlockBuilder writes that Unlocking Script for us. The builder attaches at most one signer per Input, so we build without signers, and sign once per key.

P2MSLockBuilder locker = new P2MSLockBuilder(
List.of(alice.getPublicKey(), bob.getPublicKey(), carol.getPublicKey()), 2);
Transaction fundingTx = Fixtures.fundingTransaction(locker, 100_000);
TransactionOutput utxo = fundingTx.getOutputs().get(0);
P2MSUnlockBuilder unlocker = new P2MSUnlockBuilder();
Transaction tx = new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, unlocker)
.spendTo(new P2PKHLockBuilder(recipient), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);
// We sign in the order the keys appear in the script. Sorted, that is Bob, then Alice, then Carol.
new TransactionSigner(SIGHASH, bob).sign(tx, utxo, 0);
new TransactionSigner(SIGHASH, alice).sign(tx, utxo, 0);

Each sign() call appends a Signature to the unlocker. The signers can be on different machines. We pass the unsigned Transaction around (serialised, see parsing and serialising) and collect the Signatures, as long as they end up in the unlocker in key order. Out of order, the spend fails.

// The same two Signatures in the other order fail.
P2MSUnlockBuilder swapped = new P2MSUnlockBuilder();
swapped.addSignature(unlocker.getSignatures().get(1));
swapped.addSignature(unlocker.getSignatures().get(0));
assertThrows(ScriptException.class, () -> new org.twostack.bitcoin4j.script.Interpreter().correctlySpends(
swapped.getUnlockingScript(), utxo.getScript(), tx, 0, Fixtures.FLAGS,
org.twostack.bitcoin4j.Coin.valueOf(100_000)));

Do note that the builder sizes a multisig Input from its Locking Script, allowing for m Signatures, when it knows that script. spendFromOutput() without a signer doesn’t pass it in, so for such an Input the builder assumes a P2PKH-sized Unlocking Script, and a larger m needs a fixed fee.