This document specifies the VEIL protocol — a Blind Escrow Network layered on the Blocknet privacy chain. It defines the architecture, cryptographic constructions, cross-chain settlement model, state machine semantics, p2p integration, fee economics, timeout/refund mechanics, and threat model for a trustless, private, cross-chain exchange system in which proof-of-work miners serve as blind escrow infrastructure without awareness of the escrow function they provide.
This is a protocol specification intended for engineering review and implementation. It supersedes the v0 spec by removing the escrow validator quorum, adopting a pure atomic swap settlement model, and introducing a hub-and-spoke cross-chain architecture with BNT as the settlement medium.
This document assumes familiarity with Pedersen commitments, Bulletproofs, CLSAG ring signatures, stealth addresses, Schnorr signatures, and Dandelion++, all of which are implemented in the Blocknet codebase.
These are the invariants the protocol must satisfy. Any design choice that violates one of these is wrong.
If miners can steal, the design failed. No miner, at any point, on any chain, gains the ability to unilaterally spend escrowed funds.
If miners must read a trade to validate it, the design failed. Miners validate cryptographic proofs. They do not interpret trade semantics, identify counterparties, or distinguish escrow transactions from regular transfers.
No trusted third party. No validator quorum, no federation, no multisig committee, no oracle. Settlement is enforced by mathematics and timelocks, not by any entity's honesty.
Self-custody throughout. Funds are controlled by the user's keys at every stage. Lock transactions are spendable only by the user (via refund after timeout) or by the counterparty (via adaptor secret after settlement). No intermediate custodian ever holds spending authority.
Worst case is a timeout. The worst outcome for any participant in any trade is a temporary lock of funds followed by an automatic refund. Funds are never permanently lost, stuck, or stolen by the protocol.
Privacy is structural, not optional. The Blocknet leg of every trade is fully private by construction — hidden amounts, hidden sender, hidden receiver, hidden purpose. This is not a feature flag. It is the architecture.
Maker. A user who publishes a trade intent (an order) to the p2p network. The maker's node must remain online for the order to be discoverable. When the maker goes offline, the order disappears. The maker retains full custody of their funds at all times until they construct a lock transaction, and retains refund capability even after locking via a pre-signed timelock transaction.
Taker. A user who discovers a maker's order, initiates contact through a private negotiation channel, agrees on terms, and participates in the atomic swap protocol. Like the maker, the taker retains custody until locking and retains refund capability after locking.
Miner (Blocknet). A proof-of-work miner on the Blocknet chain. Miners validate transaction mathematics (commitment balance, range proofs, ring signatures, key image uniqueness) and include valid transactions in blocks. Miners earn standard transaction fees. They cannot distinguish escrow locks, escrow claims, escrow refunds, or regular transfers from one another. They are blind escrow agents by construction.
Miner/Validator (counterparty chain). The consensus participant on whatever chain the counterparty is using (Bitcoin miners, Ethereum validators, Solana validators, Monero miners). These participants process transactions on their own chain according to their own chain's rules. They have no awareness of the VEIL protocol. From their perspective, the transactions they process are ordinary transfers or script executions native to their chain.
Relay Peer. Any Blocknet node that participates in p2p message transport. Relay peers carry order intents, negotiation handshakes, and general network traffic. They see message envelopes but not cleartext payloads. Any full node is a relay peer.
The protocol assumes:
Counterparties do not trust each other. The escrow exists precisely because neither maker nor taker is willing to send first without guarantees. The adaptor signature construction ensures atomicity: either both sides settle or neither does.
The network is adversarial. Passive observers can see traffic patterns. Active adversaries can inject messages, drop messages, and attempt Sybil attacks on the p2p layer. The protocol does not rely on network honesty for safety, only for liveness.
Miners are honest-but-curious. Miners follow the consensus rules (because it is economically rational) but may attempt to extract information from the transactions they process. The privacy primitives ensure there is no information to extract.
Endpoints are trusted. If a user's wallet keys are compromised, the protocol cannot protect them. This is a standard assumption shared with all self-custodial systems.
Counterparty chains are untrusted. The protocol does not rely on the privacy properties of the counterparty chain. If the counterparty chain is transparent (Bitcoin, Ethereum, Solana), the transaction on that chain is visible. The protocol guarantees only that the Blocknet leg is private and that the two legs cannot be linked by an observer who does not hold the adaptor secret.
Not a cryptocurrency. Blocknet has its own private currency (BNT), but the Blind Escrow Network is infrastructure built on top of it. The blockchain is not primarily a payment ledger — it is an escrow medium. The native currency is the escrow instrument, and the transaction fees are the price miners charge for the escrow service they do not know they are providing.
Not a decentralized exchange. A DEX requires both assets to exist within the same execution environment or to be synthetically represented there. The Blind Escrow Network requires nothing of the sort. The order book lives on the p2p gossip layer, not on any chain. Matching happens between individual nodes. Settlement happens natively on each party's own blockchain. No shared virtual machine, no liquidity pools, no synthetic tokens.
Not a bridge. Bridges lock assets on one chain and mint representations on another. The bridge operator is a custodian regardless of its structure (multisig, validator set, smart contract). Bridges have been hacked for billions of dollars. The Blind Escrow Network creates no representations. No wrapped tokens, no synthetic assets, no IOUs. Both parties trade native assets on their native chains. The only thing that crosses chains is a scalar secret embedded in signatures.
Not an exchange. There is no operator. The order book is a protocol. The escrow is performed by miners who cannot see it. The settlement is executed by the trading parties themselves. There is no entity to register, regulate, or shut down. There is only software running on independent nodes following open specifications.
If every supported chain requires a direct adaptor to every other supported chain, the engineering cost grows quadratically. N chains require N*(N-1)/2 unique chain-pair adaptors, each with its own curve compatibility concerns, testing surface, and maintenance burden. 10 chains = 45 adaptors. 20 chains = 190. This does not scale.
The VEIL protocol uses a hub-and-spoke model. Every supported chain has one adaptor — to Blocknet. Cross-chain swaps between any two supported chains route through BNT as an intermediary.
A BTC→ETH swap becomes two atomic swaps: 1. BTC → BNT (atomic swap on Bitcoin and Blocknet) 2. BNT → ETH (atomic swap on Blocknet and Ethereum)
The user's wallet abstracts this into a single operation. The user sees "swap BTC for ETH." The software handles both legs.
N chains require N-1 adaptors. 10 chains = 9. 20 chains = 19. Adding a new chain means building one adaptor module. The protocol core does not change.
Engineering scalability. Each chain adaptor is an independent module that implements a standard interface (lock, claim, refund, proof-of-lock). The core protocol handles order book, negotiation, state machine, and the Blocknet-side cryptography. Chain adaptors handle the counterparty chain's on-chain construction using whatever that chain natively supports.
Liquidity concentration. All trading pairs are BNT-denominated. Liquidity concentrates in BNT pairs instead of fragmenting across every possible combination. BNT gains real utility as the settlement medium — every cross-chain swap flows through it.
Privacy by routing. The Blocknet leg of every swap is fully private. For a two-hop swap (BTC→BNT→ETH), an observer on Bitcoin sees a BTC transaction. An observer on Ethereum sees an ETH transaction. Neither observer can connect the two because the BNT intermediary step is fully private on Blocknet. The hub acts as a privacy mixer by construction, without any mixing protocol.
Latency and fee cost. Two swaps instead of one means roughly double the on-chain fees (across three chains instead of two) and double the settlement latency. For the v0 protocol, this is an acceptable tradeoff. Direct cross-chain adaptors (bypassing the BNT hop) can be added later as an optimization for high-volume pairs where latency matters.
The two legs of a hub-routed swap are individually atomic but not jointly atomic. If the first leg (BTC→BNT) completes and the second leg (BNT→ETH) fails, the user holds BNT. This is not a loss — BNT is a fungible asset that can be swapped again — but it is not a clean single-operation guarantee.
For v0, this is acceptable. The wallet should clearly communicate the two-step nature and handle retries on the second leg automatically. Future versions may explore mechanisms for joint atomicity across both hops, but this introduces significant complexity (chained adaptor secrets, three-party coordination) and is deferred.
The VEIL protocol builds on the following primitives, all implemented in
crypto-rs/src/ and exposed via C FFI to the Go layer in crypto.go:
Pedersen commitments (commitment.rs). Commitments of the form
C = v*H + r*G where v is the hidden value, r is the blinding factor,
and H, G are independent generators on Ristretto255. Used today for
transaction output amounts. In VEIL, additionally used for order amount
commitments, so that relay peers and miners cannot read trade values.
Bulletproofs (rangeproof.rs). Zero-knowledge range proofs over Pedersen
commitments, proving 0 <= v < 2^64 without revealing v. Used today for
transaction outputs. In VEIL, they accompany every committed value in order
intents and escrow lock outputs to prevent negative-value attacks.
CLSAG ring signatures (ring.rs). Linkable ring signatures with ring size
16, providing signer ambiguity. Used today for transaction input authorization.
In VEIL, CLSAG remains the spend authorization mechanism for lock outputs and
for refund/release transactions. The key image mechanism prevents
double-spending of locked funds.
RingCT (ring.rs). CLSAG extended with commitment linking via
pseudo-outputs, proving that the sum of input commitments equals the sum of
output commitments plus fee, without revealing any amounts. Used for all
value-transfer transactions including escrow lock and claim transactions.
Stealth addresses (stealth.rs). Dual-key stealth scheme on Ristretto255
using spend and view keypairs. The sender derives a one-time destination key
P = H(r * V) * G + S where V is the receiver's view public key, S is
the spend public key, and r is an ephemeral scalar. In VEIL, stealth
addresses are used for lock output destinations, for unlinkable order
publisher identities, and for establishing encrypted channels between
counterparties.
Schnorr signatures (keys.rs). Standard Schnorr over Ristretto255 with
domain-separated nonce and challenge derivation. Used today for wallet-level
signing. In VEIL, Schnorr signatures are the basis for adaptor signature
constructions on the Blocknet side.
Dandelion++ (p2p/dandelion.go). Transaction propagation privacy with
stem phase (90% forwarding probability) and fluff phase. Stem epochs rotate
every 10 minutes. Used today for transaction broadcast. In VEIL, extended to
cover order intent messages and all escrow-related p2p communication.
Argon2id PoW (pow.rs). Memory-hard proof of work with 2GB memory
requirement. Used for block mining. Miners who produce blocks process escrow
transactions without knowing they are escrow transactions.
Schnorr adaptor signatures (Ristretto255). An adaptor signature is a
Schnorr signature that has been deliberately shifted by a secret scalar t.
Given an adaptor point T = t * G, a signer produces an adaptor
pre-signature s' = k + e*x (standard Schnorr) shifted to s_adapt = s' + t
such that the pre-signature can be verified as a "promise" — anyone who holds
the pre-signature and later learns t can produce the valid signature, and
anyone who sees both the pre-signature and the completed signature can extract
t. This is the mechanism that makes cross-chain settlement atomic on
compatible curves.
Adaptor signatures over CLSAG. For spending Blocknet lock outputs, the
adaptor construction must work with CLSAG rather than plain Schnorr, because
Blocknet transactions use ring signatures for input authorization. Adaptor-
CLSAG constructions have been studied in the context of Monero atomic swaps
(DLSAG/adaptor-CLSAG research). The construction extends CLSAG signing so
that the completed ring signature reveals the adaptor secret t to anyone
who held the pre-signature. This is the most novel cryptographic component
in the protocol and requires careful implementation and formal review.
As a staging strategy, the v0 implementation can use a two-step flow: the
adaptor secret t is revealed through a plain Schnorr signature on an
auxiliary message, and the CLSAG spend uses t as a derived key component
rather than embedding the adaptor directly in the ring signature. This is
less elegant but uses only well-understood primitives. Full adaptor-CLSAG
integration can follow once the construction is formally reviewed.
Cross-group discrete log equality (DLEQ) proofs. For swaps between
Ristretto255 (Blocknet) and secp256k1 (Bitcoin, Ethereum) chains, both
parties need assurance that the same scalar secret t is committed on both
curves — that T_ristretto = t * G_ristretto and T_secp = t * G_secp use
the same t. A DLEQ proof proves this across different groups without
revealing t. The scalar t is just a number; it is valid on both curves
as long as it falls within the smaller curve's scalar field order (~2^252 for
Ristretto255, ~2^256 for secp256k1). The DLEQ proof itself is a standard
Schnorr-like proof computed in parallel over both groups. This construction
has been prototyped in the Farcaster/COMIT Monero↔BTC swap implementation.
Encrypted negotiation channel. Once a taker identifies a compatible order, the taker opens a direct encrypted stream to the maker using ephemeral X25519 key exchange derived from the Ristretto255 stealth keys (via standard Ristretto-to-Montgomery conversion). The channel uses ChaCha20-Poly1305 AEAD for confidentiality and integrity, with monotonic sequence numbers for replay protection and forward secrecy via ephemeral key agreement.
Every supported counterparty chain requires an adaptor module that implements the following interface:
interface ChainAdaptor {
// Construct a conditional lock output on the counterparty chain.
// The output is spendable only with knowledge of adaptor secret t.
ConstructLock(amount, adaptor_point T, timeout, recipient) → LockTx
// Construct a claim transaction that spends the lock output.
// The act of claiming reveals t (via signature or preimage).
ConstructClaim(lock_ref, adaptor_secret t, recipient) → ClaimTx
// Construct a refund transaction that returns locked funds after timeout.
ConstructRefund(lock_ref, timeout) → RefundTx
// Verify that a lock transaction on the counterparty chain is valid
// and confirmed to sufficient depth.
VerifyLock(lock_ref, expected_amount, adaptor_point T, timeout) → bool
// Extract the adaptor secret t from a completed claim transaction.
ExtractSecret(claim_tx, adaptor_pre_signature) → scalar t
// Monitor the counterparty chain for claim or refund events.
WatchChain(lock_ref) → Event{Claimed(t) | Refunded | Timeout}
}
The Blocknet-side construction (CLSAG/RingCT, Pedersen commitments, stealth addresses) is handled by the core protocol, not by an adaptor module. The adaptor modules handle only the counterparty chain's construction.
Chains: Monero, Solana, and other Ed25519/Ristretto255 chains.
Mechanism: Direct Schnorr adaptor signatures. The adaptor secret t works
natively on both curves because they share the same scalar field. No DLEQ
proof is needed. The adaptor pre-signature on one chain becomes completable
when the claim signature on the other chain reveals t.
Monero specifics: Monero uses Ed25519 with its own ring signature scheme (CLSAG with RingCT). The Monero↔Blocknet adaptor uses the adaptor-CLSAG construction (or the two-step Schnorr fallback described in section 4.2) on both sides. Monero already has production-tested atomic swap implementations (XMR↔BTC swaps from the COMIT/Farcaster project) that demonstrate the adaptor signature approach. The VEIL adaptor for Monero builds on this prior work.
Solana specifics: Solana uses Ed25519 for transaction signing. The adaptor
construction is straightforward Schnorr adaptor over Ed25519. Solana's
account model (rather than UTXO) means the lock is implemented as a program
(smart contract) that holds funds conditionally. The adaptor secret revelation
mechanism is the same: claiming on one side reveals t, enabling claiming on
the other. Solana is transparent, so the Solana leg of the swap is publicly
visible. The Blocknet leg remains fully private.
Privacy on Tier 1 chains: Monero provides full privacy (ring signatures, stealth addresses, RingCT). Solana provides none. The VEIL protocol guarantees only that the Blocknet side is private and that the two legs cannot be linked without the adaptor secret. The counterparty chain's privacy is whatever that chain natively provides.
Chains: Bitcoin (Taproot/BIP-340).
Mechanism: Schnorr adaptor signatures on secp256k1, combined with a cross-group DLEQ proof binding the adaptor point on secp256k1 to the adaptor point on Ristretto255.
During swap setup, both parties verify a DLEQ proof that
T_ristretto = t * G_ristretto and T_secp = t * G_secp for the same t.
This ensures the adaptor secret revealed on one chain is usable on the other,
despite the different curve groups.
Bitcoin Taproot specifics: Taproot key-path spends use BIP-340 Schnorr signatures over secp256k1. The lock output is a Taproot output whose key-path requires the adaptor secret (the internal key is tweaked by the adaptor point). The script-path contains the timelock refund condition. From the chain's perspective, a successful claim looks like a standard Taproot key-path spend (a single signature, 64 bytes, no script revealed). A refund looks like a script-path spend revealing only the timelock branch. Neither reveals the existence of a cross-chain swap.
Privacy on Tier 2 chains: Bitcoin provides pseudonymity (addresses are unlinkable without additional information) but not amount or graph privacy. The Bitcoin leg of a swap is visible as a UTXO being created and spent. Taproot key-path spends hide the script structure, so the swap mechanics are not visible, but the amounts and addresses are. The Blocknet leg remains fully private and unlinkable to the Bitcoin leg.
Chains: Bitcoin (pre-Taproot/legacy), Ethereum, EVM chains, and other ECDSA-based chains.
Mechanism: Hash-timelock contracts (HTLCs). The adaptor secret t is
replaced by a hash preimage s where H(s) = h is the hashlock. The lock
on the counterparty chain is a contract (or Bitcoin script) that releases
funds to whoever presents the preimage s, or refunds to the sender after
a timeout.
This is a fallback. HTLCs are less private than adaptor signatures because
the hash h appears on both chains, creating a linkable fingerprint. An
observer who sees a hashlock on Bitcoin and the same hashlock on Ethereum
can infer they are two legs of the same swap. However:
Ethereum specifics: The lock is a simple smart contract that holds ETH (or ERC-20 tokens) and releases them when presented with the preimage. The contract has two spend paths: claim (preimage + recipient signature) and refund (timeout + sender signature). The contract is minimal and auditable.
Legacy Bitcoin specifics: The lock is a standard HTLC script:
OP_IF OP_SHA256 <hash> OP_EQUALVERIFY <recipient_pubkey> OP_CHECKSIG
OP_ELSE <timeout> OP_CHECKLOCKTIMEVERIFY OP_DROP <sender_pubkey> OP_CHECKSIG
OP_ENDIF. This is a well-understood construction with years of production
use in Lightning Network and previous atomic swap implementations.
EVM chain specifics: Same contract as Ethereum, deployed on any EVM-compatible chain (Polygon, Arbitrum, BSC, etc.).
| Tier | Counterparty chain | Blocknet leg | Counterparty leg | Cross-chain link |
|---|---|---|---|---|
| 1 (Ed25519) | Monero | Fully private | Fully private (Monero) | Unlinkable (adaptor) |
| 1 (Ed25519) | Solana | Fully private | Transparent | Unlinkable (adaptor) |
| 2 (Schnorr/secp) | Bitcoin Taproot | Fully private | Pseudonymous, script hidden | Unlinkable (adaptor + DLEQ) |
| 3 (HTLC) | Bitcoin legacy | Fully private | Pseudonymous, HTLC visible | Linkable via hashlock on counterparty chain; Blocknet leg remains unlinkable |
| 3 (HTLC) | Ethereum/EVM | Fully private | Fully transparent | Linkable via hashlock on counterparty chain; Blocknet leg remains unlinkable |
For two-hop swaps routed through BNT, the privacy properties compound. A BTC(Taproot)→BNT→ETH swap: the BTC leg uses adaptor signatures (unlinkable), the ETH leg uses HTLCs (linkable on ETH side), but the BNT intermediary breaks the connection between BTC and ETH. An observer would need to compromise the Blocknet privacy layer to connect the two transparent legs.
A maker constructs an order intent containing:
Maker ephemeral identity: a one-time public key derived from the maker's stealth key pair, so that the order cannot be linked to the maker's wallet history or to other orders by the same maker.
Trading pair: a plaintext pair tag identifying the two assets (e.g.,
BNT/XMR, BNT/BTC). Visible because takers need to filter for relevant
orders. Low sensitivity — reveals what is being traded but not by whom,
how much, or whether the order is real.
Price: the maker's offered rate. Visible for the same reason — takers need to evaluate whether the offer is attractive. Harmless in isolation: no identity, no amount, no certainty that the order is real.
Amount commitment: C_offer = v_offer * H + r_offer * G, a Pedersen
commitment to the amount being offered, with an accompanying Bulletproof
range proof. This hides the trade size from the network while allowing the
maker to prove the amount is valid (positive, within range) without
revealing it.
Timeout preference: maximum duration the maker is willing to keep the order live and the escrow locked, expressed as a block-height range.
Signature: Schnorr signature under the ephemeral identity key, binding all fields and proving the order was constructed by the holder of the corresponding private key.
The order is broadcast via Dandelion++ to the p2p network. It enters each receiving node's local order book. There is no global order book consensus; each node maintains its own view based on received messages and expiry policy.
Orders are ephemeral. They expire when:
Expired orders are garbage-collected from local order books. There is no persistent order history on the network. Historical order data is an intelligence asset for surveillance, and the protocol treats it as toxic waste.
A taker scans their local order book, filtering by trading pair and price. When a compatible order is found:
Taker generates an ephemeral identity key pair (same stealth-derived construction as the maker's).
Taker derives a shared secret from the maker's ephemeral public key using ECDH and opens a direct encrypted channel to the maker's node.
Inside the encrypted channel, taker and maker reveal their actual terms to each other: exact amounts, receiving addresses on each chain, acceptable timeout ranges, and fee expectations. This is the only context in which cleartext trade terms exist, and it is strictly between the two counterparties.
If terms are acceptable to both sides, they proceed to swap setup. If not, the channel closes and no trace of the negotiation is recorded.
The negotiation channel uses forward secrecy: each session derives fresh symmetric keys, so compromising a long-term key does not retroactively reveal past negotiations.
Every node periodically broadcasts decoy orders: structurally identical to real orders, with valid Pedersen commitments, valid Bulletproofs, valid signatures under throwaway ephemeral keys, and plausible trading pairs and prices. Decoy orders are indistinguishable from real orders at the protocol level.
A taker who attempts to negotiate a decoy order receives no response (the throwaway key has no corresponding online node) or receives a deliberate non-response after a plausible delay. From the taker's perspective, this looks identical to a maker who went offline or rejected the terms.
The decoy rate is configurable per node. Decoy generation uses the same Pedersen commitment and Bulletproof machinery as real orders, making each decoy computationally non-trivial to produce. This naturally bounds the decoy rate per node.
Once maker and taker agree on terms through the encrypted negotiation channel, they jointly derive the swap context:
Swap context ID. Both parties compute:
swap_id = H("veil_swap_ctx" || maker_ephemeral_pub || taker_ephemeral_pub || nonce)
where nonce is a random value contributed by both parties (each contributes
half, concatenated). The swap_id is a 32-byte identifier unique to this
swap, unlinkable to either party's long-term identity.
Adaptor secret generation. One party (conventionally the maker) generates a
random scalar t and computes the adaptor point T = t * G. The adaptor
point T is shared with the counterparty. The scalar t is known only to
the maker until settlement.
For cross-curve swaps (Tier 2), the maker also computes T_secp = t * G_secp
and produces a DLEQ proof that both adaptor points commit to the same t.
The taker verifies this proof.
For HTLC-based swaps (Tier 3), the maker generates a random preimage s,
computes h = SHA256(s), and shares h with the taker.
Timelock negotiation. Both parties agree on timelock parameters:
T_claim: the block height by which the first claimant must claim (on the
counterparty's chain). If the first claim does not occur by this height,
the second party's refund activates.T_refund: the block height after which the maker can reclaim their locked
BNT on Blocknet. Must be set after T_claim with sufficient margin.The timelocks are staggered: the party who claims second (the maker, who claims on the counterparty chain) has the shorter effective window. This prevents the free-option attack where one party could lock, wait for price movement, and only complete if favorable.
Pre-signed refund transactions. Before either party broadcasts their lock transaction, both construct and exchange pre-signed refund transactions. These are fully valid transactions that spend the lock output back to its original owner but carry a timelock — they cannot be confirmed until the specified block height has passed.
On Blocknet, the refund transaction is a standard RingCT transaction
(CLSAG-signed, Pedersen-committed, stealth-addressed) that spends the lock
output back to the maker. It is valid only after T_refund. On the
counterparty chain, the refund uses that chain's native timelock mechanism
(OP_CHECKLOCKTIMEVERIFY for Bitcoin, block.timestamp for Ethereum, etc.).
Both parties construct and broadcast lock transactions on their respective chains. The order of locking matters:
Maker locks BNT on Blocknet first. The lock transaction is a standard Blocknet transaction constructed as follows:
The output destination is a modified stealth address. The one-time public
key is P_lock = P_normal + T, where P_normal is the standard
stealth-derived one-time key and T is the adaptor point. This means
the output can only be spent by someone who knows both the stealth-derived
private key and the adaptor secret t.
The output amount is Pedersen-committed and Bulletproof-proven.
The transaction is signed with CLSAG/RingCT using ring members from the existing UTXO set. Ring size 16.
The transaction is broadcast via Dandelion++ and confirmed on the Blocknet chain. To a miner or any observer, it looks exactly like any other Blocknet transaction. There is no escrow flag, no special transaction type, no distinguishing metadata.
Taker verifies the Blocknet lock. The taker's wallet monitors the Blocknet chain, detects the lock output via stealth scanning (the taker knows the adaptor point and can check the modified stealth key), and verifies:
T is correctly incorporated into the output key.The timelock parameters are correct.
Taker locks on the counterparty chain. Once the Blocknet lock is verified, the taker constructs the corresponding lock on the counterparty chain using the appropriate chain adaptor module:
Tier 1 (Ed25519): An output locked to a key incorporating the same
adaptor point T. Spendable only with t.
T_secp (bound to T_ristretto via DLEQ proof).
Script-path contains the timelock refund.Tier 3 (HTLC): A hash-timelock contract with hashlock h = SHA256(s)
and timelock T_claim.
Maker verifies the counterparty lock. The maker's wallet monitors the counterparty chain and verifies the lock is valid and confirmed to sufficient depth.
Settlement is where the adaptor signature construction enforces atomicity.
Step 1: Maker claims on the counterparty chain. The maker knows t (they
generated it). They use t to spend the lock output on the counterparty
chain, sending the locked funds to their own address. The act of claiming
reveals t:
Tier 1/2 (adaptor signatures): The maker produces a completed adaptor
signature. The taker holds the pre-signature (exchanged during setup). By
comparing the completed signature (visible on-chain after the claim) with
the pre-signature, the taker extracts t. This is a mathematical
consequence of the Schnorr adaptor construction: t = s_complete - s_pre
where s_complete is the completed signature scalar and s_pre is the
pre-signature scalar.
Tier 3 (HTLC): The maker presents the hash preimage s to the
hashlock contract. The preimage is now visible on the counterparty chain.
The taker reads s from the chain.
Step 2: Taker claims on Blocknet. The taker now knows t (extracted from
the maker's claim). They compute the full spending key for the Blocknet lock
output: x_spend = x_stealth + t, where x_stealth is the stealth-derived
one-time private key and t is the adaptor secret. They construct a standard
Blocknet RingCT transaction spending the lock output to their own wallet
address. The Blocknet miner processes this as a regular transaction, earns a
standard fee, and has no idea that an escrow just settled.
Atomicity guarantee. The adaptor construction ensures:
t).T_claim, the taker's refund on the
counterparty chain activates and the maker's refund on Blocknet activates
after T_refund. Both sides get their funds back.t is revealed by claiming, the other side's claim
becomes possible. Before t is revealed, neither lock can be spent.If the swap does not complete within the agreed timelock window:
Counterparty chain refund first. After T_claim, the taker can reclaim
their locked funds on the counterparty chain using the refund path (timelock
script for Bitcoin, timeout function for Ethereum, etc.).
Blocknet refund second. After T_refund (which is later than
T_claim), the maker can broadcast their pre-signed refund transaction to
reclaim the locked BNT. The Blocknet miner processes this as a regular
transaction.
The staggered timelocks ensure that the maker cannot claim on the counterparty
chain after the taker has already refunded. T_refund > T_claim guarantees
that the maker must claim (revealing t) before the taker's refund window
opens. If the maker does not claim in time, both sides refund safely.
For a swap between two non-BNT assets (e.g., BTC→ETH), the protocol executes two sequential atomic swaps through BNT:
Leg 1: BTC → BNT
- The user (Alice) is the taker, matching with a BNT/BTC maker (Bob).
- Bob locks BNT on Blocknet. Alice verifies. Alice locks BTC on Bitcoin.
Bob verifies. Bob claims BTC (reveals t1). Alice claims BNT.
Leg 2: BNT → ETH
- Alice is now the maker with BNT, matching with a BNT/ETH taker (Carol).
- Alice locks BNT on Blocknet. Carol verifies. Carol locks ETH on Ethereum.
Alice verifies. Alice claims ETH (reveals t2). Carol claims BNT.
Each leg uses independent adaptor secrets (t1, t2). Each leg is
individually atomic. The wallet orchestrates both legs and handles the BNT
intermediary state automatically.
If Leg 1 completes but Leg 2 fails to match or settle, Alice holds BNT. The wallet can retry Leg 2, or Alice can hold BNT, or she can swap BNT back to BTC via a new Leg 1 in reverse. There is no fund loss — only a temporary state where the user holds the intermediary asset.
┌──────────────┐
│ Expired │
└──────────────┘
↑
┌──────┐ ┌────────────┐ ┌─────────┐ │ ┌──────────────┐ ┌──────────────┐
│ Init │───→│ Advertised │───→│ Matched │─┤─→│ MakerLocked │───→│ TakerLocked │
└──────┘ └────────────┘ └─────────┘ │ └──────────────┘ └──────────────┘
│ │ │ │ │
↓ ↓ │ ↓ │
┌──────────┐ ┌──────────┐ │ ┌──────────┐ │
│ Canceled │ │ Rejected │ │ │ Failed │ │
└──────────┘ └──────────┘ │ └──────────┘ │
│ ↓
│ ┌──────────────────┐
│ │ LocksVerified │
│ └──────────────────┘
│ │ │
│ ↓ ↓
│ ┌────────────┐ ┌──────────┐
└─────────────→│ Settled │ │ Refunded │
└────────────┘ └──────────┘
Terminal states: Canceled, Rejected, Expired, Failed, Settled,
Refunded.
Init → Advertised - Pre: Maker has constructed a valid order intent with all required fields (ephemeral identity, pair tag, price, amount commitment, Bulletproof, timeout, signature). - Action: Order is broadcast via Dandelion++. - Post: Order appears in receiving nodes' local order books. Maker's node maintains a liveness beacon for this order.
Advertised → Matched - Pre: A taker has opened a private negotiation channel, both parties have revealed cleartext terms, and both have explicitly accepted. - Action: Both parties derive the swap context ID, generate adaptor points, negotiate timelocks, and exchange pre-signed refund transactions. - Post: Order is removed from the maker's advertisement set. Negotiation channel remains open for lock coordination.
Advertised → Canceled - Pre: Maker broadcasts a signed cancellation, or maker's node goes offline past the liveness grace period. - Post: Order is removed from all local order books. Terminal state.
Matched → MakerLocked - Pre: Maker has constructed and broadcast the Blocknet lock transaction. - Action: Lock transaction enters mempool and is confirmed. - Post: Maker's BNT is locked. Taker monitors for confirmation.
Matched → Rejected - Pre: Either party aborts during swap setup before any lock is broadcast. - Post: Negotiation channel closes. No on-chain artifacts. Terminal state.
MakerLocked → TakerLocked - Pre: Taker has verified the Blocknet lock (correct amount commitment, correct adaptor point incorporation, sufficient confirmation depth). Taker has constructed and broadcast the counterparty chain lock transaction. - Action: Counterparty chain lock enters mempool and is confirmed. - Post: Both sides are locked. Maker monitors the counterparty chain for confirmation.
MakerLocked → Failed
- Pre: Taker fails to lock within the setup deadline (either the taker's lock
transaction fails validation, the taker goes offline, or the setup timeout
elapses).
- Action: Maker waits for T_refund and broadcasts the pre-signed refund
transaction.
- Post: Maker's BNT is refunded. Terminal state.
TakerLocked → LocksVerified - Pre: Maker has verified the counterparty chain lock (correct amount, correct adaptor point or hashlock, sufficient confirmation depth, correct timelock). - Post: Both locks are verified. Swap is ready for settlement.
LocksVerified → Settled
- Pre: Maker claims on the counterparty chain (revealing t or preimage s).
Taker extracts the secret and claims on Blocknet.
- Action: Both claim transactions are broadcast and confirmed.
- Post: Both parties have received their funds. Terminal state.
LocksVerified → Refunded (also reachable from TakerLocked via timeout)
- Pre: T_claim has passed without the maker claiming on the counterparty
chain.
- Action: Taker refunds on the counterparty chain. After T_refund, maker
refunds on Blocknet.
- Post: Both parties have their original funds back. Terminal state.
Any non-terminal state → Expired - Pre: The order-level or swap-level timeout has elapsed without advancing to the next expected state. - Post: Equivalent to the appropriate terminal state (Canceled if still Advertised, Failed if in MakerLocked, Refunded if both locks exist). Terminal.
Trade state is maintained locally by each participant. There is no global state consensus for trade progress. Trade state is private to the participants, and publishing it to a shared ledger would create a surveillance surface.
Each participant persists:
On restart, a participant can resume from persisted state. If a lock is on-chain and the counterparty is unreachable, the refund path activates normally after timeout.
VEIL requires the following new libp2p protocol identifiers:
/blocknet/mainnet/veil-order/1.0.0 — order intent broadcast and
cancellation./blocknet/mainnet/veil-negotiate/1.0.0 — private encrypted negotiation
between counterparties.No escrow attestation stream is needed (there are no escrow validators). All settlement coordination happens directly between the two counterparties over the negotiation channel or by watching the respective blockchains.
Order intents are broadcast using Dandelion++ (stem/fluff), reusing the existing Dandelion infrastructure with a new message type. Stem probability, epoch duration, and embargo timeout parameters are shared with transaction broadcast.
Each relay peer maintains a local order book as an in-memory cache of valid, unexpired orders. Orders are evicted on expiry, cancellation, or memory pressure (oldest-first eviction). There is no order persistence across node restarts.
The p2p layer generates and relays decoy order messages that are indistinguishable from real orders at the envelope level. Decoy orders use valid Pedersen commitments with valid Bulletproofs (committing to arbitrary values), signed under throwaway ephemeral keys. They have correct sizes, plausible timing, and realistic trading pairs and prices.
Decoy generation uses the same cryptographic machinery as real orders. The cost of generating Bulletproofs bounds the decoy rate per node and makes mass decoy flooding expensive.
On-chain decoy transactions (zero-value escrow locks that go through the full lock-timeout-refund cycle) are not part of the v1 protocol. The privacy primitives already make escrow transactions indistinguishable from regular transfers on-chain. P2p layer decoys protect the order book; on-chain privacy is handled by Pedersen commitments, CLSAG, and stealth addresses.
The negotiation stream is a direct, authenticated, encrypted connection between maker and taker nodes. It is not relayed through intermediaries to avoid giving relay nodes a privileged position for trade content inference.
The channel uses:
The channel is torn down when the swap concludes (success, failure, or rejection).
Orders require valid Bulletproofs, which are computationally expensive to generate. Mass order creation has a meaningful CPU cost.
Additional anti-spam mechanisms:
The VEIL protocol does not introduce a new fee mechanism. The cost of a trade is the sum of standard transaction fees on each chain involved.
For a BNT↔XMR swap: - Blocknet lock transaction fee (standard Blocknet tx fee, paid to miner). - Blocknet claim transaction fee (standard Blocknet tx fee, paid to miner). - Monero lock transaction fee (standard Monero tx fee, paid to miner). - Monero claim transaction fee (standard Monero tx fee, paid to miner).
For a two-hop BTC→BNT→ETH swap: - Bitcoin lock + claim fees. - Two Blocknet lock + claim fees (one per hop). - Ethereum lock + claim fees (including gas).
There is no spread, no trading fee, no premium for priority execution, and no fee paid to any protocol operator (because there is no protocol operator). Fees go to miners on each chain, who earn them for processing transactions they cannot distinguish from ordinary transfers.
If a swap fails and refund transactions are broadcast, the refund transaction fees are paid by each party for their own refund. These are standard transaction fees on each chain. The user who caused the failure (by disappearing or stalling) is not penalized beyond the cost of their own refund transaction and the opportunity cost of locked capital during the timeout window.
Because all fees are standard transaction fees on known chains, the cost of a swap is predictable before execution. The wallet can display the estimated total fee (summed across all chains and all transactions in the swap) before the user commits.
Threat. An adversary who can observe all p2p traffic attempts to determine who is trading with whom, what is being traded, how much, and when.
Mitigations. - Order intents are broadcast via Dandelion++; originating node is not identifiable from propagation pattern. - Order amounts are Pedersen-committed; only pair and price are visible. - Negotiation is direct and encrypted, not relayed through the network. - Decoy orders inflate the order set, making it impossible to distinguish real trading intent from noise. - Stealth-derived ephemeral identities are unlinkable to wallet addresses or to each other.
Residual risk. Timing correlation remains possible. An observer who sees a node broadcast an order and then sees that node open a direct connection to another node shortly after can infer a potential match. Mitigation: nodes should maintain persistent connections to many peers and use connection-level multiplexing to avoid observable connection pattern changes during negotiation.
Threat. An adversary analyzes the Blocknet chain to identify escrow transactions, link escrow locks to claims, or determine trade volumes.
Mitigations.
- Lock transactions use the same Pedersen commitments, CLSAG ring signatures,
stealth addresses, and Bulletproofs as every other Blocknet transaction.
There is no structural difference.
- The modified stealth key (P_lock = P_normal + T) is indistinguishable
from a standard stealth key to anyone who does not know T. Since T is
known only to the counterparties, no observer can determine that a given
output is an escrow lock.
- Claim transactions are standard CLSAG spends of the lock output. They look
like any other spend.
- Refund transactions are standard CLSAG spends with a timelock. Timelocked
transactions exist on Blocknet for non-escrow purposes, so refunds do not
stand out.
- Key images prevent double-spending but cannot be linked back to the public
key or to the lock output by an observer.
Residual risk. Heuristic analysis based on timing (lock followed by claim at a specific interval) or output graph patterns. The standard ring size of 16 provides a plausible deniability set. If the anonymity set is too small (low chain activity), timing attacks become more feasible. Higher chain activity (including ordinary payments) increases the anonymity set naturally.
Threat. An adversary analyzes the counterparty chain to identify swap transactions.
Analysis. This depends entirely on the counterparty chain and the adaptor tier:
Residual risk. On transparent chains, swap transactions may be identifiable by their structure (especially Tier 3 HTLCs). The protocol does not claim to provide privacy on the counterparty chain. It provides privacy on the Blocknet leg and unlinkability between the Blocknet leg and the counterparty leg.
Threat. An adversary monitors multiple chains simultaneously and attempts to correlate transactions across chains to identify both legs of a swap.
Mitigations.
- Tier 1/2 (adaptor signatures): No on-chain artifact links the two legs.
The adaptor secret t is embedded in signatures, not in any visible field.
An observer would need the pre-signature (which is exchanged off-chain
between counterparties) to extract t from the completed signature. Without
it, the two transactions on two chains are uncorrelated.
- Tier 3 (HTLC): The hash h appears on both chains (counterparty chain
explicitly, Blocknet implicitly in the lock construction). However, on
Blocknet, the hashlock is hidden inside the privacy-preserving transaction
structure. The hash is not visible on-chain. Cross-chain correlation via
hashlock is only possible if the adversary can break Blocknet's privacy
(extract the hash from inside a Pedersen-committed, ring-signed
transaction), which is equivalent to breaking the underlying cryptography.
- Two-hop swaps: Each hop uses an independent secret/hashlock. Correlating
the two transparent legs (e.g., BTC and ETH) of a BTC→BNT→ETH swap requires
breaking the privacy of the intermediate BNT hop.
Residual risk. Timing correlation. If a lock on Bitcoin appears at time X and a lock on Blocknet appears at time X+delta for a predictable delta, an adversary can hypothesize a link. Mitigation: randomized delays in lock broadcast timing. The negotiation channel can coordinate a randomized delay window so that lock transactions are not broadcast at predictable intervals.
Threat. A maker or taker attempts to extract information without completing the trade, lock the counterparty's funds indefinitely, or double-spend.
Analysis.
Information extraction: During negotiation, both sides reveal terms to each other. A malicious party could open many negotiations to learn market activity. Mitigation: rate-limiting negotiations per ephemeral identity, and making negotiation initiation mildly costly (proof-of-work on the negotiation handshake).
Indefinite fund locking: Impossible. All locks have timeouts. If the counterparty disappears, the refund path activates after timeout. The timeout is the maximum grief period.
Double-spending lock outputs: Impossible on Blocknet (key images prevent re-spending). On the counterparty chain, prevented by that chain's own double-spend protections.
Free-option attack: The counterparty who claims second (the taker on
Blocknet) could delay claiming to see if the price moves favorably. However,
the maker has already claimed on the counterparty chain (revealing t), so
the taker gains nothing by delaying — t is already known and the claim
can be made at any time before T_refund. The maker's staggered timelock
ensures the maker must claim first, preventing the maker from using the
option window.
Residual risk. A counterparty who locks and then intentionally stalls can cause the other side's funds to be locked until timeout. The timeout window is the maximum grief duration. Shorter timeouts reduce grief but also reduce tolerance for slow chains and network delays.
Threat. An attacker floods the network with fake orders to pollute the order book, waste takers' negotiation bandwidth, or crowd out legitimate orders.
Mitigations. - Orders require valid Bulletproofs, which are computationally expensive. Mass order creation has a meaningful CPU cost. - Peer scoring: nodes originating many failed negotiations are deprioritized. - Per-peer order broadcast rate limits. - Decoy orders already exist in the system, so the order book is expected to contain non-real orders. The protocol is designed to tolerate this.
Residual risk. A well-resourced attacker can generate valid-looking orders at scale. The defense is economic rather than absolute. Future versions may introduce stake-gating for high-volume order publishers.
Threat. A user's device is compromised, revealing wallet keys, trade state, and negotiation history.
Analysis. Outside the protocol's scope. No distributed protocol can protect a user whose endpoint is fully compromised. The protocol limits blast radius: stealth-derived ephemeral keys mean compromising one trade's keys does not reveal other trades' keys. Forward secrecy on negotiation channels means past negotiations cannot be decrypted from a current key compromise.
Threat. A 51% attack or deep reorg on a participating chain reverses a confirmed lock transaction after the counterparty has already claimed.
Mitigations. - Require sufficient confirmation depth before the claim phase. The required depth should be proportional to the trade value and inversely proportional to the chain's security budget. - The timeout window must accommodate the required confirmation depth on all involved chains. - For high-value trades, counterparties may negotiate higher confirmation requirements (trading settlement speed for safety).
Residual risk. A sufficiently powerful chain-level attack can always defeat confirmation-depth requirements. This is a fundamental limitation of all cross-chain protocols and is not specific to VEIL. The mitigation is economic: the cost of the attack must exceed the value of the trade.
| Data field | Who can see it | Why |
|---|---|---|
| Order ephemeral public key | All peers | Addressing and anti-replay; unlinkable to wallet identity |
| Order trading pair | All peers | Takers need to filter for relevant orders |
| Order price | All peers | Takers need to evaluate offers |
| Order amount commitment | All peers | Bulletproof verification; hides actual value |
| Order Bulletproof | All peers | Validity proof; reveals nothing beyond range |
| Order timeout preference | All peers | Expiry management; low sensitivity |
| Order signature | All peers | Authenticity; signed by ephemeral key |
| Cleartext trade terms | Maker and taker only | Required for trade execution; never broadcast |
| Swap context ID | Maker and taker only | State tracking; opaque hash |
| Adaptor point T | Maker and taker only | Lock construction; reveals no trade content |
| DLEQ proof (Tier 2) | Maker and taker only | Cross-curve binding; reveals no trade content |
| Hash h (Tier 3) | Maker and taker only (off-chain); visible on counterparty chain after claim | HTLC construction; hidden on Blocknet side |
| Blocknet lock transaction | All Blocknet participants | Standard UTXO; amounts hidden by Pedersen; sender hidden by CLSAG; receiver hidden by stealth |
| Counterparty chain lock | All counterparty chain participants | Visible per that chain's privacy model |
| Blocknet claim transaction | All Blocknet participants | Standard UTXO spend; indistinguishable from regular spend |
| Counterparty chain claim | All counterparty chain participants | Reveals adaptor secret only to party holding pre-signature (Tier 1/2); reveals preimage publicly (Tier 3) |
| Refund transactions | All participants on respective chains | Standard transactions; indistinguishable from regular spends |
| Pre-signed refund | Maker and taker only (until broadcast) | Safety mechanism; not broadcast unless needed |
| Negotiation channel content | Maker and taker only | Encrypted direct channel; forward secrecy |
| Decoy orders | All peers | Indistinguishable from real orders by design |
| Component | Status | Notes |
|---|---|---|
| Pedersen commitments | Implemented | crypto-rs/src/commitment.rs |
| Bulletproof range proofs | Implemented | crypto-rs/src/rangeproof.rs |
| CLSAG ring signatures | Implemented | crypto-rs/src/ring.rs |
| RingCT (commitment-linked rings) | Implemented | crypto-rs/src/ring.rs |
| Stealth addresses | Implemented | crypto-rs/src/stealth.rs |
| Schnorr signatures (Ristretto255) | Implemented | crypto-rs/src/keys.rs |
| Dandelion++ transaction broadcast | Implemented | p2p/dandelion.go |
| Argon2id proof of work | Implemented | crypto-rs/src/pow.rs |
| Schnorr adaptor signatures (Ristretto255) | Not built | Core swap mechanism; well-understood construction |
| Adaptor-CLSAG (or two-step Schnorr fallback) | Not built | Required for Blocknet lock spend; most novel component |
| Cross-group DLEQ proof (Ristretto↔secp256k1) | Not built | Required for Tier 2 chains; prototyped by COMIT/Farcaster |
| Encrypted negotiation channel | Not built | X25519 + ChaCha20-Poly1305; standard crypto |
| P2P order broadcast protocol | Not built | New libp2p stream; reuses Dandelion++ |
| Local order book | Not built | In-memory cache with expiry |
| Decoy order generation | Not built | Reuses Pedersen/Bulletproof machinery |
| Trade state machine + persistence | Not built | Local state store in wallet |
| Swap context derivation | Not built | Hash-based context ID |
| Timelock-gated output construction | Not built | Modified stealth key + adaptor point |
| Pre-signed refund construction | Not built | Standard RingCT with timelock |
| Bitcoin Taproot adaptor module | Not built | BIP-340 Schnorr + DLEQ |
| Bitcoin legacy HTLC module | Not built | Standard HTLC script |
| Ethereum HTLC module | Not built | Minimal smart contract |
| Monero adaptor module | Not built | Ed25519 adaptor; builds on COMIT work |
| Solana adaptor module | Not built | Ed25519 adaptor + Solana program |
| Chain monitoring (per adaptor) | Not built | Watch counterparty chain for lock/claim/refund |
| Wallet UI integration | Not built | Two-hop orchestration, fee estimation, state display |
Each phase depends on the completion of prior phases unless noted otherwise.
Phase 1: Schnorr adaptor signatures. Implement Schnorr adaptor signature construction over Ristretto255: adaptor point generation, pre-signature creation, pre-signature verification, signature completion (given secret), and secret extraction (given pre-signature and completed signature). This is the cryptographic foundation for all swap settlement. Test with unit tests covering the full lifecycle: generate secret, produce adaptor point, create pre-signature, verify pre-signature, complete signature, extract secret, verify extracted secret matches original.
Phase 2: Adaptor-aware lock outputs.
Implement the modified stealth address construction for lock outputs:
P_lock = P_normal + T. Implement the corresponding spend key derivation:
x_spend = x_stealth + t. Implement timelock-gated outputs (outputs that
become spendable by a fallback key after a specified block height). Implement
pre-signed refund transaction construction. Test lock, claim, and refund
paths on a local Blocknet testnet.
Phase 3: Trade state machine and persistence. Implement the state machine from section 8 as a local state store in the wallet. Define serialization formats for swap context, adaptor points, pre-signatures, pre-signed refunds, and timelock deadlines. Test all state transitions including timeout and failure paths. Implement crash recovery from persisted state.
Phase 4: P2P order protocol.
Add the /veil-order/ stream protocol. Implement order construction
(ephemeral identity, pair tag, price, Pedersen-committed amount with
Bulletproof, timeout, signature). Integrate with Dandelion++ for order
broadcast. Implement local order book with expiry and eviction. Implement
order cancellation. Test order lifecycle including liveness timeout.
Phase 5: Negotiation channel. Implement the encrypted direct-connect negotiation protocol: ephemeral X25519 key exchange, ChaCha20-Poly1305 authenticated encryption, term revelation, acceptance/rejection flow, and forward secrecy. Test with adversarial scenarios (connection drops mid-negotiation, malicious probing).
Phase 6: End-to-end BNT↔BNT swap (testnet). Integrate phases 1-5 into a complete same-chain swap on Blocknet testnet. This validates the full protocol without cross-chain complexity: one party locks BNT, the other locks BNT, settlement via adaptor signatures. This is not a useful product feature (same-chain swap is pointless) but it validates the core machinery in isolation.
Phase 7: Monero adaptor module (Tier 1). Implement the Monero chain adaptor: lock construction using Monero's transaction format, claim construction, refund construction, lock verification, chain monitoring, and secret extraction from Monero claim signatures. This builds on the COMIT/Farcaster XMR↔BTC swap research and codebase. Test BNT↔XMR atomic swaps on testnet.
Phase 8: Bitcoin Taproot adaptor module (Tier 2). Implement cross-group DLEQ proofs (Ristretto255 ↔ secp256k1). Implement Schnorr adaptor signatures over secp256k1 (BIP-340). Implement the Bitcoin Taproot lock output construction (internal key with adaptor tweak, script-path with timelock refund). Implement chain monitoring for Bitcoin. Test BNT↔BTC atomic swaps on testnet.
Phase 9: Ethereum HTLC adaptor module (Tier 3). Implement the Ethereum HTLC smart contract (hashlock + timelock, claim with preimage, refund after timeout). Implement the chain adaptor: lock construction, claim construction, refund construction, lock verification, chain monitoring, preimage extraction. Test BNT↔ETH atomic swaps on testnet.
Phase 10: Two-hop swap orchestration. Implement wallet-level orchestration of two-hop swaps (e.g., BTC→BNT→ETH). Handle both legs sequentially, manage intermediary BNT state, implement retry logic for the second leg, display fee estimates across all chains. Test end-to-end two-hop swaps on testnet.
Phase 11: Decoy traffic. Implement decoy order generation: valid Pedersen commitments with Bulletproofs, signed under throwaway keys, broadcast via Dandelion++. Tune the decoy rate based on testnet traffic analysis. Test that decoy orders are indistinguishable from real orders under protocol-level inspection.
Phase 12: Integration testing and adversarial simulation. End-to-end testing of all supported swap paths on testnet. Simulated adversarial scenarios: stalling counterparties, network partitions, timeout races, Sybil order flooding, chain reorgs during lock confirmation. Load testing of the order book under decoy traffic. Formal review of the adaptor signature and DLEQ constructions.
Phase 13: Mainnet deployment. Capped trade sizes. Conservative timeout parameters. Monitoring and alerting. Gradual parameter relaxation based on operational data. Additional chain adaptor modules (Solana, EVM chains) added incrementally based on demand.
Joint atomicity for two-hop swaps. v1 treats each hop as an independent
atomic swap. If the first hop completes and the second fails, the user holds
BNT. v2 could explore chained adaptor secrets where t2 is derived from t1,
creating a single atomic operation across both hops. This requires three-party
coordination (the user, the first counterparty, and the second counterparty)
and is significantly more complex.
Direct cross-chain adaptors. For high-volume pairs (e.g., BTC↔ETH), a direct adaptor module that bypasses the BNT hop would halve latency and fees. This requires a full-mesh adaptor for that specific pair but could coexist with the hub model for less common pairs.
Partial fills and order splitting. v1 supports only complete fills (one maker, one taker, full amount). Partial fills require order splitting, multiple escrows per order, and more complex state management. Deferred.
Market maker incentives. Liquidity in BNT pairs is critical for the hub model. v2 could introduce fee rebates, priority matching, or other incentives for nodes that maintain persistent, liquid orders.
Adaptor-CLSAG formalization. The v1 protocol can launch with the two-step Schnorr fallback for Blocknet lock spends. Full adaptor-CLSAG integration should be pursued in parallel, with formal security proofs, and deployed once the construction has been independently reviewed.
Reputation and peer scoring. v1 uses basic rate limiting and peer scoring for anti-spam. v2 could introduce a more sophisticated reputation system based on completed trade history, with privacy-preserving reputation proofs (proving you have completed N trades without revealing which trades).
Additional chain adaptors. Each new chain requires one adaptor module. Priority targets should be determined by user demand. The interface defined in section 5.1 is stable; new adaptors can be added without protocol changes.
This appendix was produced by auditing the blocknet codebase (../blocknet)
against the spec above. It identifies every assumption the spec makes that
the code does not currently satisfy, and for each gap lists the exact files,
structs, and functions that need to change, and how.
The spec assumes timelocked outputs and pre-signed refund transactions as core safety primitives (sections 7.1, 7.2, 7.4, 8.2). The codebase has zero timelock infrastructure. No field, no validation, no enforcement.
What needs to change:
| File | Object / Function | Change |
|---|---|---|
transaction.go |
TxOutput struct |
Add LockHeight uint64 field. When nonzero, the output cannot be spent by any transaction included in a block at height < LockHeight. |
transaction.go |
Transaction struct |
Bump Version to 2. V2 txs include the LockHeight field in outputs; V1 txs are still valid (LockHeight implicitly 0). |
transaction.go |
Serialize() |
Extend output serialization: after EncryptedMemo, write LockHeight (8 bytes LE). The prefix size calculation and offset logic must account for the new field. Only for Version >= 2. |
transaction.go |
SigningHash() |
Same change — LockHeight must be included in the signing prefix so it's committed to by the RingCT signature. |
transaction.go |
DeserializeTx() |
Version-aware parsing: if version >= 2, read 8-byte LockHeight per output after EncryptedMemo. If version == 1, LockHeight = 0. Update minHeaderSize and bounds checks. |
transaction.go |
ValidateTransaction() |
Accept version == 1 \|\| version == 2. For version 2 with any nonzero LockHeight output, receive current block height as a parameter and reject if the tx tries to spend an output whose LockHeight > currentHeight. This requires a new parameter: the block height context, or a lookup callback OutputLockHeight(pubKey, commitment [32]byte) uint64 that returns the lock height for a referenced ring member. |
transaction.go |
TxOutput JSON tags |
Add lock_height JSON tag. |
transaction.go |
UTXO struct |
Already has BlockHeight. Add LockHeight uint64 to track the output's lock constraint. |
transaction.go |
UTXOSet.Add() |
Store LockHeight from the output. |
block.go |
ValidateBlock() / validateBlockWithContext() |
Pass block height to ValidateTransaction() so it can enforce LockHeight. |
block.go |
CoinbaseMaturity |
Currently defined as 60 but only enforced in wallet. Move enforcement to ValidateTransaction(): when an input's ring contains a coinbase output, the output must have at least CoinbaseMaturity confirmations. This is a separate fix but should ship with the timelock work. |
mempool.go |
AddTransaction() |
Pass the current chain height to ValidateTransaction() for LockHeight enforcement. The mempool already tracks isKeyImageSpent; it needs a parallel outputLockHeight callback from the chain. |
daemon.go |
handleTx(), processTxData() |
Thread current height through to mempool validation. |
wallet/builder.go |
serializeTxPrefix() |
Version-aware: if building a v2 tx, include LockHeight in the output serialization. |
wallet/builder.go |
serializeTx() |
Same. |
wallet/builder.go |
estimateTxSizeBytes() |
Add 8 bytes per output for LockHeight. |
wallet/builder.go |
Transfer() |
Set version = 2 when building lock transactions. Accept an optional LockHeight per Recipient. |
wallet/builder.go |
Recipient struct |
Add LockHeight uint64 field. |
wallet/scanner.go |
OutputData struct |
Add LockHeight uint64. |
wallet/scanner.go |
ScanBlock() / BlockToScanData() |
Parse and propagate LockHeight. |
wallet/wallet.go |
OwnedOutput struct |
Add LockHeight uint64. |
wallet/wallet.go |
IsOutputMature() |
Check both CoinbaseMaturity / SafeConfirmations AND LockHeight. |
storage.go |
Output storage format | Store LockHeight alongside each output. The outputs bucket already stores UTXO data; extend the serialized format. |
p2p/sync.go |
Block sync | No changes needed — blocks carry the full tx data including new fields. V2 blocks are backward-incompatible at the tx level, so nodes must upgrade. |
Wire format change (version 2 output):
PublicKey(32) | Commitment(32) | EncryptedAmount(8) | EncryptedMemo(128) |
LockHeight(8) | RangeProofLen(4) | RangeProof(var)
Activation: A height-gated activation is cleanest. Before activation height H, only version 1 txs are valid. At and after H, version 2 txs are accepted. Version 1 txs remain valid indefinitely (LockHeight = 0 implicitly).
The spec describes a two-step Schnorr fallback: "the adaptor secret t is
revealed through a plain Schnorr signature on an auxiliary message, and the
CLSAG spend uses t as a derived key component." The spec does not explain
how the taker learns t after the maker claims on Blocknet.
The problem: When the maker claims the taker's lock on the counterparty
chain, the maker reveals t (via adaptor signature or HTLC preimage) on
that chain. The taker then uses t to spend the Blocknet lock. This
direction works. But in the reverse direction — the maker claiming on the
counterparty chain first — there is a subtlety:
The taker's Blocknet lock output has key P_lock = P_normal + T. The taker
can spend it with private key x_normal + t. When the taker spends it, the
on-chain transaction is a standard CLSAG ring signature. An observer (or the
maker) cannot extract t from the CLSAG signature because CLSAG does not
reveal the signing key.
For the two-step fallback, the flow must be:
t, publishes T = t*G.t via adaptor
signature or HTLC preimage on that chain.t from the counterparty chain claim.x_spend = x_stealth + t and spends the Blocknet lock
output using standard CLSAG.This flow is correct as described in section 7.3. The two-step fallback
means: the adaptor secret is carried by the counterparty chain's claim
mechanism (adaptor sig or HTLC preimage), NOT by the Blocknet CLSAG. The
Blocknet CLSAG is just a regular spend once t is known. The "auxiliary
Schnorr signature" mentioned in section 4.2 is not needed for this flow
and should be removed from the spec to avoid confusion.
What needs building:
| File | Object / Function | Change |
|---|---|---|
crypto-rs/src/keys.rs |
New functions | blocknet_schnorr_adaptor_presign(privkey, message, adaptor_point) → presig, blocknet_schnorr_adaptor_verify(pubkey, message, presig, adaptor_point) → bool, blocknet_schnorr_adaptor_complete(presig, secret) → sig, blocknet_schnorr_adaptor_extract(sig, presig) → secret. These are plain Schnorr adaptors over Ristretto255 for use on the Blocknet side when the counterparty chain is also Ristretto-compatible (Tier 1). |
crypto.go |
New FFI wrappers | Go wrappers for each new Rust function above. |
crypto-rs/src/ring.rs |
No changes for v1 | The CLSAG scheme is used as-is. The claim on Blocknet is a standard CLSAG spend with the combined key x_stealth + t. No adaptor embedding in CLSAG for v1. |
Spec correction: Section 4.2 paragraph about "auxiliary Schnorr
signature on an auxiliary message" for the two-step fallback is misleading.
The v1 fallback does not require an auxiliary on-chain Schnorr. It uses the
counterparty chain's native claim mechanism to reveal t, then the Blocknet
claim is a standard CLSAG spend. The spec should clarify this.
CoinbaseMaturity = 60 is defined in block.go line 53 but enforced only
in wallet/wallet.go IsOutputMature(). Consensus (ValidateTransaction,
validateBlockWithContext) does not check it. A manually constructed tx
spending an immature coinbase would pass validation.
What needs to change:
| File | Object / Function | Change |
|---|---|---|
transaction.go |
ValidateTransaction() |
Needs a callback or parameter to check output maturity. When a ring member is a coinbase output, its block height must be at least CoinbaseMaturity blocks before the current block. This requires knowing which ring members are coinbase outputs and their block heights — information not currently passed to validation. |
block.go |
validateBlockWithContext() |
Pass block height to ValidateTransaction(). |
storage.go |
Output storage | Store IsCoinbase bool alongside each output so the validator can look it up. Currently only BlockHeight is stored. |
transaction.go |
RingMemberChecker type |
Extend to func(pubKey, commitment [32]byte) (bool, uint64, bool) returning (exists, blockHeight, isCoinbase), or add a separate callback. |
Priority: Should be fixed before VEIL, as escrow outputs built on immature coinbase could be used in reorg attacks.
The spec says order intents reuse Dandelion++ with "a new message type." The current implementation is tx-only with no type discrimination.
Security concern — protocol-ID segregation: The earlier draft of this
section proposed adding separate libp2p protocol IDs
(/veil-order/, /veil-negotiate/). This is a privacy defect. libp2p's
multistream-select handshake announces the protocol ID in cleartext during
stream negotiation. An ISP or network observer could trivially distinguish
"this node opens veil-order streams" from "this node only does chain sync,"
producing a binary signal that a node is participating in the exchange. This
directly violates the design goal that escrow activity be indistinguishable
from normal usage.
Revised approach — type-tagged envelope over existing Dandelion:
All broadcast messages (transactions, order intents, order cancellations)
flow through the same ProtocolDandelion and ProtocolTx protocol
streams. Payloads are prefixed with a 1-byte type tag:
| Tag | Meaning |
|---|---|
0x00 |
Transaction (current behavior) |
0x01 |
Order intent |
0x02 |
Order cancellation |
The DandelionRouter dispatches to type-specific handlers after reading the
tag. From the wire, all messages are opaque blobs on the same protocol — no
new protocol negotiation, no distinguishable stream types, no segregation.
What needs to change:
| File | Object / Function | Change |
|---|---|---|
p2p/dandelion.go |
DandelionRouter |
Prepend a 1-byte type tag to every outbound payload. On inbound, read the tag first and dispatch to the appropriate handler (onTx, onOrder, etc.). The stem/fluff/epoch logic is shared across all types — only the final delivery handler differs. |
p2p/dandelion.go |
DandelionRouter struct |
Add onOrder func(from peer.ID, data []byte) and SetOrderHandler(). |
p2p/dandelion.go |
stemSanityCheck |
Extend to accept the type tag so callers can apply type-specific validation (e.g., order size limits vs tx size limits). |
p2p/node.go |
BroadcastOrder() |
New method that wraps order data with the 0x01 tag and feeds it into the existing DandelionRouter. |
p2p/node.go |
Existing handleDandelionStream |
Updated to read the type tag and route accordingly. |
protocol/params/p2p.go |
No new protocol IDs | No changes. All traffic stays on existing protocol IDs. |
Direct peer-to-peer negotiation channel: The negotiation stream
(maker ↔ taker encrypted channel) is inherently a direct connection, not a
broadcast, so it cannot use Dandelion. Using a VEIL-specific protocol ID
here would still leak participation metadata. Instead, multiplex it over a
generic direct-messaging protocol (e.g., an existing /blocknet/…/direct/
stream if one exists, or a generically-named one like
/blocknet/mainnet/msg/1.0.0) that carries encrypted, purpose-opaque
payloads. The stream content is already encrypted; the goal is to avoid
the protocol ID itself being a fingerprint.
The spec describes pre-signed refund transactions: "fully valid transactions that spend the lock output back to its original owner but carry a timelock." This depends on A.1 (timelock support) and has an additional subtlety.
Ring member availability: A pre-signed refund tx includes a CLSAG ring
signature over a specific set of ring members. Between the time the refund
is pre-signed and the time it's broadcast (potentially hours later), some
ring members might be spent (their key images consumed). The refund tx would
then fail validation because isCanonicalRingMember checks canonical chain
state, and ring members whose outputs have been spent are still in the UTXO
set (spending consumes key images, not outputs). Actually, in this codebase,
outputs are never removed from the canonical ring index — only key images
are tracked for double-spend. So ring member validity should be stable.
However, a chain reorg could invalidate ring members if the block containing
them is disconnected.
What needs to change for refund construction:
| File | Object / Function | Change |
|---|---|---|
wallet/builder.go |
Builder / Transfer() |
Add a BuildRefund() method or extend Transfer() with a LockHeight option so it can construct a transaction that spends a lock output and is only valid after a specified height. The input is the lock output (with known P_lock, amount, blinding); the output is a standard stealth output back to the maker. |
wallet/builder.go |
TransferConfig |
No structural changes needed beyond the LockHeight support from A.1. The signing key for the refund is x_stealth (the maker's stealth-derived private key for the lock output, without the adaptor component t). Wait — this is wrong. P_lock = P_normal + T. The maker knows x_normal and t (maker generated t). So the maker can compute x_lock = x_normal + t and sign the refund. But the refund should be pre-signed and given to the taker as a safety guarantee. The maker signs the refund before broadcasting the lock, using x_lock = x_stealth + t as the spending key. This works because the maker knows t. |
wallet/builder.go |
Ring selection timing | Ring members must be selected at pre-sign time and baked into the refund tx. This means the refund tx is built and signed at swap setup time (Matched → MakerLocked transition), not at refund time. |
The spec's P_lock = P_normal + T construction is compatible with the
existing stealth address scheme. Here's the exact mechanical path:
Construction (maker's wallet builds the lock tx):
| File | Object / Function | Change |
|---|---|---|
wallet/builder.go |
Recipient struct |
Add AdaptorPoint [32]byte (optional, zero = no adaptor). |
wallet/builder.go |
Transfer() output loop |
After computing oneTimePub for a recipient, if AdaptorPoint is nonzero, call PointAdd(oneTimePub, r.AdaptorPoint) to get P_lock. Use P_lock as the output public key. |
crypto.go |
PointAdd() |
Already exists as an FFI wrapper. No change needed. |
Scanning (taker's wallet detects the lock output):
| File | Object / Function | Change |
|---|---|---|
wallet/scanner.go |
ScanBlock() |
After the standard checkStealthOutput() call fails, if the wallet has active swap contexts with known adaptor points, try checkStealthOutput() against outputPub - T for each known T. If that matches, the output is a lock output for that swap. |
wallet/wallet.go |
New state | activeSwapAdaptorPoints map[[32]byte][32]byte — maps adaptor point T to swap context ID. The scanner checks these when standard stealth scanning misses. |
wallet/scanner.go |
ScannerConfig |
Add PointSub func(p1, p2 [32]byte) ([32]byte, error) for computing outputPub - T. |
crypto.go |
New FFI | PointSub() — subtract two Ristretto points. Alternatively, negate T and use PointAdd. Need to check if PointAdd handles negation or if a dedicated PointSub/PointNegate is needed. |
crypto-rs/src/stealth.rs |
No changes | The stealth derivation math is unchanged. The adaptor offset is applied after derivation, not inside it. |
These are the new FFI functions needed in the Rust crypto library and their Go wrappers:
| File | Function | Signature | Purpose |
|---|---|---|---|
crypto-rs/src/keys.rs |
blocknet_schnorr_adaptor_presign |
(privkey, message, msg_len, adaptor_point, presig_out) → i32 |
Create a pre-signature: R' = R + T, s' = k + e*x where e = H(R' \|\| P \|\| m). Output is (R', s') = 64 bytes. |
crypto-rs/src/keys.rs |
blocknet_schnorr_adaptor_verify |
(pubkey, message, msg_len, presig, adaptor_point) → i32 |
Verify pre-signature: check s'*G == R' + e*P where R' = R_presig and e = H(R' \|\| P \|\| m). Wait — this doesn't work because the verifier doesn't know R (only R'). The correct check is: s'*G == R' + e*P fails (that's the completed sig check). Instead verify: s'*G - e*P == R' and then check R' - T is a valid nonce point. Actually the standard adaptor verify is: given (R', s') and T, verify s'*G == R' + e*P - T, i.e., s'*G + T == R' + e*P. This proves the pre-signature is a valid "promise" that can be completed with t. |
crypto-rs/src/keys.rs |
blocknet_schnorr_adaptor_complete |
(presig, adaptor_secret, sig_out) → i32 |
Complete: s = s' + t, R = R' - T. Output (R, s) = 64 bytes. This is a standard Schnorr signature. |
crypto-rs/src/keys.rs |
blocknet_schnorr_adaptor_extract |
(completed_sig, presig, secret_out) → i32 |
Extract: t = s_complete - s_presig. Output 32 bytes. |
crypto.go |
SchnorrAdaptorPresign() |
Go wrapper | |
crypto.go |
SchnorrAdaptorVerify() |
Go wrapper | |
crypto.go |
SchnorrAdaptorComplete() |
Go wrapper | |
crypto.go |
SchnorrAdaptorExtract() |
Go wrapper |
Not needed for v1 launch if Tier 2 (Bitcoin Taproot) is deferred. Needed only when supporting secp256k1-based adaptor signatures. For Tier 3 (HTLC), DLEQ is not required. For Tier 1 (Ed25519/Ristretto), DLEQ is not required (same scalar field).
Given the gaps above, the spec's phase ordering needs adjustment. The critical dependency is timelock support (A.1), which must land before any on-chain escrow testing.
Phase 0 (new): Transaction v2 with LockHeight + consensus coinbase maturity
Phase 1: Schnorr adaptor signatures (as spec)
Phase 2: Adaptor-aware lock outputs + refund construction
Phase 3: Trade state machine + persistence (as spec)
Phase 4: P2P order protocol + order Dandelion router
Phase 5: Negotiation channel (as spec)
Phase 6: End-to-end BNT↔BNT swap on testnet (as spec)
Phase 7+: Chain adaptors (as spec)
Phase 0 is the only new phase. It is purely a chain-layer change with no VEIL-specific logic, so it can be built, tested, and deployed independently.