mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
feat(dchain): native co-located C<->D atomic settlement seam
Port the atomic import/export into the native dexvm matcher so the in-luxd D-Chain PRODUCES and CONSUMES the same primary-network shared-memory objects the 0x9999 precompile speaks — the owner's chosen model (D matches under its own consensus; C-Chain 0x9999 settles by consuming a node-local atomic object D produces; no BLS receipt, no external venue). - atomic.go: the seam. encodeSeamObject/decodeSeamObject reproduce the precompile's 69-byte wire (rail|owner|asset|amount|spent) BYTE-FOR-BYTE, and DeriveIntentID its id derivation — ONE wire across precompile/proxy/native. executeImport consumes a C->D object (Get under the C chain + shared-memory Remove) and funds the taker's native D account; executeExport debits the matched proceeds and writes a D->C object keyed exactly as ImportSettlement reads it, bound to the per-intent escrow (owner/asset/remaining) so Phase B's owner/asset/amount + per-taker-cap checks pass. commitSeamAtomic applies the removes/puts atomically with the block overlay (the platformvm acceptor pattern). No root change needed: the in-consensus matcher makes the atomic ops a pure function of the already-rooted txs (unlike the proxy's live venue). - vm.go: capture init.Runtime -> shared-memory handle + C-Chain id. - tx.go: TxImport (intentID) / TxExport (intentID|asset|amount|spent), both authorized by the cross-chain object, not a signature. - block.go: execute drives the seam in the account-scoped lane; Accept commits via commitSeamAtomic. - atomic_seam_e2e_test.go: two-phase e2e over REAL shared memory — Phase A intent -> native D import + a real crossing under D consensus -> native D export -> Phase B (the exact 0x9999 ImportSettlement consume) credits once; replay rejected; over-export beyond the locked principal rejected; conservation holds. Plus a golden-wire test pinning encodeSeamObject to the SAME cross-repo golden the precompile pins ImportSettlement against (native == precompile == proxy). go.sum: re-record re-tagged luxfi/[email protected] (stale hash blocked the build). go.mod: luxfi/runtime indirect -> direct (atomic.go uses it).
This commit is contained in:
@@ -17,6 +17,7 @@ require (
|
||||
github.com/luxfi/log v1.4.3
|
||||
github.com/luxfi/pq v1.0.3
|
||||
github.com/luxfi/rpc v1.1.0
|
||||
github.com/luxfi/runtime v1.1.1
|
||||
github.com/luxfi/version v1.0.1
|
||||
github.com/luxfi/vm v1.2.5
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
@@ -108,7 +109,6 @@ require (
|
||||
github.com/luxfi/precompile v0.5.56 // indirect
|
||||
github.com/luxfi/proto v1.3.5 // indirect
|
||||
github.com/luxfi/resource v0.0.1 // indirect
|
||||
github.com/luxfi/runtime v1.1.1 // indirect
|
||||
github.com/luxfi/sampler v1.1.0 // indirect
|
||||
github.com/luxfi/upgrade v1.0.1 // indirect
|
||||
github.com/luxfi/utils v1.2.0 // indirect
|
||||
|
||||
@@ -242,7 +242,7 @@ github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
|
||||
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
|
||||
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
|
||||
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
|
||||
github.com/luxfi/age v1.5.0 h1:zC/Fw/ptZwAXr9nqrxmrcf8752EIl1Lq9RECp9OmCO0=
|
||||
github.com/luxfi/age v1.5.0 h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
|
||||
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/luxfi/api v1.0.15 h1:Q5ox3Ompw/AZNMfB9wpHZosrj9C+ZldAEHKtHEotFdY=
|
||||
github.com/luxfi/api v1.0.15/go.mod h1:Eer59msIXMnOlFncG0XjEGH3TZML0Dd1bUu4GtB7f4Q=
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package dchain
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/vm/chains/atomic"
|
||||
)
|
||||
|
||||
// atomic.go is the NATIVE co-located C<->D settlement seam: the leg that lets the
|
||||
// in-luxd D-Chain (this VM) PRODUCE and CONSUME the same primary-network atomic
|
||||
// shared-memory objects the 0x9999 precompile (precompile/dex) speaks. It is the
|
||||
// D-side mirror of native_dchain_client.go:
|
||||
//
|
||||
// - IMPORT (C->D): the precompile's SubmitSwapIntent locked the taker's tokenIn on
|
||||
// C and wrote a C->D atomic INTENT object keyed by intentID. This VM CONSUMES that
|
||||
// object (Get under the C chain, then a shared-memory Remove) and FUNDS the taker's
|
||||
// native D account with the recorded asset — the matcher then trades it natively.
|
||||
// D is funded ONLY by consuming a C->D object (no mint): the value was already
|
||||
// removed from the taker on C.
|
||||
// - EXPORT (D->C): once the native matcher has filled (settle.go), the taker's
|
||||
// realized proceeds / unfilled refund are EXPORTED back to C — debited from the D
|
||||
// ledger and written as a D->C atomic SETTLEMENT object the precompile's
|
||||
// ImportSettlement consumes EXACTLY ONCE. C is credited ONLY by consuming a D->C
|
||||
// object (no mint): the value was already debited on D.
|
||||
//
|
||||
// THE WIRE IS THE PRECOMPILE'S, byte-for-byte. encodeSeamObject / decodeSeamObject
|
||||
// reproduce precompile/dex/native_wire.go's 69-byte object
|
||||
//
|
||||
// rail(1) | owner(20) | asset(32) | amount(8) | spent(8)
|
||||
//
|
||||
// and DeriveIntentID reproduces its SHA-256 intent-id derivation, so an object this
|
||||
// VM exports is consumable by ImportSettlement and an intent the precompile minted is
|
||||
// importable here — ONE wire across precompile / proxy (chains/dexvm) / native. The
|
||||
// proxy carried the SAME 69-byte value but behind its own txs codec; this is the
|
||||
// native realization that needs no proxy and no external venue.
|
||||
//
|
||||
// CONSENSUS-SAFETY (why no extra root binding is needed, unlike the proxy). The proxy
|
||||
// learned its fills from a LIVE external matcher and so had to fold the produced
|
||||
// exports into its state root to catch a node whose relay diverged. This VM's matcher
|
||||
// is IN-CONSENSUS and deterministic: the import credit is a pure function of the
|
||||
// (rooted) TxImport body + the committed shared-memory object every validator reads
|
||||
// identically; the export object is a pure function of the (rooted) TxExport body +
|
||||
// the escrow record (itself written by the rooted import). The tx root already commits
|
||||
// both, so honest validators produce byte-identical atomic operations and a divergent
|
||||
// proposer is rejected on the tx root — there is no live, unrooted input to bind.
|
||||
|
||||
// seamRail is the cross-chain object's lane discriminator (wire byte 0), IDENTICAL to
|
||||
// precompile/dex Rail. A swap fill/refund travels railSwap; an LP collect travels
|
||||
// railLP. The native swap seam writes and consumes ONLY railSwap; the rail byte lets
|
||||
// the precompile refuse a cross-rail object before it touches a pot (the H1 gate).
|
||||
type seamRail uint8
|
||||
|
||||
const (
|
||||
railSwap seamRail = 0 // swap fill / refund lane (the native seam's only lane)
|
||||
railLP seamRail = 1 // LP commit / collect lane (not produced here; named for the gate)
|
||||
)
|
||||
|
||||
// seamObjectSize is the fixed shared-memory object width — IDENTICAL to
|
||||
// precompile/dex exportedOutputSize9999 and chains/dexvm exportedOutputSize:
|
||||
// rail(1) | owner(20) | asset(32) | amount(8) | spent(8) = 69 bytes.
|
||||
const seamObjectSize = 1 + 20 + 32 + 8 + 8
|
||||
|
||||
// nativeIntentDomain scopes the intent-id derivation — IDENTICAL to precompile/dex
|
||||
// nativeIntentDomain so the id this VM recomputes for an imported object matches the
|
||||
// id the precompile minted it under.
|
||||
const nativeIntentDomain = "lux.dex.native.intent.v1"
|
||||
|
||||
// Seam state schema (under the VM's init.DB namespace, committed in the block overlay
|
||||
// alongside the ledger so the consume/credit/escrow land atomically with the block):
|
||||
//
|
||||
// seamintent:<intentID:32> -> owner(20)|assetIn(32)|remaining(8)|status(1)
|
||||
// the per-intent escrow written on IMPORT. It is BOTH the import replay guard
|
||||
// (a second import of the same intentID finds it) AND the export binding (the
|
||||
// full 20-byte owner the D->C object must name, the locked asset, and the
|
||||
// remaining principal the same-asset refund leg is capped by — the D-side analog
|
||||
// of the precompile's per-taker cap).
|
||||
// seamout:<outputID:32> -> intentID(32)|asset(32)|amount(8)
|
||||
// the per-export settlement record written on EXPORT, so a keeper/SDK can read
|
||||
// which D->C object id to claim in the precompile's Phase-B hookData.
|
||||
const (
|
||||
prefixSeamIntent = "seamintent:"
|
||||
prefixSeamOut = "seamout:"
|
||||
)
|
||||
|
||||
const (
|
||||
seamIntentOpen uint8 = 0
|
||||
seamIntentReclaimed uint8 = 1
|
||||
)
|
||||
|
||||
// seamIntentRecord is the decoded per-intent escrow.
|
||||
type seamIntentRecord struct {
|
||||
Owner common.Address // the taker's full 20-byte C address (the D->C object owner)
|
||||
AssetIn [32]byte // the locked input asset (the refund leg is capped in these units)
|
||||
Remaining uint64 // remaining locked principal not yet refunded
|
||||
Status uint8 // seamIntentOpen / seamIntentReclaimed
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrSeamNoAtomicMemory: the seam needs cross-chain shared memory + a resolved C
|
||||
// chain id; a single-chain runtime cannot import/export (no mint, fail closed).
|
||||
ErrSeamNoAtomicMemory = errors.New("dchain: native seam requires cross-chain atomic memory + C-Chain id")
|
||||
// ErrSeamIntentReplay: a C->D intent id already imported (the escrow row exists).
|
||||
ErrSeamIntentReplay = errors.New("dchain: C->D intent already imported (replay)")
|
||||
// ErrSeamObjectMissing: no C->D object found in shared memory for the claimed id
|
||||
// (not flushed yet, or already consumed) — never credit an unbacked import.
|
||||
ErrSeamObjectMissing = errors.New("dchain: no C->D atomic object found for intent id")
|
||||
// ErrSeamObjectMalformed: the recorded object is not the canonical 69-byte width.
|
||||
ErrSeamObjectMalformed = errors.New("dchain: C->D atomic object malformed")
|
||||
// ErrSeamWrongRail: a C->D object whose rail is not railSwap reached the swap seam.
|
||||
ErrSeamWrongRail = errors.New("dchain: C->D atomic object is not a swap-rail object")
|
||||
// ErrSeamBadAmount: a zero (or unrepresentable) object amount.
|
||||
ErrSeamBadAmount = errors.New("dchain: C->D atomic object amount out of range")
|
||||
// ErrSeamNoIntent: an export names no open intent escrow for the recorded owner.
|
||||
ErrSeamNoIntent = errors.New("dchain: D->C export names no open intent escrow")
|
||||
// ErrSeamExportExceedsIntent: a same-asset (refund) export exceeds the intent's
|
||||
// remaining locked principal — the D-side per-taker cap (the export analog of the
|
||||
// precompile's ErrSettleExceedsIntent), so an over-refund can never draw on other
|
||||
// takers' pooled input.
|
||||
ErrSeamExportExceedsIntent = errors.New("dchain: D->C refund export exceeds the intent's remaining locked principal")
|
||||
)
|
||||
|
||||
// encodeSeamObject serializes a cross-chain value object as the shared-memory value,
|
||||
// BYTE-IDENTICAL with precompile/dex encodeAtomicObjectSpent and chains/dexvm
|
||||
// encodeExportedOutput: rail(1) | owner(20) | asset(32) | amount(8) | spent(8). owner
|
||||
// is the full 20-byte address; asset is the full injective AssetID (native = all-zero);
|
||||
// amount is integer asset units; spent is the matched INPUT on a swap proceeds leg (0
|
||||
// on a refund / import object).
|
||||
func encodeSeamObject(rail seamRail, owner common.Address, asset [32]byte, amount, spent uint64) []byte {
|
||||
v := make([]byte, seamObjectSize)
|
||||
v[0] = byte(rail)
|
||||
copy(v[1:21], owner[:])
|
||||
copy(v[21:53], asset[:])
|
||||
binary.BigEndian.PutUint64(v[53:61], amount)
|
||||
binary.BigEndian.PutUint64(v[61:69], spent)
|
||||
return v
|
||||
}
|
||||
|
||||
// decodeSeamObject is the inverse, IDENTICAL to precompile/dex decodeAtomicObject:
|
||||
// ok=false for any value that is not EXACTLY the canonical width, so a corrupt record
|
||||
// is never reinterpreted into a credit. The consumer binds to THIS recorded value.
|
||||
func decodeSeamObject(v []byte) (rail seamRail, owner common.Address, asset [32]byte, amount, spent uint64, ok bool) {
|
||||
if len(v) != seamObjectSize {
|
||||
return 0, common.Address{}, [32]byte{}, 0, 0, false
|
||||
}
|
||||
rail = seamRail(v[0])
|
||||
copy(owner[:], v[1:21])
|
||||
copy(asset[:], v[21:53])
|
||||
amount = binary.BigEndian.Uint64(v[53:61])
|
||||
spent = binary.BigEndian.Uint64(v[61:69])
|
||||
return rail, owner, asset, amount, spent, true
|
||||
}
|
||||
|
||||
// DeriveIntentID computes the deterministic id of a C->D atomic intent object — the
|
||||
// shared-memory key the precompile PUTs it under (and that this VM's import consumes).
|
||||
// It is BYTE-IDENTICAL to precompile/dex DeriveIntentID:
|
||||
//
|
||||
// SHA-256( domain | networkID | cChainID | dChainID |
|
||||
// account | assetIn | amountIn | marketID | nonce )
|
||||
//
|
||||
// Every component is fixed width. The native VM does not re-derive ids on the import
|
||||
// path (it consumes by the key carried in the TxImport body); this is exported so the
|
||||
// keeper / SDK / tests derive the SAME id the precompile minted, off-chain.
|
||||
func DeriveIntentID(
|
||||
networkID uint32,
|
||||
cChainID, dChainID ids.ID,
|
||||
account common.Address,
|
||||
assetIn [32]byte,
|
||||
amountIn uint64,
|
||||
marketID [32]byte,
|
||||
nonce uint64,
|
||||
) ids.ID {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(nativeIntentDomain))
|
||||
var u4 [4]byte
|
||||
binary.BigEndian.PutUint32(u4[:], networkID)
|
||||
h.Write(u4[:])
|
||||
h.Write(cChainID[:])
|
||||
h.Write(dChainID[:])
|
||||
h.Write(account[:])
|
||||
h.Write(assetIn[:])
|
||||
var u8 [8]byte
|
||||
binary.BigEndian.PutUint64(u8[:], amountIn)
|
||||
h.Write(u8[:])
|
||||
h.Write(marketID[:])
|
||||
binary.BigEndian.PutUint64(u8[:], nonce)
|
||||
h.Write(u8[:])
|
||||
var out [32]byte
|
||||
copy(out[:], h.Sum(nil))
|
||||
return ids.ID(out)
|
||||
}
|
||||
|
||||
// deriveSeamOutputID computes the deterministic D->C object key from the exporting
|
||||
// tx id and the leg index — SHA-256 over txID||index, the SAME UTXO-id derivation the
|
||||
// proxy (chains/dexvm deriveUTXOID) and platformvm use, so the key is globally unique
|
||||
// and the keeper recomputes it from the export tx.
|
||||
func deriveSeamOutputID(exportTxID ids.ID, index uint32) ids.ID {
|
||||
var buf [36]byte
|
||||
copy(buf[0:32], exportTxID[:])
|
||||
binary.BigEndian.PutUint32(buf[32:36], index)
|
||||
return ids.ID(sha256.Sum256(buf[:]))
|
||||
}
|
||||
|
||||
// crossChainAccount folds a 20-byte cross-chain C address to this VM's 16-byte ledger
|
||||
// account — the SAME Account16 fold a signed D order uses, with the secp256k1 scheme
|
||||
// (a C-Chain caller is an EVM/secp address). So an imported intent funds the EXACT
|
||||
// account the taker's own signed D orders draw from, and there is ONE address->account
|
||||
// derivation across the deposit, order, and seam paths.
|
||||
func crossChainAccount(owner common.Address) userKey {
|
||||
return Account16(lx.AuthSecp256k1, owner)
|
||||
}
|
||||
|
||||
// --- TxImport / TxExport body codecs ---------------------------------------------
|
||||
|
||||
// EncodeSeamImportBody builds a TxImport body: intentID[32] — the shared-memory key of
|
||||
// the C->D object to consume. Exported for the keeper / SDK / tests.
|
||||
func EncodeSeamImportBody(intentID ids.ID) []byte {
|
||||
b := make([]byte, seamImportBodySize)
|
||||
copy(b, intentID[:])
|
||||
return b
|
||||
}
|
||||
|
||||
// EncodeSeamExportBody builds a TxExport body: intentID[32] | asset[32] | amount[8] |
|
||||
// spent[8]. asset/amount are the D->C object's value; spent is the matched input
|
||||
// witness on a proceeds leg (0 on a refund). Exported for the keeper / SDK / tests.
|
||||
func EncodeSeamExportBody(intentID ids.ID, asset [32]byte, amount, spent uint64) []byte {
|
||||
b := make([]byte, seamExportBodySize)
|
||||
copy(b[0:32], intentID[:])
|
||||
copy(b[32:64], asset[:])
|
||||
binary.BigEndian.PutUint64(b[64:72], amount)
|
||||
binary.BigEndian.PutUint64(b[72:80], spent)
|
||||
return b
|
||||
}
|
||||
|
||||
// decodeSeamExportBody parses a TxExport body. A short body is a malformed tx (block
|
||||
// abort) — the fixed bodySize gate already rejects undersized frames at parse, so this
|
||||
// only fails on internal misuse.
|
||||
func decodeSeamExportBody(body []byte) (intentID ids.ID, asset [32]byte, amount, spent uint64, err error) {
|
||||
if len(body) < seamExportBodySize {
|
||||
return ids.Empty, [32]byte{}, 0, 0, fmt.Errorf("dchain: seam export body too short: %d", len(body))
|
||||
}
|
||||
copy(intentID[:], body[0:32])
|
||||
copy(asset[:], body[32:64])
|
||||
amount = binary.BigEndian.Uint64(body[64:72])
|
||||
spent = binary.BigEndian.Uint64(body[72:80])
|
||||
return intentID, asset, amount, spent, nil
|
||||
}
|
||||
|
||||
// --- per-intent escrow (seamintent:) ---------------------------------------------
|
||||
|
||||
func seamIntentKey(intentID ids.ID) []byte {
|
||||
k := make([]byte, len(prefixSeamIntent)+32)
|
||||
copy(k, prefixSeamIntent)
|
||||
copy(k[len(prefixSeamIntent):], intentID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
func putSeamIntent(db database.KeyValueWriter, intentID ids.ID, r seamIntentRecord) error {
|
||||
v := make([]byte, 20+32+8+1)
|
||||
copy(v[0:20], r.Owner[:])
|
||||
copy(v[20:52], r.AssetIn[:])
|
||||
binary.BigEndian.PutUint64(v[52:60], r.Remaining)
|
||||
v[60] = r.Status
|
||||
return db.Put(seamIntentKey(intentID), v)
|
||||
}
|
||||
|
||||
func getSeamIntent(db database.KeyValueReader, intentID ids.ID) (seamIntentRecord, bool, error) {
|
||||
v, err := db.Get(seamIntentKey(intentID))
|
||||
if err == database.ErrNotFound {
|
||||
return seamIntentRecord{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return seamIntentRecord{}, false, err
|
||||
}
|
||||
if len(v) != 20+32+8+1 {
|
||||
return seamIntentRecord{}, false, fmt.Errorf("dchain: corrupt seam intent len=%d", len(v))
|
||||
}
|
||||
var r seamIntentRecord
|
||||
copy(r.Owner[:], v[0:20])
|
||||
copy(r.AssetIn[:], v[20:52])
|
||||
r.Remaining = binary.BigEndian.Uint64(v[52:60])
|
||||
r.Status = v[60]
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
// --- per-export settlement record (seamout:) -------------------------------------
|
||||
|
||||
func seamOutKey(outputID ids.ID) []byte {
|
||||
k := make([]byte, len(prefixSeamOut)+32)
|
||||
copy(k, prefixSeamOut)
|
||||
copy(k[len(prefixSeamOut):], outputID[:])
|
||||
return k
|
||||
}
|
||||
|
||||
func putSeamOut(db database.KeyValueWriter, outputID, intentID ids.ID, asset [32]byte, amount uint64) error {
|
||||
v := make([]byte, 32+32+8)
|
||||
copy(v[0:32], intentID[:])
|
||||
copy(v[32:64], asset[:])
|
||||
binary.BigEndian.PutUint64(v[64:72], amount)
|
||||
return db.Put(seamOutKey(outputID), v)
|
||||
}
|
||||
|
||||
// --- atomic request accumulator (the block's cross-chain mutation set) -----------
|
||||
//
|
||||
// atomicRequests collects the shared-memory removes (import) and puts (export) a
|
||||
// block produces, applied atomically with the state batch at Accept (sm.Apply). It is
|
||||
// the SAME platformvm acceptor pattern the proxy (chains/dexvm atomic.go) uses.
|
||||
|
||||
type atomicRequests struct {
|
||||
reqs map[ids.ID]*atomic.Requests
|
||||
}
|
||||
|
||||
func newAtomicRequests() *atomicRequests {
|
||||
return &atomicRequests{reqs: make(map[ids.ID]*atomic.Requests)}
|
||||
}
|
||||
|
||||
func (a *atomicRequests) empty() bool { return a == nil || len(a.reqs) == 0 }
|
||||
|
||||
func (a *atomicRequests) forChain(chainID ids.ID) *atomic.Requests {
|
||||
r, ok := a.reqs[chainID]
|
||||
if !ok {
|
||||
r = &atomic.Requests{}
|
||||
a.reqs[chainID] = r
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// seamSharedMemory returns this chain's atomic shared-memory handle (from the runtime
|
||||
// the consensus engine wired at Initialize), or nil when no cross-chain memory was
|
||||
// wired (single-chain dev / unit tests with no seam).
|
||||
func (vm *VM) seamSharedMemory() atomic.SharedMemory {
|
||||
if vm.runtime == nil {
|
||||
return nil
|
||||
}
|
||||
return vm.runtime.GetSharedMemory()
|
||||
}
|
||||
|
||||
// executeImport CONSUMES a C->D atomic intent object and FUNDS the taker's native D
|
||||
// account with the recorded asset. It mirrors precompile SubmitSwapIntent's other
|
||||
// side and the proxy executeImport, but credits the native ledger so the in-consensus
|
||||
// matcher trades the funds:
|
||||
//
|
||||
// 1. replay guard: the intent escrow must not already exist (one import per intent).
|
||||
// 2. read the RECORDED C->D object (Get under the C chain); missing => reject (never
|
||||
// credit an unbacked import). A nil shared memory / unresolved C chain => reject.
|
||||
// 3. BIND to the recorded value: rail==railSwap, amount>0; the credited asset/owner
|
||||
// are the RECORDED object's, never declared by the tx (the tx carries only the id).
|
||||
// 4. CREDIT available[crossChainAccount(owner), asset] += amount (fund the D account).
|
||||
// 5. RECORD the per-intent escrow (owner/assetIn/remaining) for the export binding.
|
||||
// 6. ACCUMULATE the shared-memory Remove under the C chain (applied at Accept), so the
|
||||
// object is consumed exactly once.
|
||||
//
|
||||
// It returns ok=false (with a nil error) for a deterministic per-tx REJECT — a missing
|
||||
// object, wrong rail, replay, or unwired seam — so a not-yet-flushed intent can be
|
||||
// retried in a later block rather than wedging the chain. A db fault returns an error
|
||||
// (block abort). The block records a successful import under seen: (idempotent replay)
|
||||
// but NOT a reject (retryable).
|
||||
func (vm *VM) executeImport(db database.KeyValueReaderWriterDeleter, ar *atomicRequests, intentID ids.ID) (credited uint64, ok bool, err error) {
|
||||
sm := vm.seamSharedMemory()
|
||||
if sm == nil || vm.cChainID == ids.Empty {
|
||||
return 0, false, nil // seam not wired: deterministic reject, no mint
|
||||
}
|
||||
|
||||
// (1) replay guard — the escrow row is the durable consumed marker.
|
||||
if _, exists, gerr := getSeamIntent(db, intentID); gerr != nil {
|
||||
return 0, false, gerr
|
||||
} else if exists {
|
||||
return 0, false, nil // already imported (ErrSeamIntentReplay condition); reject
|
||||
}
|
||||
|
||||
// (2) read the RECORDED object under the C chain partition.
|
||||
vals, gerr := sm.Get(vm.cChainID, [][]byte{intentID[:]})
|
||||
if gerr != nil {
|
||||
return 0, false, fmt.Errorf("dchain: seam import read: %w", gerr)
|
||||
}
|
||||
if len(vals) != 1 || len(vals[0]) == 0 {
|
||||
return 0, false, nil // not present (not flushed / already consumed): retryable reject
|
||||
}
|
||||
rail, owner, asset, amount, _, decOK := decodeSeamObject(vals[0])
|
||||
if !decOK {
|
||||
return 0, false, nil // malformed: reject (never reinterpret into a credit)
|
||||
}
|
||||
|
||||
// (3) BIND: swap rail only, non-zero amount.
|
||||
if rail != railSwap || amount == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
// (4) CREDIT the native D account (no mint — the value was removed from the taker
|
||||
// on C and is consumed here exactly once).
|
||||
acct := crossChainAccount(owner)
|
||||
if cerr := creditAvailable(db, acct, asset, amount); cerr != nil {
|
||||
return 0, false, cerr
|
||||
}
|
||||
|
||||
// (5) RECORD the escrow (owner/assetIn/remaining) for the export binding + cap.
|
||||
if perr := putSeamIntent(db, intentID, seamIntentRecord{
|
||||
Owner: owner,
|
||||
AssetIn: asset,
|
||||
Remaining: amount,
|
||||
Status: seamIntentOpen,
|
||||
}); perr != nil {
|
||||
return 0, false, perr
|
||||
}
|
||||
|
||||
// (6) ACCUMULATE the Remove under the C chain (consumed once, applied at Accept).
|
||||
req := ar.forChain(vm.cChainID)
|
||||
req.RemoveRequests = append(req.RemoveRequests, intentID[:])
|
||||
return amount, true, nil
|
||||
}
|
||||
|
||||
// executeExport DEBITS the taker's native D balance and writes a D->C settlement
|
||||
// object the precompile's ImportSettlement consumes. It is driven AFTER the native
|
||||
// matcher has settled (the proceeds / refund live in the taker's available balance),
|
||||
// and binds the object to the intent so Phase B's checks pass:
|
||||
//
|
||||
// 1. load the intent escrow (must be open) — the export draws against THIS intent;
|
||||
// the D->C object's owner is the escrow's RECORDED 20-byte owner, never the tx, so
|
||||
// an export can never redirect value to a different account.
|
||||
// 2. PER-TAKER CAP: a same-asset (refund) export (asset == escrow.AssetIn) is bounded
|
||||
// by the intent's remaining locked principal and decrements it — the D-side analog
|
||||
// of the precompile's per-taker cap. A proceeds export (different asset) is bounded
|
||||
// only by the available balance the matcher actually credited.
|
||||
// 3. DEBIT available[crossChainAccount(owner), asset] -= amount (refuses an over-debit:
|
||||
// the export is backed by realized D value, so it can never credit more on C than D
|
||||
// matched — conservation).
|
||||
// 4. ENCODE the D->C object (rail=railSwap, owner, asset, amount, spent) and ACCUMULATE
|
||||
// a Put under the C chain keyed by the deterministic outputID.
|
||||
// 5. RECORD the settlement (seamout:) so a keeper reads the outputID to claim.
|
||||
//
|
||||
// spent is the matched INPUT that produced a proceeds output (0 on a refund) — the
|
||||
// witness the precompile's taker-authenticated MEV floor checks. Returns ok=false for a
|
||||
// deterministic per-tx reject (no intent, over-cap, insufficient balance, unwired seam).
|
||||
func (vm *VM) executeExport(db database.KeyValueReaderWriterDeleter, ar *atomicRequests, exportTxID, intentID ids.ID, asset [32]byte, amount, spent uint64) (outputID ids.ID, ok bool, err error) {
|
||||
sm := vm.seamSharedMemory()
|
||||
if sm == nil || vm.cChainID == ids.Empty {
|
||||
return ids.Empty, false, nil // seam not wired: reject
|
||||
}
|
||||
if amount == 0 {
|
||||
return ids.Empty, false, nil
|
||||
}
|
||||
|
||||
// (1) the intent escrow must exist and be open.
|
||||
rec, exists, gerr := getSeamIntent(db, intentID)
|
||||
if gerr != nil {
|
||||
return ids.Empty, false, gerr
|
||||
}
|
||||
if !exists || rec.Status != seamIntentOpen {
|
||||
return ids.Empty, false, nil // ErrSeamNoIntent condition
|
||||
}
|
||||
|
||||
// (2) PER-TAKER CAP on the same-asset refund leg (the export analog of the
|
||||
// precompile's ImportSettlement per-taker cap). A proceeds leg (different asset) is
|
||||
// not principal-capped — output units legitimately differ from input units.
|
||||
refund := asset == rec.AssetIn
|
||||
if refund && amount > rec.Remaining {
|
||||
return ids.Empty, false, nil // ErrSeamExportExceedsIntent condition
|
||||
}
|
||||
|
||||
// (3) DEBIT the realized D balance (refuses over-debit: conservation — C can never be
|
||||
// credited more than D matched). The matcher credited exactly the proceeds/refund.
|
||||
acct := crossChainAccount(rec.Owner)
|
||||
if derr := debitAvailable(db, acct, asset, amount); derr != nil {
|
||||
if errors.Is(derr, ErrInsufficientAvailable) {
|
||||
return ids.Empty, false, nil // not enough realized value: reject
|
||||
}
|
||||
return ids.Empty, false, derr
|
||||
}
|
||||
|
||||
// (4) ENCODE + ACCUMULATE the D->C Put under the C chain, keyed by outputID. owner
|
||||
// is the escrow's RECORDED owner so the precompile binds recOwner == the taker.
|
||||
outputID = deriveSeamOutputID(exportTxID, 0)
|
||||
obj := encodeSeamObject(railSwap, rec.Owner, asset, amount, spent)
|
||||
req := ar.forChain(vm.cChainID)
|
||||
req.PutRequests = append(req.PutRequests, &atomic.Element{
|
||||
Key: outputID[:],
|
||||
Value: obj,
|
||||
Traits: [][]byte{rec.Owner[:]}, // index by recipient (lux.Addressable)
|
||||
})
|
||||
|
||||
// (5) decrement the remaining principal on a refund + RECORD the settlement.
|
||||
if refund {
|
||||
rec.Remaining -= amount
|
||||
if perr := putSeamIntent(db, intentID, rec); perr != nil {
|
||||
return ids.Empty, false, perr
|
||||
}
|
||||
}
|
||||
if perr := putSeamOut(db, outputID, intentID, asset, amount); perr != nil {
|
||||
return ids.Empty, false, perr
|
||||
}
|
||||
return outputID, true, nil
|
||||
}
|
||||
|
||||
// commitSeamAtomic commits the block's accumulated cross-chain operations atomically
|
||||
// with the state overlay — the single accept-time commit point (the platformvm /
|
||||
// proxy commitAtomic pattern). With no shared memory or no atomic ops it falls back to
|
||||
// the plain overlay commit. The overlay's CommitBatch is handed to sm.Apply so the
|
||||
// shared-memory removes/puts and the chain state land in one atomic write; the overlay
|
||||
// mem is then cleared (Abort) since its contents are already persisted via the batch.
|
||||
func (vm *VM) commitSeamAtomic(overlay versionDB, ar *atomicRequests) error {
|
||||
if ar.empty() {
|
||||
return overlay.Commit()
|
||||
}
|
||||
sm := vm.seamSharedMemory()
|
||||
if sm == nil {
|
||||
// Atomic ops were accumulated but no shared memory is wired — this cannot
|
||||
// happen on the seam path (import/export reject when sm is nil), so a non-empty
|
||||
// ar with nil sm is a programming error. Commit state only and surface nothing
|
||||
// cross-chain rather than dropping the block.
|
||||
return overlay.Commit()
|
||||
}
|
||||
batch, err := overlay.CommitBatch()
|
||||
if err != nil {
|
||||
overlay.Abort()
|
||||
return err
|
||||
}
|
||||
if err := sm.Apply(ar.reqs, batch); err != nil {
|
||||
overlay.Abort()
|
||||
return fmt.Errorf("dchain: seam atomic apply: %w", err)
|
||||
}
|
||||
overlay.Abort()
|
||||
return nil
|
||||
}
|
||||
|
||||
// versionDB is the minimal commit surface commitSeamAtomic needs from
|
||||
// *versiondb.Database, kept as an interface so the seam commit is testable in isolation.
|
||||
type versionDB interface {
|
||||
Commit() error
|
||||
CommitBatch() (database.Batch, error)
|
||||
Abort()
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
// 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/crypto"
|
||||
"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"
|
||||
)
|
||||
|
||||
// atomic_seam_e2e_test.go drives the FULL native co-located two-phase settlement
|
||||
// flow end-to-end against the real dchain.VM and REAL primary-network atomic shared
|
||||
// memory — the owner's chosen model (the D-Chain matches under its own consensus and
|
||||
// C-Chain 0x9999 settles by consuming a node-local atomic object D produces):
|
||||
//
|
||||
// PHASE A (C side, modeled): the 0x9999 precompile's SubmitSwapIntent locks the
|
||||
// taker's tokenIn on C and writes a C->D atomic INTENT object into shared memory,
|
||||
// keyed by DeriveIntentID. We reproduce exactly that object + key.
|
||||
// NATIVE D: TxImport CONSUMES the C->D object and funds the taker's native D
|
||||
// account; a signed taker order CROSSES a resting maker under D consensus; TxExport
|
||||
// debits the realized proceeds and writes a D->C atomic SETTLEMENT object.
|
||||
// PHASE B (C side, modeled): the precompile's ImportSettlement reads the D->C object
|
||||
// (Get under the D chain), binds rail/owner/asset/amount, consumes it EXACTLY ONCE
|
||||
// (shared-memory Remove), and credits C. We reproduce that consume + its binds.
|
||||
//
|
||||
// The wire is the precompile's, byte-for-byte (encodeSeamObject == native_wire.go), so
|
||||
// the object D exports here is the object 0x9999 ImportSettlement consumes. Phase B's
|
||||
// binds and one-time consumption are reproduced over the SAME real shared memory, so
|
||||
// "0x9999 settles it" is proven by the real cross-chain primitive, not a mock.
|
||||
|
||||
// seamHarness is a dchain.VM wired to REAL two-chain shared memory: the VM's own
|
||||
// handle (dSM, used by the seam's import/export) and the C-Chain's handle (cSM, the
|
||||
// modeled 0x9999 side that writes Phase-A intents and consumes Phase-B settlements).
|
||||
type seamHarness struct {
|
||||
vm *VM
|
||||
dSM atomic.SharedMemory // D-Chain shared memory (this VM uses it via runtime)
|
||||
cSM atomic.SharedMemory // C-Chain shared memory (the modeled precompile side)
|
||||
cChainID ids.ID
|
||||
dChainID ids.ID
|
||||
netID uint32
|
||||
}
|
||||
|
||||
// newSeamHarness mirrors the proxy conservation harness: ONE base db, a prefixed
|
||||
// atomic-memory namespace and a prefixed VM db over it (so sm.Apply commits the VM
|
||||
// overlay batch atomically with the shared-memory ops), two chain ids, and a runtime
|
||||
// carrying the D-Chain's shared-memory handle + the C-Chain id the seam imports from /
|
||||
// exports to.
|
||||
func newSeamHarness(t *testing.T) *seamHarness {
|
||||
t.Helper()
|
||||
logger := log.NewNoOpLogger()
|
||||
|
||||
baseDB := memdb.New()
|
||||
memoryDB := prefixdb.New([]byte{0}, baseDB)
|
||||
m := atomic.NewMemory(memoryDB)
|
||||
|
||||
dChainID := ids.GenerateTestID()
|
||||
cChainID := ids.GenerateTestID()
|
||||
dSM := m.NewSharedMemory(dChainID)
|
||||
cSM := m.NewSharedMemory(cChainID)
|
||||
|
||||
rt := &runtime.Runtime{
|
||||
ChainID: dChainID,
|
||||
CChainID: cChainID,
|
||||
NetworkID: 96369,
|
||||
Log: logger,
|
||||
SharedMemory: dSM,
|
||||
}
|
||||
|
||||
vm := &VM{}
|
||||
if err := vm.Initialize(context.Background(), block.Init{
|
||||
Runtime: rt,
|
||||
DB: prefixdb.New([]byte{1}, baseDB),
|
||||
Log: logger,
|
||||
ToEngine: make(chan block.Message, 16),
|
||||
}); err != nil {
|
||||
t.Fatalf("Initialize seam vm: %v", err)
|
||||
}
|
||||
return &seamHarness{vm: vm, dSM: dSM, cSM: cSM, cChainID: cChainID, dChainID: dChainID, netID: 96369}
|
||||
}
|
||||
|
||||
// addr returns the 20-byte C address of a test account (the cross-chain owner the
|
||||
// precompile would carry on a C->D object). crossChainAccount(addr) == acct.account,
|
||||
// so an import funds the exact account the taker's signed D orders draw from.
|
||||
func (h *seamHarness) addr(t *testing.T, name string) common.Address {
|
||||
t.Helper()
|
||||
a := acctFor(t, name)
|
||||
return common.BytesToAddress(acctAddrBytes(a))
|
||||
}
|
||||
|
||||
// writeCToDIntent reproduces the precompile Phase-A host flush: it writes a C->D
|
||||
// atomic intent object (rail=railSwap, owner, asset, amount, spent=0) into shared
|
||||
// memory keyed by intentID, under the D chain's partition — exactly what
|
||||
// cSM.Apply(map[dChainID]{Put}) does after SubmitSwapIntent stages it. The native VM's
|
||||
// TxImport then consumes it via dSM.Get(cChainID, intentID).
|
||||
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)
|
||||
err := h.cSM.Apply(map[ids.ID]*atomic.Requests{
|
||||
h.dChainID: {PutRequests: []*atomic.Element{{Key: intentID[:], Value: obj, Traits: [][]byte{owner[:]}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("write C->D intent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildAccept drains the mempool into one block and runs Build->Verify->Accept.
|
||||
func (h *seamHarness) buildAccept(t *testing.T) *Block {
|
||||
t.Helper()
|
||||
return buildVerifyAccept(t, h.vm)
|
||||
}
|
||||
|
||||
// avail reads an account's available balance for an asset from the committed ledger.
|
||||
func (h *seamHarness) avail(t *testing.T, acct userKey, asset [32]byte) uint64 {
|
||||
t.Helper()
|
||||
v, err := getAvailable(h.vm.db, acct, asset)
|
||||
if err != nil {
|
||||
t.Fatalf("getAvailable: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// TestSeam_TwoPhase_FullFill is the headline proof: a Phase-A intent funds a native D
|
||||
// account, a signed taker order crosses a resting maker under D consensus, the seam
|
||||
// exports the realized proceeds as a D->C object, and the modeled 0x9999 Phase B
|
||||
// consumes it exactly once — replay rejected, conservation holds.
|
||||
func TestSeam_TwoPhase_FullFill(t *testing.T) {
|
||||
h := newSeamHarness(t)
|
||||
|
||||
pool := [32]byte{0xDE, 0xAD, 0xBE, 0xEF, 0x01}
|
||||
base := assetID32(0xB0) // the asset the taker BUYS / the maker SELLS
|
||||
quote := assetID32(0x90) // the asset the taker LOCKS on C (tokenIn) / pays with
|
||||
|
||||
maker := acctFor(t, "seam-maker")
|
||||
taker := acctFor(t, "seam-taker")
|
||||
takerAddr := h.addr(t, "seam-taker")
|
||||
|
||||
// ---- Setup: open the custody market + seed a resting maker SELL ----
|
||||
// Maker deposits 100 base (native dchain deposit) and rests a SELL 100 base @ 2.
|
||||
h.vm.mempool.Add(openMarketTx(t, pool, base, quote))
|
||||
h.vm.mempool.Add(depositTx(t, "seam-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)
|
||||
if got := h.avail(t, maker.account, base); got != 0 {
|
||||
t.Fatalf("maker base available after resting SELL = %d, want 0 (locked)", got)
|
||||
}
|
||||
|
||||
// ---- PHASE A (C side): lock 200 quote -> C->D intent object ----
|
||||
// To buy 100 base @ 2 the taker locks 200 quote on C; the precompile writes the
|
||||
// C->D object owner=taker, asset=quote, amount=200, keyed by DeriveIntentID.
|
||||
const lockedQuote = 200
|
||||
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, lockedQuote, pool, 0)
|
||||
h.writeCToDIntent(t, intentID, takerAddr, quote, lockedQuote)
|
||||
|
||||
// ---- NATIVE D: import the intent (fund the taker's D account) ----
|
||||
h.vm.mempool.Add(importTx(t, intentID))
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, quote); got != lockedQuote {
|
||||
t.Fatalf("taker quote available after import = %d, want %d", got, lockedQuote)
|
||||
}
|
||||
// The escrow recorded the full 20-byte owner + the locked principal.
|
||||
rec, ok, err := getSeamIntent(h.vm.db, intentID)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("seam intent escrow missing after import (ok=%v err=%v)", ok, err)
|
||||
}
|
||||
if rec.Owner != takerAddr || rec.AssetIn != quote || rec.Remaining != lockedQuote {
|
||||
t.Fatalf("escrow = {owner %s assetIn %x remaining %d}, want {%s %x %d}",
|
||||
rec.Owner.Hex(), rec.AssetIn[:4], rec.Remaining, takerAddr.Hex(), quote[:4], lockedQuote)
|
||||
}
|
||||
// The C->D object was consumed (removed) at Accept: a second read is empty.
|
||||
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 (removed) after import accept")
|
||||
}
|
||||
|
||||
// ---- NATIVE D: the taker crosses the maker under D consensus ----
|
||||
// A signed limit BUY 100 base @ 2 locks 200 quote and fills against the maker.
|
||||
h.vm.mempool.Add(taker.signed(t, TxSubmit, zapwire.EncodeSubmit(pool, sideBuy, false, 2.0, 100, taker.user)))
|
||||
h.buildAccept(t)
|
||||
// Post-fill ledger: taker spent 200 quote, received 100 base; maker received 200 quote.
|
||||
if got := h.avail(t, taker.account, quote); got != 0 {
|
||||
t.Fatalf("taker quote available after fill = %d, want 0 (all spent)", got)
|
||||
}
|
||||
if got := h.avail(t, taker.account, base); got != 100 {
|
||||
t.Fatalf("taker base available after fill = %d, want 100 (proceeds)", got)
|
||||
}
|
||||
if got := h.avail(t, maker.account, quote); got != 200 {
|
||||
t.Fatalf("maker quote available after fill = %d, want 200", got)
|
||||
}
|
||||
|
||||
// ---- NATIVE D: export the realized proceeds as a D->C object ----
|
||||
// The taker received 100 base by spending 200 quote -> spent witness = 200.
|
||||
const proceeds = 100
|
||||
const spent = 200
|
||||
exportTx := exportTx(t, intentID, base, proceeds, spent)
|
||||
outputID := deriveSeamOutputID(exportTx.ID(), 0) // the key Phase B will claim
|
||||
h.vm.mempool.Add(exportTx)
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, base); got != 0 {
|
||||
t.Fatalf("taker base available after export = %d, want 0 (exported)", got)
|
||||
}
|
||||
|
||||
// ---- PHASE B (C side, modeled 0x9999 ImportSettlement) ----
|
||||
// Read the D->C object under the D chain (exactly sm.Get(dChainID, claim.OutputID)).
|
||||
vals, gerr := h.cSM.Get(h.dChainID, [][]byte{outputID[:]})
|
||||
if gerr != nil || len(vals) != 1 || len(vals[0]) == 0 {
|
||||
t.Fatalf("D->C settlement object not found for 0x9999 (err=%v)", gerr)
|
||||
}
|
||||
recRail, recOwner, recAsset, recAmount, recSpent, decOK := decodeSeamObject(vals[0])
|
||||
if !decOK {
|
||||
t.Fatal("D->C object malformed (0x9999 decodeAtomicObject would reject)")
|
||||
}
|
||||
// The exact binds ImportSettlement enforces (rail/owner/asset/amount + the MEV witness).
|
||||
if recRail != railSwap {
|
||||
t.Fatalf("D->C object rail = %d, want railSwap (0)", recRail)
|
||||
}
|
||||
if recOwner != takerAddr {
|
||||
t.Fatalf("D->C object owner = %s, want taker %s (recOwner==Recipient bind)", recOwner.Hex(), takerAddr.Hex())
|
||||
}
|
||||
if recAsset != base {
|
||||
t.Fatalf("D->C object asset mismatch (recAsset==claim.Asset bind)")
|
||||
}
|
||||
if recAmount != proceeds {
|
||||
t.Fatalf("D->C object amount = %d, want %d (recAmount==claim.Amount bind)", recAmount, proceeds)
|
||||
}
|
||||
if recSpent != spent {
|
||||
t.Fatalf("D->C object spent witness = %d, want %d (MEV-floor binding)", recSpent, spent)
|
||||
}
|
||||
|
||||
// CONSUME ONCE: the precompile applies a shared-memory Remove under the D chain.
|
||||
if err := h.cSM.Apply(map[ids.ID]*atomic.Requests{h.dChainID: {RemoveRequests: [][]byte{outputID[:]}}}); err != nil {
|
||||
t.Fatalf("0x9999 Phase-B consume (remove): %v", err)
|
||||
}
|
||||
// REPLAY REJECTED: a second read finds nothing -> ImportSettlement reverts
|
||||
// ErrNativeNoSettlement; the object can never be settled twice.
|
||||
if vals2, _ := h.cSM.Get(h.dChainID, [][]byte{outputID[:]}); len(vals2) == 1 && len(vals2[0]) != 0 {
|
||||
t.Fatal("D->C object must be consumed exactly once (replay must find it gone)")
|
||||
}
|
||||
|
||||
// CONSERVATION across the seam: 200 quote entered D (C->D), 100 base left D (D->C);
|
||||
// inside D the maker provided 100 base and now holds 200 quote. Nothing minted.
|
||||
if got := h.avail(t, maker.account, quote); got != 200 {
|
||||
t.Fatalf("maker quote (the taker's spent input, conserved on D) = %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)
|
||||
}
|
||||
// The recorded settlement (seamout:) bound the object to its intent for the keeper.
|
||||
t.Logf("PASS: native D fill -> D->C object %x (rail=%d owner=%s asset=%x amount=%d spent=%d) consumed once by 0x9999",
|
||||
outputID[:6], recRail, recOwner.Hex(), recAsset[:4], recAmount, recSpent)
|
||||
}
|
||||
|
||||
// TestSeam_Import_ReplayNoDoubleCredit proves a re-submitted TxImport for an
|
||||
// already-consumed intent never double-credits: the escrow row (and the seen: index)
|
||||
// make a second import a deterministic no-op, and the C->D object is already gone.
|
||||
func TestSeam_Import_ReplayNoDoubleCredit(t *testing.T) {
|
||||
h := newSeamHarness(t)
|
||||
pool := [32]byte{0x11, 0x22, 0x33}
|
||||
quote := assetID32(0x90)
|
||||
takerAddr := h.addr(t, "replay-taker")
|
||||
taker := acctFor(t, "replay-taker")
|
||||
|
||||
h.vm.mempool.Add(openMarketTx(t, pool, assetID32(0xB0), quote))
|
||||
h.buildAccept(t)
|
||||
|
||||
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, 200, pool, 0)
|
||||
h.writeCToDIntent(t, intentID, takerAddr, quote, 200)
|
||||
|
||||
h.vm.mempool.Add(importTx(t, intentID))
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, quote); got != 200 {
|
||||
t.Fatalf("after first import quote = %d, want 200", got)
|
||||
}
|
||||
|
||||
// Re-submit the SAME import. seen: returns the prior outcome (idempotent); the
|
||||
// escrow already exists and the C->D object is consumed — no second credit.
|
||||
h.vm.mempool.Add(importTx(t, intentID))
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, quote); got != 200 {
|
||||
t.Fatalf("after replay import quote = %d, want 200 (no double credit)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeam_OverExportRefund_Rejected proves the D-side per-taker cap: a same-asset
|
||||
// (refund) export that exceeds the intent's remaining locked principal is rejected,
|
||||
// so an over-refund can never draw on other takers' pooled input — the export analog
|
||||
// of the precompile's ErrSettleExceedsIntent.
|
||||
func TestSeam_OverExportRefund_Rejected(t *testing.T) {
|
||||
h := newSeamHarness(t)
|
||||
pool := [32]byte{0x44, 0x55, 0x66}
|
||||
base := assetID32(0xB0)
|
||||
quote := assetID32(0x90)
|
||||
takerAddr := h.addr(t, "cap-taker")
|
||||
taker := acctFor(t, "cap-taker")
|
||||
|
||||
h.vm.mempool.Add(openMarketTx(t, pool, base, quote))
|
||||
h.buildAccept(t)
|
||||
|
||||
// Import 200 quote; the taker does NOT trade, so all 200 stays available and the
|
||||
// intent's remaining principal is 200.
|
||||
const locked = 200
|
||||
intentID := DeriveIntentID(h.netID, h.cChainID, h.dChainID, takerAddr, quote, locked, pool, 0)
|
||||
h.writeCToDIntent(t, intentID, takerAddr, quote, locked)
|
||||
h.vm.mempool.Add(importTx(t, intentID))
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, quote); got != locked {
|
||||
t.Fatalf("imported quote = %d, want %d", got, locked)
|
||||
}
|
||||
|
||||
// Over-export: a REFUND (asset == intent.AssetIn == quote) of 201 > remaining 200.
|
||||
// Rejected by the per-taker cap BEFORE any debit — the ledger is untouched.
|
||||
over := exportTx(t, intentID, quote, locked+1, 0)
|
||||
h.vm.mempool.Add(over)
|
||||
blk := h.buildAccept(t)
|
||||
if st := outcomeStatus(blk, over.ID()); st != zapwire.StatusRejected {
|
||||
t.Fatalf("over-export status = %d, want StatusRejected (%d)", st, zapwire.StatusRejected)
|
||||
}
|
||||
if got := h.avail(t, taker.account, quote); got != locked {
|
||||
t.Fatalf("quote after rejected over-export = %d, want %d (untouched)", got, locked)
|
||||
}
|
||||
// No D->C object was produced for the rejected export.
|
||||
outID := deriveSeamOutputID(over.ID(), 0)
|
||||
if vals, _ := h.cSM.Get(h.dChainID, [][]byte{outID[:]}); len(vals) == 1 && len(vals[0]) != 0 {
|
||||
t.Fatal("a rejected over-export must produce NO D->C object")
|
||||
}
|
||||
|
||||
// A refund AT the cap (== remaining 200) succeeds and exports a valid object.
|
||||
ok := exportTx(t, intentID, quote, locked, 0)
|
||||
okID := deriveSeamOutputID(ok.ID(), 0)
|
||||
h.vm.mempool.Add(ok)
|
||||
h.buildAccept(t)
|
||||
if got := h.avail(t, taker.account, quote); got != 0 {
|
||||
t.Fatalf("quote after at-cap refund = %d, want 0", got)
|
||||
}
|
||||
vals, _ := h.cSM.Get(h.dChainID, [][]byte{okID[:]})
|
||||
if len(vals) != 1 || len(vals[0]) == 0 {
|
||||
t.Fatal("at-cap refund must produce a D->C object")
|
||||
}
|
||||
_, recOwner, recAsset, recAmount, _, decOK := decodeSeamObject(vals[0])
|
||||
if !decOK || recOwner != takerAddr || recAsset != quote || recAmount != locked {
|
||||
t.Fatalf("at-cap refund object = {owner %s asset %x amount %d}, want {%s %x %d}",
|
||||
recOwner.Hex(), recAsset[:4], recAmount, takerAddr.Hex(), quote[:4], locked)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeamWire_GoldenMatchesPrecompile pins encodeSeamObject against the SAME
|
||||
// cross-repo golden the precompile (precompile/dex native_seam_parity_test.go) and the
|
||||
// proxy (chains/dexvm) pin THEIR encoders to: rail(1)|owner(20)|asset(32)|amount(8)|
|
||||
// spent(8) = 69 bytes, big-endian. The precompile's parity test asserts its
|
||||
// encodeAtomicObjectSpent == this golden; this test asserts the native dexvm
|
||||
// encodeSeamObject == the SAME golden. By transitivity the three encoders are
|
||||
// byte-identical — the ONE-wire reconciliation, proven without a module cycle (dex
|
||||
// cannot import precompile). Any one-sided change to the width/offsets breaks exactly
|
||||
// one of the three pins.
|
||||
func TestSeamWire_GoldenMatchesPrecompile(t *testing.T) {
|
||||
// The identical inputs the precompile parity golden uses.
|
||||
owner := common.HexToAddress("0x112233445566778899aabbccddeeff0102030405")
|
||||
var asset [32]byte
|
||||
for i := range asset {
|
||||
asset[i] = byte(0xA0 + i)
|
||||
}
|
||||
const amount, spent = uint64(0x0102030405060708), uint64(0x1112131415161718)
|
||||
|
||||
const goldenHex = "00" + // rail = railSwap
|
||||
"112233445566778899aabbccddeeff0102030405" + // owner (20)
|
||||
"a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + // asset (32)
|
||||
"0102030405060708" + // amount (8)
|
||||
"1112131415161718" // spent (8)
|
||||
|
||||
got := encodeSeamObject(railSwap, owner, asset, amount, spent)
|
||||
if len(got) != seamObjectSize || seamObjectSize != 69 {
|
||||
t.Fatalf("seam object width %d (const %d) != canonical 69 — the wire changed on ONE side", len(got), seamObjectSize)
|
||||
}
|
||||
if gotHex := hexEncode(got); gotHex != goldenHex {
|
||||
t.Fatalf("native seam wire DIVERGED from the precompile/proxy golden:\n got=%s\nwant=%s", gotHex, goldenHex)
|
||||
}
|
||||
// Round-trip the trailing spent witness (the field the precompile MEV floor reads).
|
||||
rail, gotOwner, gotAsset, gotAmount, gotSpent, ok := decodeSeamObject(got)
|
||||
if !ok || rail != railSwap || gotOwner != owner || gotAsset != asset || gotAmount != amount || gotSpent != spent {
|
||||
t.Fatalf("round-trip mismatch: ok=%v rail=%d amount=%d spent=%d", ok, rail, gotAmount, gotSpent)
|
||||
}
|
||||
}
|
||||
|
||||
func hexEncode(b []byte) string {
|
||||
const hexdigits = "0123456789abcdef"
|
||||
out := make([]byte, len(b)*2)
|
||||
for i, c := range b {
|
||||
out[i*2] = hexdigits[c>>4]
|
||||
out[i*2+1] = hexdigits[c&0x0f]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// --- small builders -------------------------------------------------------------
|
||||
|
||||
func assetID32(b byte) [32]byte {
|
||||
var a [32]byte
|
||||
a[0] = b
|
||||
a[31] = 0x01 // non-native (native is all-zero)
|
||||
return a
|
||||
}
|
||||
|
||||
func importTx(t *testing.T, intentID ids.ID) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxImport, EncodeSeamImportBody(intentID))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx import: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
func exportTx(t *testing.T, intentID ids.ID, asset [32]byte, amount, spent uint64) *Tx {
|
||||
t.Helper()
|
||||
tx, err := NewTx(TxExport, EncodeSeamExportBody(intentID, asset, amount, spent))
|
||||
if err != nil {
|
||||
t.Fatalf("NewTx export: %v", err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
// outcomeStatus returns the recorded zapwire status for a tx id in an accepted block.
|
||||
func outcomeStatus(blk *Block, txID ids.ID) uint8 {
|
||||
for _, o := range blk.outcomes {
|
||||
if o.txID == txID {
|
||||
return o.status
|
||||
}
|
||||
}
|
||||
return 0xFF
|
||||
}
|
||||
|
||||
// acctAddrBytes returns the 20-byte address bytes of a test account's secp key — the
|
||||
// SAME derivation acctFor uses, so crossChainAccount(addr)==acct.account and an import
|
||||
// funds the exact account the taker's signed D orders draw from.
|
||||
func acctAddrBytes(a *testAcct) []byte {
|
||||
return crypto.PubkeyToAddress(a.priv.PublicKey).Bytes()
|
||||
}
|
||||
+71
-4
@@ -77,6 +77,13 @@ type Block struct {
|
||||
// Verify or Build). Accept resolves any parked ZAP handler from these. nil
|
||||
// until execute runs against this block.
|
||||
outcomes []txOutcome
|
||||
|
||||
// atomic is the block's accumulated cross-chain shared-memory operations (the
|
||||
// native settlement seam's C->D removes / D->C puts), derived by execute and
|
||||
// applied atomically with the state overlay at Accept (atomic.go commitSeamAtomic).
|
||||
// nil/empty for a block with no seam txs. Stashed on the block alongside the
|
||||
// overlay so the same instance a later Accept resolves carries both.
|
||||
atomic *atomicRequests
|
||||
}
|
||||
|
||||
// newBlock assembles a block from its fields and computes its bytes + id. The
|
||||
@@ -185,6 +192,7 @@ func (b *Block) Verify(ctx context.Context) error {
|
||||
|
||||
b.overlay = overlay
|
||||
b.outcomes = result.outcomes
|
||||
b.atomic = result.atomic
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,6 +213,7 @@ func (b *Block) Accept(ctx context.Context) error {
|
||||
}
|
||||
b.overlay = overlay
|
||||
b.outcomes = res.outcomes
|
||||
b.atomic = res.atomic
|
||||
}
|
||||
|
||||
// Persist the accept watermarks into the SAME overlay so the commit is one
|
||||
@@ -230,7 +239,12 @@ func (b *Block) Accept(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.overlay.Commit(); err != nil {
|
||||
// Commit the overlay AND the block's cross-chain seam operations (C->D removes /
|
||||
// D->C puts) in one atomic write (atomic.go). With no seam txs this is the plain
|
||||
// overlay commit; with seam txs the shared-memory ops land atomically with the
|
||||
// state so a consumed intent / produced settlement can never diverge from the
|
||||
// committed ledger.
|
||||
if err := b.vm.commitSeamAtomic(b.overlay, b.atomic); err != nil {
|
||||
return fmt.Errorf("dchain: commit block %s: %w", b.id, err)
|
||||
}
|
||||
|
||||
@@ -283,8 +297,9 @@ func (b *Block) Reject(ctx context.Context) error {
|
||||
type execResult struct {
|
||||
root [Size]byte
|
||||
fills []lx.DEXTrade
|
||||
rows []lx.DEXOrder // canonical resting rows across all touched markets
|
||||
outcomes []txOutcome // one per tx, in block order
|
||||
rows []lx.DEXOrder // canonical resting rows across all touched markets
|
||||
outcomes []txOutcome // one per tx, in block order
|
||||
atomic *atomicRequests // cross-chain seam removes/puts to apply at Accept
|
||||
}
|
||||
|
||||
// execute runs the block's txs against the overlay, writing order/market/trade
|
||||
@@ -321,6 +336,10 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
|
||||
|
||||
var allFills []lx.DEXTrade
|
||||
var tradeSeq uint64
|
||||
// ar accumulates the block's cross-chain seam operations (C->D removes / D->C
|
||||
// puts); it is applied atomically with this overlay at Accept. Empty for a block
|
||||
// with no TxImport/TxExport.
|
||||
ar := newAtomicRequests()
|
||||
outcomes := make([]txOutcome, 0, len(b.txs))
|
||||
for i, tx := range b.txs {
|
||||
// Idempotency (all tx types): a tx whose id is already committed (or
|
||||
@@ -421,6 +440,54 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
|
||||
return execResult{}, err
|
||||
}
|
||||
continue
|
||||
|
||||
case TxImport:
|
||||
// CONSUME a C->D atomic intent object and fund the taker's native D
|
||||
// account (atomic.go). Authority is the consumed object, not a signature
|
||||
// (TxImport.requiresAuth() is false), so the auth gate above skipped it.
|
||||
var intentID ids.ID
|
||||
copy(intentID[:], tx.Body) // the body is exactly the 32-byte intent id
|
||||
credited, ok, ierr := b.vm.executeImport(overlay, ar, intentID)
|
||||
if ierr != nil {
|
||||
return execResult{}, fmt.Errorf("dchain: tx %d seam import: %w", i, ierr)
|
||||
}
|
||||
if !ok {
|
||||
// Deterministic per-tx reject (object not flushed yet / replay / unwired
|
||||
// seam). NOT marked seen, so a not-yet-available intent retries later.
|
||||
outcomes = append(outcomes, txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusRejected})
|
||||
continue
|
||||
}
|
||||
oc := txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusPlaced, orderID: credited}
|
||||
outcomes = append(outcomes, oc)
|
||||
if err := markSeen(overlay, txID, oc); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
continue
|
||||
|
||||
case TxExport:
|
||||
// DEBIT the taker's settled native D balance and write a D->C settlement
|
||||
// object the precompile's ImportSettlement consumes (atomic.go). The
|
||||
// exporting tx id seeds the deterministic output id; object-authorized.
|
||||
intentID, asset, amount, spent, derr := decodeSeamExportBody(tx.Body)
|
||||
if derr != nil {
|
||||
return execResult{}, fmt.Errorf("dchain: tx %d seam export decode: %w", i, derr)
|
||||
}
|
||||
outputID, ok, eerr := b.vm.executeExport(overlay, ar, txID, intentID, asset, amount, spent)
|
||||
if eerr != nil {
|
||||
return execResult{}, fmt.Errorf("dchain: tx %d seam export: %w", i, eerr)
|
||||
}
|
||||
if !ok {
|
||||
outcomes = append(outcomes, txOutcome{txID: txID, typ: tx.Type, status: zapwire.StatusRejected})
|
||||
continue
|
||||
}
|
||||
// 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])}
|
||||
outcomes = append(outcomes, oc)
|
||||
if err := markSeen(overlay, txID, oc); err != nil {
|
||||
return execResult{}, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// ---- Market-scoped txs (begin with poolId) ----
|
||||
@@ -576,7 +643,7 @@ func (b *Block) execute(ctx context.Context, overlay *versiondb.Database) (execR
|
||||
sortRowsCanonical(allRows)
|
||||
|
||||
root, _, _, _ := ExecutionRoot(b.vm.lastRoot, allRows, allFills, b.txs, b.height)
|
||||
return execResult{root: root, fills: allFills, rows: allRows, outcomes: outcomes}, nil
|
||||
return execResult{root: root, fills: allFills, rows: allRows, outcomes: outcomes, atomic: ar}, nil
|
||||
}
|
||||
|
||||
// lockOrderSpend reserves the asset a place/submit will spend, moving it from the
|
||||
|
||||
@@ -56,6 +56,19 @@ const (
|
||||
// value-checked. Body = zapwire OpenMarket payload (96B): poolId[32] +
|
||||
// baseAsset[32] + quoteAsset[32].
|
||||
TxOpenMarket
|
||||
// TxImport CONSUMES a C->D atomic intent object and funds the taker's native D
|
||||
// account with the recorded asset (atomic.go executeImport). Body = intentID[32]:
|
||||
// the shared-memory key the 0x9999 precompile PUT the C->D object under. It is NOT
|
||||
// signature-authorized — its authority is the consumed cross-chain object (the
|
||||
// taker already authenticated + locked on C), exactly as the proxy's ImportTx is
|
||||
// authorized by the UTXO it consumes, not a per-account signature.
|
||||
TxImport
|
||||
// TxExport DEBITS the taker's settled native D balance and writes a D->C atomic
|
||||
// settlement object the precompile's ImportSettlement consumes (atomic.go
|
||||
// executeExport). Body = intentID[32] | asset[32] | amount[8] | spent[8]. Also
|
||||
// object-authorized (the D->C object it produces is owned by the intent's recorded
|
||||
// owner, so it can only settle value back to the rightful taker), so no signature.
|
||||
TxExport
|
||||
)
|
||||
|
||||
// requiresAuth reports whether a tx type moves USER funds and therefore must
|
||||
@@ -87,11 +100,23 @@ func (t TxType) String() string {
|
||||
return "withdraw"
|
||||
case TxOpenMarket:
|
||||
return "open_market"
|
||||
case TxImport:
|
||||
return "seam_import"
|
||||
case TxExport:
|
||||
return "seam_export"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// seamImportBodySize is the TxImport body width: intentID[32].
|
||||
// seamExportBodySize is the TxExport body width: intentID[32] | asset[32] |
|
||||
// amount[8] | spent[8].
|
||||
const (
|
||||
seamImportBodySize = 32
|
||||
seamExportBodySize = 32 + 32 + 8 + 8
|
||||
)
|
||||
|
||||
// Errors returned by the tx layer.
|
||||
var (
|
||||
ErrEmptyTx = errors.New("dchain: empty transaction")
|
||||
@@ -243,6 +268,10 @@ func bodySize(t TxType) (int, bool) {
|
||||
return zapwire.WithdrawReqSize, true
|
||||
case TxOpenMarket:
|
||||
return zapwire.OpenMarketReqSize, true
|
||||
case TxImport:
|
||||
return seamImportBodySize, true
|
||||
case TxExport:
|
||||
return seamExportBodySize, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/version"
|
||||
)
|
||||
|
||||
@@ -49,6 +50,17 @@ type VM struct {
|
||||
log log.Logger
|
||||
db database.Database
|
||||
|
||||
// runtime is the chain-scoped wiring the consensus engine hands the VM at
|
||||
// Initialize (block.Init.Runtime). It is the source of the cross-chain atomic
|
||||
// shared-memory handle (GetSharedMemory) and the C-Chain id the native settlement
|
||||
// seam (atomic.go) imports from / exports to. nil when no runtime was wired
|
||||
// (single-chain unit tests with no seam) — the seam then refuses (no mint).
|
||||
runtime *runtime.Runtime
|
||||
// cChainID is the C-Chain id resolved from the runtime: the partition a C->D
|
||||
// intent object is read under (import) and a D->C settlement object is written
|
||||
// under (export). ids.Empty when no runtime / no C-Chain — the seam stays closed.
|
||||
cChainID ids.ID
|
||||
|
||||
mempool *mempool
|
||||
|
||||
// outcomes lets a ZAP handler block until its tx is decided by an accepted
|
||||
@@ -107,6 +119,13 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
|
||||
}
|
||||
vm.db = init.DB
|
||||
vm.toEngine = init.ToEngine
|
||||
// Capture the cross-chain wiring for the native settlement seam (atomic.go). The
|
||||
// runtime carries the shared-memory handle + the C-Chain id; both are absent in a
|
||||
// single-chain unit test, where the seam refuses rather than mint.
|
||||
vm.runtime = init.Runtime
|
||||
if init.Runtime != nil {
|
||||
vm.cChainID = init.Runtime.CChainID
|
||||
}
|
||||
vm.mempool = newMempool(init.ToEngine)
|
||||
vm.outcomes = newOutcomeRegistry()
|
||||
vm.books = map[[32]byte]*lx.OrderBook{}
|
||||
|
||||
Reference in New Issue
Block a user