Skip to content

P2SH

Pay-to-script-hash locks an Output to the hash of a redeem script. The spender reveals the script and then satisfies it. The standard script templates page in the Developer Guide describes the mechanism.

// The redeem script. Here it is an ordinary P2PKH script for Alice.
Script redeemScript = new P2PKHLockBuilder(aliceAddress).getLockingScript();
// The Output commits to HASH160(redeemScript), as OP_HASH160 <hash> OP_EQUAL.
P2SHLockBuilder locker = new P2SHLockBuilder(redeemScript);
Transaction fundingTx = Fixtures.fundingTransaction(locker, 100_000);
TransactionOutput utxo = fundingTx.getOutputs().get(0);

new P2SHLockBuilder(ByteBuffer.wrap(hash)) takes a hash we already have.

Two things differ from a direct spend:

  • the Signature covers the redeem script, not the P2SH Output’s script, so we sign against an Output made from the redeem script,
  • the Unlocking Script ends with the serialised redeem script.

P2SHUnlockBuilder takes the finished Unlocking Script, so the Signature has to exist first. We build with a placeholder unlocker, sign, and then replace the Input with one that holds the Unlocking Script.

Transaction tx = new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new DefaultUnlockBuilder())
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);
// We sign over the redeem script, not over the P2SH Locking Script.
TransactionSigner signer = new TransactionSigner(SIGHASH, alice);
signer.sign(tx, new TransactionOutput(utxo.getAmount(), redeemScript), 0);
// The Unlocking Script is <sig> <pubkey> <redeemScript>.
Script unlockingScript = new ScriptBuilder()
.data(signer.getSignature().toTxFormat())
.data(alice.getPublicKey().getPubKeyBytes())
.data(redeemScript.getProgram())
.build();
TransactionInput input = tx.getInputs().get(0);
tx.replaceInput(0, new TransactionInput(input.getPrevTxnId(), input.getPrevTxnOutputIndex(),
input.getSequenceNumber(), new P2SHUnlockBuilder(unlockingScript)));
// P2SH evaluation only happens under the pre-Genesis rules, i.e. the P2SH flag without UTXO_AFTER_GENESIS.
Set<Script.VerifyFlag> legacyFlags = EnumSet.of(
Script.VerifyFlag.P2SH, Script.VerifyFlag.SIGHASH_FORKID, Script.VerifyFlag.STRICTENC);
assertDoesNotThrow(() -> new Interpreter().correctlySpends(
tx.getInputs().get(0).getScriptSig(), utxo.getScript(), tx, 0, legacyFlags, Coin.valueOf(100_000)));

With the post-Genesis flags, the interpreter only checks that the pushed data hashes to the right value.

// Under the post-Genesis rules the Output is a plain hash puzzle.
// Pushing the redeem script alone, with no Signature, satisfies it.
Script redeemScriptOnly = new ScriptBuilder().data(redeemScript.getProgram()).build();
assertDoesNotThrow(() -> new Interpreter().correctlySpends(
redeemScriptOnly, utxo.getScript(), tx, 0, Fixtures.FLAGS, Coin.valueOf(100_000)));