Trustless P2P Atomic Swaps — Bitcoin, Fractal Bitcoin, Litecoin, Bellscoin
Using Discreet Log Contracts (DLCs) with Adaptor Signatures on Taproot
|
Bitcoin BTC |
Fractal Bitcoin FB |
Litecoin LTC |
Bellscoin BEL |
Non-Custodial · Atomic · On-Chain Verified · Open Protocol
Bitcoin and Fractal Bitcoin and few other bitcoin forks, share the same architecture, the same scripting language, and the same UTXO model — yet moving value between them today requires trusting a centralized bridge with your funds. Wrapped tokens introduce counterparty risk. Centralized exchanges add KYC friction, withdrawal delays, and custodial exposure. Users deserve a way to swap BTC ↔ FB that is as trustless as Bitcoin itself.
NexumBit exists because cross-chain swaps should not require trust.
NexumBit is a non-custodial, peer-to-peer atomic swap protocol purpose-built for Bitcoin and Fractal Bitcoin. It replaces traditional HTLC-based bridges with Discreet Log Contracts (DLCs) on Taproot, using adaptor signatures to cryptographically bind two independent on-chain transactions into a single atomic operation.
- No wrapped tokens. You send real BTC; you receive real FB (and vice versa).
- No custodian. Funds are locked in on-chain Taproot contracts that only the rightful owner can spend.
- No trust. Every rule — who can claim, when refunds unlock, how amounts are verified — is enforced by Bitcoin Script on both chains.
- No revealed secrets. Unlike HTLC preimages, adaptor secrets never appear on-chain, preserving privacy.
The backend serves as a matchmaker and PSBT builder — it pairs compatible orders, constructs the contracts, and pre-signs its part of the adaptor signatures. It never holds keys, never custodies funds, and can be replaced without affecting locked contracts.
- Two users create opposite orders — one wants to send BTC and receive FB; the other wants to send FB and receive BTC.
- The backend matches them, generates a shared adaptor secret, and builds Taproot DLC addresses on both chains — each with a claim path (requiring the adaptor secret + receiver's key) and a refund path (requiring only the sender's key after a timelock).
- Both users fund their respective DLCs — User A sends BTC to DLC A; User B sends FB to DLC B. The backend monitors confirmations on both chains.
- Once both are confirmed, claims become available. When either user claims, the adaptor secret is revealed to the counterparty through the pre-signed adaptor signature, enabling both sides to claim. This is the atomicity guarantee.
- If anything goes wrong, timelocks ensure each user can reclaim their own funds after expiry — no counterparty cooperation needed.
The entire flow is coordinated through PSBTs (Partially Signed Bitcoin Transactions) that the user signs in their own wallet. The backend constructs them; the user approves them. Sovereignty is never surrendered.
- Overview
- Architecture
- Protocol Flow
- On-Chain Construction
- Adaptor Signatures & Atomicity
- Timelock Security Model
- Cross-Swap Data Linking
- Failure Scenarios & Recovery
- Worked Example
- API Reference
- Configuration Parameters
- BIP Compliance
- License
- What to do next — grants and using the MVP
NexumBit is a fully non-custodial, peer-to-peer bridge supporting Bitcoin (BTC), Fractal Bitcoin (FB), Litecoin (LTC), and Bellscoin (BEL) — Taproot-capable chains with a shared script model.
The protocol uses Discreet Log Contracts (DLCs) built on Taproot (P2TR) outputs with adaptor signatures to achieve atomic cross-chain swaps. At no point does any third party hold user funds. The NexumBit backend acts solely as a matchmaker and PSBT builder — all value transfer happens on-chain, verified by Bitcoin Script.
| Property | Mechanism |
|---|---|
| Non-custodial | Funds locked in on-chain Taproot contracts; backend never holds keys |
| Atomic | Shared adaptor secret ensures both claims succeed or neither does |
| Trustless | Bitcoin Script enforces all conditions; backend is replaceable |
| Private | Adaptor secrets are never revealed on-chain (unlike HTLC preimages) |
| Recoverable | Timelock refund paths guarantee fund recovery without counterparty |
┌──────────────────┐ ┌──────────────────┐
│ User A │ │ User B │
│ (sends BTC) │ │ (sends FB) │
│ UniSat Wallet │ │ UniSat Wallet │
└────────┬─────────┘ └────────┬─────────┘
│ │
│ HTTPS/JSON │ HTTPS/JSON
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ NexumBit Backend │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Matching │ │ DLC Builder │ │ PSBT Builder │ │
│ │ Service │ │ │ │ │ │
│ │ ────────── │ │ ────────── │ │ ────────────── │ │
│ │ Pairs │ │ Generates │ │ Builds funding, │ │
│ │ compatible │ │ adaptor │ │ claim, and refund │ │
│ │ orders by │ │ secrets & │ │ PSBTs with pre- │ │
│ │ rate and │ │ Taproot │ │ embedded adaptor │ │
│ │ amount │ │ scripts │ │ signatures │ │
│ └─────────────┘ └──────────────┘ └────────────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Swap │ │ Script │ │ Taproot │ │
│ │ Monitor │ │ Builder │ │ Helpers │ │
│ │ ────────── │ │ ────────── │ │ ────────────── │ │
│ │ Watches │ │ success & │ │ Leaf hashes, │ │
│ │ mempool + │ │ refund │ │ merkle trees, │ │
│ │ confirms │ │ Tapscripts │ │ tweaked keys, │ │
│ │ for both │ │ (BIP-342) │ │ control blocks │ │
│ │ chains │ │ │ │ (BIP-341) │ │
│ └─────────────┘ └──────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Bitcoin Network │ │ Fractal Bitcoin │
│ (BTC) │ │ (FB) │
│ + conf required │ │ + conf required │
└──────────────────┘ └──────────────────┘
Every swap progresses through a deterministic state machine. Invalid transitions are rejected by the SwapStateMachine validator.
stateDiagram-v2
[*] --> WAITING_FOR_MATCH: User creates order
WAITING_FOR_MATCH --> MATCHED: Auto or manual match found
WAITING_FOR_MATCH --> CANCELLED: User cancels
MATCHED --> FUND_A: User funds DLC A
MATCHED --> WAITING_FOR_MATCH: User unmatches
MATCHED --> CANCELLED: User cancels (before funding)
FUND_A --> WAIT_CONFS: Tx detected on-chain
FUND_A --> REFUND_AVAILABLE: Counterparty timeout
WAIT_CONFS --> READY_TO_CLAIM: Both sides fully confirmed
WAIT_CONFS --> REFUND_AVAILABLE: Counterparty timeout
READY_TO_CLAIM --> DONE: Claim broadcast
READY_TO_CLAIM --> REFUND_AVAILABLE: Emergency timeout
REFUND_AVAILABLE --> REFUNDED: Refund broadcast
REFUND_AVAILABLE --> READY_TO_CLAIM: Recovery (counterparty appeared)
DONE --> [*]
REFUNDED --> [*]
CANCELLED --> [*]
- User A posts an order: "I want to swap 0.00001010 BTC for ~1.535 FB"
- User B posts an order: "I want to swap 1.535 FB for ~0.00001010 BTC"
- Matching Service finds them compatible (amounts and rates within configured tolerance)
- Backend generates a single shared adaptor secret
sand public pointP = s·G - Backend builds two DLC contracts:
- DLC A on BTC: User A locks BTC; User B can claim with adaptor sig + their key
- DLC B on FB: User B locks FB; User A can claim with adaptor sig + their key
- Both users fund their DLC A (sign and broadcast funding transactions)
- Swap Monitor watches both chains for confirmations (3 for BTC, 10 for FB)
- Once both sides are confirmed, state transitions to
READY_TO_CLAIM - User A claims DLC B (FB) using a pre-signed adaptor signature + their own key
- User B claims DLC A (BTC) using a pre-signed adaptor signature + their own key
- Both swaps marked
DONE
sequenceDiagram
participant A as 👤 User A (BTC → FB)
participant BE as 🖥️ NexumBit Backend
participant B as 👤 User B (FB → BTC)
participant BTC as ₿ Bitcoin Chain
participant FB as 🔷 Fractal Chain
Note over A,B: 1. Order Creation
A->>BE: POST /swap/create (BTC→FB, auto_match=true)
Note over BE: State: WAITING_FOR_MATCH
B->>BE: POST /swap/create (FB→BTC, auto_match=true)
Note over BE: 2. Matching & DLC Generation
Note over BE: Generate shared adaptor_secret (s)
Note over BE: adaptor_point P = s·G
Note over BE: Build DLC A (BTC) & DLC B (FB)
Note over BE: Both → MATCHED
BE-->>A: DLC A address (BTC) + funding PSBT
BE-->>B: DLC A address (FB) + funding PSBT
Note over A,B: 3. Funding
A->>BTC: Sign & broadcast DLC A funding tx
A->>BE: POST /confirm-dlc-a {txid}
B->>FB: Sign & broadcast DLC A funding tx
B->>BE: POST /confirm-dlc-a {txid}
Note over BE: Both → WAIT_CONFS
Note over A,B: 4. Confirmation Monitoring
loop Every 30 seconds
BE->>BTC: Check confirmations (need 3)
BE->>FB: Check confirmations (need 10)
Note over BE: Sync dlc_b_confirmations cross-swap
end
Note over BE: Both confirmed → READY_TO_CLAIM
Note over A,B: 5. Claiming
A->>BE: POST /claim-dlc-b
BE-->>A: Claim PSBT (adaptor sig pre-embedded)
A->>FB: Sign with own key + broadcast
B->>BE: POST /claim-dlc-b
BE-->>B: Claim PSBT (adaptor sig pre-embedded)
B->>BTC: Sign with own key + broadcast
Note over BE: Monitor detects DLC B spent → DONE
Each DLC output is a Taproot (P2TR) address containing two spending paths in a script tree:
flowchart TD
subgraph P2TR["DLC Output — P2TR"]
IK["Internal Key<br/>(deterministic, no known private key)"]
end
P2TR --> SUCCESS
P2TR --> REFUND
subgraph SUCCESS["🟢 Success Path — Claim"]
S1["OP_DATA_32 <adaptor_xonly>"]
S2["OP_CHECKSIGVERIFY"]
S3["OP_DATA_32 <receiver_xonly>"]
S4["OP_CHECKSIG"]
S5["<b>Witness:</b> <adaptor_sig> <receiver_sig>"]
S6["No timelock — always spendable"]
end
subgraph REFUND["🔴 Refund Path — Timeout"]
R1["OP_PUSH <timeout_height>"]
R2["OP_CHECKLOCKTIMEVERIFY"]
R3["OP_DROP"]
R4["OP_DATA_32 <sender_xonly>"]
R5["OP_CHECKSIG"]
R6["<b>Witness:</b> <sender_sig>"]
R7["Only after nLockTime ≥ timeout"]
end
The claim script requires two signatures: one from the adaptor point (pre-signed by the backend using the shared secret) and one from the receiver's key.
<adaptor_xonly_pubkey> OP_CHECKSIGVERIFY
<receiver_xonly_pubkey> OP_CHECKSIG
Witness stack (bottom to top):
<receiver_schnorr_signature>
<adaptor_schnorr_signature>
<success_script>
<control_block>
The adaptor signature is constructed server-side from the shared adaptor secret, then embedded into the claim PSBT. The user only needs to add their own Schnorr signature.
The refund script allows the original sender to reclaim funds after a block height timeout:
<timeout_block_height> OP_CHECKLOCKTIMEVERIFY OP_DROP
<sender_xonly_pubkey> OP_CHECKSIG
Witness stack:
<sender_schnorr_signature>
<refund_script>
<control_block>
Transaction must set nLockTime >= timeout_block_height.
The DLC address is derived following BIP-341 Taproot output construction:
1. Build leaf scripts:
success_script = CHECKSIGVERIFY(adaptor) + CHECKSIG(receiver)
refund_script = CLTV(timeout) + CHECKSIG(sender)
2. Compute leaf hashes (BIP-341 TapLeaf):
leaf_hash = TaggedHash("TapLeaf", 0xC0 || compact_size(script) || script)
3. Build merkle tree:
merkle_root = TaggedHash("TapBranch", sort(success_hash, refund_hash))
4. Derive internal key:
internal_key = deterministic_from(adaptor_point, receiver, sender, timeout)
5. Tweak to output key:
tweak = TaggedHash("TapTweak", internal_key || merkle_root)
output_key = internal_key + tweak·G
6. Encode as bech32m address:
address = bech32m_encode("bc", 1, output_key)
Critical: Leaf version MUST be
0xC0(Tapscript). Using0x00creates unspendable outputs per BIP-342.
All transactions are built as PSBTs (BIP-174 / BIP-370) and signed client-side via the UniSat wallet:
| Transaction | Built By | Signed By | Contains |
|---|---|---|---|
| Funding | Backend | User (UniSat) | Sends exact amount to DLC P2TR address |
| Claim | Backend | User (UniSat) | Spends DLC via success path; adaptor sig pre-embedded |
| Refund | Backend | User (UniSat) | Spends DLC via refund path after timeout; nLockTime set |
Claim PSBTs include the adaptor signature in taproot_sigs (BIP-371), so the user only signs with their own key.
Unlike HTLCs (which reveal a preimage on-chain via OP_HASH160), DLCs use adaptor signatures — a cryptographic construction where knowledge of a secret scalar allows completing an otherwise-incomplete Schnorr signature.
1. Backend generates random scalar: s (adaptor secret)
2. Derives public point: P = s · G (adaptor point)
3. Both DLC scripts include P as the adaptor_pubkey
4. Backend creates Schnorr signature using s for each claim PSBT
5. User adds their own signature to complete the witness
The adaptor secret s is the atomic link between both DLCs. Both claim transactions require a valid signature under the adaptor point P, and only someone who knows s can produce that signature.
DLC A (BTC chain): claim requires sig_from(adaptor_secret) + sig_from(User B key)
DLC B (FB chain): claim requires sig_from(adaptor_secret) + sig_from(User A key)
Both DLCs use the same adaptor point P. The backend holds s and pre-signs both adaptor signatures. Since both users receive their claim PSBTs with adaptor sigs already embedded, both can claim. If one claims, the other can always claim too (the adaptor sig is already in their PSBT).
If neither claims, both can refund after their respective timelocks expire.
The adaptor signature is pre-embedded into the claim PSBT by the backend using standard BIP-371 Taproot PSBT fields. This means:
- The adaptor secret is never transmitted to users over the network
- Users only see the adaptor point (public, safe to share)
- The backend constructs the adaptor signature and embeds it into the PSBT
- Users sign only with their own private key via their wallet
Timeline:
Block 0 Block T_A Block T_B
│ │ │
▼ ▼ ▼
├─── DLC A valid ─┤ │
│ (claim ok) │ refund available │
│ │ │
├──────────── DLC B valid ───────────────┤
│ (claim ok) │ refund available
- DLC A timeout (shorter): allows the first funder to reclaim sooner if counterparty disappears
- DLC B timeout (longer): gives the second funder adequate time to fund and claim
This ordering is critical:
- User A funds DLC A first (the one with the shorter timeout)
- User B sees DLC A funded, then funds DLC B
- Both claim during the window when both DLCs are active
- If User B never funds, User A can refund DLC A after
T_A - If User A claims DLC B but somehow User B can't claim DLC A, User B refunds DLC B after
T_B
| Attack | Prevention |
|---|---|
| Double-spend (RBF) | Claims only allowed after full confirmations (3 BTC / 10 FB) |
| Counterparty disappears | Timelock refund path guarantees fund recovery |
| One-sided claim | Shared adaptor secret means if one can claim, both can |
| Reorg attack | Confirmation gates prevent premature claiming |
| Backend compromise | Backend never holds user keys; worst case = DoS, not theft |
Both sides must reach their required confirmation targets before either side can claim:
┌───────────────────────────┐
│ BOTH chains confirmed? │
│ BTC ≥ target AND │
│ FB ≥ target │
└────────────┬──────────────┘
│ YES
▼
┌───────────────────────────┐
│ READY_TO_CLAIM │
│ Both users can claim │
└───────────────────────────┘
This prevents a scenario where User A claims on a fast-confirming chain while their own funding transaction gets reorganized.
When two swaps are matched, their DLC contracts are cross-referenced:
flowchart LR
subgraph SwapA ["Swap A — User A sends BTC"]
A_dlcA["<b>DLC A</b><br/>BTC chain<br/>User A funds here"]
A_dlcB["<b>DLC B</b><br/>FB chain<br/>User A claims here"]
end
subgraph SwapB ["Swap B — User B sends FB"]
B_dlcA["<b>DLC A</b><br/>FB chain<br/>User B funds here"]
B_dlcB["<b>DLC B</b><br/>BTC chain<br/>User B claims here"]
end
A_dlcB ---|"Same address"| B_dlcA
B_dlcB ---|"Same address"| A_dlcA
A_dlcA ---|"dlc_b_confs synced"| B_dlcA
B_dlcA ---|"dlc_b_confs synced"| A_dlcA
- Swap A's DLC B = Swap B's DLC A (same on-chain address on FB)
- Swap B's DLC B = Swap A's DLC A (same on-chain address on BTC)
- Both DLCs share the same adaptor point (derived from the same secret)
- Confirmation counts are synced bidirectionally
flowchart TD
Start["Both users matched"] --> BothFund{Both fund<br/>their DLC A?}
BothFund -->|Yes| BothConfirm{Both reach<br/>confirmation targets?}
BothFund -->|"Only one funds"| TimeoutCheck{"Timeout<br/>block reached?"}
BothFund -->|"Neither funds"| NothingHappens["No action needed<br/>Cancel anytime"]
TimeoutCheck -->|Yes| RefundAvailable["REFUND_AVAILABLE<br/>Funder signs refund PSBT"]
TimeoutCheck -->|No| WaitMore["Keep waiting<br/>in WAIT_CONFS"]
RefundAvailable --> Refunded["REFUNDED ✓"]
BothConfirm -->|Yes| ReadyToClaim["READY_TO_CLAIM"]
BothConfirm -->|No| WaitConfs["WAIT_CONFS<br/>Monitor polls both chains"]
WaitConfs --> BothConfirm
ReadyToClaim --> UserClaims{User claims?}
UserClaims -->|Yes| Done["DONE ✓"]
UserClaims -->|"Never claims"| StillClaimable["Stays claimable<br/>(no expiry on claim)"]
StillClaimable -->|"After DLC timeout"| BothPaths["Both paths valid<br/>First to broadcast wins"]
For eligible swap states, users can download a Recovery Kit containing all data needed to independently complete or exit the swap without the NexumBit backend. This ensures full self-sovereignty — even if the backend goes offline permanently, users can always recover their funds using standard Bitcoin tooling.
A simplified walkthrough of a completed BTC ↔ FB swap:
| User A | User B | |
|---|---|---|
| Direction | BTC → FB | FB → BTC |
| Sends | X sats on BTC | Y sats on FB |
| Receives | Y sats on FB | X sats on BTC |
Shared adaptor point: P (same for both DLCs, derived from a single random secret s)
DLC A (BTC chain) — User A locks X sats:
Address: bc1p<taproot_address_A>
Timeout: Block H_a (current_btc_height + timeout_delta)
Success script: <P_xonly> CHECKSIGVERIFY <userB_xonly> CHECKSIG
Refund script: H_a CLTV DROP <userA_xonly> CHECKSIG
DLC B (FB chain) — User B locks Y sats:
Address: bc1p<taproot_address_B>
Timeout: Block H_b (current_fb_height + timeout_delta, where H_b > H_a in real time)
Success script: <P_xonly> CHECKSIGVERIFY <userA_xonly> CHECKSIG
Refund script: H_b CLTV DROP <userB_xonly> CHECKSIG
1. User A funds DLC A on BTC chain → bc1p<address_A>
2. User B funds DLC B on FB chain → bc1p<address_B>
3. Monitor confirms both chains reach required confirmations ✓
4. User A claims DLC B on FB:
Witness: <adaptor_sig> <userA_sig> <success_script> <control_block>
5. User B claims DLC A on BTC:
Witness: <adaptor_sig> <userB_sig> <success_script> <control_block>
6. Both swaps → DONE ✓
The Claim PSBTs contains the adaptor signature pre-embedded in taproot_sigs. The user only needs to add their own Schnorr signature.
| Parameter | Description |
|---|---|
CONF_BTC |
Required Bitcoin confirmations before claim is allowed |
CONF_FB |
Required Fractal Bitcoin confirmations before claim is allowed |
TIMEOUT_A |
DLC A refund timeout — shorter, protects the first funder |
TIMEOUT_B |
DLC B refund timeout — longer, gives second funder more time |
INTENT_TTL |
How long an unmatched order stays active before expiring |
SLIPPAGE_BPS |
Configurable per-order slippage tolerance for auto-matching |
Exact values are configurable at deployment and not disclosed here.
| BIP | Usage |
|---|---|
| BIP-174 | PSBT v0 format for all transaction construction |
| BIP-341 | Taproot output construction, merkle trees, tweaked keys |
| BIP-342 | Tapscript execution (leaf version 0xC0) |
| BIP-340 | Schnorr signatures for all script-path spending |
| BIP-322 | Message signing for wallet ownership verification |
| BIP-371 | Taproot PSBT fields (taproot_sigs, tap_leaf_script) |
This protocol specification and the open-source DLC builder are released under the MIT License:
The protocol is based on well-established Bitcoin primitives (Taproot, Schnorr signatures, CLTV timelocks) and does not rely on any proprietary or patented technology. Chain logos in this document are used for identification; see each project’s terms for logo usage (Bitcoin, Litecoin, Fractal Bitcoin, Bellscoin/Nintondo).
Built in Solitude · Powered by Bitcoin Script