mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
C and D each own value while it is on that chain, and a unit is spendable on exactly one of them at a time: C --Export--> C_TO_D --Import--> D --Export--> D_TO_C --Import--> C Those four transitions and no others. Never C to D directly, which would mean a moment where value is in both places or neither. The consequence is the point. Once imported, D is not matching against hypothetical C collateral — it has real balances, and every order, match and trade is a local movement. A million trades cost zero boundary crossings. That is what makes D an exchange ledger rather than a remote matching oracle attached to C. So the atomic rail is narrowed to one job: move ownership exactly once. It knows nothing about orders, executions, trades or settlement. The per-trade cross-chain certificate, the C-parent reservation and the accept-or-release round trip stay only for the V4 adapter, where a contract genuinely demands synchronous C composability. Import consumes and credits in one operation. Splitting them either destroys value or lets a retry, a reorg, a racing relayer or a restart import the same object twice, which is a double-spend of real money. Refusal is loud rather than silent — a caller that imported twice has a bug worth seeing. Export draws only on available balance, so value committed to an open order cannot also leave the chain. A claim names its beneficiary, which is what makes import safely permissionless: delivering one can only credit the account it already names. Co-location buys latency, never correctness. Execution now carries MarketVersion and the exact token amounts D resolved, so C verifies amounts rather than recomputing lots from whatever the market says now — recomputing would replay a block to a different result after a market update. Price is gone; it was Output over Input stated twice inside a signed struct. Partial execution is bounded by cross-multiplication, executionOutput * orderInput >= executionInput * orderMinOutput, so there is no division, no rounding convention, and the rule composes: if every slice satisfies it, so does their sum. 60 tests. Conservation asserted after all 600 operations of a random run: imported 4143, exported 2576, D owns 1567. Co-authored-by: Hanzo Dev <[email protected]>
528 lines
20 KiB
Go
528 lines
20 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package dexprotocol is the consensus-side vocabulary of the C<->D exchange seam.
|
|
//
|
|
// THE PROTOCOL, IN ONE PARAGRAPH:
|
|
//
|
|
// The C transaction contains an Order. D matches it and certifies an Execution —
|
|
// price and quantity guaranteed and reserved. The C block carries that Execution.
|
|
// EVM execution verifies it and returns the resulting BalanceDelta. When the child
|
|
// of the Execution's C parent is accepted, D settles it into a Trade if that child
|
|
// used it, or Releases the reservation if it did not. Batch certification is an
|
|
// optional optimization.
|
|
//
|
|
// THE INVARIANT IT EXISTS TO HOLD: C never queries D consensus state during EVM
|
|
// execution. D converts live exchange state into certified, single-consumption
|
|
// statements. C deterministically consumes those attestations with full synchronous
|
|
// V4 semantics. D finalizes or releases the reservation solely from proof of the
|
|
// accepted C successor. On replay D can be dead, the GPU gone, D a billion blocks
|
|
// ahead — C has parent state, transactions, the D statement, and the certificate.
|
|
// That is the entire input set.
|
|
//
|
|
// THE NORMATIVE RULE:
|
|
//
|
|
// Lux DEX follows FIX Latest terminology and order-state semantics. ExecType
|
|
// denotes an event; OrdStatus denotes the resulting order state. A D-side
|
|
// Execution does not become a FIX Trade until the corresponding C-chain
|
|
// execution is accepted.
|
|
//
|
|
// That last sentence is the whole cross-chain contract in one line.
|
|
//
|
|
// TWO VOCABULARIES, DELIBERATELY SPLIT.
|
|
//
|
|
// INTERNAL (cross-chain machinery): Order -> Execution -> Trade | Release,
|
|
// with Match as the matching-engine verb. This is small and Lux-specific because
|
|
// the provisional reservation is internal consensus machinery.
|
|
//
|
|
// EXTERNAL (client-facing): FIX Latest, emitted ONLY once C acceptance makes an
|
|
// execution final — 35=8 ExecutionReport, 150=F ExecType=Trade, 39=1|2
|
|
// OrdStatus=PartiallyFilled|Filled, 17/31/32/14/151. Clients see a normal order
|
|
// lifecycle and never learn about C-parent reservations or cross-chain consensus.
|
|
// There is deliberately NO FIX state for the provisional reservation: mapping it
|
|
// onto one would make every FIX client responsible for understanding Lux's
|
|
// cross-chain machinery.
|
|
//
|
|
// Every earlier name for the provisional object — PreparedExecution,
|
|
// ExecutionFrame, Fill — asserted that execution had happened while C could still
|
|
// reject it and D could still restore the liquidity. That is economically false,
|
|
// which is why the internal name is plain Execution and the word Trade is reserved
|
|
// for the post-acceptance fact.
|
|
//
|
|
// Match stays an internal matching-engine action. It is not a cross-chain protocol
|
|
// object and must not appear on the wire.
|
|
//
|
|
// MARKET DATA: publish a Trade only after C acceptance. A reserved Execution must
|
|
// never appear in the trades feed, volume, candles, or history.
|
|
//
|
|
// WHAT THE CERTIFICATE PROVES, AND WHAT IT DOES NOT. It proves "this is the
|
|
// execution D reported, and D has reserved the price and quantity behind it for
|
|
// exactly this C opportunity." It does NOT say C must accept the transaction. C
|
|
// still applies minAmountOut, sqrtPriceLimit, hook rules, deadline and EVM
|
|
// authorization locally, and reverts when the trader's own bounds are unmet. D owns
|
|
// execution facts; C owns whether they satisfy the call.
|
|
//
|
|
// THE HONEST COST is not determinism — that is bought outright by moving the
|
|
// statement into the block. The cost is that D must hold the reservation until C
|
|
// consensus decides whether it was used. That is the fundamental cross-chain price.
|
|
package dexprotocol
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// Side is the order's direction on the book.
|
|
type Side uint8
|
|
|
|
const (
|
|
SideBuy Side = iota
|
|
SideSell
|
|
)
|
|
|
|
func (s Side) String() string {
|
|
switch s {
|
|
case SideBuy:
|
|
return "buy"
|
|
case SideSell:
|
|
return "sell"
|
|
default:
|
|
return fmt.Sprintf("invalid(%d)", uint8(s))
|
|
}
|
|
}
|
|
|
|
// ExecType is FIX ExecType(150) — the EVENT being reported. ExecType and OrdStatus
|
|
// are kept as separate fields, never collapsed into one, because that split is
|
|
// normative in FIX. Both exist ON THE WIRE only; internally, state is the object's
|
|
// TYPE and its storage domain (see state.go). There is no entry for the provisional
|
|
// reservation: it is never reported to a FIX client.
|
|
type ExecType uint8
|
|
|
|
const (
|
|
ExecNew ExecType = iota
|
|
ExecTrade
|
|
ExecCanceled
|
|
ExecRejected
|
|
)
|
|
|
|
func (e ExecType) String() string {
|
|
switch e {
|
|
case ExecNew:
|
|
return "New"
|
|
case ExecTrade:
|
|
return "Trade"
|
|
case ExecCanceled:
|
|
return "Canceled"
|
|
case ExecRejected:
|
|
return "Rejected"
|
|
default:
|
|
return fmt.Sprintf("invalid(%d)", uint8(e))
|
|
}
|
|
}
|
|
|
|
// OrdStatus is FIX OrdStatus(39) — the resulting ORDER STATE. Wire-only, same
|
|
// reasoning as ExecType.
|
|
type OrdStatus uint8
|
|
|
|
const (
|
|
OrdNew OrdStatus = iota
|
|
OrdPartiallyFilled
|
|
OrdFilled
|
|
OrdCanceled
|
|
OrdRejected
|
|
)
|
|
|
|
func (o OrdStatus) String() string {
|
|
switch o {
|
|
case OrdNew:
|
|
return "New"
|
|
case OrdPartiallyFilled:
|
|
return "PartiallyFilled"
|
|
case OrdFilled:
|
|
return "Filled"
|
|
case OrdCanceled:
|
|
return "Canceled"
|
|
case OrdRejected:
|
|
return "Rejected"
|
|
default:
|
|
return fmt.Sprintf("invalid(%d)", uint8(o))
|
|
}
|
|
}
|
|
|
|
// Order lives in order.go. It is the trader's signed authorization and carries NO
|
|
// C parent: an order is portable across its validity window, and the same unmatched
|
|
// order may be matched again after an execution against it is released. CParent
|
|
// belongs to Execution, below, because the reservation — not the authorization — is
|
|
// what is scoped to one C opportunity.
|
|
|
|
// Execution is the object D certifies — a FIX ExecutionReport reduced to the
|
|
// fields that cross the seam.
|
|
//
|
|
// ExecID is D-assigned and the commitment COVERS it, so a certificate binds that id
|
|
// to exactly this content: an id can never be asserted independently of what it
|
|
// names, and two executions cannot share an id without sharing every byte.
|
|
//
|
|
// There is no reservation field: the reservation is keyed by
|
|
// (CChainID, CParent, ExecID) — see ReservationKey — so there is no separate value
|
|
// that could disagree with the key it is supposed to name.
|
|
// D RESOLVES, C VERIFIES. The execution carries the EXACT token amounts that will
|
|
// move, converted once by D using the market version named here. C checks those
|
|
// amounts against the order's bounds and moves them; it never recomputes lots from
|
|
// whatever the market metadata happens to say now. Recomputing would reintroduce the
|
|
// replay problem at a smaller scale: an execution certified under one lot size would
|
|
// settle differently after a market update, so the same block would replay to a
|
|
// different result.
|
|
//
|
|
// MarketVersion is therefore load-bearing, not documentation. It pins which metadata
|
|
// produced Quantity -> Input/Output, and the commitment covers it.
|
|
//
|
|
// There is no Price field. Price is Output/Input and would be a second statement of
|
|
// the same fact inside a signed struct — two things that can disagree. FIX LastPx(31)
|
|
// is derived at the edge where the ExecutionReport is built.
|
|
type Execution struct {
|
|
ExecID ids.ID
|
|
OrderID ids.ID
|
|
MarketID ids.ID
|
|
MarketVersion uint64
|
|
Quantity uint64 // lots — the book's unit; FIX LastQty(32)
|
|
Input AssetAmount // exact token base units leaving the swapper
|
|
Output AssetAmount // exact token base units reaching the recipient
|
|
Fee AssetAmount
|
|
CParent ids.ID // the C opportunity this execution may be traded under
|
|
DBlock ids.ID // the D block that produced the match
|
|
}
|
|
|
|
// ExecutionEncodedLen is the canonical encoding's exact width. Every field is fixed
|
|
// width, so concatenation is INJECTIVE by construction — no length prefixes, no
|
|
// separators. That is what lets the commitment rest on SHA-256 alone rather than on
|
|
// the hash plus an argument about parsing.
|
|
const ExecutionEncodedLen = 32 + 32 + 32 + // ExecID, OrderID, MarketID
|
|
8 + 8 + // MarketVersion, Quantity
|
|
AssetAmountEncodedLen*3 + // Input, Output, Fee
|
|
32 + 32 // CParent, DBlock
|
|
|
|
// execDomain domain-separates the execution commitment. It is hashed once into a
|
|
// fixed 32-byte tag so the preimage is tag(32) || encoding(ExecutionEncodedLen) —
|
|
// two fixed-width parts, hence unambiguous. Prefixing a variable-length ASCII
|
|
// string directly would reintroduce the parsing ambiguity the fixed-width encoding
|
|
// removes.
|
|
const execDomain = "lux.dex.execution.v1"
|
|
|
|
var execDomainTag = sha256.Sum256([]byte(execDomain))
|
|
|
|
var (
|
|
ErrExecWidth = errors.New("dexprotocol: encoded Execution has the wrong width")
|
|
ErrExecScope = errors.New("dexprotocol: Execution is missing a required scope field")
|
|
ErrExecQty = errors.New("dexprotocol: Execution Quantity must be non-zero")
|
|
)
|
|
|
|
// Encode returns the canonical fixed-width big-endian encoding. Total, so a
|
|
// malformed execution still has a well-defined commitment — a verifier must be able
|
|
// to say "this attestation is over execution X and X is invalid" rather than being
|
|
// unable to name what it rejected.
|
|
func (e *Execution) Encode() []byte {
|
|
b := make([]byte, ExecutionEncodedLen)
|
|
o := 0
|
|
o += copy(b[o:], e.ExecID[:])
|
|
o += copy(b[o:], e.OrderID[:])
|
|
o += copy(b[o:], e.MarketID[:])
|
|
binary.BigEndian.PutUint64(b[o:], e.MarketVersion)
|
|
o += 8
|
|
binary.BigEndian.PutUint64(b[o:], e.Quantity)
|
|
o += 8
|
|
for _, a := range [...]AssetAmount{e.Input, e.Output, e.Fee} {
|
|
o += copy(b[o:], a.Asset[:])
|
|
o += copy(b[o:], a.Amount[:])
|
|
}
|
|
o += copy(b[o:], e.CParent[:])
|
|
o += copy(b[o:], e.DBlock[:])
|
|
if o != ExecutionEncodedLen {
|
|
panic(fmt.Sprintf("dexprotocol: encoder wrote %d bytes, layout says %d", o, ExecutionEncodedLen))
|
|
}
|
|
return b
|
|
}
|
|
|
|
// DecodeExecution parses the canonical encoding, requiring the EXACT width. A
|
|
// longer buffer is refused rather than truncated: accepting a suffix would let two
|
|
// distinct wire messages decode to the same execution while carrying different
|
|
// attested bytes.
|
|
func DecodeExecution(b []byte) (Execution, error) {
|
|
if len(b) != ExecutionEncodedLen {
|
|
return Execution{}, fmt.Errorf("%w: got %d, want %d", ErrExecWidth, len(b), ExecutionEncodedLen)
|
|
}
|
|
var e Execution
|
|
o := 0
|
|
o += copy(e.ExecID[:], b[o:o+32])
|
|
o += copy(e.OrderID[:], b[o:o+32])
|
|
o += copy(e.MarketID[:], b[o:o+32])
|
|
e.MarketVersion = binary.BigEndian.Uint64(b[o:])
|
|
o += 8
|
|
e.Quantity = binary.BigEndian.Uint64(b[o:])
|
|
o += 8
|
|
for _, a := range [...]*AssetAmount{&e.Input, &e.Output, &e.Fee} {
|
|
o += copy(a.Asset[:], b[o:o+32])
|
|
o += copy(a.Amount[:], b[o:o+32])
|
|
}
|
|
o += copy(e.CParent[:], b[o:o+32])
|
|
copy(e.DBlock[:], b[o:o+32])
|
|
return e, nil
|
|
}
|
|
|
|
// Commitment is the execution's identity and THE THING D VALIDATORS SIGN:
|
|
//
|
|
// commitment = SHA-256( SHA-256(domain) || canonical encoding )
|
|
//
|
|
// Both halves are fixed width, so distinct executions give distinct preimages and a
|
|
// collision requires breaking SHA-256 rather than confusing a parser.
|
|
func (e *Execution) Commitment() ids.ID {
|
|
h := sha256.New()
|
|
h.Write(execDomainTag[:])
|
|
h.Write(e.Encode())
|
|
var id ids.ID
|
|
copy(id[:], h.Sum(nil))
|
|
return id
|
|
}
|
|
|
|
// Validate refuses an execution missing a field the security argument depends on.
|
|
// A zero CParent would unbind it from a C opportunity — precisely the property that
|
|
// makes the reserved quantity settleable at most once.
|
|
func (e *Execution) Validate() error {
|
|
if e.CParent == ids.Empty {
|
|
return fmt.Errorf("%w: CParent", ErrExecScope)
|
|
}
|
|
if e.ExecID == ids.Empty {
|
|
return fmt.Errorf("%w: ExecID", ErrExecScope)
|
|
}
|
|
if e.OrderID == ids.Empty {
|
|
return fmt.Errorf("%w: OrderID", ErrExecScope)
|
|
}
|
|
if e.MarketID == ids.Empty {
|
|
return fmt.Errorf("%w: MarketID", ErrExecScope)
|
|
}
|
|
// MarketVersion 0 would leave the lot conversion unattributed, which is the one
|
|
// thing that makes the amounts reproducible.
|
|
if e.MarketVersion == 0 {
|
|
return fmt.Errorf("%w: MarketVersion", ErrExecScope)
|
|
}
|
|
if e.Quantity == 0 {
|
|
return ErrExecQty
|
|
}
|
|
// An execution moving nothing in or out is not a trade, whatever the lots say.
|
|
if e.Input.IsZero() || e.Output.IsZero() {
|
|
return ErrExecAmount
|
|
}
|
|
if e.Input.Asset == ids.Empty || e.Output.Asset == ids.Empty || e.Fee.Asset == ids.Empty {
|
|
return fmt.Errorf("%w: settlement asset", ErrExecScope)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ScopedTo reports whether this execution may be settled by a block whose parent is
|
|
// cParent. Named rather than inlined because it is the single check the
|
|
// settle-at-most-once property rests on, and it should be greppable.
|
|
func (e *Execution) ScopedTo(cParent ids.ID) bool { return e.CParent == cParent }
|
|
|
|
// --- Authenticated values: an unauthenticated execution must not compile -------
|
|
//
|
|
// A function like Apply(Execution) is a standing invitation to apply something
|
|
// nobody verified, and review does not reliably catch the one call site that
|
|
// skipped it. So the lifecycle does not accept an Execution at all. It accepts a
|
|
// VerifiedExecution: an INTERFACE with an unexported method, so nothing outside
|
|
// this package can implement it, and the concrete type is unexported, so nothing
|
|
// outside can construct it. The only way to obtain one is VerifyExecution.
|
|
|
|
// VerifiedExecution is an execution whose certificate has been checked. It cannot
|
|
// be constructed outside this package.
|
|
type VerifiedExecution interface {
|
|
Execution() Execution
|
|
Commitment() ids.ID
|
|
// verified is unexported and therefore unimplementable outside this package.
|
|
// It is the whole enforcement mechanism; do not export it.
|
|
verified()
|
|
}
|
|
|
|
type verifiedExecution struct {
|
|
exec Execution
|
|
commitment ids.ID
|
|
}
|
|
|
|
func (v verifiedExecution) Execution() Execution { return v.exec }
|
|
func (v verifiedExecution) Commitment() ids.ID { return v.commitment }
|
|
func (verifiedExecution) verified() {}
|
|
|
|
// AcceptedBlock is a C block that C consensus has ACCEPTED, together with the
|
|
// ExecRoot of the executions that block consumed.
|
|
//
|
|
// CPARENT MEANS ACCEPTED C BLOCK. PERIOD. A reservation may only ever be taken
|
|
// against an accepted parent, which is what removes the abandoned-parent case
|
|
// entirely: acceptance is the finality boundary, so an accepted P is guaranteed
|
|
// exactly one accepted child, and that child permanently decides every execution
|
|
// reserved against P. Allowing a merely proposed parent would force two questions —
|
|
// "did P itself win?" and "which child of P won?" — plus conflict, height and
|
|
// ancestry proofs. One question is better than two.
|
|
//
|
|
// There is NO public constructor. The fields are unexported and the only producer
|
|
// is VerifyAcceptedBlock, so an unverified C parent cannot reach Reserve, Settle or
|
|
// Release. The `verified` sentinel closes the remaining hole that unexported fields
|
|
// alone leave open: a zero AcceptedBlock{} is constructible outside this package,
|
|
// so every consumer requires the sentinel rather than merely non-zero ids.
|
|
type AcceptedBlock struct {
|
|
verified bool
|
|
id ids.ID
|
|
parentID ids.ID
|
|
height uint64
|
|
execRoot [32]byte
|
|
}
|
|
|
|
func (a AcceptedBlock) ID() ids.ID { return a.id }
|
|
func (a AcceptedBlock) ParentID() ids.ID { return a.parentID }
|
|
func (a AcceptedBlock) Height() uint64 { return a.height }
|
|
func (a AcceptedBlock) ExecRoot() [32]byte { return a.execRoot }
|
|
|
|
// AcceptedBlockEncodedLen is the canonical width of an acceptance statement:
|
|
// id(32) | parent(32) | height(8) | execRoot(32). Fixed width, so injective.
|
|
const AcceptedBlockEncodedLen = 32 + 32 + 8 + 32
|
|
|
|
const acceptedDomain = "lux.dex.accepted-block.v1"
|
|
|
|
var acceptedDomainTag = sha256.Sum256([]byte(acceptedDomain))
|
|
|
|
var (
|
|
ErrAcceptedWidth = errors.New("dexprotocol: encoded acceptance has the wrong width")
|
|
ErrAcceptedScope = errors.New("dexprotocol: acceptance is missing the block or parent id")
|
|
ErrAcceptedUnverified = errors.New("dexprotocol: this C parent was never verified; only VerifyAcceptedBlock may produce one")
|
|
)
|
|
|
|
// EncodeAcceptedBlock is the canonical encoding D validators attest over. Exported
|
|
// so the C side can produce the statement it signs.
|
|
func EncodeAcceptedBlock(id, parentID ids.ID, height uint64, execRoot [32]byte) []byte {
|
|
b := make([]byte, AcceptedBlockEncodedLen)
|
|
o := 0
|
|
o += copy(b[o:], id[:])
|
|
o += copy(b[o:], parentID[:])
|
|
binary.BigEndian.PutUint64(b[o:], height)
|
|
o += 8
|
|
copy(b[o:], execRoot[:])
|
|
return b
|
|
}
|
|
|
|
// AcceptanceCommitment is what a certificate over an acceptance signs.
|
|
func AcceptanceCommitment(msg []byte) ids.ID {
|
|
h := sha256.New()
|
|
h.Write(acceptedDomainTag[:])
|
|
h.Write(msg)
|
|
var id ids.ID
|
|
copy(id[:], h.Sum(nil))
|
|
return id
|
|
}
|
|
|
|
// VerifyAcceptedBlock is the ONLY producer of an AcceptedBlock. It authenticates a
|
|
// C acceptance statement against a certificate.
|
|
//
|
|
// NEVER USE LOCAL C STATE AS THE PROOF. Even with C co-located, a check like
|
|
// `localC.LastAccepted() != execution.CParent` is wrong inside D consensus:
|
|
// different D validators observe C advancement at different instants, so the answer
|
|
// is not a function of the block being executed. The local C VM is DELIVERY; the
|
|
// authenticated statement is the consensus input. This is the same rule already
|
|
// proven in the other direction for C reading D.
|
|
func VerifyAcceptedBlock(msg []byte, cert []byte, v CertificateVerifier) (AcceptedBlock, error) {
|
|
if v == nil {
|
|
return AcceptedBlock{}, ErrNoVerifier
|
|
}
|
|
if len(msg) != AcceptedBlockEncodedLen {
|
|
return AcceptedBlock{}, fmt.Errorf("%w: got %d, want %d", ErrAcceptedWidth, len(msg), AcceptedBlockEncodedLen)
|
|
}
|
|
var a AcceptedBlock
|
|
o := 0
|
|
o += copy(a.id[:], msg[o:o+32])
|
|
o += copy(a.parentID[:], msg[o:o+32])
|
|
a.height = binary.BigEndian.Uint64(msg[o:])
|
|
o += 8
|
|
copy(a.execRoot[:], msg[o:o+32])
|
|
if a.id == ids.Empty || a.parentID == ids.Empty {
|
|
return AcceptedBlock{}, ErrAcceptedScope
|
|
}
|
|
if err := v.VerifyCertificate(AcceptanceCommitment(msg), cert); err != nil {
|
|
return AcceptedBlock{}, fmt.Errorf("%w: %s", ErrBadCertificate, err)
|
|
}
|
|
a.verified = true
|
|
return a, nil
|
|
}
|
|
|
|
// CertificateVerifier checks a D-validator certificate over an execution
|
|
// commitment. It is an interface so the consensus package depends on the
|
|
// ABSTRACTION and never on a client, a socket, or a GPU — the import ban is
|
|
// structural, not advisory.
|
|
type CertificateVerifier interface {
|
|
VerifyCertificate(commitment ids.ID, cert []byte) error
|
|
}
|
|
|
|
// VerifyContext is everything verification is allowed to consult. It is a VALUE:
|
|
// no clients, no connections, no callbacks into a live process. If a check cannot
|
|
// be made from this struct plus the witness bytes, it does not belong in the
|
|
// consensus path.
|
|
type VerifyContext struct {
|
|
NetworkID uint32
|
|
CChainID ids.ID
|
|
DChainID ids.ID
|
|
// CParent is the ACTUAL parent of the block being verified, supplied by the
|
|
// verifier — never taken from the witness, or the binding would be self-asserted.
|
|
CParent ids.ID
|
|
Verifier CertificateVerifier
|
|
}
|
|
|
|
var (
|
|
ErrNoVerifier = errors.New("dexprotocol: verification requires a certificate verifier")
|
|
ErrWrongCParent = errors.New("dexprotocol: execution is scoped to a different C opportunity")
|
|
ErrBadCertificate = errors.New("dexprotocol: execution certificate did not verify")
|
|
)
|
|
|
|
// VerifyExecution is the ONLY producer of a VerifiedExecution. It checks, in order:
|
|
// the execution is well formed; it is scoped to the C opportunity actually being
|
|
// built on; and its commitment carries a valid D-validator certificate.
|
|
//
|
|
// The CParent check uses the VERIFIER's view of the parent, never the witness's
|
|
// claim about it. A witness that asserted its own scope would be asserting the one
|
|
// thing the at-most-once property depends on.
|
|
func VerifyExecution(witness []byte, ctx VerifyContext) (VerifiedExecution, error) {
|
|
if ctx.Verifier == nil {
|
|
return nil, ErrNoVerifier
|
|
}
|
|
e, err := DecodeExecution(witnessBody(witness))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := e.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if !e.ScopedTo(ctx.CParent) {
|
|
return nil, fmt.Errorf("%w: scoped to %s, building on %s", ErrWrongCParent, e.CParent, ctx.CParent)
|
|
}
|
|
commitment := e.Commitment()
|
|
if err := ctx.Verifier.VerifyCertificate(commitment, witnessCertificate(witness)); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrBadCertificate, err)
|
|
}
|
|
return verifiedExecution{exec: e, commitment: commitment}, nil
|
|
}
|
|
|
|
// witnessBody / witnessCertificate split a witness into the canonical execution
|
|
// encoding and the certificate that follows it. The execution is fixed width, so
|
|
// the split is unambiguous.
|
|
func witnessBody(witness []byte) []byte {
|
|
if len(witness) < ExecutionEncodedLen {
|
|
return witness
|
|
}
|
|
return witness[:ExecutionEncodedLen]
|
|
}
|
|
|
|
func witnessCertificate(witness []byte) []byte {
|
|
if len(witness) <= ExecutionEncodedLen {
|
|
return nil
|
|
}
|
|
return witness[ExecutionEncodedLen:]
|
|
}
|