multi: Add xmr/btc swaps.#3592
Draft
JoeGruffins wants to merge 58 commits into
Draft
Conversation
Implements the Aumayr et al. Schnorr adaptor construction under BIP-340 (x-only pubkeys, tagged hashes, s = k + e*d). Structurally mirrors the existing DCRv0 adaptor in adaptor.go and reuses the 97-byte wire format. Intended for BTC/XMR adaptor-swap support, where the BTC side uses a Taproot tapscript 2-of-2 and the adaptor sig binds one party's signature to the hidden scalar that also controls the XMR shared-address spend. Four BIP-340 official test vectors (0-3) pass bit-exactly through the shared nonce/challenge/signing primitives, validating the crypto foundation that the adaptor functions reuse. Also adds two planning docs alongside the existing xmrswap reference implementation: - PROTOCOL.md - spec of the DCR/XMR adaptor-swap state machine implemented in internal/cmd/xmrswap/main.go, including all four failure scenarios (success, alice-bails-before-xmr, cooperative refund, bob-bails-after-xmr) and the refund topology. Basis for porting to BTC/XMR. - XMR_WALLET_AUDIT.md - inventory of client/asset/xmr against the primitives an adaptor swap needs. Conclusion: zero cgo primitives are missing; the work is Go-level glue for per-swap wallet lifecycle (watch, sweep) and a small swap.go exposing SendToSharedAddress, WatchSharedAddress, SweepSharedAddress.
Companion to PublicKeyTweakedAdaptorSigBIP340 for API parity with the DCRv0 adaptor. A party who already has a valid BIP-340 signature and knows the hidden scalar t builds an adaptor that only the tweak-learner can complete. Needed to model the classic two-party adaptor-sig swap pattern where each party commits to the other's message under the same hidden T; one side's completion on-chain reveals the secret that unlocks the other. VerifyBIP340 and DecryptBIP340 now handle both tweak types, dispatching on pubKeyTweak. RecoverTweakBIP340 stays restricted to pub-key-tweaked adaptors since recovery is only meaningful in that direction. Tests: private-key-tweaked roundtrip, two-party swap simulation (privkey-tweaked + pubkey-tweaked adaptors exchanged, one completes on-chain, the other recovers the tweak and decrypts). All existing BIP-340 vector and roundtrip tests continue to pass.
BTC-side script primitives for the BTC/XMR adaptor swap, analogous to internal/adaptorsigs/dcr for the existing DCR/XMR tool. The BTC leg uses P2TR outputs with an unspendable NUMS internal key, forcing all spends through the script path: - Lock output: single tap leaf with 2-of-2 tapscript (<kal> CHECKSIGVERIFY <kaf> CHECKSIG). - Refund output: two-leaf tree. Cooperative-refund leaf is the same 2-of-2 (where Bob's sig is produced as an adaptor that leaks his XMR-key half on completion). Punish leaf is <locktime> CSV DROP <kaf> CHECKSIG, spendable by Alice alone after the relative locktime. NewLockTxOutput and NewRefundTxOutput return everything callers need to fund and later spend these outputs: tweaked output key, full scriptPubKey, leaf scripts, leaf hashes, and the per-branch control blocks for witness assembly. Tests exercise all three spend paths end-to-end through btcd's script engine with taproot and CSV verification enabled: happy-path 2-of-2 spend of the lock output, cooperative-refund 2-of-2 spend of the refund output, and punish-path single-sig spend of the refund output with CSV satisfied.
End-to-end in-memory validation of the happy-path BTC/XMR adaptor swap on the BTC side only. Exercises all crypto pieces built so far: - ed25519 key generation + DLEQ proofs (internal/adaptorsigs/dleq.go) - BIP-340 adaptor sign, verify, decrypt, recover (bip340.go) - Taproot lock output with 2-of-2 tapscript (this package) - Tapscript sighash and witness assembly - btcd script engine validation of the completed spend The test walks through Bob producing a pub-key-tweaked adaptor under Alice's DLEQ-extracted secp pubkey, Alice decrypting with her ed25519 scalar reinterpreted as a secp256k1 scalar, assembling the tapscript witness, engine-verifying the spend, and Bob recovering Alice's scalar from the completed signature. The recovered scalar is cross-checked against Alice's original ed25519 private-key bytes and the DLEQ public key point, confirming the full round-trip works. No RPC dependencies; validates that the crypto pieces plug together correctly before porting to a CLI with live simnet wallets.
Extends the existing happy-path integration test with cooperative-refund and punish-refund scenarios, paralleling three of the four scenarios in internal/cmd/xmrswap/main.go (the alice-bails-before-xmr case is the same BTC flow as cooperative refund and does not need a separate test). Both new tests chain funding -> lockTx -> refundTx -> spendRefundTx and validate each step through btcd's script engine. TestBTCSwapCooperativeRefund: Alice adaptor-signs spendRefundTxCoop under Bob's XMR-key-half secp pubkey. Bob decrypts using his own ed25519 scalar (reinterpreted as secp256k1), publishes the completed sig. Alice then runs RecoverTweakBIP340 on her adaptor + the on-chain sig to recover Bob's ed25519 scalar, which she needs to sweep the shared-address XMR. TestBTCSwapPunishRefund: Alice alone spends refundTx via the CSV punish leaf. No adaptor sig involved; by design she does not learn Bob's ed25519 scalar, so her XMR remains stranded - the asymmetric punish property that disincentivizes Bob from stalling. Extracts swapParties and buildAndVerifyRefundTx helpers to keep the two tests readable without obscuring their structural differences.
Fills gaps in the adaptor-sig negative coverage before the port of the xmrswap CLI to BTC. Targets: input-validation paths, tamper of fields other than s (r and T), scheme-flag confusion, RecoverTweak with mismatched sigs, RecoverTweak refusal on privKey-tweaked adaptors, and script-builder range checks. New tests: - TestBIP340SignErrors: rejects wrong hash length, rejects zero private key. - TestBIP340AdaptorTamperR: rejects verify after r tamper (the existing TestBIP340AdaptorTamper only covered s tamper). - TestBIP340AdaptorTamperT: rejects verify after substituting an unrelated tweak point. - TestBIP340AdaptorSchemeMismatch: rejects verify when the pubKeyTweak flag is flipped, modeling priv/pub tweak confusion bugs. - TestBIP340RecoverWrongSig: RecoverTweakBIP340 rejects a completed sig that didn't actually come from this adaptor. - TestBIP340RecoverRejectsPrivKeyTweak: RecoverTweakBIP340 refuses to operate on private-key-tweaked adaptors. - TestScriptInputValidation: LockLeafScript and PunishLeafScript reject wrong-length pubkeys and out-of-range CSV values.
Port of internal/cmd/xmrswap to BTC/XMR, replacing DCR-specific calls with BTC equivalents. Uses: - internal/adaptorsigs (BIP-340 adaptor sigs + DLEQ) - internal/adaptorsigs/btc (tapscript 2-of-2 lock + refund tree) - github.com/btcsuite/btcd/rpcclient for bitcoind RPC - github.com/bisoncraft/go-monero/rpc for XMR wallet RPC (unchanged from the reference) This first commit is a scaffold and not yet a running demo. It covers: - Config / connection setup for both parties' XMR wallets and a bitcoind regtest RPC with a loaded wallet. - Party types (initClient for Bob, partClient for Alice). - (partClient).generateDleag: Alice's ed25519 spend-key half, BTC signing key, DLEQ proof. - (initClient).generateLockTxn: Bob's keys, lockTx funding via bitcoind fundrawtransaction, unsigned refundTx + spendRefundTx chain, and the tapscript lock/refund output materials. - Happy-path scenario up through Bob signing and broadcasting lockTx. Deferred to a follow-up session: - generateRefundSigs, initBtc, initXmr, sendLockTxSig, redeemBtc, redeemXmr, refundBtc, refundXmr, takeBtc, startRefund, waitBTC. - The three failure scenarios (alice-bail, cooperative refund, bob-bail). - Simnet validation against a live bitcoind regtest + monerod harness. All crypto primitives the TODO methods will depend on are already committed and validated by the unit tests under internal/adaptorsigs and internal/adaptorsigs/btc.
Adds the remaining swap-state methods to complete the happy path: - (partClient).generateRefundSigs: Alice pre-signs refundTx and adaptor-signs spendRefundTx under Bob's pubSpendKeyl tweak, so cooperative refund later leaks Bob's XMR-key-half. - (initClient).signRefundTx: Bob's cooperative sig on refundTx. - (initClient).buildSpendTx: constructs the unsigned spendTx that moves lockTx funds to Alice's address. - (initClient).sendLockTxSig: Bob adaptor-signs spendTx under Alice's pubSpendKeyProof tweak. - (partClient).redeemBtc: Alice decrypts Bob's adaptor with her ed25519 scalar (reinterpreted as secp), signs her tapscript half, assembles the witness, and broadcasts spendTx. - (initClient).redeemXmr + openSweepXMRWallet: Bob recovers Alice's XMR-key-half via RecoverTweakBIP340 on the completed sig, sums with his own half, and opens a view+spend wallet to sweep the shared address. - (partClient).initXmr: Alice sends XMR to the shared address derived from (pubSpendKey, viewKey). success() now runs the full happy-path flow end-to-end: key exchange, refund pre-signing, lockTx broadcast, XMR lock, adaptor exchange on spendTx, redeem, and XMR sweep. Still requires a running bitcoind-regtest + monerod simnet to execute; unit-testable crypto paths are already covered by tests under internal/adaptorsigs. Deferred to task decred#12: the three failure scenarios (aliceBailsBeforeXmrInit, refund, bobBailsAfterXmrInit) plus the matching methods (refundBtc, refundXmr, takeBtc, startRefund, waitBTC) and live simnet validation.
Adds the remaining methods and three failure scenarios, completing the port of the xmrswap CLI to BTC/XMR. The binary now runs all four scenarios end-to-end - matching the coverage of the DCR reference at internal/cmd/xmrswap. New methods: - (client).startRefund: broadcasts the pre-signed refundTx with both cooperative sigs. - (client).waitBTC: polls bitcoind until lockBlocks have elapsed from a captured start height. - (initClient).refundBtc: Bob spends refundTx via the coop leaf. His decrypt of Alice's pre-signed adaptor produces a sig whose on-chain s value reveals his XMR-key-half to Alice. - (partClient).refundXmr + openSweepXMRWalletAlice: Alice recovers Bob's scalar and sweeps the shared XMR address. - (partClient).takeBtc: Alice alone punish-spends refundTx through the CSV-gated Alice-only leaf. No XMR recovery is possible via this path; Alice forfeits the XMR she locked. New scenarios: - aliceBailsBeforeXmrInit: Bob locks BTC, Alice never locks XMR, Bob refunds cooperatively. His XMR-key leak is harmless since no XMR was locked. - refundScenario: both parties lock, decide to unwind. Bob's cooperative refund reveals his scalar; Alice sweeps the shared XMR. - bobBailsAfterXmrInit: both parties lock, Bob goes silent. Alice broadcasts refundTx, waits the CSV window, and punishes alone. Her XMR is stranded - the asymmetric cost that disincentivizes Bob from stalling. run() dispatches all four in sequence, matching the xmrswap reference behavior. Still deferred to task decred#12: simnet harness setup and live validation. Each scenario needs bitcoind-regtest blocks generated externally for the waitBTC polls to complete.
Makes the CLI self-contained on regtest: after each broadcast and inside waitBTC, we call GenerateToAddress to advance the chain. This is needed because bitcoind regtest does not produce blocks on its own, and the protocol's confirmation and CSV-maturity steps assume the chain is moving. The --no-mine flag disables automining for testnet runs where blocks arrive naturally. README documents: - Which harnesses to start (dex/testing/btc and dex/testing/xmr). - RPC port layout (BTC alpha/beta on 20556/20557, XMR Charlie/Bill/Own on 28284/28184/28484). - How to invoke the CLI (go run) and flag meanings. - Protocol summary pointing to the upstream PROTOCOL.md for the full state machine. - A config.json template for testnet runs.
Phase 2 design proposal. Compares two directions for how dcrdex's client asset layer should expose adaptor-swap primitives: - Direction A: rich AdaptorSwapper interfaces on the backends. - Direction B: thin chain-specific primitives on the backends, with protocol logic in a new client/core/adaptorswap.go orchestrator. Recommends Direction B, because it: 1. Mirrors the server-side state machine structure. 2. Keeps per-swap key material and protocol state at a natural owner (the orchestrator) rather than bleeding into wallet state. 3. Minimizes backend surface area. The XMR additions are the three primitives already identified in XMR_WALLET_AUDIT.md; the BTC additions are narrow (FundBroadcastTaproot + ObserveSpend). Sketches the orchestrator shape, the per-swap state struct, and the phase enum. Lists incremental build order for the remaining phases (1. XMR swap primitives, 2. BTC taproot helpers, 3. orchestrator, 4. msgjson, 5. server state machine, 6. market enforcement, 7. server XMR backend). No code changes. Design doc intended for review before committing to interface shapes.
Step 1 of the Phase-2 build order laid out in internal/cmd/xmrswap/ASSET_INTERFACE_DESIGN.md. Three new methods on ExchangeWallet exposing the chain-interaction primitives that a BTC/XMR adaptor swap needs on the XMR side: - SendToSharedAddress: fund the shared XMR address from the primary wallet. Captures the daemon height at send time for later sweep-wallet restoration. - WatchSharedAddress: open a view-only wallet at the shared address so a counterparty watching from the BTC side can verify the lock. Returns an XMRWatchHandle with HasFunds / Synced / Close. - SweepSharedAddress: open a spendable wallet from the full spend key (after scalar recovery via adaptor-sig math) and sweep to a destination address. The existing HTLC-shaped Swap/AuditContract/Redeem stubs are left alone - they return ErrUnsupported as before. The adaptor-swap orchestrator will route through the new primitives, not the HTLC interface. Per-swap auxiliary wallets (watch + sweep) are tracked in a process-global map keyed by caller-supplied swap ID, with a serializing mutex around map access. This addresses concurrency between separate auxiliary wallets at the map level; whether monero_c itself is safe under concurrent wallet access remains an open question flagged in XMR_WALLET_AUDIT.md and is expected to be verified in simnet testing. Build tag "xmr" required; uses cgo into the existing cxmr bindings and no new C surface.
Narrow primitive for the adaptor-swap orchestrator: given an outpoint and a start height, polls the chain until a spending tx is found, then returns the input's witness stack. The orchestrator uses this to extract the completed BIP-340 signature from an on-chain taproot spend and feed it into RecoverTweakBIP340 to recover the counterparty's XMR-key-half scalar. Two variants: - ObserveSpend: block-scanning with configurable polling interval. Walks blocks forward from startHeight; waits for new blocks when caught up to the tip. - ObserveSpendInMempool: faster pre-confirm check using the mempool. Requires txindex=1 on the bitcoind instance. Returns nil if no spend is yet visible. Kept as stateless helpers taking *rpcclient.Client so the function can be reused by the existing btcxmrswap CLI and a future client/core/adaptorswap.go orchestrator without introducing a new interface layer.
Completes Phase-2 step 2: wraps FundRawTransaction + SignRawTransaction + SendRawTransaction + a caller-supplied confirmation-wait into a single primitive. Returns the confirmed tx, the vout of the caller-identified taproot output, and the confirmation block height. The confirmation-wait is injected via a callback so the helper does not hard-code any mining or polling policy. Tests and live runs share the same signing/broadcasting path but plug in different wait behaviors (auto-mine regtest blocks vs. pure polling on mainnet). Combined with ObserveSpend, this gives the orchestrator everything it needs from the BTC chain-interaction layer without modifying the larger client/asset/btc package.
Phase-3 wire format proposal. Ten new message types covering the BIP-340 adaptor-swap state machine: Setup: - AdaptorSetupPart (participant -> initiator): XMR spend-key half pubkey, view-key half private, secp sign pubkey, DLEQ proof. - AdaptorSetupInit (initiator -> participant): full spend pubkey, view key, secp sign pubkey, DLEQ proof, unsigned refundTx + spendRefundTx, all leaf scripts, CSV lock blocks. - AdaptorRefundPresigned (participant -> initiator): cooperative refund sig + adaptor sig on spendRefundTx. Lock: - AdaptorLocked (initiator -> participant): lockTx txid + vout + value. - AdaptorXmrLocked (participant -> initiator): XMR txid + restore height. Redeem: - AdaptorSpendPresig (initiator -> participant): adaptor sig on spendTx + unsigned spendTx skeleton. - AdaptorSpendBroadcast (participant -> initiator): spendTx txid so the initiator can observe the completed sig and recover the participant's scalar. Refund / punish: - AdaptorRefundBroadcast (either -> server): refundTx txid, broadcaster id. - AdaptorCoopRefund (initiator -> participant): coop spendRefundTx txid - the on-chain sig reveals the initiator's scalar, letting the participant sweep XMR. - AdaptorPunish (participant -> server): punish spendRefundTx txid; participant forfeits XMR but takes BTC. Each message is a Signable with a Serialize method producing a deterministic byte string for signing. No server-side dispatch wiring or client handlers yet - those come after spec review. File is isolated (dex/msgjson/adaptor.go) so the existing HTLC types stay untouched.
Phase-2 step 3 design scaffold. Defines the types and surface area for the client-side adaptor-swap orchestrator: - Role (Initiator/Participant) and Phase (16 values) enums. - State: the full per-swap state record, including fresh keys, peer material, BTC-side tx artifacts, collected sigs, and XMR-side lock/sweep bookkeeping. - Snapshot: the persistable form; serialization TBD. - Event interface + concrete event types for each input the state machine consumes (peer messages, wallet callbacks, timeouts). - Orchestrator struct plus the three adapter interfaces it consumes: BTCAssetAdapter (matches the primitives already in internal/adaptorsigs/btc), XMRAssetAdapter (matches client/asset/xmr/swap.go), and MessageSender + StatePersister for transport + persistence. - Handle: the event dispatcher skeleton; a switch over Phase with inline comments describing the expected transition for each combination. Bodies intentionally empty. Lives in a new subpackage (client/core/adaptorswap) so it can be reviewed and unit-tested without pulling in client/core's full 12k-line Core. Wire-up into Core happens in a follow-up, ideally after the CLI has been live-validated against a simnet so the state transitions can be filled in against tested behavior. Compiles clean and go vet silent. No behavior yet - pure design scaffold.
Phase-3 server-side mirror of client/core/adaptorswap. The server does not participate in the swap crypto - it routes messages between matched clients, validates each message against the expected phase, audits on-chain events through its asset backends, and enforces timeouts. This commit lands the structural skeleton: - Phase enum (12 values): mostly awaiting-X states since the server is almost always waiting for a client message or a chain event. - Outcome enum: how a swap ended, translated to db.Outcome* by the reputation system. Distinguishes initiator bail (harmless leak) from participant bail (no XMR lock) from punished initiator (stalled after XMR lock). - State: per-match record. Holds peer pubkeys/DLEQ proofs/unsigned txs only; never private keys or adaptor secrets. - Event types: ten message events mapping to the Adaptor* msgjson types, six chain-observation events from the asset backends, and EventTimeout. - Coordinator: per-match state machine with four narrow adapters - PeerRouter (server/comms), BTCAuditor (server/asset/btc), XMRAuditor (server/asset/xmr, does not exist yet), and OutcomeReporter (server/auth). - Handle dispatcher: switch over Phase with inline comments describing the expected server action for each (phase, event) pair. Bodies intentionally empty; implementation follows the live-tested client behavior now that Phase 1 is validated. Isolated subpackage so it can be reviewed and unit-tested without touching server/swap's 2900-line HTLC coordinator.
Phase-4 groundwork: market configuration now carries a SwapType field (HTLC or Adaptor) plus a ScriptableAsset that names which side of an adaptor-swap market must provide makers under Option 1. MarketInfo.IsAdaptor and MakerAssetIsScriptable helpers make the downstream enforcement check readable in one line. Zero values preserve backward compatibility: existing markets default to SwapTypeHTLC with ScriptableAsset=0, and the helpers do not affect HTLC markets. Order-router enforcement (rejecting limit orders that sell the non-scriptable asset on adaptor markets) is follow-up work that requires extending the MarketTunnel interface in server/market to expose MarketInfo to handleLimit. Tracking as the remaining piece of task decred#18.
Extends MarketTunnel with SwapInfo(), implemented on *Market by pulling from the underlying MarketInfo (SwapType + ScriptableAsset). handleLimit now rejects standing limit orders whose seller holds the non-scriptable asset on adaptor-swap markets. Immediate TiF orders are allowed regardless, since they can only match as takers. HTLC markets are unaffected (default zero-valued SwapInfo skips the check entirely). Tests: - TMarketTunnel gains swapType + scriptableAsset fields with HTLC defaults. - TestLimitAdaptorOption1 covers both directions: rejects selling non-scriptable (both Sell and Buy by flipping which side is scriptable), and confirms the error code is OrderParameterError. Existing server/market test suite passes unchanged.
Wires the scaffolds up to real logic modeled on the simnet-validated btcxmrswap CLI. client/core/adaptorswap/orchestrator.go: - NewOrchestrator generates fresh per-swap ed25519 and secp256k1 keys plus a DLEQ proof. - Start sends AdaptorSetupPart when called on a participant. - Handle dispatches 15 phases. Happy path is complete end-to-end: initiator consumes AdaptorSetupPart, derives combined XMR view key and pubkey, builds Taproot lock/refund outputs, replies with AdaptorSetupInit. Participant validates, pre-signs refundTx cooperatively, adaptor-signs spendRefundTx under initiator's XMR-key-half pubkey, emits AdaptorRefundPresigned. Initiator funds + broadcasts lockTx via FundBroadcastTaproot. Both sides advance on EventLockConfirmed. Participant sends XMR to shared address, initiator adaptor-signs spendTx, participant decrypts with her ed25519 scalar, assembles tapscript witness and broadcasts. Initiator consumes the on-chain witness, recovers Alice's scalar via RecoverTweakBIP340, and sweeps the shared XMR via SweepSharedAddress. - Refund/punish handlers stubbed with pointers to the CLI flows they port from. Deferred as a follow-up so this commit stays on one validated path. server/swap/adaptor/coordinator.go: - NewCoordinator starts in PhaseAwaitingPartSetup with phase- specific timeouts from Config. - Handle dispatches 9 phases + EventTimeout. Validates each inbound message against its expected phase + structure (DLEQ proof deserializes, adaptor sig parses, refund-tx chain unmars- hals, leaf scripts round-trip through btcadaptor rebuilders). - Routes messages to the peer unmodified; the server does not re-sign or re-derive - only audits. - On-chain events (EventLockConfirmed, EventSpendOnChain, EventCoopRefundOnChain, EventPunishOnChain) drive terminal transitions. Reports Outcome values to the reputation layer via OutcomeReporter for each terminal. - Timeout handler maps current phase to a specific Outcome (initiator-bailed, participant-bailed, punished-initiator, protocol-error). State struct extended with tapscript leaf scripts and SpendTxID. Orchestrator and Coordinator gain a cfg field carrying the runtime-only Config. Still TODO: - Client refund/punish handlers (port from btcxmrswap). - Full-set unit tests using in-memory mock adapters. - Wire into client/core/Core and server/swap/Swapper. - Persistence serialization beyond Phase/Updated in Snapshot. Both packages compile clean under go vet. The existing HTLC path is untouched.
Client orchestrator tests (client/core/adaptorswap): - TestInitiatorHappyPathThroughLockBroadcast: drives a fresh initiator orchestrator from PhaseInit through PhaseLockBroadcast. Verifies the AdaptorSetupInit outbound, lock/refund output construction, and the lockTx fund+broadcast on AdaptorRefundPresigned intake. - TestParticipantStartEmitsSetupPart: Start on a participant emits AdaptorSetupPart and moves to PhaseKeysSent. - TestHandleRejectsWrongEvent: mismatched events return errors without advancing phase. Server coordinator tests (server/swap/adaptor): - TestCoordinatorSetupForwards: PartSetup -> InitSetup -> Presigned each routes to the correct peer role and advances phase. Uses a real participant btc pubkey so the validator's coop/punish leaf rebuild matches. - TestCoordinatorTimeoutMapsOutcome: timeouts in each awaiting phase produce the correct Outcome (initiator-bailed, participant- bailed, punished-initiator). - TestCoordinatorHappyPathTerminal: EventSpendOnChain in PhaseAwaitingSpendBroadcast reports OutcomeSuccess for both roles and transitions to PhaseComplete. Also fixes a lock-recursion bug: State.Snapshot now expects the caller to hold the state mutex (it is always called from inside a Handle handler which already holds it). All tests pass with no external dependencies; run via go test.
Fills in the three refund-path handlers in the client orchestrator,
completing the port of the CLI's aliceBailsBeforeXmrInit / refund /
bobBailsAfterXmrInit scenarios into the state-machine model.
New events:
- EventRefundCSVMatured: refundTx output has matured past its CSV
locktime and may now be spent.
- EventCoopRefundObserved: initiator's coop spendRefund hit the
chain; the witness carries the completed participant sig from
which the initiator's XMR scalar is recovered.
- EventPunishObserved: informational.
New method:
- InitiateRefund: both roles may call to broadcast the pre-signed
refundTx when they decide the swap has stalled. Assembles the
2-of-2 witness from OwnRefundSig + PeerRefundSig (initiator
produces its own sig at refund time via initiatorSignRefundTx).
Handler bodies:
- handleRefundTxBroadcast: dispatches by role.
- Initiator: on EventRefundCSVMatured, initiatorCoopRefund
decrypts the participant's adaptor sig on spendRefundTx using
its own ed25519 scalar, signs its tapscript half, broadcasts
the coop-leaf witness. Terminal.
- Participant: on EventRefundCSVMatured, participantPunish signs
the punish leaf alone and broadcasts. Terminal; XMR is
forfeited.
- Participant: on EventCoopRefundObserved (initiator cooperated),
participantRecoverAndSweep parses the completed sig, calls
RecoverTweakBIP340 on the stored adaptor, reconstructs the
full XMR spend key, and calls SweepSharedAddress.
- handleCoopRefund / handlePunish: terminal guards.
Participant setup also now rebuilds Lock/Refund locally via
btcadaptor so it has the control blocks available at refund time
and can sanity-check the received leaf scripts match its local
rebuild.
New test:
- TestInitiatorRefundPath: drives PhaseLockBroadcast ->
InitiateRefund -> PhaseRefundTxBroadcast -> EventRefundCSVMatured
-> PhaseComplete. Verifies both broadcasts (refundTx + coop
spendRefund) hit the asset adapter.
All three refund paths now have code; no TODO stubs remain in the
orchestrator. Refund-path server coordinator tests are the next
piece.
Phase-3 wiring: a thin integration layer between client/core/Core and client/core/adaptorswap. Lives in two new files; does not modify core.go. client/core/adaptorswap_bridge.go: - AdaptorSwapManager: per-match orchestrator pool keyed by order.MatchID. Exposes three methods - StartSwap (match creation), Handle (inbound peer message), Stop (teardown). - AdaptorSwapManagerConfig: dependency-injection bundle. Each orchestrator gets these as defaults; per-swap Config can override. - routeToEvent: maps msgjson Adaptor* routes + payloads to the orchestrator's Event type. Handles the inbound-peer case; chain events are fed separately. - errPurelyInformational + IsInformational: sentinel for routes (AdaptorLocked, AdaptorSpendBroadcast) that carry no state transition on the receiver - advancement comes from chain observation. Callers can ignore this error rather than surface it. - BTCRPCAdapter: implementation of adaptorswap.BTCAssetAdapter against a btcd-compatible RPC endpoint. Uses internal/adaptorsigs/btc's FundBroadcastTaproot and ObserveSpend helpers; BroadcastTx uses RawRequest to bypass btcd's getnetworkinfo version check that fails on Bitcoin Core 28+ (the same workaround the btcxmrswap CLI uses). - NoopSender and noopAdaptorPersister: defaults usable in tests and before Core wires up the real message transport / DB. client/core/adaptorswap_bridge_test.go: - TestManagerRouteToEventMapping: 8 subtests, one per supported Adaptor* route, asserting the event type and payload handling. Separately exercises the two informational routes and the unknown-route error path. - TestManagerStartAndHandle: confirms StartSwap emits AdaptorSetupPart for a participant, Handle routes to the correct orchestrator, unknown match IDs error, informational routes return the sentinel, and Stop removes the match. Core still needs to: instantiate the manager at startup for adaptor-swap markets, register the "adaptor_" routes with its existing message dispatcher, and call StartSwap/Handle/Stop from its match lifecycle. Those are per-call-site hooks that can be done incrementally without risking the HTLC path.
Mirror of client/core/adaptorswap_bridge on the server side. Lives in server/swap; does not modify the existing HTLC swap.go. server/swap/adaptor_bridge.go: - AdaptorCoordinators: per-match coordinator pool keyed by order.MatchID. Start registers a new coordinator for a match (duplicate Start errors); Handle routes an inbound msgjson Adaptor* payload; Dispatch feeds non-message events (chain observations, timeouts); Stop tears down. - adaptorRouteToEvent: maps each of the 10 server-inbound routes to its adaptor.EventX type, propagating type-assertion failures as errors. - RouterFunc: function-to-PeerRouter adapter so server/comms can wire its ws dispatch in as a one-liner. - NoopReporter and NoopPersister: defaults usable by tests and operators without a reputation backend. server/swap/adaptor_bridge_test.go: - TestAdaptorCoordinatorsLifecycle: full Start/Handle/Stop flow. Verifies duplicate Start errors, that a real AdaptorSetupPart advances the coordinator to PhaseAwaitingInitSetup and routes to the initiator, that unknown match IDs error on Handle, and that Handle errors after Stop. - TestAdaptorRouteToEventMapping: 10 subtests, one per supported route, asserting the Event type. Plus unknown-route and wrong-payload-type error cases. Existing server/swap HTLC test suite passes unchanged. Full go vet clean across the module.
Replaces the stub Snapshot with a full State serialization round- trip. JSON encoding is used for human-inspectability at modest size cost; a future migration to a compact binary format does not change the State surface. New code in persist.go: - fullSnapshot: on-disk schema. Each field is either a fixed scalar size or a length-prefixed byte slice. Complex types (private keys, *wire.MsgTx, *adaptorsigs.AdaptorSignature) are encoded via their existing Serialize methods. - (*State).FullSnapshot: emits the JSON bytes. Caller holds s.mu. - RestoreState: parses bytes back into a State. Reconstructs Lock/Refund taproot output materials by calling btcadaptor's builders with the saved leaf scripts + pubkeys, so the restored State has working ControlBlock and PkScript fields without needing to persist them. - FilePersister: file-per-swap on-disk storage. SaveFull/LoadFull use the full schema; the old Snapshot stub Save/Load remain for compatibility with the existing StatePersister interface. orchestrator.go: - initiatorConsumePartSetup now mirrors lock/refund leaf scripts into the script-fields on State, so snapshot/restore is uniform across initiator and participant roles. Previously only the participant populated those fields (because it received them over the wire); the initiator built them locally and stored them only in s.Lock/s.Refund. Tests: - TestStateFullSnapshotRoundtrip: drives an initiator to PhaseLockBroadcast (most state-rich pre-redemption phase), serializes the State, restores it, and verifies key fields match including the rebuilt Lock/Refund. - TestFilePersisterRoundtrip: full disk round-trip via FilePersister.SaveFull/LoadFull, plus the missing-file case returning (nil, nil). Builds clean; existing tests still pass.
Mirrors the client orchestrator's persistence layer. Server-side state is public-only (no private keys, no adaptor secrets), so plain JSON round-trip is sufficient and even disk-access attackers cannot recover swap funds from these snapshots. persist.go: - serverSnapshot: on-disk schema with all 25 fields of the server State. - (*State).Marshal / Unmarshal: round-trip pair. - FilePersister: per-match JSON file persistence; missing snapshots return (nil, nil). Suitable for low-volume single- process deployments. Production servers should hook into server/db. Tests: - TestStateMarshalRoundtrip: populates every field and verifies field-by-field equality after Marshal/Unmarshal. - TestServerFilePersisterRoundtrip: full disk round-trip plus the missing-file case.
New package implementing the XMRAuditor interface defined in server/swap/adaptor. Talks to a monero-wallet-rpc endpoint over HTTP/JSON-RPC; opens per-swap view-only wallets via generate_from_keys at the participant-reported shared address and waits for an unspent incoming transfer of the expected amount. server/asset/xmr/auditor.go: - Auditor: holds a *rpc.Client (bisoncraft go-monero) and a per-swap open-wallet bookkeeping map. - NewAuditor: input validation + default poll interval (30s). - WaitOutputAtAddress: opens the wallet (or reuses if open), polls IncomingTransfers until total available amount meets the expected threshold, returns nil. Honors ctx for deadlines. - ensureWalletOpen: tries generate_from_keys first; falls back to open_wallet on collision (the bisoncraft package surfaces wallet-rpc errors as untyped Go errors so we always attempt the alternative). - Compile-time assertion that *Auditor satisfies server/swap/adaptor.XMRAuditor; the interface is mirrored locally as adaptorXMRAuditor to avoid an import cycle. server/swap/adaptor/state.go: - XMRAuditor signature extended with ctx, swapID, viewKeyHex, and restoreHeight - the auditor needs all four to manage a per-swap view-only wallet against monero-wallet-rpc. Tests: - TestNewAuditorValidation: rejects nil/empty config; default poll interval applied. - TestAuditorContextCancel: WaitOutputAtAddress respects ctx cancellation when the underlying RPC is unreachable. Operators can omit this auditor entirely - the server-side adaptor coordinator handles a nil XMRAuditor by skipping the XMR confirm-check phase and trusting counterparty reports.
Adds the dispatch surface for adaptor swaps without modifying any HTLC code paths. Three additions to Swapper: - Config.AdaptorCoordinators: optional pool. Nil for HTLC-only deployments. - adaptorCoords: stored on Swapper and threaded from Config in NewSwapper. - HandleAdaptor / StartAdaptorMatch / StopAdaptorMatch: thin wrappers over the pool. nil-safe (returns errors when the pool isn't configured). These methods are the integration handles server/comms and Negotiate will call into. The actual call-site hookup (consulting the market's SwapType in Negotiate's match-creation flow and dispatching adaptor matches to StartAdaptorMatch instead of the HTLC matchTracker setup) is the remaining wire-up step and is called out in the comment on Config.AdaptorCoordinators. Tests: - TestSwapperHandleAdaptorWithoutPool: nil-pool returns errors from Handle/StartAdaptorMatch and is a no-op for Stop. - TestSwapperHandleAdaptorWithPool: full dispatch path with a real pool, real AdaptorSetupPart payload, and verification that the coordinator routes to the initiator role. The existing 17-second server/swap HTLC test suite passes unchanged.
Closes the wire-protocol gap that prevented the client from detecting which markets use SwapTypeAdaptor. Without these fields, client/core has no signal to divert match dispatch from the HTLC trackedTrade path to AdaptorSwapManager. dex/msgjson/types.go: - Add SwapType (uint8), ScriptableAsset (uint32), and LockBlocks (uint32) to Market with omitempty json tags so HTLC markets serialize identically to before, preserving wire compatibility with pre-adaptor servers. server/market/market.go: - New Market.LockBlocks() accessor mirrors the existing SwapInfo() shape. Returns marketInfo.LockBlocks, zero for HTLC markets. server/dex/dex.go: - Populate the three new Market fields in the ConfigResult assembly. SwapType + ScriptableAsset come from SwapInfo(); LockBlocks from the new accessor. dex/msgjson/msg_test.go: - TestMarketAdaptorFields: round-trips an adaptor Market through JSON and asserts all three new fields survive. Also asserts an HTLC Market (zero values) does not emit any of the new keys, so pre-adaptor clients see the same on-the-wire bytes. Builds clean across ./...; existing dex/msgjson, server/dex, server/market, and server/swap test suites pass.
Wraps client/asset/xmr ExchangeWallet's three swap primitives (SendToSharedAddress, WatchSharedAddress, SweepSharedAddress) so they satisfy adaptorswap.XMRAssetAdapter. Symmetric to BTCRPCAdapter in adaptorswap_bridge.go. The file is build-tagged xmr to match client/asset/xmr; without the tag client/core builds without any XMR symbols, preserving the status quo for non-XMR builds. *xmr.XMRWatchHandle already has the methods adaptorswap.XMRWatch requires (Synced, HasFunds, Close), so it satisfies the interface directly without an adapter wrapper. A var _ adaptorswap.XMRAssetAdapter = (*XMRWalletAdapter)(nil) guard locks in the interface conformance at compile time so future interface changes surface here rather than only at AdaptorSwapManager construction. Plain build, xmr-tagged build, and existing client/core test suite (non-xmr) all pass.
Wires the AdaptorSwapManager into Core's lifecycle and the inbound message dispatcher. After this commit Core is ready to receive adaptor_* messages from the server; what is still missing is the per-match StartSwap call from the match-creation path (step 4) and production population of asset adapters. client/core/core.go: - Add adaptorMgr field on Core. Always non-nil so adaptor route handlers do not need a nil check. - Construct it in New() with NoopSender as default. Asset adapters (BTCRPCAdapter and the build-tagged XMRWalletAdapter) are nil until installed by the per-build wiring. Until then StartSwap fails at orchestrator construction, which is the correct behavior since no match has been routed in yet. - Register all 10 adaptor_* routes in noteHandlers, all dispatching to a single handleAdaptorMsg. client/core/adaptorswap_bridge.go: - handleAdaptorMsg: routeHandler entry point. Calls decodeAdaptorMsg to extract the typed payload + match ID, then forwards through c.adaptorMgr.Handle. Informational sentinels (AdaptorLocked, AdaptorSpendBroadcast - state advances via chain observation, not the message itself) are silently absorbed. - decodeAdaptorMsg: switch on msg.Route to unmarshal into the correct msgjson Adaptor* type, validate matchid length, and return both the typed payload and the order.MatchID. Unknown routes and short matchids surface as errors. client/core/adaptorswap_bridge_test.go: - TestHandleAdaptorMsg: starts an orchestrator for a match, then for every non-informational adaptor route round-trips a real msgjson.Message through msgjson.NewNotification + handleAdaptorMsg, asserting the dispatch reaches the manager cleanly. Separately verifies (a) informational routes return nil (sentinel suppressed by the handler), (b) short matchid errors out at decode, (c) unknown adaptor route errors out at decode. Plain build, xmr-tagged build, go vet, and full client/core test suite (2.5s) all pass.
Closes the client-side call-site hookup symmetric to the server's NegotiateAdaptor wire-up. Match-creation traffic for adaptor markets now flows through AdaptorSwapManager instead of the HTLC trackedTrade.negotiate path. HTLC markets are unaffected. client/core/core.go: - In negotiateMatches, look up the per-trade market config and, if SwapType == SwapTypeAdaptor, dispatch to startAdaptorMatches and return early. The HTLC matchTracker setup is incompatible with adaptor swaps - it builds HTLC scripts and presumes an HTLC state machine - so it must not run for these matches. client/core/adaptorswap_bridge.go: - startAdaptorMatches: per-match conversion from msgjson.Match into an adaptorswap.Config, then AdaptorSwapManager.StartSwap. The Config carries identity (SwapID/OrderID/MatchID), the pair-asset IDs derived from the market's ScriptableAsset, base-vs-quote-aware amount conversion via calc.BaseToQuote, role assignment under Option-1 semantics (Side==Maker -> initiator), and the operator-set CSV window from MarketInfo.LockBlocks. Wallet-dependent fields (PeerBTCPayoutScript, OwnXMRSweepDest, XmrNetTag) remain unset; sourcing them via the existing CounterPartyAddress route plus per-asset wallet address lookups is the next wiring step. The orchestrator constructs cleanly without them and only needs them later to build the lock/spend transactions. client/core/adaptorswap_bridge_test.go: - TestStartAdaptorMatches: feeds a maker match and a taker match through startAdaptorMatches and asserts (a) both register an orchestrator in the manager, (b) the maker (BTC holder under Option 1) becomes the initiator and emits no setup message, (c) the taker (XMR holder) becomes the participant and emits AdaptorSetupPart on Start. Plain build, xmr-tagged build, go vet (both flavors), and the full client/core test suite (3.0s) all pass.
Two of the three wallet-dependent Config fields can be populated deterministically (XmrNetTag) or via a best-effort wallet lookup (OwnXMRSweepDest) at startAdaptorMatches time. The third (PeerBTCPayoutScript) requires the CounterPartyAddress message flow and is wired separately. client/core/adaptorswap_bridge.go: - xmrNetTagForNet: maps dex.Network to the Monero address-tag byte: 18 for mainnet (and the simnet harness, which uses monerod regtest with mainnet-shaped addresses), 24 (stagenet) for testnet because monero_c has known address-validation bugs on testnet -- this is the workaround the btcxmrswap CLI also uses. - adaptorOwnXMRDest: best-effort lookup of a deposit address from the connected XMR wallet (asset.NewAddresser). Returns empty if the wallet is not connected or doesn't implement the interface; the orchestrator will fail at sweep with a clear error rather than block setup. - startAdaptorMatches: populates Config.XmrNetTag and Config.OwnXMRSweepDest from the helpers above; comments note that PeerBTCPayoutScript still arrives via a separate path. client/core/adaptorswap/orchestrator.go: - Cfg() accessor on Orchestrator so package core (and tests) can read the runtime configuration without reaching into the unexported field. client/core/adaptorswap_bridge_test.go: - TestStartAdaptorMatches asserts Cfg().XmrNetTag == 18 (Simnet) and Cfg().OwnXMRSweepDest == "" with no wallet attached. - TestXmrNetTagForNet table-tests all three networks. Plain build, xmr vet, and full client/core test suite all pass.
Closes the third wallet-dependent Config field (PeerBTCPayoutScript). Adaptor matches receive the counterparty's BTC payout address through the same wire route as HTLC matches; the handler now branches on whether the manager owns the match before falling through to the HTLC path. client/core/core.go: - handleCounterPartyAddressMsg: before the HTLC tracker.matches lookup, call adaptorMgr.OnCounterPartyAddress. Returns handled=false for unknown matches so HTLC behavior is unchanged for non-adaptor markets. client/core/adaptorswap/orchestrator.go: - SetPeerBTCPayoutScript: idempotent setter that accepts the counterparty's pkScript. Mismatch on a follow-up call errors so a spoofed second message is visible. client/core/adaptorswap_bridge.go: - AdaptorSwapManager.OnCounterPartyAddress: looks up the orchestrator for the match, decodes the address into a pkScript via btcAddressToScript, and delegates to SetPeerBTCPayoutScript. - btcAddressToScript: chaincfg.Params chosen from dex.Network, btcutil.DecodeAddress + IsForNet check + txscript.PayToAddrScript. client/core/adaptorswap_bridge_test.go: - TestOnCounterPartyAddress: exercises the happy path (orchestrator's PeerBTCPayoutScript becomes non-empty), unknown match (handled=false so HTLC takes over), mismatched follow-up rejection, idempotent re-set, and bad-address decode error. - genRegtestAddr helper produces fresh P2PKH addresses so the test doesn't depend on external fixtures. client/core/core_test.go: - newTestRig now constructs adaptorMgr so the existing TestHandleCounterPartyAddressMsg (and any future test that exercises the divert path) doesn't panic on a nil receiver. The manager is unconditional in production. Plain build, xmr build, vet (both flavors), and the full client/core + adaptorswap test suites pass.
First test that exercises the full client-side message exchange between an initiator orchestrator and a participant orchestrator through their AdaptorSwapManagers. Validates that wire formats, routing, and handshake-order assumptions hold up when both sides run together, not just in isolation. client/core/adaptorswap/orchestrator.go: - Phase() accessor on Orchestrator. Symmetric to Cfg(); takes the state mutex so it is safe to call concurrently with Handle. client/core/adaptorswap_bridge_test.go: - TestSetupPhaseRoundTrip: two managers wired with senders that push outbound messages into the other manager's Handle queue, pumped in a bounded loop until both queues drain. The initiator's BTC adapter returns an error from FundBroadcastTaproot so the state machine halts at PhaseKeysReceived without needing a real chain backend - on receipt of AdaptorRefundPresigned, the initiator validates and stores the refund material, then errors out at fund+broadcast and stays in phase. Asserts the participant reaches PhaseRefundPresigned and the initiator reaches PhaseKeysReceived, confirming the AdaptorSetupPart -> AdaptorSetupInit -> AdaptorRefundPresigned exchange goes through end to end. - senderFunc adapter and errorBTC test double. The synchronous, queue-based pump avoids the reentrancy that would deadlock if SendToPeer were to call the other side's Handle inline (both Handle calls take the per-orchestrator state.mu). Plain build, xmr build, and full client/core + adaptorswap test suites all pass.
Companion to TestSetupPhaseRoundTrip. Routes the client orchestrators' wire payloads through a real server-side adaptor.Coordinator instead of directly between two managers, so we exercise the server's validators and per-role relay logic in the same test that exercises the client orchestrators. client/core/adaptorswap_e2e_test.go (new): - TestServerMediatedSetupRoundTrip wires three state machines: participant orchestrator, server coordinator, initiator orchestrator. Each client's sender drops outbound on a single toServer queue; the server's PeerRouter pushes per-role to toInit / toPart. The pump drains all three queues round-robin. Asserts the participant reaches PhaseRefundPresigned, the server coordinator advances through PhaseAwaitingPartSetup -> PhaseAwaitingInitSetup -> PhaseAwaitingPresigned -> PhaseAwaitingLocked (relaying each setup message to the other party along the way), and the initiator halts at PhaseKeysReceived (errorBTC short- circuits FundBroadcastTaproot, same trick as the all-client test). End-to-end coverage of the wire format on both server and client validators in one test. - routerFunc adapts a function to adaptor.PeerRouter for the test; noopServerPersister stands in for adaptor.StatePersister. - Inline route -> adaptor.Event mapping for the three setup-phase routes the test exercises (avoids importing the server/swap-internal adaptorRouteToEvent and the heavy server/swap transitive deps). server/swap/adaptor and server/swap test suites unchanged; full client/core + server/swap test runs and xmr vet pass.
Two tests covering coordinator behavior the existing setup-phase test does not exercise. server/swap/adaptor/coordinator_test.go: - TestCoordinatorLockPhaseAdvances: walks the coordinator from PhaseAwaitingLocked through the BTC and XMR lock phases driven by EventLocked + EventLockConfirmed + EventXmrLocked. Asserts routing role per event (Locked -> participant, XmrLocked -> initiator), confirms EventLocked alone does not advance phase (the on-chain confirm must come first), and confirms that with no XMR auditor wired the coordinator trust-skips the audit and auto-advances PhaseAwaitingXmrLocked -> PhaseAwaitingSpendPresig on EventXmrLocked. - TestCoordinatorOutOfPhaseEventErrors: delivers an EventLocked while the coordinator is still in PhaseAwaitingPartSetup; the handler must reject without changing state, and a subsequent in-phase EventPartSetup must still progress normally (no permanent corruption from the rejection). Existing TestCoordinatorSetupForwards / TestCoordinatorTimeoutMapsOutcome / TestCoordinatorHappyPathTerminal etc. remain green; full server/swap/adaptor + server/swap + client/core test suites pass.
Five-subtest table covering the validators that gate the setup
phase. Each subtest feeds a deliberately-malformed message into
Handle and asserts the coordinator advances to PhaseFailed with
OutcomeProtocolError, rather than just an error return (fail() is
a state transition that returns nil to the caller, by design).
server/swap/adaptor/coordinator_test.go:
- TestCoordinatorRejectsMalformedSetup subtests:
- part-bad-spend-pub-len: PubSpendKeyHalf shorter than 32 bytes,
caught by length check in validatePartSetup.
- part-empty-dleq: nil DLEQProof, caught before extraction.
- part-bad-dleq-bytes: garbage bytes that fail
ExtractSecp256k1PubKeyFromProof.
- init-mismatched-coop-script: validator rebuilds the leaf
script from the advertised pubkeys and rejects when the
counterparty-supplied CoopLeafScript does not match.
- presigned-bad-refund-sig-len: RefundSig != 64 bytes.
These complete the validator coverage that
TestCoordinatorSetupForwards (happy path) and the lock-phase /
out-of-phase tests (PhaseAwaitingLocked transitions) leave open.
Full server/swap/adaptor + server/swap + client/core test runs
pass.
Closes the restart-resilience gap end-to-end. Previously TestStateFullSnapshotRoundtrip only verified the JSON round trip (snapshot bytes -> RestoreState gives back the same fields). This adds the next step - rehydrating an Orchestrator from a restored State - so the full restart path is covered. client/core/adaptorswap/orchestrator.go: - NewOrchestratorFromState constructor: builds an Orchestrator bound to a freshly-supplied Config but reusing a State that came out of RestoreState. Used at process start to resume in-flight swaps. The state owns the swap-specific identity, key material, and per-phase artifacts; the Config owns the runtime deps (asset adapters, sender, persister) which are not persisted. client/core/adaptorswap/orchestrator_test.go: - TestResumeFromSnapshot drives an orchestrator to PhaseLockBroadcast, takes a FullSnapshot, restores via RestoreState, hands the restored state to NewOrchestratorFromState with a fresh Config (different sender / asset-adapter instances, as a real restart would have), and asserts that (a) the resumed orchestrator reports the saved phase, (b) BtcSignKey and XmrSpendKeyHalf match the originals byte-for-byte, and (c) Cfg() returns the resume Config (not the original). Full client/core + adaptorswap + server/swap test suites pass.
Mirror of TestResumeFromSnapshot for the server-side coordinator. Closes the parallel restart-resilience gap on the server. server/swap/adaptor/coordinator.go: - NewCoordinatorFromState constructor: builds a Coordinator bound to a freshly-supplied Config but reusing a State that came out of Unmarshal. Used at process start to resume in-flight matches. The state holds the per-match identity, phase, and public artifacts; the Config holds the runtime deps (router, auditors, reporter, persister) which are not persisted. server/swap/adaptor/coordinator_test.go: - TestCoordinatorResumeFromSnapshot drives a coordinator through EventPartSetup + EventInitSetup so the saved state has substantive content (peer pubkeys, dleq proofs, leaf scripts). Snapshots via Marshal, restores via Unmarshal, hands the restored State to NewCoordinatorFromState with a fresh Router. Asserts (a) the resumed coordinator reports the saved phase (PhaseAwaitingPresigned), (b) feeding the next valid event (EventPresigned) advances to PhaseAwaitingLocked, (c) the presigned message is routed to the initiator on the new router (not the original), confirming the resume Cfg's Router is the one actually in use after the restart. Full server/swap/adaptor + server/swap + client/core test suites pass.
Validates that several concurrent matches share a single manager without interference: distinct orchestrators per match, Handle for one match leaves the others untouched, Stop on one removes only that entry, and Handle on the stopped match errors. Exercises the per-match registry + mutex that all the other tests touch only single-handedly. client/core/adaptorswap_bridge_test.go: - TestManagerMultipleSwapsIsolated: starts three swaps with distinct match IDs (two initiators, one participant) on one manager, captures each orchestrator's initial phase, drives only one match's state machine via a Handle, then asserts the other two orchestrators' phases are unchanged. Stops one of the three and confirms the registry contains exactly the other two and the stopped match's Handle now errors. Full server/swap, server/swap/adaptor, client/core, and client/core/adaptorswap test suites pass.
Mirror of TestManagerMultipleSwapsIsolated on the server side. Validates that the AdaptorCoordinators per-match registry behaves analogously to the client AdaptorSwapManager: distinct coordinators per match, Handle for one match leaves the others untouched, Stop on one removes only that entry. server/swap/adaptor_bridge_test.go: - TestAdaptorCoordinatorsMultipleMatchesIsolated: starts three matches with distinct match IDs in one pool, drives only one through a real AdaptorSetupPart, and asserts the other two coordinators' phases are unchanged. Stops one of the three and confirms the registry contains exactly the other two and the stopped match's Handle errors. Full server/swap test suite passes.
Adds the two participant refund-branch tests that were the largest remaining test gap, and fixes a real bug surfaced by the new test: participantConsumeInitSetup parsed the counterparty's combined spend pubkey but discarded the result with _ =, leaving state.FullSpendPub nil. The participant nil-deref'd later when trying to derive the shared XMR address for sweep. client/core/adaptorswap/orchestrator.go: - participantConsumeInitSetup now parses PubSpendKey via edwards.ParsePubKey first (matching the initiator's emit path which uses ed25519 SerializeCompressed) and assigns the result to s.FullSpendPub. The btcec fallback now returns an explicit error rather than silently accepting it, since the participant cannot derive a shared XMR address from a non-ed25519 pubkey. client/core/adaptorswap/orchestrator_test.go: - driveSetupPhase helper: runs paired initiator + participant orchestrators through the setup-phase exchange and returns both with the participant in PhaseRefundPresigned. Used by both new tests so they don't reinvent the bootstrap. - TestParticipantPunishPath: drives the participant from setup- complete through InitiateRefund + EventRefundCSVMatured (no coop refund from the silent initiator) and asserts PhasePunish, two total BTC broadcasts (refundTx + punish spendRefund), and that no XMR sweep happened (XMR is forfeited on this branch - the asymmetric punishment Bob gets for stalling). - TestParticipantRecoverAndSweepPath: drives the alternate refund branch where Bob coop-refunded first. Runs the initiator through its full coop-refund path (consuming the participant's presigned, calling InitiateRefund, then handling EventRefundCSVMatured) to capture a real on-chain spendRefundTx witness, then feeds that witness to the participant via EventCoopRefundObserved. Asserts PhaseComplete and that the XMR sweep was directed to OwnXMRSweepDest. Full client/core/adaptorswap + client/core + server/swap test suites pass.
Closes the largest of the production-readiness gaps: outbound
adaptor_* messages now actually leave the client. Previously the
manager defaulted to NoopSender and the orchestrator's emits were
silently dropped, so no real swap could progress past the first
SendToPeer call.
client/core/adaptorswap_bridge.go:
- dcSender: new adaptorswap.MessageSender that wraps the payload
in a notification via msgjson.NewNotification and pushes it
through the trade's dexConnection.WsConn.Send. The notification
goes to the server, which validates and routes to the matched
counterparty (the inbound side is already wired through Core's
noteHandlers + handleAdaptorMsg).
- startAdaptorMatches: per-match SendMsg is now &dcSender{dc: tracker.dc}
instead of inheriting NoopSender from the manager defaults.
Each match's outbound flows through its own trade's
dexConnection so the trade-message ordering invariants Core
already maintains apply equally to adaptor traffic.
client/core/adaptorswap_bridge_test.go:
- TestDcSender unit: minimal capturing WsConn confirms the sender
emits a notification on the right route, the matchid round-trips
through the JSON encode/decode, and Send errors propagate to
the caller.
- TestStartAdaptorMatches updated to reflect that outbound now
flows through the per-match dcSender (tracker.dc.WsConn) rather
than the manager's default sender. Tracker now carries a
dexConnection backed by captureWsConn for the test.
Plain build, xmr build, and full client/core + adaptorswap +
server/swap test suites all pass.
Closes the second of the production-readiness blockers. The client now actually sends adaptor_* notifications (commit 0924d73); this commit makes the server pick them up: previously they hit comms.Server.Route, found no registered handler, and got a "unknown route" error back to the client. server/swap/swap.go: - NewSwapper now registers all 10 adaptor_* routes with the AuthManager when adaptorCoords is configured. HTLC-only deployments (no AdaptorCoordinators in Config) leave the routes unregistered, so HTLC behavior is unchanged and clients sending adaptor messages to an HTLC-only server get the standard unknown-route response. server/swap/adaptor_bridge.go: - handleAdaptorMsg: AuthManager-route handler that decodes the payload, validates the user's signature via authUser, extracts the match ID, and calls HandleAdaptor. - adaptorMsgPayload: per-route decoder that returns the typed payload + match ID. Mirrors client/core's decodeAdaptorMsg so the wire format is parsed consistently on both sides; kept local to server/swap so the server doesn't import client/core. - adaptorRoutes: declared list of the 10 routes used by NewSwapper to register them in one place. server/swap/adaptor_bridge_test.go: - TestHandleAdaptorMsg subtests cover (a) no pool configured -> RPCInternalError, (b) happy path: real AdaptorSetupPart with a registered coordinator advances the phase and routes to the initiator, (c) unknown route at decode -> RPCParseError, (d) short matchid at decode -> RPCParseError, (e) auth failure -> SignatureError. Adds a minimal fakeAuthMgr satisfying the AuthManager interface. End-to-end the wire path now flows: client orchestrator SendToPeer -> dcSender.Send -> server comms -> AuthMgr.Route -> swapper.handleAdaptorMsg -> swapper.HandleAdaptor -> AdaptorCoordinators.Handle -> coordinator.Handle. Full server/swap, server/swap/adaptor, client/core, client/core/adaptorswap test suites pass; go vet clean.
Closes the simnet blocker where operators had no way to actually declare a market as adaptor-shaped. Previously the markets.json schema had no swapType field, so every market silently parsed as HTLC regardless of operator intent and the entire adaptor path stayed dormant. server/dex/dex.go: - Market (the operator-config JSON struct) gets three new fields: swapType (string, "htlc" default or "adaptor"), scriptableAsset (asset symbol, e.g. "btc"), and lockBlocks (uint32, BTC CSV window). The first is omitempty so existing HTLC-only configs serialize unchanged. - loadMarketConf parses the new fields after building MarketInfo. HTLC stays the default. "adaptor" requires both scriptableAsset (resolved via dex.BipSymbolID and validated to equal Base or Quote) and lockBlocks > 0; otherwise loadMarketConf errors loudly so a misconfigured operator finds out at startup rather than at first match. - Unknown swapType values (e.g. typos) are rejected. server/dex/dex_test.go (new): - TestLoadMarketConfAdaptor exercises (a) a valid adaptor market produces SwapTypeAdaptor + the right ScriptableAsset / LockBlocks on the resulting MarketInfo, (b) a config without the new fields defaults to HTLC, (c) missing scriptableAsset errors, (d) lockBlocks=0 errors, (e) scriptableAsset that is neither base nor quote errors, (f) unknown swapType errors. Full server/... + client/core/... test suites pass.
Closes the second simnet blocker: the initiator had no way to
learn the participant's BTC payout address (where the spendTx
output should pay), because the existing CounterPartyAddress flow
fires only on the HTLC processMatchAcks path that NegotiateAdaptor
deliberately bypasses. Without it, PeerBTCPayoutScript stayed
empty and spendTx construction would have failed at use time.
The cleanest fix is to piggy-back on a setup message that already
flows participant -> initiator: AdaptorSetupPart now carries the
participant's BTC deposit address, the initiator decodes it on
receipt.
dex/msgjson/adaptor.go:
- AdaptorSetupPart gains BTCPayoutAddr (string, omitempty so wire
compat with pre-adaptor servers stays clean). Serialize appends
it after DLEQProof.
client/core/adaptorswap/orchestrator.go:
- Config gains OwnBTCPayoutAddr (participant fills in
AdaptorSetupPart) and DecodeBTCAddr (a pluggable addr -> script
decoder; supplied by the bridge so the orchestrator does not
need chain params).
- sendPartSetup now stamps OwnBTCPayoutAddr into the emitted
AdaptorSetupPart.
- initiatorConsumePartSetup decodes BTCPayoutAddr into a pkScript
via cfg.DecodeBTCAddr and assigns to cfg.PeerBTCPayoutScript at
the top of the handler. Empty address with no decoder is allowed
(legacy / out-of-band CounterPartyAddress flow can still
populate via SetPeerBTCPayoutScript). Empty address with
configured decoder is also allowed - just no-ops.
client/core/adaptorswap_bridge.go:
- Renames adaptorOwnXMRDest -> adaptorOwnDepositAddr (the helper
is chain-agnostic - it just looks up the deposit address from
the local wallet for assetID).
- startAdaptorMatches now pre-fetches the role-appropriate deposit
address: initiator gets OwnXMRSweepDest from the local XMR
wallet; participant gets OwnBTCPayoutAddr from the local BTC
wallet. Both sides install a DecodeBTCAddr closure over c.net so
the initiator can decode whatever address the participant sends.
client/core/adaptorswap/orchestrator_test.go:
- TestBTCPayoutAddrFlowsThroughSetupPart drives the round trip
end-to-end with a stub decoder: participant cfg has
OwnBTCPayoutAddr; initiator cfg has DecodeBTCAddr. After Start +
Handle(EventKeysReceived{Setup: AdaptorSetupPart}), the
initiator's cfg.PeerBTCPayoutScript holds the decoded script.
Plain build, xmr-tagged build, and full server/... + client/core/...
+ dex/msgjson test suites all pass.
In the absence of independent chain watchers (which the production
build will eventually add), the swap was stalling at three points:
participant after RefundPresigned waiting for EventLockConfirmed,
initiator after lockTx broadcast for the same event, and initiator
after AdaptorXmrLocked waiting for EventXmrConfirmed. None of
those events have anyone firing them in the wired-up code path.
This commit makes each transition self-fire from information the
orchestrator already has at the moment of transition. The wire
audit replaces the chain audit, which is acceptable for simnet and
for production-with-trusted-counterparties; a real chain watcher
would be a strict-mode replacement.
client/core/adaptorswap/orchestrator.go:
- handleKeysReceived (initiator): after broadcasting lockTx and
emitting AdaptorLocked, immediately self-fires EventLockConfirmed
with the height returned by FundBroadcastTaproot (which already
blocks on confirmation in the production BTCRPCAdapter). Advances
PhaseLockBroadcast -> PhaseLockConfirmed inline.
- handleRefundPresigned (participant): after recording the lock
height from the inbound EventLockConfirmed, chains directly into
handleLockConfirmed so the XMR send happens in the same Handle()
call instead of waiting for a non-existent second wakeup.
- handleLockConfirmed (initiator branch): after recording the
participant's XMR locked claim, chains into handleXmrConfirmed so
the spendTx adaptor sig is built and emitted inline. Adds an
explicit comment noting that production with a real XMR auditor
would gate this on EventXmrConfirmed.
client/core/adaptorswap_bridge.go:
- AdaptorLockedRoute: was errPurelyInformational; now produces
EventLockConfirmed{Height: 0} so the participant proceeds to
send XMR. Height=0 is fine because XmrRestoreHeight (the only
user of LockHeight downstream) comes from the participant's own
XMR wallet at SendToSharedAddress time, not from the BTC chain.
AdaptorSpendBroadcast remains the only purely-informational
route.
Test updates:
- TestInitiatorHappyPathThroughLockBroadcast and
TestInitiatorRefundPath now expect PhaseLockConfirmed (not
PhaseLockBroadcast) as the post-presigned phase, since the
initiator no longer pauses there.
- TestResumeFromSnapshot: same.
- TestManagerStartAndHandle / TestHandleAdaptorMsg /
TestManagerRouteToEventMapping: AdaptorLocked moves out of the
informational test cases and into the event-firing ones; the
remaining informational route is AdaptorSpendBroadcast.
Plain build, full client/core/... + server/swap/... test suites
all pass.
…MR sweep. Closes the last remaining stall point on the happy path: the initiator at PhaseSpendPresig was waiting for EventSpendObservedOnChain (carrying the participant's completed sig in the lockTx witness) so RecoverTweakBIP340 could extract the participant's XMR scalar. Nobody was calling assetBTC.ObserveSpend, so the swap stalled there forever and the initiator never got XMR. The orchestrator stays passive (still purely event-driven). The side-effect of "kick a polling watcher" lives in the bridge, invoked via a callback at the moment of phase entry. client/core/adaptorswap/orchestrator.go: - Config gains a SpendObserver callback. nil leaves the orchestrator in test-mode behavior (no automatic observation, swap stalls - matches the documented behavior of the unit-test fakeBTC). - handleXmrConfirmed (initiator) calls SpendObserver after sending AdaptorSpendPresig, passing the lockTx outpoint + height it just recorded. Synchronous call, but the closure is expected to dispatch to a goroutine; the orchestrator's locked Handle() returns immediately. client/core/adaptorswap_bridge.go: - AdaptorSwapManager.spendObserverFor(matchID) returns the per-match closure. Spawns a goroutine that calls m.btc.ObserveSpend(outpoint, startHeight); on observation rechecks that the swap is still registered (handles the Stop()-during-poll race), then dispatches EventSpendObservedOnChain via Handle so the orchestrator runs RecoverTweakBIP340 + SweepSharedAddress and reaches PhaseComplete. - startAdaptorMatches wires the closure into Config.SpendObserver for every match (initiator + participant; the participant just doesn't invoke it). Plain build, full client/core/... + server/swap/... test suites all pass.
Closes the third simnet blocker: terminal phases now run a
callback the bridge uses to record the outcome and tear down
state. Without this, the orchestrator silently sat on
PhaseComplete / PhaseFailed / PhasePunish forever - the operator
saw no signal that the swap was done and the order's status
stayed at Booked indefinitely.
client/core/adaptorswap/orchestrator.go:
- Config gains an OnTerminal callback. nil disables it (test
mode); the orchestrator is otherwise unchanged.
- save() now detects the first transition into a terminal phase
and fires OnTerminal exactly once. terminalFired guard prevents
double-invocation if save() runs again with the same terminal
phase (e.g. handleXmrSwept's "Phase = PhaseXmrSwept; Phase =
PhaseComplete" double-write).
client/core/adaptorswap/state.go:
- Orchestrator gains terminalFired bool, held under state.mu like
every other state field.
- Phase.IsTerminal now includes PhasePunish in addition to
PhaseComplete and PhaseFailed. handlePunish already treated it
as terminal ("event %T in terminal PhasePunish") - this just
fixes the inconsistency.
client/core/adaptorswap_bridge.go:
- Core.adaptorTerminalCallback returns a per-match closure that
logs the outcome with phase-specific severity (Info for
Complete, Warn for Failed/Punish), unregisters the orchestrator
via adaptorMgr.Stop(matchID), and best-effort marks the
trackedTrade.metaData.Status as OrderStatusExecuted so the order
exits the active set.
- startAdaptorMatches wires the callback into Config.OnTerminal
for every match.
client/core/adaptorswap/orchestrator_test.go:
- TestOnTerminalFiresOnce drives the punish path and asserts
OnTerminal saw PhasePunish exactly once (the terminalFired
guard).
Plain build, full client/core/... + server/swap/... test suites
all pass.
Documents how to actually drive a BTC/XMR adaptor swap end-to-end through the dcrdex server (not the standalone btcxmrswap CLI). The infrastructure is wired - what's left is operator setup (markets.json, harness, two clients, scripted Trade() calls). This runbook captures that operator-side path until a real harness script and a UI exist. dex/testing/dcrdex/ADAPTOR_SIMNET.md (new): - Status table covering what's wired and what's not (persistence, auditors, UI, server-restore-on-startup, order-intake gaps). - Prerequisites: BTC + XMR + DCR harnesses, build with -tags xmr. - markets.json template with swapType / scriptableAsset / lockBlocks fields. - How to drive an order match via Core.Trade() since there is no UI yet. - Expected log progression on both server and clients including the exact phase sequence for initiator vs participant. - Definition of "complete" (BTC paid, XMR swept, order status updated). - Known fragility points: SendToSharedAddress timeouts, leaked SpendObserver goroutine on counterparty death, restart drops in-flight swaps. - "When this runbook will be obsolete" - the eventual landing conditions for genmarkets-adaptor.sh, harness tag passthrough, persistence, and a frontend.
README for the client/core/adaptorswap package documenting what
works today, what's still needed before production, and a
recommended landing order for the remaining work. Cross-references
the operator simnet runbook, the protocol spec, and the standalone
btcxmrswap demonstrator so a future maintainer has one document to
start from.
client/core/adaptorswap/README.md (new):
- Overview + cross-references to mirror packages (server-side
coordinator, cryptography, wire types, operator runbook,
protocol spec).
- "What works today" - covers the cryptography, the dcrdex
integration end-to-end, all three sourced Config fields, the
auto-advance + spend-observation watcher, terminal-phase
callback, snapshot/restore plumbing, and the test surface.
- "What's left for production" - 11 items in rough release-blocking
order:
1. Persistence (DB-backed StatePersister on both sides + server
restore-on-startup loop)
2. Server-side BTC/XMR auditors (closes the trust-skip security
gap)
3. Client-side strict-mode chain watchers (companion to #2)
4. Match outcome -> DB / order history (depends on #1)
5. Order-intake completeness (Option-1 enforcement gaps for
market orders, IoC limits, cancels)
6. UI (wallet config, order placement, swap status, refund flow,
XMR-specific config wizard)
7. Wallet-rpc lifecycle robustness (monero_c concurrency, sweep
wallet cleanup)
8. Reorg handling (lockTx / refundTx / spendTx)
9. Operator harness automation (-tags xmr passthrough,
genmarkets-adaptor, scripted test driver)
10. External security review (cryptography + state-machine
invariants + trust-mode shortcuts + msg auth)
11. Mainnet/testnet deployment plan (asset registration,
lockBlocks decision, stagenet workaround)
- Recommended landing order for the work, plus a snapshot of the
branch state for whoever picks this up.
The adaptor-swap participant (non-scriptable-side seller - XMR on a btc_xmr market) does not lock funds at order placement. The real XMR send-to-shared-address fires after EventLockConfirmed, driven peer-to- peer by the orchestrator. The existing HTLC-shaped order-intake path still calls FundOrder / FundingCoins / ReturnCoins / SignCoinMessage on the client and validates coins / signatures on the server, so a participant order currently fails with ErrUnsupported on the client and coin-validation errors on the server. This commit works around that until order-intake gains real adaptor awareness (README TODO decred#5): - client/asset/xmr/xmr.go: FundOrder, FundingCoins, ReturnCoins, SignCoinMessage return synthetic stub values (a fake one-coin commitment, zero-byte pubkey+sig) so the client-side shape checks in Core.prepareTradeRequest pass. - server/market/orderrouter.go: when an order's funding asset is the non-scriptable side of an adaptor market, skip the entire coin validation block in processTrade and submit directly. The real fix is to teach prepareTradeRequest and processTrade to route adaptor-participant orders through a separate path that does not ask the wallet for funding. This hack exists to unblock simnet end-to-end testing; remove with the proper implementation.
NegotiateAdaptor was failing every match with "Matches lost!" because server/dex/dex.go built the swap.Config without ever constructing an AdaptorCoordinators pool, so adaptorCoords stayed nil at runtime even on adaptor-marked markets. Wire it up in trust mode for simnet: - server/dex/dex.go: build a NoopReporter / NoopPersister pool with nil BTC/XMR auditors. PeerRouter wraps authMgr.Send, addressing outbound coordinator messages by (matchID, role) and dispatching to the user account recorded for that match. Set the pool on swap.Config.AdaptorCoordinators. - server/swap/adaptor_bridge.go: AdaptorCoordinators now records the initiator + participant account.AccountID at Start time and exposes UserFor(matchID, role) so the router closure can map outbound (matchID, role) to a user. Stop clears the entry. - NegotiateAdaptor passes match.Maker.User() (initiator under Option 1) and match.Taker.User() (participant) into Start. - Tests updated to match the new Start / StartAdaptorMatch signatures. Production gaps still standing per README: real BTC/XMR auditors (TODO #2), strict-mode client watchers (decred#3), real persisters and startup restore (#1). This commit is the trust-mode minimum needed to take a simnet match past "Matches lost!".
The participant's redemption address on an adaptor market is its
own BTC payout address, not a peer-supplied one - the participant
gets BTC at an address derived from its own wallet, announced to
the initiator via AdaptorSetupPart. The maker's redemption address
delivered through the per-match counterparty_address notification
is the maker's XMR sweep destination, which means the participant
side currently tries to decode an XMR address as BTC and fails:
decode peer btc address "82eAcL...": DecodeAddress: checksum mismatch
Fix:
- client/core/adaptorswap/orchestrator.go: expose Role() so callers
can branch on initiator vs participant without reaching into State.
- client/core/adaptorswap_bridge.go: in OnCounterPartyAddress, return
early when the orchestrator role is participant. The initiator
still consumes the address (the participant's BTC payout).
- client/core/adaptorswap_bridge_test.go: TestStartAdaptorMatches and
TestDcSender now build a dexAccount with a real privKey so the
signing introduced for outbound adaptor messages does not nil-deref.
Core.New builds the AdaptorSwapManager with Send:NoopSender and no
BTC or XMR asset adapters, with a comment saying they'd be installed
later by per-asset wiring - but no wiring code ever landed. Every
orchestrator started from a match therefore got AssetBTC=nil and
panicked on the first FundBroadcastTaproot call:
panic: runtime error: invalid memory address or nil pointer
...(*Orchestrator).handleKeysReceived orchestrator.go:620
Work around it with two per-swap builders invoked inline before
StartSwap:
- client/core/adaptorswap_bridge.go: buildAdaptorBTCAdapter reads
DCRDEX_ADAPTOR_BTC_{RPC,USER,PASS} env vars, constructs a
btcd/rpcclient.Client against the simnet bitcoind RPC, and wraps
it in BTCRPCAdapter with a poll-for-confirm waitConfirm. Returns
nil if env vars unset.
- client/core/adaptorswap_xmr_bridge.go (xmr tag): buildAdaptorXMR
Adapter looks up the connected xmr.ExchangeWallet from Core's
wallets map and wraps it in XMRWalletAdapter.
- client/core/adaptorswap_xmr_bridge_noxmr.go (!xmr tag): stub that
returns nil so non-xmr builds still compile.
- startAdaptorMatches: sets cfg.AssetBTC / cfg.AssetXMR from the
builders before calling StartSwap. With these wired, the initiator
can fund + broadcast the lockTx and both sides can exercise the
XMR wallet adapter.
Still needed for production (README TODO decred#5): expose the BTC wallet's
rpcclient.Client on bisonw's BTC asset wallet so env-var side-channels
are not required. Drop the env lookup and source the client from the
connected wallet.
The adaptor setup phase flows silently through the server (AuthManager
only logs handling failures, not successes) and through the client
runJob loop (which logs only when a handler takes >=250ms). That
makes it hard to diagnose a stuck swap: a participant that never
receives the relayed AdaptorLocked looks identical to one where the
inbound handler is blocked inside the cgo XMR wallet.
Add four log lines, all at INFO, to distinguish those cases on the
next simnet run:
- client handleAdaptorMsg: log route + matchID on entry, and route +
matchID + dispatch elapsed time + err on exit.
- server AdaptorCoordinators.Handle: log the inbound route + matchID +
phase before c.Handle, and the resulting phase + err after.
- server dex.adaptorRouter: log each outbound relay (route, matchID,
role, user, err).
No behavior change. Test rig Core{} in TestHandleAdaptorMsg now sets
log: tLogger so handleAdaptorMsg doesn't nil-deref the logger.
Revert (or downgrade to Tracef) once the simnet swap is green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DO NOT MERGE. This is currently an experiment.
What it adds
A complete second swap protocol alongside HTLC, selectable per-market via
operator config, that lets dcrdex match BTC against XMR (and in principle any
scriptable / non-scriptable pair).
Code added, by area
Cryptography (new)
internal/adaptorsigs/btc/- BIP-340 Schnorr adaptor signatures, DLEQproofs binding ed25519 scalars to secp256k1 points, Taproot tapscript
helpers. Validated by unit tests + four BIP-340 official vectors + full
integration tests through btcd's script engine.
Standalone demonstrator
internal/cmd/btcxmrswap/- CLI that runs all four protocol scenariosend-to-end on simnet (success, alice-bails-before-xmr-init, refund-coop,
bob-bails-after-xmr-init / punish branch). Bypasses the DEX server; proves
the cryptography against bitcoind regtest 28.1 + the dex/testing/xmr
harness.
Reference protocol implementation (new in this lineage)
internal/cmd/xmrswap/- DCR/XMR equivalent of the above plus the designdocuments the rest of the work was based on:
PROTOCOL.md(full protocolspec),
XMR_WALLET_AUDIT.md(gap analysis ofclient/asset/xmr),ASSET_INTERFACE_DESIGN.md(the Direction-B design recommendation thatthe orchestrators implement).
Wire types
dex/msgjson/adaptor.go- 10adaptor_*route definitions and messagepayload types (
AdaptorSetupPart,AdaptorSetupInit,AdaptorRefundPresigned,AdaptorLocked,AdaptorXmrLocked,AdaptorSpendPresig,AdaptorSpendBroadcast,AdaptorRefundBroadcast,AdaptorCoopRefund,AdaptorPunish).dex/msgjson/types.go- extendsMarketwithswapType,scriptableAsset,lockBlocks(omitempty for wire compat withpre-adaptor servers).
dex/market.go-MarketInfogainsSwapType,ScriptableAsset,LockBlocksand anIsAdaptor()helper.Client-side (new package)
client/core/adaptorswap/- orchestrator state machine, per-swap keygeneration, snapshot serialization, file persister, in-process resume
(
NewOrchestratorFromState). The asset adapter interfaces(
BTCAssetAdapter,XMRAssetAdapter) keep chain specifics out.client/core/adaptorswap_bridge.go- bridge betweenCoreand theorchestrator: per-match
AdaptorSwapManager,dcSenderfor outboundnotifications, route decoder,
BTCRPCAdapter,OnTerminal/SpendObserver/DecodeBTCAddrcallbacks plumbed.client/core/adaptorswap_xmr_bridge.go(build-taggedxmr) - wrapsclient/asset/xmr.ExchangeWalletto satisfyXMRAssetAdapter.client/core/core.go- instantiates the manager atNew, registers all10 adaptor routes in
noteHandlers, divertsnegotiateMatchesto themanager when the market is adaptor-shaped, routes
CounterPartyAddressfor adaptor matches into the orchestrator.
client/asset/xmr/swap.go- three new XMR primitives(
SendToSharedAddress,WatchSharedAddress,SweepSharedAddress) plusper-swap watch/sweep wallet lifecycle.
Server-side (new package)
server/swap/adaptor/- coordinator state machine (server-side mirror ofclient orchestrator), wire validators, per-phase timeouts, persistence
types,
NewCoordinatorFromStatefor restart-resume.server/swap/adaptor_bridge.go-AdaptorCoordinatorsper-match pool,Swapper.NegotiateAdaptor(sibling ofNegotiatefor adaptor markets),Swapper.handleAdaptorMsgroute handler + decoder, route registrationin
NewSwapper.server/swap/swap.go- extendsConfigwithAdaptorCoordinatorspointer;
NewSwapperregistersadaptor_*routes onAuthManagerwhenthe pool is configured.
server/asset/xmr/- XMR audit backend.server/dex/dex.go-markets.jsonschema accepts the three adaptorfields with full validation (scriptable asset must be in the pair,
lockBlocks > 0, etc.).
server/market/market.go-Market.SwapInfo/Market.LockBlocksaccessors; dispatches matches to
swapper.NegotiateAdaptorwhenmarketInfo.IsAdaptor().Operator + maintainer documentation
dex/testing/dcrdex/ADAPTOR_SIMNET.md- operator runbook for anend-to-end simnet swap through the DEX (markets.json template, harness
prep, scripted Trade() calls, expected log output, definition of "done",
known fragility points).
client/core/adaptorswap/README.md- package overview, what workstoday, 11-item production-readiness TODO with severity ordering and a
recommended landing sequence.
Test coverage added
~50 new test functions across 7 packages. Coverage spans:
participantConsumeInitSetupwhereFullSpendPubwas discarded).AdaptorSetupPart.dcSendernotification round-trip).