mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
201 lines
10 KiB
Go
201 lines
10 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// achain_client.go — the precompile's handle to the node-LOCAL A-Chain (aivm)
|
|
// ON-RAMP. It is the EXACT shape of dex/dchain_client.go (the C->D on-ramp),
|
|
// adapted to AI inference and PROOF/RECEIPT-oriented (never a live-query surface).
|
|
//
|
|
// WHY A SEPARATE PACKAGE. The 0x0303 inference precompile runs a deterministic int8
|
|
// transformer entirely in pure Go on the consensus path — every validator computes
|
|
// byte-identical tokens with no GPU. That is the CANONICAL in-consensus brain for
|
|
// SMALL models (zen-nano), and it is left BYTE-IDENTICALLY UNCHANGED (it lives in
|
|
// its own package; this bridge never touches it). This bridge is its COMPLEMENT: an
|
|
// on-ramp so an EVM contract can request inference from a LARGE model / off-chain
|
|
// operator that CANNOT run inside C-Chain block execution (too big, non-deterministic
|
|
// across hardware, GPU-bound). That work lives on the A-Chain (aivm), validated by
|
|
// its own consensus + provider quorum; the C side only SUBMITS an inference job
|
|
// intent (Pattern A) or VERIFIES a committed A receipt (Pattern B).
|
|
//
|
|
// THE CONSENSUS-SAFETY RULE (the #1 law, copied in spirit from dex/dchain_client.go):
|
|
// an implementation MUST NOT make C-Chain consensus state depend on observing live
|
|
// A-Chain process state, and MUST NOT mutate live A-Chain state from C Run(). The
|
|
// C-Chain state root must be derivable WITHOUT observing any live A process. A
|
|
// SYNCHRONOUS in-block call to A's live engine FORKS: two validators executing the
|
|
// same C block at different instants observe different A output / latency / queue =>
|
|
// divergent StateRoot. Therefore the ONLY two safe shapes are:
|
|
//
|
|
// - Pattern A — SubmitInferenceIntent: a C tx WRITES a COMMITTED C-side outbox
|
|
// intent (a storage record keyed by a deterministic intent_id) and returns the
|
|
// id. The id is a PURE function of the calldata + C tx identity — NOT of any A
|
|
// response. The intent is later IMPORTED by A's own consensus (Blue-B's importer
|
|
// reads the committed C outbox / the staged atomic object). This NEVER calls into
|
|
// or mutates aivm.
|
|
//
|
|
// - Pattern B — VerifyInferenceReceipt: a C tx carries an A receipt + a merkle
|
|
// inclusion proof as CALLDATA. The C side deterministically verifies the receipt
|
|
// hash is included under a receipt_root that C ALREADY HOLDS COMMITTED in its own
|
|
// state (a checkpoint slot the A->C atomic boundary / warp populates), matches a
|
|
// pending C outbox intent, and records the canonical output. The verification
|
|
// depends ONLY on C-committed state + the proof — NEVER on a live A query.
|
|
//
|
|
// NOTE on method naming (deliberate): there is NO GetInferenceReceipt(id) method.
|
|
// Such a name invites a live mutable A query (exactly the fork-unsafe shape a prior
|
|
// build shipped). Verification takes the receipt + proof as calldata, so the C side
|
|
// never reaches into A to fetch anything.
|
|
//
|
|
// The precompile is a LEAF library: it must NOT reach up into the node chain manager.
|
|
// A node that does not wire its local A on-ramp leaves the default achainUnavailable
|
|
// client in place and every bridge selector reverts cleanly.
|
|
|
|
package aivmbridge
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// AChainClient is the precompile's handle to the node-LOCAL A-Chain (aivm) on-ramp.
|
|
//
|
|
// THE MODEL (decomplected — one concern, one name): A is a normal Lux chain running
|
|
// aivm, validated by its own consensus + provider quorum (the staking/challenge layer
|
|
// lives in aivm, NOT here). This bridge is ONLY an ON-RAMP: it RECORDS inference
|
|
// intents into committed C state for A to import, and VERIFIES committed A receipts.
|
|
// It is NOT an inference engine and NOT a provider selector.
|
|
//
|
|
// Both methods are consensus-safe by construction: neither observes live A state.
|
|
// SubmitInferenceIntent writes only C state + a deterministic id; VerifyInferenceReceipt
|
|
// reads only C-committed state + the caller-supplied proof.
|
|
type AChainClient interface {
|
|
// Brand returns the human-readable identity of the client (white-label). The
|
|
// precompile uses it in boot logs / error wrapping. Implementations MUST return
|
|
// a non-empty constant; an empty value trips a sanity check at InstallAChainClient.
|
|
Brand() string
|
|
|
|
// AChainID returns the A-Chain (aivm) peer id this on-ramp targets. It is bound
|
|
// into the deterministic intent_id so an intent is scoped to exactly one C<->A
|
|
// rail. It is COMMITTED on-ramp configuration (a constant for the life of the
|
|
// process), NOT a live A observation.
|
|
AChainID() [32]byte
|
|
|
|
// SubmitInferenceIntent is Pattern A: deterministic intent submission. It WRITES a
|
|
// COMMITTED C-side outbox intent (via the supplied IntentStore) and returns its id.
|
|
// It MUST NOT call into or mutate aivm. The returned id is a pure function of `in`.
|
|
SubmitInferenceIntent(ctx context.Context, store IntentStore, in InferenceIntent) (IntentID [32]byte, err error)
|
|
|
|
// VerifyInferenceReceipt is Pattern B: verify a COMMITTED A receipt against a
|
|
// committed receipt root the C side already holds (via the supplied ReceiptVerifierState).
|
|
// It MUST NOT live-query aivm. The receipt + proof arrive as calldata; the root
|
|
// authenticity comes from C-committed state.
|
|
VerifyInferenceReceipt(ctx context.Context, vs ReceiptVerifierState, r AInferenceReceipt, p AInferenceProof) (VerifiedAInferenceReceipt, error)
|
|
}
|
|
|
|
// VerifiedAInferenceReceipt is the result of a successful Pattern-B verification: the
|
|
// settled intent id, the canonical output digest the C side now trusts, and the
|
|
// status (always StatusCompleted on success — the type exists so a caller reads named
|
|
// fields rather than re-decoding the receipt).
|
|
type VerifiedAInferenceReceipt struct {
|
|
IntentID [32]byte
|
|
CanonicalOutputHash [32]byte
|
|
Status ReceiptStatus
|
|
}
|
|
|
|
// achainUnavailable is the precompile's client when the node-LOCAL A-Chain on-ramp is
|
|
// not wired. It runs NO inference and holds NO state: Pattern A returns
|
|
// ErrAChainUnavailable so a node without its local A on-ramp reverts cleanly. (Pattern
|
|
// B is a pure C-state verification and does NOT depend on the client — see bridge.go —
|
|
// so receipt verification keeps working even with the default client; that is correct,
|
|
// because verifying a committed receipt against committed C state never needs A.)
|
|
//
|
|
// This is NOT a "mode" and NOT a selectable engine — it is the honest default for a
|
|
// precompile whose A on-ramp is absent. Mirror of dex.dchainUnavailable. Contract:
|
|
// never panics, never fabricates a result.
|
|
type achainUnavailable struct{}
|
|
|
|
var _ AChainClient = (*achainUnavailable)(nil)
|
|
|
|
func (achainUnavailable) Brand() string { return "aivmbridge (local A-Chain on-ramp unavailable)" }
|
|
func (achainUnavailable) AChainID() [32]byte { return [32]byte{} }
|
|
|
|
func (achainUnavailable) SubmitInferenceIntent(context.Context, IntentStore, InferenceIntent) ([32]byte, error) {
|
|
return [32]byte{}, ErrAChainUnavailable
|
|
}
|
|
|
|
// VerifyInferenceReceipt on the unavailable client still performs the pure C-state
|
|
// verification — verifying a committed receipt against committed C state needs no A
|
|
// on-ramp. (bridge.go calls the package verify routine directly rather than routing
|
|
// Pattern B through the client; this method exists to satisfy the interface and is
|
|
// equivalent.)
|
|
func (achainUnavailable) VerifyInferenceReceipt(ctx context.Context, vs ReceiptVerifierState, r AInferenceReceipt, p AInferenceProof) (VerifiedAInferenceReceipt, error) {
|
|
return verifyInferenceReceipt(vs, r, p)
|
|
}
|
|
|
|
// aChainClient is the singleton the bridge resolves through, held behind an
|
|
// atomic.Pointer so Run() reads it race-free across concurrent EVM threads and
|
|
// InstallAChainClient publishes a new client with a single atomic store. The DEFAULT
|
|
// is achainUnavailable (the on-ramp is closed until a node wires its local aivm).
|
|
var aChainClient atomic.Pointer[AChainClient]
|
|
|
|
// installed guards InstallAChainClient to install-once: a second install errors
|
|
// rather than silently swapping the live client (no late re-resolution race / TOCTOU
|
|
// against in-flight Run() calls). Mirror of dex's install-once discipline.
|
|
var installed atomic.Bool
|
|
|
|
func init() {
|
|
def := AChainClient(achainUnavailable{})
|
|
aChainClient.Store(&def)
|
|
}
|
|
|
|
// currentAChainClient returns the live client (never nil — defaults to
|
|
// achainUnavailable). The single read point used by the bridge.
|
|
func currentAChainClient() AChainClient {
|
|
if p := aChainClient.Load(); p != nil {
|
|
return *p
|
|
}
|
|
return achainUnavailable{}
|
|
}
|
|
|
|
// InstallAChainClient resolves the singleton against the node-LOCAL A-Chain (aivm)
|
|
// on-ramp client. MIRROR of dex.InstallDChainClient.
|
|
//
|
|
// THIS IS NOT A BACKEND SWAP. There is exactly ONE valid target: the co-located
|
|
// A-Chain this node runs (the staged-import counterpart Blue-B builds). The EVM
|
|
// plugin calls this exactly ONCE at boot, BEFORE the VM starts serving precompiles. A
|
|
// node without its local A on-ramp leaves the default achainUnavailable in place.
|
|
//
|
|
// SANITY + SAFETY at install time (fail-secure):
|
|
// - c must be non-nil (a nil client would NPE the bridge).
|
|
// - c.Brand() must be non-empty (white-label / log-line discipline).
|
|
// - c.AChainID() must be non-zero (an unscoped peer would mint cross-rail-aliasable
|
|
// intent ids — the rail binding is load-bearing for replay isolation).
|
|
// - install is ONCE: a second call errors rather than swapping the live client, so a
|
|
// misconfigured plugin cannot race a late re-resolution against in-flight Run().
|
|
//
|
|
// The atomic store makes the publication visible to the concurrent Run() readers that
|
|
// follow. The host binary is the single caller during process startup.
|
|
func InstallAChainClient(c AChainClient) error {
|
|
if c == nil {
|
|
return errors.New("aivmbridge: local A-Chain client must not be nil")
|
|
}
|
|
if c.Brand() == "" {
|
|
return errors.New("aivmbridge: local A-Chain client must report a non-empty Brand()")
|
|
}
|
|
if isZero32(c.AChainID()) {
|
|
return errors.New("aivmbridge: local A-Chain client must report a non-zero AChainID() (the C<->A rail peer)")
|
|
}
|
|
if !installed.CompareAndSwap(false, true) {
|
|
return errors.New("aivmbridge: local A-Chain client already installed (install-once; cannot re-resolve a live client)")
|
|
}
|
|
cc := c
|
|
aChainClient.Store(&cc)
|
|
return nil
|
|
}
|
|
|
|
// resetAChainClientForTest restores the closed-on-ramp default so tests can drive the
|
|
// install path repeatedly. Test-only (not part of the production surface).
|
|
func resetAChainClientForTest() {
|
|
def := AChainClient(achainUnavailable{})
|
|
aChainClient.Store(&def)
|
|
installed.Store(false)
|
|
}
|