Skip to content

Hodlocker

HodlLockBuilder locks coins to a Public Key hash until a block height. Its script is a P2PKH check sitting behind an OP_CHECKLOCKTIMEVERIFY:

<height> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyHash> OP_EQUALVERIFY OP_CHECKSIG

The spending Transaction’s nLockTime must be at least the height, and its Input’s sequence number must be below the maximum so that nLockTime applies. The page on lock time and sequence numbers covers those fields. The examples use the setup from the Transaction builder.

// Locked to Alice's Public Key hash until block 800,000.
HodlLockBuilder locker = new HodlLockBuilder(alice.getPublicKey().getPubKeyHash(), BigInteger.valueOf(800_000));
Transaction fundingTx = Fixtures.fundingTransaction(locker, 100_000);

The builder cannot set nLockTime, and the Signature covers it, so we build without a signer, set the lock time, and then sign.

// The Input's sequence number must be below the maximum, otherwise nLockTime is ignored.
HodlUnlockBuilder unlocker = new HodlUnlockBuilder(null, alice.getPublicKey(), null);
Transaction tx = new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER - 1, unlocker)
.spendTo(new P2PKHLockBuilder(aliceAddress), BigInteger.valueOf(99_000))
.build(true);
// The builder has no lock-time setter, so we set it on the Transaction and then sign.
tx.setLockTime(lockTime);
new TransactionSigner(SIGHASH, alice).sign(tx, fundingTx.getOutputs().get(0), 0);

In the preceding example HodlUnlockBuilder takes three parameters:

  • the Signature, which is null here because the signer adds it,
  • the Public Key,
  • an optional extra push, also null here.

With the CHECKLOCKTIMEVERIFY flag and without UTXO_AFTER_GENESIS (i.e. the pre-Genesis rules), the lock holds.

// OP_CHECKLOCKTIMEVERIFY is enforced with the CHECKLOCKTIMEVERIFY flag, and without UTXO_AFTER_GENESIS.
Set<Script.VerifyFlag> cltvFlags = EnumSet.of(Script.VerifyFlag.SIGHASH_FORKID, Script.VerifyFlag.CHECKLOCKTIMEVERIFY);
Interpreter interpreter = new Interpreter();
// With nLockTime at 800,000 the spend is accepted.
assertDoesNotThrow(() -> interpreter.correctlySpends(onTime.getInputs().get(0).getScriptSig(),
utxo.getScript(), onTime, 0, cltvFlags, Coin.valueOf(100_000)));
// One block early, at 799,999, it is rejected.
assertThrows(ScriptException.class, () -> interpreter.correctlySpends(early.getInputs().get(0).getScriptSig(),
utxo.getScript(), early, 0, cltvFlags, Coin.valueOf(100_000)));

With the post-Genesis flags from Fixtures.FLAGS, the early spend passes as well.

// Under the post-Genesis rules OP_CHECKLOCKTIMEVERIFY is a no-op, so the early spend passes.
assertDoesNotThrow(() -> Fixtures.verifySpend(early, 0, utxo));
HodlLockBuilder parsed = HodlLockBuilder.fromScript(utxo.getScript());
BigInteger lockHeight = parsed.getLockHeight();

The Script template registry also recognises these scripts, under the name HODLocker.