Add a time lock vault contract example

Adds src/test/kotlin/timeLockVaultContract.kt — a time lock vault expressed in NPL, following the shape of the existing secretSaleContract.kt example.

A time lock vault locks coins to an address until a committed unlock value is reached, after which only the holder of a committed key can spend them. The unlock value is read as a block height below 500,000,000 and as a Unix timestamp at or above it; the rule is identical either side of that split, so a timestamp vault is simply one whose committed value sits on the other side.

What it demonstrates

The contract splits its committed data across both argument kinds, which the other examples do not:

Value Kind Why
ownerPubKey hidden hashed into the address, revealed at spend time
unlockValue visible cleartext, because the script reads it
hdIndex visible cleartext, and never read by the script

hdIndex exists purely so a wallet restoring from a seed can read it off the locking script and re-derive the owning key rather than searching the index space. The script drops it — a useful illustration that a visible arg need not be a script input.

One rule, Claim, with two gates: checkLockTimeVerify on the committed value and checkSigVerify on the committed key.

Tests

Seven, using the repo's existing setupUtxoTxWithPublicArgs and validateSpend helpers:

  • compile, and print the bytecode and template hash
  • claim at the unlock time, and a day after
  • a height-committed vault rather than a timestamp one
  • three rejections: claiming early, claiming with a final sequence number (which would otherwise disable the locktime gate), and claiming with the wrong key

Each rejection is checked to fail at the gate it targets rather than incidentally — the two locktime cases fail at the locktime check and the wrong-key case at the signature check.

Notes for review

  • The compiled bytecode is not byte-identical to Nexa's deployed time lock vaults, whose hand-written template hashes to 461ad25081cb0119d034385ff154c8d3ad6bdd76. NPL prepends its own rule dispatcher, so this contract has its own template hash (59bf8333189da05ada52aadb4526363aed0cccb6) and its own address space. The comment in the file says so; worth confirming that framing is how you want such examples described.
  • The satisfier is built with the shared satisfier() helper, which does not append the rule index, so the index is passed as the last spender arg. If that helper is meant to append it, this example should change rather than the helper.

Based on the time lock vault specification in nexa/specification and the TimeLockVaultDestination implementation in libnexakotlin.

Marked draft pending human review.

Update: Human review complete

Co-authored with Claude Code

Edited by Jørgen S. Notland

Merge request reports

Loading
Loading