mirror of
https://github.com/luxfi/chains.git
synced 2026-07-27 03:39:41 +00:00
HIGH-1 (THE happy-path break, swapsWork=NO): settleFromFills exported the taker's
PROCEEDS leg under assetFromRef(ref,leg)=SHA256(ref||leg), a synthetic routing handle.
The C-side ImportSettlement requires recAsset==assetID(currency_out) (the injective id
that also keys seamReserve[assetOut]); a SHA256 handle never matched, so the proceeds
credit ALWAYS reverted (ErrNativeSettleAsset) and the taker never received their swap
output. FIX: settleFromFills exports the proceeds leg under the REAL output-asset id
carried with the settling relay (RelayOrderTx.AssetOut, signature-bound, keeper-asserted
and C-bound => a wrong value is taker-liveness-only, never theft/mint). Removed the dead
assetFromRef SHA256 helper. Kept the prior pass's owner-bind + one-time-consume +
per-intent binding intact.
MEDIUM (bounded sandwich/MEV): settleFromFills now enforces a worst-acceptable CLOB
price carried from the relay (RelayOrderTx.PriceLimit/LimitIsUpper, derived from the V4
SqrtPriceLimitX96): a fill worse than the limit is refused (BUY ceiling, SELL floor;
0=no limit), leaving the escrow reclaimable.
MEDIUM (single-proposer fabricated fills): populate+verify the reserved carried-fills
attestation. The venue signs blake3(domain||blockHash||entries) with Ed25519; every
validator verifies it before settling (verifyFillAttestation), gated behind
Config.FillAttestationPubKey (default-on once set; fail-secure full-refund when a block's
fills are unattested/forged). Config note: the pubkey MUST be identical across validators
(consensus-affecting, same class as DexZapEndpoint).
Tests (CGO_ENABLED=0, default + redteam): TestRED_SwapProceeds_* (unit + e2e happy-path
credit under the real output asset), TestRED_SwapSlippageLimit_*, TestRED_FillAttestation_*
(verify/forge/fabricated-refund/off-by-default). Full dexvm suite green.
Extends fix/swap-rail-escrow-owner-bind (f173650). Within chains v1.3.15.
119 lines
6.1 KiB
Go
119 lines
6.1 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dexvm
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/luxfi/crypto/ed25519"
|
|
"github.com/luxfi/ids"
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
// fill_attestation.go closes the MEDIUM single-proposer carried-fills trust gap: the
|
|
// d-chain MATCHER (the venue) ATTESTS the fills it returned with an Ed25519 signature
|
|
// over a canonical message; every validator verifies that signature before settling
|
|
// from the carried fills. Without it, a malicious or MITM proposer can carry FABRICATED
|
|
// fills (carried_fills.go documents this as the interim, conservation-bounded trust
|
|
// surface — a lying proposer is bounded to ONE taker's own escrow, never supply
|
|
// inflation, but it CAN mis-settle that taker). With it, the proceeds a validator
|
|
// settles are ATTESTED by the venue, not merely proposer-asserted.
|
|
//
|
|
// THE RESERVED SLOT. The carried-fills wire (carried_fills.go encode/decodeCarriedFills)
|
|
// already reserves a self-delimiting `sig` field. This module fills it: the signature
|
|
// rides there with NO further wire-format change (the reserved-but-unimplemented slot
|
|
// noted at vm.go BuildBlockResult). The trustless evolution path (P3Q -> starkfri ZK
|
|
// proof of the matching) can later replace the Ed25519 verify here behind the SAME slot.
|
|
//
|
|
// SCHEME. Ed25519 (luxfi/crypto) over blake3(domain || blockHash || canonical-entries).
|
|
// The message is a DETERMINISTIC function of the block hash + the carried entries, so
|
|
// every validator recomputes the identical message and a single venue signature covers
|
|
// the whole block's fills. The message binds the BLOCK HASH so a venue signature for one
|
|
// block's fills cannot be replayed onto another block (a different block hash => a
|
|
// different message => the signature fails).
|
|
//
|
|
// ENFORCEMENT GATE (config.RequireFillAttestation, derived: ON iff a venue pubkey is
|
|
// configured). For an UNTRUSTED validator set the operator configures the venue's
|
|
// attestation pubkey; enforcement is then ON and a block whose carried fills lack a
|
|
// valid attestation is settled as a FULL REFUND (zero-fill) — fail-secure: an
|
|
// unattested/forged fill never moves proceeds. For a single-trusted-operator dev
|
|
// network no pubkey is configured; enforcement is OFF and the carried fills settle as
|
|
// before (the documented interim model). Default-on the moment a pubkey exists.
|
|
|
|
// fillAttestationDomain scopes the attestation message so a venue signature over a
|
|
// carried-fills block can never be confused with any other Ed25519 message the venue
|
|
// key might sign.
|
|
const fillAttestationDomain = "lux.dex.dexvm.fills.attestation.v1"
|
|
|
|
var (
|
|
// ErrFillAttestationRequired is returned when enforcement is on (a venue pubkey is
|
|
// configured) but the block carried NO attestation signature.
|
|
ErrFillAttestationRequired = errors.New("dexvm: fill attestation required but the block carried no signature")
|
|
// ErrFillAttestationInvalid is returned when the carried signature does not verify
|
|
// against the configured venue pubkey over the canonical (blockHash, entries) message.
|
|
ErrFillAttestationInvalid = errors.New("dexvm: carried fill attestation signature is invalid")
|
|
)
|
|
|
|
// fillAttestationMessage builds the canonical, deterministic message a venue signs to
|
|
// attest the fills it returned for a block: blake3(domain || blockHash || entries),
|
|
// where the entries are serialized in their canonical block-order form (the SAME
|
|
// length-prefixed encoding the block carries, MINUS the reserved sig — a signature can
|
|
// obviously not cover itself). Every validator recomputes this identically, so one
|
|
// venue signature covers the whole block's carried fills.
|
|
func fillAttestationMessage(blockHash ids.ID, entries []carriedFill) []byte {
|
|
// encodeCarriedFills(entries, nil) is the canonical entries serialization with an
|
|
// empty sig tail — a deterministic function of (txIndex, fills) in block order.
|
|
body := encodeCarriedFills(entries, nil)
|
|
h := blake3.New()
|
|
_, _ = h.Write([]byte(fillAttestationDomain))
|
|
_, _ = h.Write(blockHash[:])
|
|
_, _ = h.Write(body)
|
|
var sum [32]byte
|
|
h.Digest().Read(sum[:])
|
|
return sum[:]
|
|
}
|
|
|
|
// signFillAttestation produces the venue's Ed25519 attestation over the canonical
|
|
// (blockHash, entries) message. The proposer uses this ONLY when it holds the venue
|
|
// signing key (a co-located single-operator venue); otherwise the venue returns the
|
|
// signature alongside its fills and the proposer carries it verbatim. A nil/empty key
|
|
// yields a nil signature (no attestation produced).
|
|
func signFillAttestation(signer ed25519.PrivateKey, blockHash ids.ID, entries []carriedFill) []byte {
|
|
if len(signer) != ed25519.PrivateKeySize {
|
|
return nil
|
|
}
|
|
return ed25519.Sign(signer, fillAttestationMessage(blockHash, entries))
|
|
}
|
|
|
|
// verifyFillAttestation checks the carried signature against the configured venue
|
|
// pubkey over the canonical (blockHash, entries) message, applying the enforcement
|
|
// gate:
|
|
//
|
|
// - pubKey empty (no venue key configured: single-trusted-operator / dev): enforcement
|
|
// is OFF; returns nil regardless of sig (the documented interim trust model).
|
|
// - pubKey set (untrusted validator set): enforcement is ON; a missing signature is
|
|
// ErrFillAttestationRequired and an invalid one is ErrFillAttestationInvalid. Only a
|
|
// signature that verifies against the venue pubkey permits the carried fills to
|
|
// settle — so a fabricated fill with no/invalid attestation is refused.
|
|
func verifyFillAttestation(pubKey []byte, sig []byte, blockHash ids.ID, entries []carriedFill) error {
|
|
if len(pubKey) == 0 {
|
|
return nil // enforcement off (no venue attestation key configured)
|
|
}
|
|
if len(pubKey) != ed25519.PublicKeySize {
|
|
// A misconfigured (wrong-width) pubkey must FAIL CLOSED — never silently treat a
|
|
// garbage key as "no enforcement", which would re-open the proposer-trust gap.
|
|
return ErrFillAttestationInvalid
|
|
}
|
|
if len(sig) == 0 {
|
|
return ErrFillAttestationRequired
|
|
}
|
|
if len(sig) != ed25519.SignatureSize {
|
|
return ErrFillAttestationInvalid
|
|
}
|
|
if !ed25519.Verify(ed25519.PublicKey(pubKey), fillAttestationMessage(blockHash, entries), sig) {
|
|
return ErrFillAttestationInvalid
|
|
}
|
|
return nil
|
|
}
|