Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 to H.
  • 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

  • timeout too tight. If the lock tx isn't even confirmed by the time timeout hits, 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.
  • timeout too 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.
  • preimage mismatch. The hash you advertise must be exactly SHA256(preimage_bytes). Common bug: hashing the hex string of the preimage instead of the bytes. The example above uses xxd -r -p | shasum -a 256 to 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.

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.