mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
290 lines
13 KiB
Go
290 lines
13 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// state.go — the DURABLE C-side state of the inference bridge: the C->A OUTBOX
|
|
// (pending intents), the consumed-intent set (Pattern-B double-consume guard), and
|
|
// the committed RECEIPT-ROOT checkpoint set (the A->C atomic-import seam's landing
|
|
// slot). Everything lives under ONE namespace (aivmbridge.v1.*) so the bridge's
|
|
// state is one auditable region, and everything is content-addressed by the
|
|
// deterministic intent_id / receipt_root so a re-executed / reorged block replays
|
|
// identically on every validator (consensus-shared, durable, reverted atomically
|
|
// with the tx via the EVM snapshot).
|
|
//
|
|
// THE CONSENSUS-SAFETY MODEL (why this is the whole point):
|
|
// - The OUTBOX is C state. A's importer (Blue-B) reads the COMMITTED outbox under A
|
|
// consensus; C never calls A. (Pattern A writes here and nowhere into A.)
|
|
// - The RECEIPT-ROOT checkpoint is C state, populated by the A->C atomic boundary /
|
|
// warp at block accept (the seam). Pattern B trusts a receipt root ONLY if it is
|
|
// recorded here. The verify reads C-committed state + the proof — never A.
|
|
//
|
|
// SEAM BOUNDARY (honest, for RED): in production the receipt-root checkpoint slot is
|
|
// written by the A->C atomic import / warp handler (the host's block-accept path,
|
|
// the same shape as dex's FlushAcceptedAtomicOps but in the A->C direction). This
|
|
// package ships the C-side store + verification; the test harness and the staged
|
|
// native client write the checkpoint directly to exercise the verify path. The
|
|
// authenticity of a committed root therefore rests on that import handler — clearly
|
|
// marked, never papered over: a root C has not recorded is untrusted and inert.
|
|
|
|
package aivmbridge
|
|
|
|
import (
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
// BridgeAddress is the storage account under which ALL bridge state lives. It equals
|
|
// the precompile address (the bridge's own slot in the AI range) so the state and the
|
|
// contract share one identity. Defined in module.go (ContractAddress); aliased here
|
|
// for the state helpers.
|
|
var bridgeStateAddr = ContractAddress
|
|
|
|
// stateNamespace is the durable storage namespace prefix. Every key the bridge writes
|
|
// derives from this so the bridge's state is a single, auditable region.
|
|
const stateNamespace = "aivmbridge.v1."
|
|
|
|
// Storage-key prefixes (one per orthogonal pot — no aliasing across concerns).
|
|
var (
|
|
outboxPrefix = []byte(stateNamespace + "outbox") // intent_id -> packed OutboxIntent
|
|
intentConsumedPref = []byte(stateNamespace + "consumed") // intent_id -> blockNumber+1 sentinel
|
|
receiptRootPrefix = []byte(stateNamespace + "rcptroot") // receipt_root -> committed-height+1 sentinel
|
|
)
|
|
|
|
// makeStorageKey derives a storage slot key from a prefix and an id. blake3 (the same
|
|
// KDF dex uses for makeStorageKey) — collision-resistant key derivation so distinct
|
|
// (prefix,id) pairs never share a slot.
|
|
func makeStorageKey(prefix []byte, id []byte) common.Hash {
|
|
h := blake3.New()
|
|
_, _ = h.Write(prefix)
|
|
_, _ = h.Write(id)
|
|
var key common.Hash
|
|
_, _ = h.Digest().Read(key[:])
|
|
return key
|
|
}
|
|
|
|
// StateKV is the MINIMAL key-value state surface the bridge needs — just
|
|
// GetState/SetState. Decomplecting it from the full contract.StateDB (which braids in
|
|
// balances) lets the bridge's pure-metadata helpers be tested with a tiny fake and
|
|
// keeps the bridge honest: it touches NO balances. contract.StateDB satisfies this
|
|
// (its SetState returns the prior value; this narrower form ignores the return).
|
|
type StateKV interface {
|
|
GetState(addr common.Address, key common.Hash) common.Hash
|
|
SetState(addr common.Address, key common.Hash, value common.Hash) common.Hash
|
|
}
|
|
|
|
// OutboxStatus is the C-side lifecycle of a recorded intent. It is ORTHOGONAL to the
|
|
// A-Chain ReceiptStatus: this tracks the C outbox slot, not the A job.
|
|
type OutboxStatus uint8
|
|
|
|
const (
|
|
// OutboxNone — no intent recorded at this id (the zero value / absent slot).
|
|
OutboxNone OutboxStatus = 0
|
|
// OutboxPending — the intent was recorded and is awaiting A import + settlement.
|
|
OutboxPending OutboxStatus = 1
|
|
// OutboxConsumed — a verified A receipt settled this intent; it can never settle
|
|
// again (double-consume guard).
|
|
OutboxConsumed OutboxStatus = 2
|
|
)
|
|
|
|
// OutboxIntent is the COMMITTED C-side record of a submitted intent. It binds the
|
|
// fields a Pattern-B receipt must match (model/prompt/requester/chain/fan-out) so a
|
|
// receipt cannot settle an intent it does not correspond to. Stored across fixed-width
|
|
// slots keyed by intent_id.
|
|
type OutboxIntent struct {
|
|
Status OutboxStatus
|
|
Caller common.Address
|
|
CChainID [32]byte
|
|
AChainID [32]byte
|
|
ModelSpecHash [32]byte
|
|
PromptHash [32]byte
|
|
N uint16
|
|
Threshold uint16
|
|
BlockNumber uint64
|
|
}
|
|
|
|
// IntentStore is the write surface Pattern A uses: record a pending intent (replay-
|
|
// guarded). It is an interface so the client depends on a capability, not a concrete
|
|
// StateDB — the same orthogonality dex applies. The concrete implementation is
|
|
// stateKVStore (over contract.StateDB); tests use a map-backed fake.
|
|
type IntentStore interface {
|
|
// IntentStatus returns the recorded outbox status for intent_id (OutboxNone if
|
|
// absent).
|
|
IntentStatus(intentID [32]byte) OutboxStatus
|
|
// PutPendingIntent records a new pending intent. It is only ever called after the
|
|
// caller has checked IntentStatus == OutboxNone (replay guard, CEI before write).
|
|
// The store stamps OutboxIntent.BlockNumber from its own captured height (it is the
|
|
// authority on "now"); any BlockNumber on the passed-in value is ignored.
|
|
PutPendingIntent(intentID [32]byte, oi OutboxIntent)
|
|
}
|
|
|
|
// ReceiptVerifierState is the read/update surface Pattern B uses: read a committed
|
|
// receipt root, read the bound pending intent, and mark it consumed. It is the read
|
|
// counterpart to IntentStore (same backing store; split by concern so verify cannot
|
|
// accidentally create an intent).
|
|
type ReceiptVerifierState interface {
|
|
// ReceiptRootCommitted reports whether receiptRoot is recorded as committed in C
|
|
// state (the A->C atomic-import seam wrote it). A root not recorded here is
|
|
// untrusted and the verify is inert (fail-secure).
|
|
ReceiptRootCommitted(receiptRoot [32]byte) bool
|
|
// LoadIntent returns the committed OutboxIntent for intent_id (Status==OutboxNone
|
|
// if absent).
|
|
LoadIntent(intentID [32]byte) OutboxIntent
|
|
// MarkIntentConsumed flips a pending intent to consumed (idempotent at the storage
|
|
// level; the caller guards against re-consume by checking Status first).
|
|
MarkIntentConsumed(intentID [32]byte, blockNumber uint64)
|
|
}
|
|
|
|
// --- the concrete store over a StateKV ----------------------------------------
|
|
|
|
// stateKVStore adapts a StateKV (i.e. contract.StateDB) to IntentStore +
|
|
// ReceiptVerifierState. The blockNumber is captured at construction (the current
|
|
// block) so writes stamp a consistent height.
|
|
type stateKVStore struct {
|
|
kv StateKV
|
|
blockNumber uint64
|
|
}
|
|
|
|
var (
|
|
_ IntentStore = (*stateKVStore)(nil)
|
|
_ ReceiptVerifierState = (*stateKVStore)(nil)
|
|
)
|
|
|
|
func newStateKVStore(kv StateKV, blockNumber uint64) *stateKVStore {
|
|
return &stateKVStore{kv: kv, blockNumber: blockNumber}
|
|
}
|
|
|
|
// --- outbox slot layout (fixed-width, content-addressed by intent_id) ---------
|
|
//
|
|
// The OutboxIntent spans consecutive blake3-derived slots. Each field gets its own
|
|
// derived slot (sub-keyed by a field tag) so a layout change is explicit and a field
|
|
// read never depends on packing offsets. Slots:
|
|
//
|
|
// slot("st") word[31] = status (1) ; word[0:8] high = blockNumber (redundant w/ "bn", kept in "bn")
|
|
// slot("cl") word[12:32] = caller (20, right-aligned like an address word)
|
|
// slot("cc") word = CChainID (32)
|
|
// slot("ac") word = AChainID (32)
|
|
// slot("ms") word = ModelSpecHash (32)
|
|
// slot("pr") word = PromptHash (32)
|
|
// slot("nt") word[28:30]=N, word[30:32]=Threshold (u16be each)
|
|
// slot("bn") word[24:32] = blockNumber (u64be)
|
|
//
|
|
// Reading status alone (the hot path: replay/consume checks) touches ONE slot.
|
|
|
|
func outboxFieldKey(intentID [32]byte, tag string) common.Hash {
|
|
id := make([]byte, 0, 32+len(tag))
|
|
id = append(id, intentID[:]...)
|
|
id = append(id, tag...)
|
|
return makeStorageKey(outboxPrefix, id)
|
|
}
|
|
|
|
func (s *stateKVStore) IntentStatus(intentID [32]byte) OutboxStatus {
|
|
w := s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "st"))
|
|
return OutboxStatus(w[31])
|
|
}
|
|
|
|
func (s *stateKVStore) PutPendingIntent(intentID [32]byte, oi OutboxIntent) {
|
|
// status
|
|
var st common.Hash
|
|
st[31] = byte(OutboxPending)
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "st"), st)
|
|
// caller (right-aligned 20 bytes, address-word convention)
|
|
var cl common.Hash
|
|
copy(cl[12:32], oi.Caller.Bytes())
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "cl"), cl)
|
|
// chain ids + commitments (full words)
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "cc"), common.BytesToHash(oi.CChainID[:]))
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "ac"), common.BytesToHash(oi.AChainID[:]))
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "ms"), common.BytesToHash(oi.ModelSpecHash[:]))
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "pr"), common.BytesToHash(oi.PromptHash[:]))
|
|
// N || Threshold (u16be each in the low 4 bytes)
|
|
var nt common.Hash
|
|
nt[28] = byte(oi.N >> 8)
|
|
nt[29] = byte(oi.N)
|
|
nt[30] = byte(oi.Threshold >> 8)
|
|
nt[31] = byte(oi.Threshold)
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "nt"), nt)
|
|
// blockNumber (u64be in the low 8 bytes) — stamped from the store's captured
|
|
// height, the authority on "now" (the passed-in oi.BlockNumber is ignored).
|
|
var bn common.Hash
|
|
putU64(bn[24:32], s.blockNumber)
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "bn"), bn)
|
|
}
|
|
|
|
func (s *stateKVStore) LoadIntent(intentID [32]byte) OutboxIntent {
|
|
st := s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "st"))
|
|
status := OutboxStatus(st[31])
|
|
if status == OutboxNone {
|
|
return OutboxIntent{Status: OutboxNone}
|
|
}
|
|
var oi OutboxIntent
|
|
oi.Status = status
|
|
oi.Caller = common.BytesToAddress(s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "cl")).Bytes())
|
|
copy(oi.CChainID[:], s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "cc")).Bytes())
|
|
copy(oi.AChainID[:], s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "ac")).Bytes())
|
|
copy(oi.ModelSpecHash[:], s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "ms")).Bytes())
|
|
copy(oi.PromptHash[:], s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "pr")).Bytes())
|
|
nt := s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "nt"))
|
|
oi.N = uint16(nt[28])<<8 | uint16(nt[29])
|
|
oi.Threshold = uint16(nt[30])<<8 | uint16(nt[31])
|
|
bn := s.kv.GetState(bridgeStateAddr, outboxFieldKey(intentID, "bn"))
|
|
oi.BlockNumber = bytesToU64(bn[24:32])
|
|
return oi
|
|
}
|
|
|
|
func (s *stateKVStore) MarkIntentConsumed(intentID [32]byte, blockNumber uint64) {
|
|
// Flip the outbox status to Consumed (the authoritative double-consume guard the
|
|
// hot path reads via IntentStatus).
|
|
var st common.Hash
|
|
st[31] = byte(OutboxConsumed)
|
|
s.kv.SetState(bridgeStateAddr, outboxFieldKey(intentID, "st"), st)
|
|
// Also stamp a consumed-set sentinel keyed by intent_id (audit + redundant guard).
|
|
var v common.Hash
|
|
putU64(v[24:32], blockNumber+1)
|
|
s.kv.SetState(bridgeStateAddr, makeStorageKey(intentConsumedPref, intentID[:]), v)
|
|
}
|
|
|
|
// --- committed receipt-root checkpoint ----------------------------------------
|
|
|
|
func receiptRootKey(receiptRoot [32]byte) common.Hash {
|
|
return makeStorageKey(receiptRootPrefix, receiptRoot[:])
|
|
}
|
|
|
|
func (s *stateKVStore) ReceiptRootCommitted(receiptRoot [32]byte) bool {
|
|
// An all-zero root is never a valid committed root (it would let a forged proof
|
|
// claiming root==0 pass; the empty slot reads as zero).
|
|
if isZero32(receiptRoot) {
|
|
return false
|
|
}
|
|
return s.kv.GetState(bridgeStateAddr, receiptRootKey(receiptRoot)) != (common.Hash{})
|
|
}
|
|
|
|
// CommitReceiptRoot records receiptRoot as committed in C state at committedHeight.
|
|
// THIS IS THE A->C SEAM LANDING POINT: in production the host's A->C atomic-import /
|
|
// warp handler calls it at block accept; the test harness and the staged native
|
|
// client call it to exercise the verify path. It is exported so the host import
|
|
// handler (and tests) can populate the trusted set. A zero root is rejected.
|
|
func CommitReceiptRoot(kv StateKV, receiptRoot [32]byte, committedHeight uint64) {
|
|
if isZero32(receiptRoot) {
|
|
return
|
|
}
|
|
var v common.Hash
|
|
putU64(v[24:32], committedHeight+1)
|
|
kv.SetState(bridgeStateAddr, receiptRootKey(receiptRoot), v)
|
|
}
|
|
|
|
// --- small fixed-width helpers ------------------------------------------------
|
|
|
|
func putU64(b []byte, v uint64) {
|
|
for i := 7; i >= 0; i-- {
|
|
b[i] = byte(v)
|
|
v >>= 8
|
|
}
|
|
}
|
|
|
|
func bytesToU64(b []byte) uint64 {
|
|
var v uint64
|
|
for _, x := range b {
|
|
v = v<<8 | uint64(x)
|
|
}
|
|
return v
|
|
}
|