Skip to content

P2SH (legacy)

P2SH (Pay-to-Script-Hash) locks coins to the hash of a script, the redeem script, which the spender reveals along with whatever that script needs. Bitcoin SV removed P2SH in the Genesis upgrade (February 2020). Nodes no longer accept new P2SH Outputs, and Outputs created since then are treated as plain hash checks. The short of it, is that you only need P2SH to spend Outputs created before Genesis.

The examples use the keys and the fundingTransaction helper described in About the examples.

P2SHLockBuilder.fromScript hashes a redeem script, while P2SHLockBuilder(hashHex) takes the hash directly.

final redeemScript = ScriptBuilder()
.addData(Uint8List.fromList(HEX.decode(aliceKey.publicKey.toHex())))
.opCode(OpCodes.OP_CHECKSIG)
.build();
final locker = P2SHLockBuilder.fromScript(redeemScript); // hashes the redeem script
locker.getScriptPubkey().toString(type: 'asm'); // OP_HASH160 <20-byte script hash> OP_EQUAL
final address = Address.fromScript(redeemScript, NetworkType.TEST); // starts with 2

The Unlocking Script is the redeem script’s own unlocking data, followed by the serialised redeem script. For a redeem script of <pubkey> OP_CHECKSIG that gives us <signature> <redeem script>:

/// Unlocking Script for a P2SH Output whose redeem script is `<pubkey> OP_CHECKSIG`:
/// the Signature, followed by the serialised redeem script.
class P2SHPubkeyUnlocker extends UnlockingScriptBuilder {
final SVScript redeemScript;
P2SHPubkeyUnlocker(this.redeemScript);
@override
SVScript getScriptSig() {
if (signatures.isEmpty) return SVScript();
return ScriptBuilder()
.addData(Uint8List.fromList(HEX.decode(signatures[0].toTxFormat())))
.addData(Uint8List.fromList(redeemScript.buffer))
.build();
}
@override
void parse(SVScript script) {}
}

The Signature commits to the redeem script, so the outpoint we give the signer carries the redeem script, and not the P2SH Locking Script. We verify with VerifyFlag.P2SH and without UTXO_AFTER_GENESIS, since those are the rules a pre-Genesis Output is spent under.

// The Signature commits to the redeem script, not to the P2SH Locking Script,
// so the outpoint we hand to the signer carries the redeem script.
final outpoint = TransactionOutpoint(fundingTx.id, 0, BigInt.from(50000), redeemScript);
final tx = TransactionBuilder()
.spendFromOutpointWithSigner(DefaultTransactionSigner(sighashAll, aliceKey), outpoint,
TransactionInput.MAX_SEQ_NUMBER, P2SHPubkeyUnlocker(redeemScript))
.spendToPKH(aliceKey.toAddress(networkType: NetworkType.TEST), BigInt.from(49000))
.build(false);
// Let's verify with the pre-Genesis P2SH rule switched on.
Interpreter().correctlySpends(tx.inputs[0].script!, p2shScript, tx, 0,
{VerifyFlag.P2SH, VerifyFlag.SIGHASH_FORKID}, Coin.ofSat(BigInt.from(50000)));