Files
dex/pkg/rfq/timeout.go
T
zeekay f4bcdbca39 feat(rfq): non-custodial cross-chain swap coordinator (pkg/rfq)
The venue V of the HTLC atomic-swap design: a pure relay that serves a
maker-signed quote book and sequences a swap session between taker U (the
secret-holder) and a CEX-backed maker M, validating each typed signed
transition — role bindings (HTLC_A recipient=U/refund=M, HTLC_B
recipient=M/refund=U), the cross-leg hashlock, and the timeout ordering
T_B > T_A + Δ (maker locks first/shorter; spec Δ table: Lux<->BTC 2h,
EVM<->EVM 5m).

Non-custody is STRUCTURAL: the Coordinator holds no private key of any
asset, is the recipient/refund of no HTLC, and has no fund-moving path —
TestNonCustody reflects over it and fails if any field is a key type. After
the hashlock commit V is irrelevant to safety; its crashing or lying can
only deny service or relay a stale quote.

Composes pkg/htlc for the shared hashlock/preimage and authenticates every
message through crypto.Ecrecover directly (recoverAddr) with its own venue
domain tags — a leaf, so the relay never pulls the GPU CLOB matcher. Builds
and passes go test -race off a non-GPU box.
2026-06-26 04:26:54 +00:00

95 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package rfq
import "time"
// timeout.go is the timeout-ordering policy from the HTLC atomic-swap spec
// ("The Timeout-Ordering Rule"). It is a PURE table: no state, no clock reads.
//
// The maker's leg (HTLC_A) expires first; the taker's leg (HTLC_B) expires last,
// and the safety buffer between them must satisfy
//
// T_B > T_A + Δ, Δ ≥ R_late + C_late
//
// where R_late + C_late is the reorg-safety depth plus worst-case claim
// confirmation time of whichever chain can reorg. Δ is the FLOOR; AssignTimeouts
// recommends timeouts strictly above it, and a session rejects any submitted leg
// pair that does not clear it.
// ChainKind identifies a settlement chain by its reorg/finality character — the
// only chain property the timeout policy needs. It is a value: the coordinator
// dials no chain and verifies no proof, so a chain is just its safety class.
type ChainKind uint8
const (
ChainLux ChainKind = iota // sub-second deterministic finality (R ≈ 0)
ChainEVM // probabilistic EVM L1/L2 (a few minutes)
ChainBitcoin // 6-confirmation reorg safety (~2h with claim confirm)
)
func (c ChainKind) String() string {
switch c {
case ChainLux:
return "lux"
case ChainEVM:
return "evm"
case ChainBitcoin:
return "bitcoin"
default:
return "unknown"
}
}
// reorgBuffer is R + C per chain: the time a leg's timeout must lead any
// dependent claim by, to survive that chain's worst-case reorg plus claim
// confirmation. From the spec's "Concrete parameters": Bitcoin ≥ 2h (6-block
// reorg ≈ 60min plus claim confirmation), EVM a few minutes, Lux ≈ 0.
var reorgBuffer = map[ChainKind]time.Duration{
ChainLux: 0,
ChainEVM: 5 * time.Minute,
ChainBitcoin: 2 * time.Hour,
}
const (
// MakerLegLifetime is T_A − t0: the short window the maker's leg stays locked
// before auto-refund (spec example: 40 min), bounding maker capital lockup.
MakerLegLifetime = 40 * time.Minute
// ClaimMargin is the cushion AssignTimeouts adds above the Δ floor so the
// recommended (T_A, T_B) strictly satisfy T_B > T_A + Δ.
ClaimMargin = 5 * time.Minute
)
// Delta is the minimum safety buffer Δ between the two legs' timeouts for a swap
// over (chainA → chainB). Δ must cover whichever involved chain can reorg, so it
// is the larger of the two chains' reorg buffers — Lux↔BTC yields 2h, EVM↔EVM a
// few minutes, exactly as the spec's concrete parameters.
func Delta(chainA, chainB ChainKind) time.Duration {
a, b := reorgBuffer[chainA], reorgBuffer[chainB]
if a > b {
return a
}
return b
}
// OrderingHolds reports the atomicity invariant T_B > T_A + Δ for a maker-leg
// timeout tA and taker-leg timeout tB (both absolute unix seconds) under buffer
// delta. This is the single predicate the session enforces on a submitted leg
// pair.
func OrderingHolds(tA, tB uint64, delta time.Duration) bool {
return tB > tA+uint64(delta/time.Second)
}
// AssignTimeouts recommends the absolute timeouts (tA, tB) for a swap started at
// t0 over (chainA → chainB): the maker's leg expires at t0 + MakerLegLifetime,
// the taker's leg at T_A + Δ + ClaimMargin. The result strictly satisfies
// OrderingHolds(tA, tB, Delta(chainA, chainB)). Parties may choose their own
// timeouts; this is the venue's advisory, and the session validates whatever is
// submitted against the same invariant.
func AssignTimeouts(t0 time.Time, chainA, chainB ChainKind) (tA, tB uint64) {
at := t0.Add(MakerLegLifetime)
bt := at.Add(Delta(chainA, chainB) + ClaimMargin)
return uint64(at.Unix()), uint64(bt.Unix())
}