Tutorial · Regtest

Atomic Swaps on Regtest

An atomic swap trades coins on two chains with no one in the middle. Each side locks coins in a hash time-locked contract (HTLC): a script that pays whoever shows a secret, or pays the owner back after a deadline. This page runs the Divi half of a swap on your own regtest chain. You lock coins, claim them with the secret, watch a refund fail before its deadline, and watch it work after. It also says plainly what a full two-chain swap on regtest still needs.

About 25 minutes · Checked 2026-10-22
What this page proves, and what it does not

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.

Before you start

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.

  1. Alice picks a random secret and keeps it. She tells Bob only its SHA-256 hash.
  2. 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.
  3. 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.
  4. Alice claims the BTC. To do it she must put the secret on the Bitcoin chain, where anyone can read it.
  5. 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
PathWhat the spender suppliesWhat the script checks
Claim (top)A signature, the 32-byte secret, then 1The SHA-256 of the secret equals the hash, and the signature matches the claim key
Refund (bottom)A signature, then 0The 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.

In a real swap the two keys differ

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:

OutputAmountPaysDeadline
0, HTLC A1008yjgZMMH…Newest block time + 24 hours (1792752671)
1, HTLC B1008rprYbmM…Now + 90 seconds (1792666363)
2, change49.99The key's own addressNone

The tool pays a fee of 0.01 DIVI. Set REFUND_DELAY_SECS before fund to change B's 90 seconds.

state.json holds the secret

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 0.2.5 accepts non-final transactions

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.

You only have to wait

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:

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

SymptomCauseFix
The tool stops with a panic that mentions SWAP_KEY_REF or resolve keyThe environment variables from step 3 are not set in this shell, or the key file is missingRun the three export lines again and check creds/swap-key exists.
Panic with funding tx does not pay the keyThe txid given to fund did not pay the key's addressRun $SWAP with no command, and send to the address it prints.
Panic with funding too smallThe payment is not more than 200.01 DIVISend 250, as in step 4.
refund rejected: … non-finalThe deadline has not passed yetWait. B's deadline is the locktime the tool prints, as a Unix time. Compare with date +%s.
transaction already in block chainYou ran claim or refund again for an HTLC that is already spentRun fund again for a new pair.

Still stuck? Ask the Divi chatbot, or see all tutorials.