Skip to content

Custom builders

The TransactionBuilder does not care which scripts it handles. It asks each Output’s LockingScriptBuilder for a Locking Script, and each Input’s UnlockingScriptBuilder for an Unlocking Script. To use a script of our own, we write a pair of small classes, and everything else (fees, change, signing, the checks in build()) keeps working.

As an example, let’s lock coins with a hash puzzle. Anyone who knows the secret can spend the Output, and no key is involved. The Developer Guide’s page on Bitcoin Script explains how such a script evaluates.

Do note that a bare hash puzzle can be spent by anyone who sees the secret, and the secret becomes public as soon as the spending Transaction is broadcast. It is a teaching example, not a way to protect coins.

A locking builder extends LockingScriptBuilder and implements getLockingScript().

/** Locks coins to whoever reveals a secret, with OP_SHA256 <sha256(secret)> OP_EQUAL. */
public class HashPuzzleLockBuilder extends LockingScriptBuilder {
private final byte[] hash;
public HashPuzzleLockBuilder(byte[] secret) {
this.hash = Sha256Hash.hash(secret);
}
@Override
public Script getLockingScript() {
return new ScriptBuilder()
.op(OP_SHA256)
.data(hash)
.op(OP_EQUAL)
.build();
}
}

Our builder goes wherever the library takes a LockingScriptBuilder, e.g. in spendTo().

byte[] secret = "open sesame".getBytes(StandardCharsets.UTF_8);
// Our builder goes wherever the library takes a LockingScriptBuilder.
Transaction puzzleTx = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, alice), aliceFunding, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new HashPuzzleLockBuilder(secret), BigInteger.valueOf(10_000))
.sendChangeTo(aliceAddress)
.build(true);

An unlocking builder extends UnlockingScriptBuilder and implements getUnlockingScript().

/** Unlocks a HashPuzzleLockBuilder Output by pushing the secret. No Signature is involved. */
public class HashPuzzleUnlockBuilder extends UnlockingScriptBuilder {
private final byte[] secret;
public HashPuzzleUnlockBuilder(byte[] secret) {
this.secret = secret;
}
@Override
public Script getUnlockingScript() {
return new ScriptBuilder().data(secret).build();
}
}

Since no Signature is needed, we spend the puzzle without a signer.

// No signer this time. The Unlocking Script needs no Signature, only the secret.
Transaction claimTx = new TransactionBuilder()
.spendFromTransaction(puzzleTx, 0, TransactionInput.MAX_SEQ_NUMBER, new HashPuzzleUnlockBuilder(secret))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(9_744))
.build(true);

The test behind this example runs the spend through the interpreter, and checks that a wrong secret is rejected.

When an Input has a signer, TransactionSigner.sign() hands the finished TransactionSignature to the unlocker through addSignature(), and the unlocker reads it back with getSignatures() when the builder asks for the Unlocking Script. That is all a signature-aware builder needs to do.

This one writes OP_0 followed by each Signature in the order they were added. It is a minimal version of the library’s own P2MSUnlockBuilder, used on the multisig page, and a good starting point for any script that needs Signatures from the signer.

/**
* Unlocks a bare multisig Output with OP_0 <sig 1> ... <sig m>.
* TransactionSigner.sign() hands us each Signature through addSignature(), in the order we sign.
*/
public class MultisigUnlockBuilder extends UnlockingScriptBuilder {
@Override
public Script getUnlockingScript() {
ScriptBuilder builder = new ScriptBuilder().op(ScriptOpCodes.OP_0); // the dummy item that CHECKMULTISIG pops
try {
for (TransactionSignature signature : getSignatures()) {
builder.data(signature.toTxFormat()); // the DER Signature plus its Sighash byte
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return builder.build();
}
}

Please note that an Unlocking Script must push each Signature with its Sighash byte, which is what TransactionSignature.toTxFormat() gives us. getSignatureBytes() leaves that byte out, and the spend is rejected.

The library’s builders also have a constructor that takes a Script, and fill in their fields from it. For our own scripts we can do the same, or describe them to the Script template registry, which also lets the rest of an application recognise them.

For a one-off script that needs no logic at all, DefaultLockBuilder(script) and DefaultUnlockBuilder(script) wrap a finished Script as it is.