feat(dchain): keeper-less proposer-side drive for the native co-located atomic seam

Closes the DRIVE gap in the native C<->D settlement seam: the EXECUTION half
(atomic.go executeImport/executeExport) was real and unit-tested, but NOTHING in
production generated TxImport/TxExport — only the e2e harness, playing the keeper
by hand. So a real 0x9999 SubmitSwapIntent staged a C->D object that nothing
imported: no taker funding, no crossing, no D->C settlement object, no DEXFill.

drive.go is the proposer-side DRIVE — the keeper, but in-consensus. During normal
block production BuildBlock now:

  - IMPORT: enumerates the committed C->D intent objects in shared memory (tagged
    with the fixed SeamPendingTrait so they are discoverable owner-agnostically),
    filters those already imported (committed seamintent: escrow), sorts by
    intentID, and prepends a TxImport for each. Double-import-guarded by the
    escrow filter AND executeImport's own replay-reject.
  - EXPORT: enumerates the open intent escrows whose owner now holds realized
    OUTPUT in committed state (the output gate => no premature refund, no
    export-before-trade), and appends a TxExport binding owner/asset/amount/spent
    exactly as 0x9999 ImportSettlement reads (byte-identical seam wire). The
    spent witness = original principal - leftover input. execute then CLOSES the
    escrow once the taker's cross-chain account is drained (one-shot per swap).

