Files
zeekayandHanzo Dev 4a90738c69 dchain: the funding rail carries value, and nothing else
Move once, trade many. Value crosses the C<->D boundary twice in its life; in
between, D owns it and every order, match and trade is a local balance movement.
So the crossing has exactly one job — move ownership exactly once — and the
object shrinks to what that job needs:

    beneficiary(20) | asset(32) | amount(8)   = 60 bytes

Out of the wire: the 49-byte market/side/limit/size tail, the `spent` execution
witness, and the lane byte that told C which trading pot to debit. Out of the
package: the per-order escrow, the per-order ledger account, the proposer-side
drive that scanned shared memory for orders to run, and TxOrderSubmit. An
import now credits the beneficiary's OWN account — the one their signed orders
already draw from — so the money is tradable the moment it lands, with no second
crossing and no order created by the crossing.

TxExport is now signed. It used to draw on a per-order account whose only
possible payee was the escrow's recorded owner, so the object was its own
authority. Draining a real account to a caller-named address is not, so the
body asserts its owner and txWireUser binds that to the recovered signer, the
same way a withdraw does.

One claim id, both directions, both repos: SHA-256(domain | source | dest |
sourceTx | index). It replaces DeriveOrderID and deriveSeamOutputID, which were
two derivations of the same fact.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:18:14 -07:00

138 lines
4.7 KiB
Go
Raw Permalink 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 dchain
import (
"sync"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/ids"
)
// results.go is the seam between consensus and a synchronous client: the per-tx
// OUTCOME a block's execution produced, and the VM's registry that lets a ZAP
// handler block until "its" tx is decided by an accepted block.
//
// The decomplected shape: a dex_* write is NOT executed when it arrives — it is
// queued, ordered by consensus, matched at Verify, committed at Accept. But the
// caller (the proxy's synchronous Relay, the precompile's swap) needs the
// resulting fills/ack back. So the handler registers a waiter keyed by the tx's
// content-addressed id, Adds the tx to the mempool, and parks. When the block
// carrying that tx is Accepted, the VM resolves the waiter with the consensus
// outcome. Verify/Reject never resolve (a rejected block's txs return to the
// mempool to be retried; only an accepted block produces a final outcome).
// txOutcome is the consensus-decided result of one transaction, attributed by tx
// id. For a submit it carries the fills; for a place it carries the resting
// order id; for a cancel the cancelled id; for ensure_market just status. status
// uses the FROZEN zapwire status bytes so the handler encodes a byte-identical
// ack/fills response without re-deriving anything.
type txOutcome struct {
txID ids.ID
typ TxType
orderID uint64 // placed / cancelled order id (0 for submit/ensure)
fills []zapwire.Fill // submit fills (nil otherwise)
status uint8 // zapwire.StatusPlaced / StatusCanceled / StatusRejected
}
// toWireFills converts a submit's matched trades into the FROZEN wire Fill form
// (price, size, takerSide). This is the single conversion from the internal
// dex.Trade to the 17-byte wire fill; the handler then calls zapwire.EncodeFills.
func toWireFills(fills []dex.Trade, takerSide dex.Side) []zapwire.Fill {
out := make([]zapwire.Fill, len(fills))
for i, f := range fills {
// Exact integers: price = PriceInt (×1e8), size = atomic base units. The
// authoritative integer lane (PriceUnits/BaseUnits), never the float view.
var size uint64
if f.BaseUnits != nil {
size = f.BaseUnits.Uint64()
}
out[i] = zapwire.Fill{
Price: uint64(f.PriceUnits),
Size: size,
TakerSide: uint8(takerSide),
}
}
return out
}
// outcomeFromApply builds the per-tx outcome for an applied tx. It is called in
// execution order so the outcome's identity (the tx id) is exactly what the
// handler registered its waiter under.
func outcomeFromApply(tx *Tx, res applyResult) txOutcome {
o := txOutcome{txID: tx.ID(), typ: tx.Type}
switch tx.Type {
case TxPlace:
if res.Placed != nil {
o.orderID = res.Placed.ID
o.status = zapwire.StatusPlaced
} else {
o.status = zapwire.StatusRejected
}
case TxCancel:
if res.Canceled != 0 {
o.orderID = res.Canceled
o.status = zapwire.StatusCanceled
} else {
o.status = zapwire.StatusRejected
}
case TxSubmit:
o.fills = toWireFills(res.Fills, res.TakerSide)
o.status = zapwire.StatusPlaced
case TxEnsureMarket:
o.status = zapwire.StatusPlaced
}
return o
}
// outcomeRegistry maps a pending tx id to the channel a parked handler waits on.
// A buffered (cap 1) channel means resolve never blocks even if the waiter has
// already given up (ctx timeout) and stopped receiving.
type outcomeRegistry struct {
mu sync.Mutex
waiters map[ids.ID]chan txOutcome
}
func newOutcomeRegistry() *outcomeRegistry {
return &outcomeRegistry{waiters: map[ids.ID]chan txOutcome{}}
}
// register returns a channel that will receive the outcome for txID. If a waiter
// already exists for the id (a duplicate in-flight submit), the existing channel
// is returned so both callers observe the same single consensus outcome.
func (r *outcomeRegistry) register(txID ids.ID) chan txOutcome {
r.mu.Lock()
defer r.mu.Unlock()
if ch, ok := r.waiters[txID]; ok {
return ch
}
ch := make(chan txOutcome, 1)
r.waiters[txID] = ch
return ch
}
// resolve delivers an outcome to its waiter (if any) and removes the entry. The
// send is non-blocking on a cap-1 channel; a missing waiter (no local handler
// parked — e.g. on a follower validator) is a no-op.
func (r *outcomeRegistry) resolve(o txOutcome) {
r.mu.Lock()
ch, ok := r.waiters[o.txID]
if ok {
delete(r.waiters, o.txID)
}
r.mu.Unlock()
if ok {
ch <- o // cap-1, never blocks
}
}
// cancel drops a waiter without delivering (used when the handler abandons the
// wait, so a later resolve does not leak the entry).
func (r *outcomeRegistry) cancel(txID ids.ID) {
r.mu.Lock()
delete(r.waiters, txID)
r.mu.Unlock()
}