Everything below ran on a private Divi regtest chain: two HTLCs funded, one claimed with its secret, one refunded after its deadline, and two early refunds rejected. That is the Divi half of a swap. A swap with a second chain, such as Bitcoin, cannot run on regtest with today's code, for the reasons in step 9. The full swap engine was tested on public test networks instead.
You need a running C++ regtest node with coins, as in start your first regtest node: data folder ~/divi-regtest, RPC port 51475, at least 101 blocks. Step 6 also uses an IronDivi node following it, as in two regtest nodes, on RPC port 51485. The output below was captured on a test machine whose nodes used other ports; ids, addresses and times on yours will differ.
1How a swap stays fair
Say Alice has DIVI and Bob has BTC, and they agree a price.
- Alice picks a random secret and keeps it. She tells Bob only its SHA-256 hash.
- Alice locks her DIVI in an HTLC on the Divi chain. Bob can take it by showing the secret, or Alice gets it back after, say, 24 hours.
- Bob locks his BTC in an HTLC with the same hash on the Bitcoin chain. Alice can take it with the secret, or Bob gets it back after, say, 12 hours.
- Alice claims the BTC. To do it she must put the secret on the Bitcoin chain, where anyone can read it.
- Bob reads the secret and claims the DIVI with it.
If anyone stops halfway, nobody loses: each side waits out its deadline and takes its own coins back. Bob's deadline is shorter than Alice's, so Alice cannot claim the BTC at the last moment and then also refund her DIVI.
On one regtest chain you can do every Divi step of this. The tool on this page plays both parts with one key, so it locks two HTLCs from one wallet. HTLC A is claimed with its secret. HTLC B is left unclaimed and refunded after a 90-second deadline.
2The HTLC script
This is the script IronDivi's swap code builds, decoded by a C++ node with decodescript. It is HTLC B from the run below:
OP_IF
OP_SHA256 ecb2d7e53d410fcddddd1dd319aa2b019c9bfee0aef00dae1a1d95285aafc217 OP_EQUALVERIFY
0327184ae4c687c982df0cc893fe8d51b3dfc82307569521a20c11aa2820fa454a
OP_ELSE
1792666363 OP_CHECKLOCKTIMEVERIFY OP_DROP
0327184ae4c687c982df0cc893fe8d51b3dfc82307569521a20c11aa2820fa454a
OP_ENDIF
OP_CHECKSIG
| Path | What the spender supplies | What the script checks |
|---|---|---|
| Claim (top) | A signature, the 32-byte secret, then 1 | The SHA-256 of the secret equals the hash, and the signature matches the claim key |
| Refund (bottom) | A signature, then 0 | The spending transaction's lock time is at least the deadline, and the signature matches the refund key. 1792666363 is a Unix time. |
The coins are not sent to the script itself. They go to the script's hash, a pay-to-script-hash (P2SH) address. On Divi test networks these start with 8. The spender reveals the full script when spending.
The claim key belongs to the other party and the refund key to you. The tool here uses one key for both so that one wallet can run every step.
3Build the tool
The HTLC code lives in IronDivi, in the divi-swap crate. IronDivi ships an example that runs these steps on the public Divi testnet. Download divi_cltv_proof_regtest.rs, a copy of that example made for this tutorial that talks to your own node instead. It is not part of IronDivi itself. Building needs Rust, a C compiler and libclang-dev, as IronDivi's README says.
git clone https://github.com/DiviDomains/IronDivi ~/IronDivi cp divi_cltv_proof_regtest.rs ~/IronDivi/crates/swap-chain-divi/examples/ cd ~/IronDivi cargo build --release -p swap-chain-divi --example divi_cltv_proof_regtest SWAP=~/IronDivi/target/release/examples/divi_cltv_proof_regtest
The tool signs with a private key it reads from a file, never from the command line. Make a fresh random key that only you can read. It is a regtest key, so it guards nothing of value:
mkdir -p ~/regtest-swap && cd ~/regtest-swap ( umask 077; mkdir -p creds; openssl rand -hex 32 > creds/swap-key )
Tell the tool where the key and the node are. The password is read from divi.conf so it never appears in your shell history:
export CREDENTIALS_DIRECTORY=$HOME/regtest-swap/creds SWAP_KEY_REF=credential:swap-key export DIVI_RPC_URL=http://127.0.0.1:51475/ DIVI_RPC_USER=regtest export DIVI_RPC_PASS="$(grep '^rpcpassword=' ~/divi-regtest/divi.conf | cut -d= -f2)"
Run it with no command to see the key's address:
$SWAP
key address yJUpZkzC1kNbR2QHuFicrkLM6mPG7oJZAD mtp 1792666630
usage: divi_cltv_proof_regtest fund <txid> <state> | claim <state> | refund <state>
The number after mtp is the time of the newest block, as a Unix time. The C++ node does not report a median time, so the tool falls back to the block's own time.
4Lock coins in two HTLCs
Pay the key from your node's wallet, and make a block so the payment confirms:
divi-cli -datadir=$HOME/divi-regtest sendtoaddress yJUpZkzC1kNbR2QHuFicrkLM6mPG7oJZAD 250
d7787f86b13fdb03134292c8a4d4ba9240f32fada485c1c5ceeddd6cba1d2b64
divi-cli -datadir=$HOME/divi-regtest setgenerate 1
Now spend that payment into two HTLCs of 100 DIVI each. The tool makes two new secrets, builds both scripts, and checks that the node computes the same P2SH addresses. Then it sends the transaction and saves everything it needs later in state.json:
$SWAP fund d7787f86b13fdb03134292c8a4d4ba9240f32fada485c1c5ceeddd6cba1d2b64 state.json
key address yJUpZkzC1kNbR2QHuFicrkLM6mPG7oJZAD mtp 1792666271
fund txid 663e584558c4d0b7485719680776ec8453672fafb8568f93c94f6a67d2269fc5
HTLC A: ours 8yjgZMMH3JUoPBZfe7PRUpPnFGK7zgUvb9 node Ok(String("8yjgZMMH3JUoPBZfe7PRUpPnFGK7zgUvb9"))
HTLC B: ours 8rprYbmM8bqkVL9iKa6n3y92YLMC5M3oFy node Ok(String("8rprYbmM8bqkVL9iKa6n3y92YLMC5M3oFy"))
divi-cli -datadir=$HOME/divi-regtest setgenerate 1
The funding transaction has three outputs:
| Output | Amount | Pays | Deadline |
|---|---|---|---|
| 0, HTLC A | 100 | 8yjgZMMH… | Newest block time + 24 hours (1792752671) |
| 1, HTLC B | 100 | 8rprYbmM… | Now + 90 seconds (1792666363) |
| 2, change | 49.99 | The key's own address | None |
The tool pays a fee of 0.01 DIVI. Set REFUND_DELAY_SECS before fund to change B's 90 seconds.
HTLC A's secret is saved in plain text so that claim can use it. That is fine on regtest. Real swap software must guard the secret until it means to claim.
5Claim with the secret
Spend HTLC A by its top path, with a signature and the secret:
$SWAP claim state.json
key address yJUpZkzC1kNbR2QHuFicrkLM6mPG7oJZAD mtp 1792666273
claim: locktime 0 txid 2d25efd7825bf3535261a199cc2043c4b744037918cf162e26e2660738fb8f82
claim txid 2d25efd7825bf3535261a199cc2043c4b744037918cf162e26e2660738fb8f82
divi-cli -datadir=$HOME/divi-regtest setgenerate 1
Now look at what the claim put on the chain. This is the step that makes a swap atomic: the secret is public the moment anyone claims.
divi-cli -datadir=$HOME/divi-regtest getrawtransaction 2d25efd7825bf3535261a199cc2043c4b744037918cf162e26e2660738fb8f82 1
…
"scriptSig" : {
"asm" : "304402202e99…f7ee0501 4a1d7c324ac96fb499fbf8af7ee734ac398faff6a339cc1707b6493cf4f33b2e 1 63a820eea6…68ac",
…
The four items are the signature, the secret, the 1 that picks the claim path, and the script itself. Hash the secret and you get HTLC A's hash:
echo -n 4a1d7c324ac96fb499fbf8af7ee734ac398faff6a339cc1707b6493cf4f33b2e | xxd -r -p | sha256sum
eea60e3d27928dfe6659927d0ee06eda39e699ffe3450092ca06ef18f8af63b5 -
In a real swap, the other party would now take this secret to the other chain and claim there.
6Try to refund too early
HTLC B's deadline is still in the future. refund-early builds the refund anyway, with its lock time set to the deadline, and sends it:
$SWAP refund-early state.json
refund-early: locktime 1792666363 txid 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe
refund-early rejected: sendrawtransaction: {"code":-26,"message":"64: non-final"}
The C++ node refuses it. A transaction whose lock time is still in the future is non-final, and C++ Divi keeps it out of its memory pool and out of blocks. For the memory pool it compares the lock time with its own clock. For a block it compares with the block's time.
Now send the same refund to the IronDivi node:
DIVI_RPC_URL=http://127.0.0.1:51485/ $SWAP refund-early state.json refund-early: locktime 1792666363 txid 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe refund-early txid 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe irondivi-cli --regtest --rpcport 51485 getrawmempool [ …, "10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe" ]
IronDivi takes the early refund into its memory pool and passes it on. Its C++ peer refuses it, and IronDivi's log shows Peer 1 rejected tx: code=64 reason='non-final'. The coins are still safe, because C++ nodes make the blocks and will not include it. But do not treat IronDivi accepting a transaction as proof the chain will. Check timing rules against a C++ node.
7Try to cheat the deadline
The lock time check in step 6 guards the transaction. The script guards the coins. To see it, wait until the deadline has passed, then send a refund whose lock time is one second below the deadline. That transaction is final, so only OP_CHECKLOCKTIMEVERIFY can stop it:
sleep 100
$SWAP refund-cltv-violation state.json
local interpreter on CLTV violation: Err(UnsatisfiedLocktime)
refund-cltv-violation: locktime 1792666362 txid 59668c14898fbc922fa687daad3bb11a447b95d824c6a9e76b34a8dc9c9effd1
refund-cltv-violation rejected: sendrawtransaction: {"code":-26,"message":"64: non-mandatory-script-verify-flag (No error)"}
IronDivi's own script engine, built into the tool, rejects it first with UnsatisfiedLocktime. Then the C++ node refuses it too. The words in the C++ message are confusing. "Non-mandatory" means C++ checks OP_CHECKLOCKTIMEVERIFY in a second group of rules, apart from the oldest ones. It does not mean the rule is optional. C++ Divi applies it to every block since 23 August 2023, and every regtest block is newer than that.
8Refund after the deadline
Now send the honest refund, with its lock time equal to the deadline:
$SWAP refund state.json refund: locktime 1792666363 txid 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe refund txid 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe divi-cli -datadir=$HOME/divi-regtest setgenerate 1 divi-cli -datadir=$HOME/divi-regtest getrawtransaction 10326428e1afe1568eda3e44f09ad44edde8d57b340511db02ba346bcf6c1bfe 1 … "confirmations" : 1, …
It is the same transaction, with the same id, that the C++ node refused in step 6. Only the clock changed. The IronDivi node shows it with 1 confirmation too, and its memory pool no longer holds it.
On Divi you do not need to make blocks to pass a time deadline. In a second run, the refund entered the C++ memory pool as soon as the clock passed the deadline, while the newest block was still about 90 seconds older than it. Bitcoin compares lock times with the median time of recent blocks, so there you would also need new blocks.
9What a full swap on regtest still needs
IronDivi has a swap engine, divi-swapd, that runs both sides of a DIVI–BTC swap: offers, locking, claiming, and refunds after a crash. It is not usable on regtest today:
- A second chain. A swap needs a second blockchain, such as a Bitcoin regtest node. Divi's own regtest is only one chain.
- The Bitcoin side reads public servers.
divi-swapdtalks to Bitcoin through Esplora web servers, not to a Bitcoin node. It accepts only thesignetandtestnetnetworks and makes onlytb1addresses. Bitcoin regtest usesbcrt1addresses. - Only the Divi side can point anywhere. Its Divi node address can be set, so it could use your regtest node. The Bitcoin side cannot follow.
Instead, IronDivi's developers ran the full engine between the public Divi testnet and Bitcoin testnet3 in October 2026. They tested the normal swap, a refund when the other side never claims, a late claim, and recovery after a crash. The notes are in docs/plans/swap-poc/RESULTS.md in the IronDivi repository.
To try a whole swap on your own computers, you would need regtest support in IronDivi's Bitcoin backend: a Bitcoin regtest node, an Esplora server in front of it, and a network setting that makes bcrt1 addresses. That support does not exist yet.
If something goes wrong
| Symptom | Cause | Fix |
|---|---|---|
The tool stops with a panic that mentions SWAP_KEY_REF or resolve key | The environment variables from step 3 are not set in this shell, or the key file is missing | Run the three export lines again and check creds/swap-key exists. |
Panic with funding tx does not pay the key | The txid given to fund did not pay the key's address | Run $SWAP with no command, and send to the address it prints. |
Panic with funding too small | The payment is not more than 200.01 DIVI | Send 250, as in step 4. |
refund rejected: … non-final | The deadline has not passed yet | Wait. B's deadline is the locktime the tool prints, as a Unix time. Compare with date +%s. |
transaction already in block chain | You ran claim or refund again for an HTLC that is already spent | Run fund again for a new pair. |
Still stuck? Ask the Divi chatbot, or see all tutorials.