Skip to content
LogoLogo

Cross-chain swaps and finality proofs

Arrekta connects to other chains without a custodial bridge:

  • atomic swaps: both legs lock to the same SHA-256 digest;
  • finality proofs: a transaction's settlement can be checked against the checkpoint signer group alone, with no node and no relayer to trust.

Policy and rationale: Interoperability. Wire formats: protocol §4.2 (policy op 9) and §9.9.

1. Prove a transaction is final

Every node serves GET /v1/txs/{txid}/proof once a signed checkpoint covers the transaction's block (409 tx-not-final before that). Verify the proof yourself:

import { createClient, getFinalityProof, verifyFinalityProof, networkSignerSet } from 'arrekta-sdk'
 
const client = createClient({ url: 'http://127.0.0.1:28332' })
// fetches and verifies locally against the genesis checkpoint group
const { finality, hex } = await getFinalityProof(client, { txid, network: 'regtest' })
console.log(finality.height, finality.checkpointHeight)
 
// later, anywhere (no node): only the trusted signer set is needed
verifyFinalityProof(hex, networkSignerSet('regtest')!, 'regtest')
from arrekta_sdk import Client, get_finality_proof, verify_finality_proof
 
p = get_finality_proof(Client("http://127.0.0.1:28332"), txid)
f = verify_finality_proof(p["hex"], None, "regtest")   # None: the network's genesis group
print(f.height, f.checkpoint_height)

A proof carries the transaction, its merkle branch, the headers up to the checkpointed block, and every signer-group hand-over since genesis. Verification fails with proof-bad-merkle, proof-broken-headers, proof-bad-handover, proof-checkpoint-mismatch or proof-not-final.

2. Swap ARRK for BTC

Alice has ARRK and wants BTC; Bob has BTC. The party holding the secret locks first, with the later refund time.

import { newSwapSecret, arrkSwapPolicy, policyLock, encodeAddress,
         btcHtlcScript, p2wshAddress, findSwapPreimage } from 'arrekta-sdk'
 
// Alice
const { preimage, hash } = newSwapSecret()             // keep `preimage`, send `hash` to Bob
const lock = policyLock(arrkSwapPolicy({ receiver: bobKey, refund: aliceKey, hash, timeoutHeight: tArrk }))
// …pay ARRK to encodeAddress(lock, 'mainnet')
 
// Bob, after checking Alice's output is final (section 1)
const script = btcHtlcScript({ hash, claimPubkey: aliceBtcPubkey, refundPubkey: bobBtcPubkey, locktime: tBtc })
const btcAddress = p2wshAddress(script, 'mainnet')     // fund it from a Bitcoin wallet
 
// Alice claims the BTC (witness: btcClaimWitness), revealing `preimage`.
// Bob watches that witness and claims the ARRK before tArrk:
const secret = findSwapPreimage(aliceBtcWitness, hash)

The Python SDK has the same helpers in arrekta_sdk.swap (new_swap_secret, arrk_swap_policy, btc_htlc_script, p2wsh_address, btc_claim_witness, btc_refund_witness, find_swap_preimage).

Swapping with Ethereum. The Arrekta leg is the same arrkSwapPolicy. The Ethereum leg is the ArrektaHTLC contract (contracts/src/ArrektaHTLC.sol), which holds ETH or any ERC-20 under the same SHA-256 hash:

import { ETH_HTLC_ABI, ethHtlcSwapId } from 'arrekta-sdk'
 
// with viem: lock(recipient, token (0x0 = ETH), amount, hash, deadline)
await wallet.writeContract({ address: htlc, abi: ETH_HTLC_ABI, functionName: 'lock',
  args: [bob, zeroAddress, amount, `0x${toHex(hash)}`, BigInt(deadline)], value: amount })
// the counterparty can compute the id in advance and watch for it
const id = ethHtlcSwapId({ chainId: 1, contract: htlc, sender: alice, recipient: bob, amount, hash, deadline })
// claim(id, preimage) emits Claimed(id, preimage): the other side reads the secret from the event

The contract has no owner and no fees. It refuses fee-on-transfer tokens, and its claim and refund windows don't overlap. It has not been audited yet: don't lock real funds before it is.

Before you swap:

  • tBtc must expire well before tArrk, with hours of margin for confirmations and downtime;
  • wait for the counterparty's lock to be final before locking yours;
  • keep a watcher running until both legs settle, because a swap does not force the other side to finish;
  • the Bitcoin script pins the preimage to 32 bytes (OP_SIZE 32), matching Arrekta's rule. Do not use a Bitcoin HTLC without that check.