Consensus-safe BY DESIGN (the seam's own note, atomic.go:47-55): both legs are
pure, deterministic functions of committed state (shared memory + the escrow
index + the ledger) — no wallclock, no mempool, no nondeterministic iteration, no
external I/O — so every validator re-derives the identical effects in execute and
a divergent proposer is rejected on the tx root. Enumeration results are sorted
into a total order so two proposers on the same committed partition build
byte-identical blocks. Keeper-less and co-located-correct: no external venue.

The seam wire and executeImport/executeExport are REUSED unchanged. autoDriveSeam
is enabled exactly when the seam is wired (runtime shared memory + resolved
C-Chain id); BuildBlock builds a pure-seam block on an empty mempool and
WaitForEvent signals seam work so the engine drives it.

Tests (drive through the REAL BuildBlock, not manual mempool.Add):
  - TestDrive_AutonomousImportCrossExport: the VM's OWN BuildBlock imports the
    intent, the taker's signed order crosses, the VM's OWN BuildBlock exports the
    D->C object the modeled 0x9999 Phase B consumes once; conservation holds;
    escrow closed.
  - TestDrive_NoExportBeforeImportOrTrade, TestDrive_ReplayImportedOnce,
    TestDrive_DeterministicOrdering (two proposers => identical blocks).
  - The existing manual-drive seam mechanics tests stay green WITH the drive on,
    proving the autonomous drive emits byte-identical txs to a correct keeper
    (deterministic convergence + tx dedup).

Companion (coordinated cutover, NOT in this commit): precompile/dex
native_staging.go collectRange must add SeamPendingTrait to the swap intent's
Element.Traits (one line) so a production 0x9999 intent is discoverable here;
pinned to the drive.go constant by the existing wire-golden discipline. The e2e
models that corrected flush. dex v1.9.2 readiness.
This commit is contained in:
2026-06-26 21:51:39 -07:00
parent 1951c41aea
commit f0eef3db06
5 changed files with 865 additions and 4 deletions
+10 -1
View File
@@ -106,8 +106,17 @@ func (h *seamHarness) addr(t *testing.T, name string) common.Address {
func (h *seamHarness) writeCToDIntent(t *testing.T, intentID ids.ID, owner common.Address, asset [32]byte, amount uint64) {
t.Helper()
obj := encodeSeamObject(railSwap, owner, asset, amount, 0)
// Tag the object with BOTH the per-owner trait (the precompile's existing index) AND
// the fixed SeamPendingTrait — modeling the corrected C-side flush (native_staging.go
// collectRange must add SeamPendingTrait so the D-side proposer drive can enumerate the
// intent owner-agnostically; see drive.go). The trait is inert for the manual-drive
// mechanics tests (Get-by-key ignores traits) and is what the autonomous drive walks.
err := h.cSM.Apply(map[ids.ID]*atomic.Requests{
h.dChainID: {PutRequests: []*atomic.Element{{Key: intentID[:], Value: obj, Traits: [][]byte{owner[:]}}}},
h.dChainID: {PutRequests: []*atomic.Element{{
Key: intentID[:],
Value: obj,
Traits: [][]byte{owner[:], SeamPendingTrait},
}}},
})
if err != nil {
t.Fatalf("write C->D intent: %v", err)
+9
View File
@@ -480,6 +480,15 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
outcomes = append(outcomes, txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusRejected})
continue
}
// CLOSE the intent escrow once this export has fully drained the taker's
// cross-chain account (drive.go) — making each cross-chain swap one-shot, so a
// settled intent is not re-exported by a later block's drive and the owner can
// import a fresh intent. Order-independent: it fires on whichever leg empties
// the account (the lone proceeds leg of a full fill, or the refund leg of a
// partial fill). A still-funded account is left open for a subsequent leg.
if cerr := b.vm.closeSeamIntentIfDrained(overlay, intentID); cerr != nil {
return execResult{}, fmt.Errorf("dchain: tx %d seam intent close: %w", i, cerr)
}
// orderID carries a compact handle of the output id for the ack; the keeper
// reads the full id from the seamout: record (or recomputes deriveSeamOutputID).
oc := txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusPlaced, orderID: binary.BigEndian.Uint64(outputID[24:32])}
+436
View File
@@ -0,0 +1,436 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dchain
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"sort"
"github.com/luxfi/database"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chains/atomic"
)
// drive.go is the PROPOSER-SIDE DRIVE of the native co-located settlement seam — the
// "keeper, but in-consensus". atomic.go is the EXECUTION half (executeImport /
// executeExport: how a C->D object is consumed and a D->C object is produced); this is
// the DRIVE half (what to import / export and when). Before this file the seam could
// only be driven by an external actor manually injecting TxImport/TxExport into the
// mempool (the atomic_seam_e2e_test harness plays exactly that role); NO production path
// generated them, so a real 0x9999 SubmitSwapIntent staged a C->D object that nothing
// imported — no taker funding, no crossing, no D->C settlement object, no DEXFill.
//
// The drive closes that gap WITHOUT a keeper and WITHOUT an external venue: during normal
// block production the VM ITSELF (BuildBlock, vm.go) enumerates the committed cross-chain
// state and emits the import/export txs, which every validator then re-verifies
// deterministically against the SAME committed state. It is keeper-less and consensus-safe
// BY DESIGN (atomic.go:47-55): the import credit is a pure function of the rooted TxImport
// body + the committed shared-memory object; the export object is a pure function of the
// rooted TxExport body + the escrow the import wrote. The drive only DECIDES which txs to
// emit; the txs themselves carry their full authority and are checked by executeImport /
// executeExport on every node, so a divergent proposer is rejected on the tx root.
//
// DETERMINISM CONTRACT (the matcher-at-Verify rule — non-determinism is a chain fork):
// - The drive reads ONLY committed state: the C->D objects in shared memory (the source
// chain has accepted), the seamintent: escrow index, and the ledger. No wallclock, no
// mempool, no node-local counter, no external I/O.
// - Every enumeration result is SORTED into a total order (imports by intentID, escrows
// and outputs by their committed key order) before any tx is built, so two proposers
// on the SAME committed state build byte-identical blocks (the leaderless-build
// requirement) and a validator re-running execute derives the identical effects.
// - The drive is bounded per block (maxSeamDrivePerBlock); an excess backlog is drained
// across subsequent blocks (each leg is independently retryable — a not-yet-imported
// intent is simply re-enumerated next block).
// SeamPendingTrait is the FIXED, owner-agnostic discovery trait every C->D swap-intent
// object is tagged with in shared memory, so the D-side proposer can ENUMERATE all pending
// intents without first knowing their owners. atomic.SharedMemory.Indexed can only look up
// keys that possess a KNOWN trait (state.go getKeys); the precompile's existing per-owner
// trait (native_staging.go) cannot be enumerated without the owner set, so the seam needs
// one shared constant the writer tags and the reader queries. It is a domain-separated
// 32-byte tag so it can never collide with a 20-byte owner trait.
//
// COMPANION (coordinated cutover): the C-side flush MUST add this trait to the swap
// intent's Element.Traits — precompile/dex native_staging.go collectRange, where the
// PutRequests for the C->D object are built (today Traits=[owner] only). That is the ONE
// line that makes a production 0x9999 intent discoverable here; it is pinned to THIS
// constant (the same wire-golden discipline that pins encodeSeamObject). The native VM
// e2e models that corrected flush (writeCToDIntent tags the object with SeamPendingTrait).
var SeamPendingTrait = func() []byte {
d := sha256.Sum256([]byte("lux.dex.native.intent.pending.v1"))
return d[:]
}()
// maxSeamDrivePerBlock bounds how many import (and, separately, export) legs one block's
// drive emits, so a large backlog cannot produce an unbounded block. The excess is picked
// up by the next block's drive (the enumeration is a pure function of the still-pending
// committed state, so nothing is lost — just deferred). Sized generously for the in-flight
// cross-chain swap count a single block realistically faces.
const maxSeamDrivePerBlock = 256
// driveSeamImports enumerates the committed C->D swap-intent objects in shared memory and
// returns a deterministic, intentID-sorted list of TxImport txs for those NOT YET imported
// (no committed seamintent: escrow). It is the autonomous replacement for a keeper's
// "watch IntentSubmitted -> inject ImportTx" loop, run inside BuildBlock.
//
// Pure function of committed state: the pending set comes from shared memory (the C chain
// accepted the staging block) and the already-imported filter comes from the committed
// escrow index. The double-guard against re-import — the committed-escrow filter here AND
// executeImport's own replay-reject — means a re-enumerated intent (object still present
// because its consuming Remove has not yet been accepted) is imported at most once.
func (vm *VM) driveSeamImports() ([]*Tx, error) {
if !vm.autoDriveSeam {
return nil, nil
}
sm := vm.seamSharedMemory()
if sm == nil || vm.cChainID == ids.Empty {
return nil, nil // seam not wired: nothing to drive (no mint)
}
keys, err := enumerateSeamPending(sm, vm.cChainID, maxSeamDrivePerBlock)
if err != nil {
return nil, fmt.Errorf("dchain: seam import enumerate: %w", err)
}
// Filter to un-imported intents, collecting their ids for a total sort.
pending := make([]ids.ID, 0, len(keys))
for _, k := range keys {
if len(k) != 32 {
continue // a key that is not an intent id is not ours (defensive)
}
var intentID ids.ID
copy(intentID[:], k)
_, imported, gerr := getSeamIntent(vm.db, intentID)
if gerr != nil {
return nil, gerr
}
if imported {
continue // already imported in committed state: skip (no re-import)
}
pending = append(pending, intentID)
}
// DETERMINISM: total order by intentID, independent of shared-memory iteration order,
// so every proposer on the same committed partition emits the same import sequence.
sort.Slice(pending, func(i, j int) bool {
return bytes.Compare(pending[i][:], pending[j][:]) < 0
})
out := make([]*Tx, 0, len(pending))
for _, intentID := range pending {
tx, err := NewTx(TxImport, EncodeSeamImportBody(intentID))
if err != nil {
return nil, fmt.Errorf("dchain: build TxImport: %w", err)
}
out = append(out, tx)
}
return out, nil
}
// driveSeamExports enumerates the OPEN intent escrows whose owner holds realized OUTPUT in
// committed state and returns deterministic TxExport txs that settle the realized proceeds
// (and any leftover input refund) back to C as D->C objects the 0x9999 ImportSettlement
// consumes. It is the autonomous replacement for a keeper's "watch the D fill -> inject the
// settlement export" loop, run inside BuildBlock AFTER the matcher has produced proceeds in
// a prior accepted block.
//
// THE OUTPUT GATE (why this never refunds a still-pending swap, and satisfies
// no-export-before-import): an intent is exported ONLY when its owner's cross-chain account
// holds a realized OUTPUT — a non-AssetIn balance > 0. Right after an import the account
// holds only the imported AssetIn (no output) so nothing is exported; only once a signed
// taker order has CROSSED (producing a different asset) does the swap's proceeds become
// exportable. An intent that was never imported has no escrow, is never enumerated, and is
// never exported.
//
// ATTRIBUTION + CONSERVATION: the export's recipient/owner is the escrow's RECORDED 20-byte
// owner (executeExport binds it), so value can only return to the rightful taker; the
// debit draws the REALIZED ledger balance (executeExport refuses an over-debit) and a
// same-asset refund is capped by the intent's remaining principal — so C can never be
// credited more than D matched. The drive emits the refund leg BEFORE the proceeds leg so
// the per-taker cap is applied against the open escrow, and execute then CLOSES the escrow
// once the account is fully drained (closeSeamIntentIfDrained), making each swap one-shot.
//
// SCOPE: the realistic cross-chain swap is an IOC taker (TxSubmit) against a single market,
// which resolves in one block to one output asset (+ optional leftover). The drive settles
// exactly that. A cross-chain account that mixes direct (non-imported) D activity, or a
// multi-hop swap with several output assets, is outside this first-cut drive and noted as a
// boundary (the spent witness is attributed to the single output; extra outputs remain the
// owner's D balance rather than being mis-bound to this intent's recorded MEV floor).
func (vm *VM) driveSeamExports() ([]*Tx, error) {
if !vm.autoDriveSeam {
return nil, nil
}
sm := vm.seamSharedMemory()
if sm == nil || vm.cChainID == ids.Empty {
return nil, nil // seam not wired: an export cannot be written (no D->C object)
}
escrows, err := listOpenSeamIntents(vm.db, maxSeamDrivePerBlock)
if err != nil {
return nil, fmt.Errorf("dchain: seam export enumerate: %w", err)
}
out := make([]*Tx, 0, len(escrows))
for _, e := range escrows {
acct := crossChainAccount(e.Owner)
// The realized OUTPUT (non-AssetIn balances) and the leftover INPUT (AssetIn),
// both read from committed state. No output => the swap has not produced proceeds
// yet => do not export (no premature refund; no-export-before-trade).
outputs, oerr := listAccountOutputs(vm.db, acct, e.AssetIn)
if oerr != nil {
return nil, oerr
}
if len(outputs) == 0 {
continue
}
leftoverIn, lerr := getAvailable(vm.db, acct, e.AssetIn)
if lerr != nil {
return nil, lerr
}
// The spent witness the precompile's taker-authenticated MEV floor reads: the
// AssetIn actually CONSUMED to produce the output = original locked principal minus
// the leftover input. Computed from committed state, so it is stable regardless of
// the in-block leg order.
spent := uint64(0)
if e.Remaining > leftoverIn {
spent = e.Remaining - leftoverIn
}
// REFUND leg FIRST (same-asset, per-taker-capped, decrements Remaining) so it runs
// against the still-open escrow.
if leftoverIn > 0 {
refund := leftoverIn
if refund > e.Remaining {
refund = e.Remaining
}
if refund > 0 {
tx, terr := NewTx(TxExport, EncodeSeamExportBody(e.IntentID, e.AssetIn, refund, 0))
if terr != nil {
return nil, fmt.Errorf("dchain: build TxExport refund: %w", terr)
}
out = append(out, tx)
}
}
// PROCEEDS legs (sorted by asset id via the committed key order); the draining one
// triggers the escrow close in execute.
for _, o := range outputs {
tx, terr := NewTx(TxExport, EncodeSeamExportBody(e.IntentID, o.asset, o.amount, spent))
if terr != nil {
return nil, fmt.Errorf("dchain: build TxExport proceeds: %w", terr)
}
out = append(out, tx)
if len(out) >= maxSeamDrivePerBlock {
return out, nil
}
}
}
return out, nil
}
// hasSeamWork reports whether the drive would emit ANY tx from the current committed state
// — a pending un-imported intent, or an open escrow with realized output to export. It is
// the WaitForEvent trigger so the engine calls BuildBlock for a pure-seam block (one with
// no mempool txs). It short-circuits on the first match to stay cheap on the poll path.
func (vm *VM) hasSeamWork() bool {
if !vm.autoDriveSeam {
return false
}
sm := vm.seamSharedMemory()
if sm == nil || vm.cChainID == ids.Empty {
return false
}
// Any un-imported pending intent?
keys, err := enumerateSeamPending(sm, vm.cChainID, maxSeamDrivePerBlock)
if err == nil {
for _, k := range keys {
if len(k) != 32 {
continue
}
var intentID ids.ID
copy(intentID[:], k)
if _, imported, gerr := getSeamIntent(vm.db, intentID); gerr == nil && !imported {
return true
}
}
}
// Any open escrow with realized output?
escrows, eerr := listOpenSeamIntents(vm.db, maxSeamDrivePerBlock)
if eerr != nil {
return false
}
for _, e := range escrows {
outputs, oerr := listAccountOutputs(vm.db, crossChainAccount(e.Owner), e.AssetIn)
if oerr == nil && len(outputs) > 0 {
return true
}
}
return false
}
// closeSeamIntentIfDrained marks an OPEN intent escrow seamIntentReclaimed once its owner's
// cross-chain account has been FULLY drained (no remaining ledger balance), so a settled
// intent is not re-enumerated by a later export drive and the owner can import a fresh
// intent. Called from execute after a successful TxExport; order-independent (it fires on
// whichever export — proceeds or refund — empties the account), so it correctly closes a
// full-fill (one proceeds leg) and a partial-fill (proceeds + refund) alike. A still-funded
// account (more output not yet exported in this block) is left open for a subsequent leg.
func (vm *VM) closeSeamIntentIfDrained(db database.Database, intentID ids.ID) error {
rec, exists, err := getSeamIntent(db, intentID)
if err != nil {
return err
}
if !exists || rec.Status != seamIntentOpen {
return nil
}
drained, err := accountFullyDrained(db, crossChainAccount(rec.Owner))
if err != nil {
return err
}
if !drained {
return nil
}
rec.Status = seamIntentReclaimed
return putSeamIntent(db, intentID, rec)
}
// --- committed-state enumeration helpers (pure reads; the drive's only inputs) ----------
// enumerateSeamPending walks the C->D shared-memory partition for THIS chain and returns
// the keys (intent ids) of every object tagged SeamPendingTrait, up to [max]. The object
// VALUE is not self-identifying (it carries owner/asset/amount but not its own key), so the
// keys are recovered through atomic.SharedMemory.Indexed's lastKey: each page advances by
// one element and surfaces that element's key as lastKey. The walk is the documented
// shared-memory enumeration (the GetAtomicUTXOs pagination, adapted to recover keys rather
// than parse self-identifying values). Deterministic over committed shared memory; the
// caller sorts the result so the page order does not matter.
func enumerateSeamPending(sm atomic.SharedMemory, peerChainID ids.ID, max int) ([][]byte, error) {
keys := make([][]byte, 0, 16)
seen := make(map[ids.ID]struct{}, 16)
var startKey []byte
limit := 1 // first page: surface the head element's key as lastKey
for len(keys) < max {
_, _, lastKey, err := sm.Indexed(peerChainID, [][]byte{SeamPendingTrait}, SeamPendingTrait, startKey, limit)
if err != nil {
return nil, err
}
if len(lastKey) == 0 {
break // empty partition
}
if startKey != nil && bytes.Equal(lastKey, startKey) {
break // tail reached: the page returned only the (inclusive) start element
}
if len(lastKey) == 32 {
var id ids.ID
copy(id[:], lastKey)
if _, dup := seen[id]; !dup {
seen[id] = struct{}{}
keys = append(keys, append([]byte(nil), lastKey...))
}
}
startKey = lastKey
limit = 2 // subsequent pages re-include the inclusive start element + one new key
}
return keys, nil
}
// seamIntentEntry is one OPEN escrow paired with its intent id (the seamintent: key), the
// unit the export drive iterates.
type seamIntentEntry struct {
IntentID ids.ID
Owner [20]byte
AssetIn [32]byte
Remaining uint64
}
// listOpenSeamIntents streams the seamintent: index and returns every OPEN escrow, in
// intentID order (the prefix iterator already yields keys — hence intent ids — in ascending
// order, so the result is the total sort the drive needs without an extra sort). Settled
// (reclaimed) escrows are skipped. Bounded by [max].
func listOpenSeamIntents(db database.Iteratee, max int) ([]seamIntentEntry, error) {
it := db.NewIteratorWithPrefix([]byte(prefixSeamIntent))
defer it.Release()
out := make([]seamIntentEntry, 0, 16)
for it.Next() && len(out) < max {
key := it.Key()
if len(key) != len(prefixSeamIntent)+32 {
continue
}
v := it.Value()
if len(v) != 20+32+8+1 {
return nil, fmt.Errorf("dchain: corrupt seam intent len=%d", len(v))
}
if v[60] != seamIntentOpen {
continue // settled: not exportable
}
var e seamIntentEntry
copy(e.IntentID[:], key[len(prefixSeamIntent):])
copy(e.Owner[:], v[0:20])
copy(e.AssetIn[:], v[20:52])
e.Remaining = binary.BigEndian.Uint64(v[52:60])
out = append(out, e)
}
return out, it.Error()
}
// accountOutput is one realized-output (asset, amount) leg of a cross-chain account.
type accountOutput struct {
asset [32]byte
amount uint64
}
// listAccountOutputs returns an account's available balances for every asset OTHER than
// [assetIn], in asset-id order — the realized swap OUTPUT the export drive settles. The
// balance: ledger keys by <user:16><asset:32>, so a prefix scan over <user> yields the
// account's per-asset rows in ascending asset order (the total sort the drive needs).
func listAccountOutputs(db database.Iteratee, acct userKey, assetIn [32]byte) ([]accountOutput, error) {
prefix := make([]byte, len(prefixBalance)+len(acct))
copy(prefix, prefixBalance)
copy(prefix[len(prefixBalance):], acct[:])
it := db.NewIteratorWithPrefix(prefix)
defer it.Release()
out := make([]accountOutput, 0, 4)
for it.Next() {
key := it.Key()
if len(key) != len(prefix)+32 {
continue
}
var asset [32]byte
copy(asset[:], key[len(prefix):])
if asset == assetIn {
continue // leftover input, handled as the refund leg, not output
}
v := it.Value()
if len(v) != 8 {
return nil, fmt.Errorf("dchain: corrupt balance len=%d", len(v))
}
amount := binary.BigEndian.Uint64(v)
if amount == 0 {
continue
}
out = append(out, accountOutput{asset: asset, amount: amount})
}
return out, it.Error()
}
// accountFullyDrained reports whether an account holds NO available balance in any asset (no
// balance: row under its prefix). writeUint64 deletes a row at zero, so a present row is a
// non-zero balance; the absence of any row is a fully-settled account.
func accountFullyDrained(db database.Iteratee, acct userKey) (bool, error) {
prefix := make([]byte, len(prefixBalance)+len(acct))
copy(prefix, prefixBalance)
copy(prefix[len(prefixBalance):], acct[:])
it := db.NewIteratorWithPrefix(prefix)
defer it.Release()
if it.Next() {
return false, it.Error()
}
return true, it.Error()
}
+349
View File
@@ -0,0 +1,349 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dchain
import (
"context"
"testing"
"github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/dex/pkg/zapwire"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/runtime"
"github.com/luxfi/vm/chains/atomic"
)
// drive_seam_e2e_test.go proves the PROPOSER-SIDE DRIVE (drive.go): the dexvm's OWN
// BuildBlock — NOT a manual mempool.Add — autonomously imports C->D intents and exports
// D->C settlements during normal block production. atomic_seam_e2e_test.go drives the seam
// by hand (it plays the keeper); these tests inject NO TxImport/TxExport at all. The only
// inputs are (a) a Phase-A C->D intent object placed in shared memory (modeling 0x9999
// SubmitSwapIntent's flush) and (b) the taker's own signed crossing order. The VM does the
// rest, and the D->C object 0x9999 ImportSettlement consumes is produced by the VM itself.
// findTx returns the first tx of a given type in an accepted block (the drive-generated
// import/export the test asserts the VM produced on its own).
func findTx(blk *Block, typ TxType) (*Tx, bool) {
for _, tx := range blk.txs {
if tx.Type == typ {
return tx, true
}
}
return nil, false
}
// TestDrive_AutonomousImportCrossExport is the headline proof of the closed gap: with NO
// manual TxImport/TxExport, the VM's OWN BuildBlock (a) imports a Phase-A C->D intent and
// funds the taker, the taker's signed order (b) crosses a resting maker, and the VM's OWN
// BuildBlock (c) exports the realized proceeds as a D->C object the modeled 0x9999 Phase B
// consumes exactly once — conservation holds and the intent escrow is closed.
func TestDrive_AutonomousImportCrossExport(t *testing.T) {
h := newSeamHarness(t)
if !h.vm.autoDriveSeam {
t.Fatal("seam is wired, so the proposer drive must be enabled by default")
}
pool := [32]byte{0xDE, 0xAD, 0xBE, 0xEF, 0x42}
base := assetID32(0xB0) // the asset the taker BUYS / the maker SELLS / the export proceeds
quote := assetID32(0x90) // the asset the taker LOCKS on C (tokenIn) / the import funds
maker := acctFor(t, "drive-maker")
taker := acctFor(t, "drive-taker")
takerAddr := h.addr(t, "drive-taker")
// ---- Setup: open the custody market + rest a maker SELL 100 base @ 2 ----
h.vm.mempool.Add(openMarketTx(t, pool, base, quote))
h.vm.mempool.Add(depositTx(t, "drive-maker", base, 100))
h.buildAccept(t)
h.vm.mempool.Add(maker.signed(t, TxPlace, zapwire.EncodePlace(pool, sideSell, 2.0, 100, maker.user)))
h.buildAccept(t)
// ---- PHASE A (C side, modeled): lock 200 quote -> a tagged C->D intent object ----
const lockedQuote = 200
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, lockedQuote, pool, 0)
h.writeCToDIntent(t, intentID, takerAddr, quote, lockedQuote)
// ---- (b) AUTONOMOUS IMPORT: BuildBlock with an EMPTY mempool drives the import ----
if h.vm.mempool.Len() != 0 {
t.Fatalf("mempool must be empty so the import is purely drive-generated, have %d", h.vm.mempool.Len())
}
if got := h.avail(t, taker.account, quote); got != 0 {
t.Fatalf("pre-import taker quote = %d, want 0", got)
}
importBlk := h.buildAccept(t)
importTx, ok := findTx(importBlk, TxImport)
if !ok {
t.Fatal("the VM's OWN BuildBlock must produce a TxImport for the pending intent (none found)")
}
if body := EncodeSeamImportBody(intentID); string(importTx.Body) != string(body) {
t.Fatal("drive-generated TxImport names the wrong intent id")
}
if got := h.avail(t, taker.account, quote); got != lockedQuote {
t.Fatalf("autonomous import: taker quote = %d, want %d", got, lockedQuote)
}
// The C->D object was consumed (removed) at Accept by the drive's import.
if vals, _ := h.dSM.Get(h.cChainID, [][]byte{intentID[:]}); len(vals) == 1 && len(vals[0]) != 0 {
t.Fatal("C->D intent object must be consumed after autonomous import")
}
// The escrow now records the intent (so it is import-replay-guarded and export-bindable).
rec, exists, err := getSeamIntent(h.vm.db, intentID)
if err != nil || !exists || rec.Owner != takerAddr || rec.Remaining != lockedQuote {
t.Fatalf("seam escrow after autonomous import: exists=%v err=%v rec=%+v", exists, err, rec)
}
// ---- The taker submits a SIGNED crossing order (the normal mempool flow) ----
// The drive funds and settles; the taker still authors their own trade.
h.vm.mempool.Add(taker.signed(t, TxSubmit, zapwire.EncodeSubmit(pool, sideBuy, false, 2.0, 100, taker.user)))
crossBlk := h.buildAccept(t)
// The cross is realized IN this block; at this block's BUILD the committed state had no
// output yet, so the drive correctly emitted NO export here (no-export-before-trade).
if _, drove := findTx(crossBlk, TxExport); drove {
t.Fatal("export must not be driven in the same block the cross is realized (no committed output at build)")
}
if got := h.avail(t, taker.account, base); got != 100 {
t.Fatalf("post-cross taker base = %d, want 100 (proceeds)", got)
}
if got := h.avail(t, taker.account, quote); got != 0 {
t.Fatalf("post-cross taker quote = %d, want 0 (all spent)", got)
}
// ---- (c) AUTONOMOUS EXPORT: BuildBlock with an EMPTY mempool drives the export ----
if h.vm.mempool.Len() != 0 {
t.Fatalf("mempool must be empty so the export is purely drive-generated, have %d", h.vm.mempool.Len())
}
exportBlk := h.buildAccept(t)
exportTx, ok := findTx(exportBlk, TxExport)
if !ok {
t.Fatal("the VM's OWN BuildBlock must produce a TxExport for the realized proceeds (none found)")
}
// Decode the drive's export and assert it settles the proceeds (base, 100) with the
// matched-input spent witness (200), exactly what a correct keeper would build.
gotIntent, gotAsset, gotAmount, gotSpent, derr := decodeSeamExportBody(exportTx.Body)
if derr != nil || gotIntent != intentID || gotAsset != base || gotAmount != 100 || gotSpent != 200 {
t.Fatalf("drive export body = {intent %x asset %x amount %d spent %d}, want {%x %x 100 200}",
gotIntent[:6], gotAsset[:4], gotAmount, gotSpent, intentID[:6], base[:4])
}
if got := h.avail(t, taker.account, base); got != 0 {
t.Fatalf("post-export taker base = %d, want 0 (exported)", got)
}
// The intent escrow is CLOSED (one-shot) once the account is drained.
if rec, _, _ := getSeamIntent(h.vm.db, intentID); rec.Status != seamIntentReclaimed {
t.Fatalf("escrow status after autonomous export = %d, want reclaimed (%d)", rec.Status, seamIntentReclaimed)
}
// ---- PHASE B (C side, modeled 0x9999 ImportSettlement): consume the D->C object ----
outputID := deriveSeamOutputID(exportTx.ID(), 0)
vals, gerr := h.cSM.Get(h.dChainID, [][]byte{outputID[:]})
if gerr != nil || len(vals) != 1 || len(vals[0]) == 0 {
t.Fatalf("the VM-produced D->C settlement object is not where 0x9999 reads it (err=%v)", gerr)
}
recRail, recOwner, recAsset, recAmount, recSpent, decOK := decodeSeamObject(vals[0])
if !decOK || recRail != railSwap || recOwner != takerAddr || recAsset != base || recAmount != 100 || recSpent != 200 {
t.Fatalf("D->C object binds = {rail %d owner %s asset %x amount %d spent %d}, want {0 %s %x 100 200}",
recRail, recOwner.Hex(), recAsset[:4], recAmount, recSpent, takerAddr.Hex(), base[:4])
}
// Consume once; replay must find it gone (0x9999 reverts ErrNativeNoSettlement).
if err := h.cSM.Apply(map[ids.ID]*atomic.Requests{h.dChainID: {RemoveRequests: [][]byte{outputID[:]}}}); err != nil {
t.Fatalf("0x9999 Phase-B consume: %v", err)
}
if vals2, _ := h.cSM.Get(h.dChainID, [][]byte{outputID[:]}); len(vals2) == 1 && len(vals2[0]) != 0 {
t.Fatal("D->C object must be consumable exactly once")
}
// CONSERVATION across the seam: 200 quote entered D (C->D import), 100 base left D
// (D->C export); inside D the maker provided 100 base and now holds 200 quote.
if got := h.avail(t, maker.account, quote); got != 200 {
t.Fatalf("maker quote (taker's conserved input) = %d, want 200", got)
}
if got := h.avail(t, maker.account, base); got != 0 {
t.Fatalf("maker base after selling all = %d, want 0", got)
}
t.Logf("PASS: VM's OWN BuildBlock imported intent %x and exported D->C object %x (no manual injection)",
intentID[:6], outputID[:6])
}
// TestDrive_NoExportBeforeImportOrTrade proves the two gates that keep the export drive from
// ever running ahead of the swap: (1) an intent that was never imported has no escrow, so
// driveSeamExports never enumerates it; (2) an imported-but-not-yet-traded intent holds only
// its input (no realized output), so the drive emits NO export and BuildBlock with an empty
// mempool reports nothing to do — the imported principal is never refunded out from under a
// pending swap.
func TestDrive_NoExportBeforeImportOrTrade(t *testing.T) {
h := newSeamHarness(t)
pool := [32]byte{0x07, 0x07, 0x07}
base := assetID32(0xB0)
quote := assetID32(0x90)
takerAddr := h.addr(t, "noexp-taker")
h.vm.mempool.Add(openMarketTx(t, pool, base, quote))
h.buildAccept(t)
// (1) Intent staged but NOT imported (no escrow). The export drive must produce nothing.
const locked = 200
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, locked, pool, 0)
h.writeCToDIntent(t, intentID, takerAddr, quote, locked)
if exports, err := h.vm.driveSeamExports(); err != nil || len(exports) != 0 {
t.Fatalf("no-export-before-import: driveSeamExports = %d txs err=%v, want 0", len(exports), err)
}
// Autonomously import it (drive emits the TxImport), funding the taker — but the taker
// does NOT trade.
h.buildAccept(t)
if got := h.avail(t, idOf(t, "noexp-taker"), quote); got != locked {
t.Fatalf("after autonomous import, taker quote = %d, want %d", got, locked)
}
// (2) Imported, not traded: only the input is held, no realized output. No export, and an
// empty-mempool BuildBlock has nothing to do (the input is NOT refunded prematurely).
if exports, err := h.vm.driveSeamExports(); err != nil || len(exports) != 0 {
t.Fatalf("no-export-before-trade: driveSeamExports = %d txs err=%v, want 0", len(exports), err)
}
if _, err := h.vm.BuildBlock(context.Background()); err != ErrEmptyMempool {
t.Fatalf("BuildBlock with an imported-but-untraded intent = %v, want ErrEmptyMempool (no premature refund)", err)
}
}
// TestDrive_ReplayImportedOnce proves the import drive funds a given intent EXACTLY ONCE.
// After the autonomous import, the C->D object is consumed; even if the source re-flushes
// the SAME object (a reorg/retry), the committed escrow filter (and executeImport's own
// replay-reject) make a second enumeration a no-op — no double credit.
func TestDrive_ReplayImportedOnce(t *testing.T) {
h := newSeamHarness(t)
pool := [32]byte{0x11, 0x22, 0x33}
base := assetID32(0xB0)
quote := assetID32(0x90)
takerAddr := h.addr(t, "replay-drive-taker")
takerAcct := idOf(t, "replay-drive-taker")
h.vm.mempool.Add(openMarketTx(t, pool, base, quote))
h.buildAccept(t)
const locked = 200
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, locked, pool, 0)
h.writeCToDIntent(t, intentID, takerAddr, quote, locked)
// First autonomous import.
h.buildAccept(t)
if got := h.avail(t, takerAcct, quote); got != locked {
t.Fatalf("after first autonomous import, quote = %d, want %d", got, locked)
}
// Re-flush the SAME C->D object (object was consumed; simulate a source retry/reorg that
// re-stages it) and drive again. The escrow already exists, so the import drive filters
// it out: no re-import, no double credit.
h.writeCToDIntent(t, intentID, takerAddr, quote, locked)
imports, err := h.vm.driveSeamImports()
if err != nil {
t.Fatalf("driveSeamImports: %v", err)
}
if len(imports) != 0 {
t.Fatalf("re-flushed already-imported intent must NOT be re-imported, got %d TxImport", len(imports))
}
// And an empty-mempool build has nothing to do (no re-import, no export — taker untraded).
if _, err := h.vm.BuildBlock(context.Background()); err != ErrEmptyMempool {
t.Fatalf("BuildBlock after replay = %v, want ErrEmptyMempool", err)
}
if got := h.avail(t, takerAcct, quote); got != locked {
t.Fatalf("after replay, quote = %d, want %d (no double credit)", got, locked)
}
}
// TestDrive_DeterministicOrdering proves two independent proposers build the SAME import
// sequence from the SAME committed shared-memory partition — the leaderless-build /
// matcher-at-Verify determinism requirement. Two VMs share one atomic memory (so they see
// the identical pending intents) but have independent state stores; their BuildBlock import
// tx lists must be byte-identical AND in ascending intentID order, regardless of the order
// the intents were flushed.
func TestDrive_DeterministicOrdering(t *testing.T) {
ctx := context.Background()
logger := log.NewNoOpLogger()
baseDB := memdb.New()
memoryDB := prefixdb.New([]byte{0}, baseDB)
m := atomic.NewMemory(memoryDB)
dChainID := ids.GenerateTestID()
cChainID := ids.GenerateTestID()
cSM := m.NewSharedMemory(cChainID)
newVM := func(statePrefix byte) *VM {
vm := &VM{}
if err := vm.Initialize(ctx, block.Init{
Runtime: &runtime.Runtime{
ChainID: dChainID,
CChainID: cChainID,
NetworkID: 96369,
Log: logger,
SharedMemory: m.NewSharedMemory(dChainID),
},
DB: prefixdb.New([]byte{statePrefix}, baseDB),
Log: logger,
ToEngine: make(chan block.Message, 16),
}); err != nil {
t.Fatalf("Initialize vm: %v", err)
}
if !vm.autoDriveSeam {
t.Fatal("seam wired: drive must be enabled")
}
return vm
}
vm1 := newVM(1)
vm2 := newVM(2)
// Flush several tagged intents. The nonce varies the intent id so the keys land in NO
// particular order in shared memory; the drive must impose the total intentID sort.
owner := common.HexToAddress("0x00112233445566778899aabbccddeeff00112233")
quote := assetID32(0x90)
pool := [32]byte{0xAB, 0xCD}
const n = 6
want := make([][]byte, 0, n)
for i := 0; i < n; i++ {
amount := uint64(100 + i)
intentID := DeriveIntentID(96369, cChainID, dChainID, owner, quote, amount, pool, uint64(i*7919))
obj := encodeSeamObject(railSwap, owner, quote, amount, 0)
if err := cSM.Apply(map[ids.ID]*atomic.Requests{
dChainID: {PutRequests: []*atomic.Element{{
Key: intentID[:],
Value: obj,
Traits: [][]byte{owner[:], SeamPendingTrait},
}}},
}); err != nil {
t.Fatalf("flush intent %d: %v", i, err)
}
want = append(want, EncodeSeamImportBody(intentID))
}
b1, err := vm1.BuildBlock(ctx)
if err != nil {
t.Fatalf("vm1 BuildBlock: %v", err)
}
b2, err := vm2.BuildBlock(ctx)
if err != nil {
t.Fatalf("vm2 BuildBlock: %v", err)
}
txs1 := b1.(*Block).txs
txs2 := b2.(*Block).txs
if len(txs1) != n || len(txs2) != n {
t.Fatalf("import tx counts = (%d, %d), want (%d, %d)", len(txs1), len(txs2), n, n)
}
// Two proposers => byte-identical import sequence.
for i := range txs1 {
if string(txs1[i].Bytes()) != string(txs2[i].Bytes()) {
t.Fatalf("proposers diverged at tx %d: %x vs %x", i, txs1[i].Bytes(), txs2[i].Bytes())
}
}
// And that sequence is ascending intentID order (the deterministic total order).
for i := 0; i < n; i++ {
if txs1[i].Type != TxImport {
t.Fatalf("tx %d type = %s, want import", i, txs1[i].Type)
}
if i > 0 && string(txs1[i-1].Body) >= string(txs1[i].Body) {
t.Fatalf("imports not in ascending intentID order at %d: %x then %x", i, txs1[i-1].Body, txs1[i].Body)
}
}
}
+61 -3
View File
@@ -99,6 +99,16 @@ type VM struct {
zapIngest rpc.Server
zapIngestCancel context.CancelFunc
// autoDriveSeam enables the PROPOSER-SIDE DRIVE (drive.go): when set, BuildBlock
// autonomously enumerates the committed cross-chain atomic state and emits the
// TxImport / TxExport txs that import C->D intents and export D->C settlements —
// the keeper-less native settlement seam. It is set true at Initialize exactly when
// the cross-chain seam is wired (a runtime with shared memory + a resolved C-Chain
// id); a single-chain runtime (no seam) leaves it false, and the seam unit tests
// (atomic_seam_e2e_test) clear it to exercise executeImport/executeExport in
// isolation via manual mempool injection. Guarded by vm.mu.
autoDriveSeam bool
initialized bool
}
@@ -142,6 +152,12 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
if init.Runtime != nil {
vm.cChainID = init.Runtime.CChainID
}
// Drive the native settlement seam autonomously exactly when it is wired: a runtime
// with cross-chain shared memory AND a resolved C-Chain id. With either absent the
// seam is closed (executeImport/executeExport refuse — no mint), so the drive has
// nothing to do and stays off. This is the keeper-less production default; the seam
// mechanics tests clear it to inject TxImport/TxExport by hand.
vm.autoDriveSeam = vm.runtime != nil && vm.runtime.GetSharedMemory() != nil && vm.cChainID != ids.Empty
vm.mempool = newMempool(init.ToEngine)
vm.outcomes = newOutcomeRegistry()
vm.books = map[[32]byte]*lx.OrderBook{}
@@ -406,7 +422,34 @@ func (vm *VM) BuildBlock(ctx context.Context) (block.Block, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
txs := vm.mempool.Drain(0)
// PROPOSER-SIDE DRIVE (drive.go): before draining the mempool, enumerate the committed
// cross-chain atomic state and autonomously generate the seam txs — TxImport for every
// un-imported C->D intent, TxExport for every open intent whose owner now holds realized
// proceeds. Both are pure, deterministic functions of committed state (shared memory +
// the seamintent: escrow + the ledger), so every validator re-derives the identical
// effects in execute and a divergent proposer is rejected on the tx root. This is the
// keeper-less native settlement seam — the VM imports C->D intents and exports D->C
// settlements during normal block production. A no-op when the seam is unwired.
imports, err := vm.driveSeamImports()
if err != nil {
return nil, fmt.Errorf("dchain: drive imports: %w", err)
}
exports, err := vm.driveSeamExports()
if err != nil {
return nil, fmt.Errorf("dchain: drive exports: %w", err)
}
mtxs := vm.mempool.Drain(0)
// Assemble [imports || exports || mempool]. Imports come FIRST so a same-block taker
// order draws from the just-imported funds; exports come before the mempool so they
// settle committed proceeds before any new mempool order can touch the same account.
// The seam txs and the mempool txs are disjoint by construction (an intent is imported,
// then traded, then exported across distinct blocks).
txs := make([]*Tx, 0, len(imports)+len(exports)+len(mtxs))
txs = append(txs, imports...)
txs = append(txs, exports...)
txs = append(txs, mtxs...)
if len(txs) == 0 {
return nil, ErrEmptyMempool
}
@@ -422,8 +465,10 @@ func (vm *VM) BuildBlock(ctx context.Context) (block.Block, error) {
overlay.Abort()
if err != nil {
// A tx that cannot execute (e.g. unknown market) must not wedge the
// chain: drop the batch back to the mempool and surface the error.
vm.mempool.Requeue(txs)
// chain: drop the MEMPOOL batch back so it is retried. The seam txs are
// regenerated from committed state on the next build, so they are not
// requeued (they are not mempool-owned).
vm.mempool.Requeue(mtxs)
return nil, fmt.Errorf("dchain: build execute: %w", err)
}
@@ -583,6 +628,12 @@ func (vm *VM) HealthCheck(ctx context.Context) (block.HealthCheckResult, error)
func (vm *VM) WaitForEvent(ctx context.Context) (block.Message, error) {
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
// The seam drive (drive.go) can have work even with an empty mempool — a pending
// C->D intent to import, or an open intent with realized proceeds to export. Those
// appear at the source chain's block cadence (seconds), so they are polled on a
// slower, separate tick to keep the hot mempool path cheap.
seamTicker := time.NewTicker(time.Second)
defer seamTicker.Stop()
for {
if vm.mempool != nil && vm.mempool.Len() > 0 {
return block.Message{Type: block.PendingTxs}, nil
@@ -591,6 +642,13 @@ func (vm *VM) WaitForEvent(ctx context.Context) (block.Message, error) {
case <-ctx.Done():
return block.Message{}, ctx.Err()
case <-ticker.C:
case <-seamTicker.C:
vm.mu.Lock()
work := vm.hasSeamWork()
vm.mu.Unlock()
if work {
return block.Message{Type: block.PendingTxs}, nil
}
}
}
}