HTLC payments
A Hash Time-Locked Contract (HTLC) is a payment that the recipient can claim only by revealing a preimage. If they don't claim before a deadline, the sender reclaims. This is the foundation of trustless atomic swaps, machine-to-machine escrow, and Lightning-style payment channels.
Exfer ships HTLC as a first-class script type — no smart-contract VM, no
gas. Three CLI commands cover the full lifecycle: htlc-lock,
htlc-claim, htlc-reclaim.
Concept
Sender A Receiver B
│ │
│ share hash H = SHA256(p) │ (B picks secret p)
│ ◄────────────────────────────┤
│ │
│ htlc-lock(H, timeout) │
│ ─────────────────────────► │
│ │
│ │ htlc-claim(p) ─► funds to B
│ │
│ (if B never claims) │
│ htlc-reclaim after timeout │ ─► funds back to A
│ ─────────────────────────► │
The script enforces:
- Before
timeout: spendable only by B presenting a preimage that hashes toH. - At or after
timeout: spendable only by A.
Both branches are signature-checked, so an outside observer who learns the preimage still cannot claim the funds — they need B's signature too.
End-to-end worked example
Two agents, A (sender) and B (receiver).
B: generate preimage and hash
PREIMAGE=$(openssl rand -hex 32)
HASH_LOCK=$(echo -n "$PREIMAGE" | xxd -r -p | shasum -a 256 | cut -d' ' -f1)
echo "Share with A: $HASH_LOCK"
# B keeps $PREIMAGE secret until ready to claim
A: get current block height and lock the funds
RPC="http://82.221.100.201:9334"
HEIGHT=$(curl -s -X POST "$RPC" \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block_height","params":{},"id":1}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])")
TIMEOUT=$((HEIGHT + 500)) # ~83 minutes from now
exfer script htlc-lock \
--wallet ~/agent-a.key \
--receiver <AGENT_B_PUBKEY> \
--hash-lock "$HASH_LOCK" \
--timeout "$TIMEOUT" \
--amount "10 EXFER" \
--rpc "$RPC" \
--json
# Output includes tx_id — share it with B
A: wait for the lock tx to confirm
TX_ID="<tx_id from above>"
for i in $(seq 1 60); do
R=$(curl -s -X POST "$RPC" -H 'content-type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"get_transaction\",\"params\":{\"hash\":\"$TX_ID\"},\"id\":1}")
echo "$R" | grep -q '"block_height"' && { echo "Lock confirmed"; break; }
sleep 10
done
B: claim by revealing the preimage
exfer script htlc-claim \
--wallet ~/agent-b.key \
--tx-id "$TX_ID" \
--preimage "$PREIMAGE" \
--sender <AGENT_A_PUBKEY> \
--timeout "$TIMEOUT" \
--rpc "$RPC" \
--json
The preimage is now visible on-chain inside B's claim transaction. A (and anyone else) can read it from the block — which is exactly what makes cross-chain atomic swap work: once B claims on chain X, A learns the preimage and can claim the mirror HTLC on chain Y.
A: reclaim if B never showed up
# only succeeds if current_height >= TIMEOUT
exfer script htlc-reclaim \
--wallet ~/agent-a.key \
--tx-id "$TX_ID" \
--receiver <AGENT_B_PUBKEY> \
--hash-lock "$HASH_LOCK" \
--timeout "$TIMEOUT" \
--rpc "$RPC" \
--json
The CLI pre-checks the current height against TIMEOUT and refuses to
submit if the lock is still active.
Command reference
htlc-lock
exfer script htlc-lock \
--wallet ~/sender.key \
--receiver <RECEIVER_PUBKEY> \
--hash-lock <SHA256_HEX> \
--timeout <BLOCK_HEIGHT> \
--amount "10 EXFER" \
--fee "0.001 EXFER" \
--rpc "$RPC" \
--json
htlc-claim
exfer script htlc-claim \
--wallet ~/receiver.key \
--tx-id <LOCK_TX_ID> \
--preimage <PREIMAGE_HEX> \
--sender <SENDER_PUBKEY> \
--timeout <TIMEOUT_HEIGHT> \
--rpc "$RPC" \
--json
htlc-reclaim
exfer script htlc-reclaim \
--wallet ~/sender.key \
--tx-id <LOCK_TX_ID> \
--receiver <RECEIVER_PUBKEY> \
--hash-lock <SHA256_HEX> \
--timeout <TIMEOUT_HEIGHT> \
--rpc "$RPC" \
--json
Common pitfalls
timeouttoo tight. If the lock tx isn't even confirmed by the timetimeouthits, the HTLC is effectively dead-on-arrival — A can reclaim immediately, B has no window to claim. Leave at least a few hundred blocks of cushion past tx confirmation.timeouttoo loose. A's funds are locked for the duration. For atomic swaps, choose the longer chain's timeout strictly greater than the shorter chain's, so B always has time to react.preimagemismatch. The hash you advertise must be exactlySHA256(preimage_bytes). Common bug: hashing the hex string of the preimage instead of the bytes. The example above usesxxd -r -p | shasum -a 256to hash the bytes.- Coinbase maturity. If A's wallet is funded only from recent coinbase outputs, the lock will fail with "immature coinbase". Either wait, or fund A's wallet from a non-coinbase source.
Related script types
The CLI also has multisig, vault, escrow, and delegation patterns — see Protocol specification for the underlying script language. The CLI subcommands are documented in CLI reference.