aichain

package
v1.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: BSD-3-Clause Imports: 13 Imported by: 0

README

aichain — Lux A-Chain (Beluga) inference SDK

github.com/luxfi/sdk/aichain

The developer-facing client for on-chain LLM inference + embeddings on the Lux A-Chain ("Thinking Chains" / Beluga). It wraps the three AI precompiles in the 0x0300…00xx range and pins the cross-chain wire spec byte-for-byte to the A-Chain settlement engine (github.com/luxfi/chains/aivm).

Precompile Address What it is
AI Bridge (LP-5301) 0x0300…0004 Tier-2 large-model inference: submit a committed intent, settle by A-Chain quorum
Deterministic inference (LP-0303) 0x0300…0003 Tier-1 small model, answered in-consensus inside one EVM call
Model Registry 0x0300…0002 Governance adoption of the canonical model

Tier 1 vs Tier 2 — the core distinction

  • Tier 1 — GenerateDeterministic runs the small int8 transformer that every validator computes identically. It is answered synchronously inside a single eth_call — no quorum, no waiting, no gas (it's a call, not a tx). Deterministic token-for-token across CPU/Metal/CUDA/HIP. Use it for fast, cheap, small-model inference where the result is part of consensus.

  • Tier 2 — SubmitInferenceIntent / WaitReceipt / Infer requests a large model that runs on the A-Chain and is settled by an M-of-N quorum. This is asynchronous: SubmitInferenceIntent writes a committed C-Chain intent and returns an intent_id; the result arrives later as a committed A-Chain receipt (InferenceReceipt) the bridge verifies via a Merkle proof against a committed receipt_root. The chain commits the canonical output hash, not the bytes — fetch the bytes from your provider/DA layer by that hash.

Install

It's a package in the Lux SDK module:

import "github.com/luxfi/sdk/aichain"

The live transport constructor aichain.Dial (which imports luxfi/evm/ethclient) is behind the livedial build tag, so the core SDK builds with no heavy node dependency and GOWORK=off go test ./aichain/... stays green. Inside the lux workspace, or with -tags livedial, Dial is available. Everywhere else, build a Client from any EVMBackend (your node client already satisfies it).

Quick start

Tier 1 — deterministic, in one call
c, _ := aichain.NewClient(backend, "" /* read-only, eth_call needs no key */)

// promptTokens are model token ids; returns prompt+generated tokens.
tokens, err := c.GenerateDeterministic(ctx, /*nNew*/ 10, []uint32{1, 7, 13, 2})
Tier 2 — large model, async quorum settlement
c, _ := aichain.Dial(rpcURL, privKeyHex, aichain.WithReceiptStore(store))

model := aichain.ModelSpec{
    Name:         "zenlm/zen-omni",
    Version:      3,
    WeightCommit: weightCommitHash, // the on-chain weight commitment
    Quantization: "int8",
}

// One-call convenience: submit -> wait for quorum -> return the canonical result.
res, err := c.Infer(ctx, model, []byte("Explain post-quantum signatures."),
    /*N*/ 5, /*threshold*/ 3, /*fee*/ big.NewInt(1e15))
// res.Receipt.CanonicalOutputHash is the agreed output digest (fetch bytes by it).

Or drive the two steps yourself:

intentID, txHash, err := c.SubmitInferenceIntent(ctx, aichain.SubmitOptions{
    ModelSpecHash: model.Hash(),
    PromptHash:    aichain.PromptHash(prompt),
    N:             5,
    Threshold:     3,
    Fee:           big.NewInt(1e15),
})
// ...later, after the A-Chain settles and the A->C boundary commits the root...
receipt, err := c.WaitReceipt(ctx, intentID) // blocks until Completed or deadline
Register a model (governance)
txHash, err := c.RegisterModel(ctx, model) // caller must be a registry admin
approved, err := c.GetModel(ctx, model.RegistryName()) // (version, weightCommit)

The ReceiptStore seam

WaitReceipt / Infer / GetReceipt read committed A-Chain receipts through a ReceiptStore you supply (poll the A-Chain RPC, or watch the bridge receipt-root checkpoint + A→C export). It is separate from the EVM tx path — the SDK ships no live store (it depends on your node-local A-Chain transport), keeping the SDK decoupled from any single A-Chain wire. Implement:

type ReceiptStore interface {
    ReceiptByIntent(ctx, intentID) (InferenceReceipt, MerkleProof, found bool, err error)
}

An optional AChainID() common.Hash method lets the SDK reproduce the exact cross-chain intent_id; otherwise the authoritative id is always receipt.IntentID.

Wire compatibility

Every encoder produces exactly the bytes the on-chain side hashes/decodes:

  • ComputeIntentIDkeccak(DomainIntent || c_chain || a_chain || c_tx || u32be(call_index) || caller || model_spec || prompt || u16be(N) || u16be(threshold) || u256be(fee)) — identical to chains/aivm.ComputeIntentID.
  • InferenceReceipt.Encode — the pinned 355-byte canonical encoding; Hash() = keccak(DomainReceipt || Encode()).
  • EncodeSubmitInferenceIntent / EncodeVerifyInferenceReceipt — the LP-5301 calldata frames.
  • EncodeGenerate — the inference precompile's tight u32be(nNew) || u32be tokens….
  • EncodeRegisterModel — the registry adopt(bytes32,uint256,bytes32) ABI.

Golden vectors (shared with the TypeScript client @luxfi/aichain):

intent_id     = 0x5e967be3e83750c25fb91887a125d67d2440fb41825d24d63a0c00e6fb2bfbde
receipt_hash  = 0xfe0a1e45baf5255e2461c5f8f38b8446a691ec8bf0ca260750259d8bb5677851
modelSpecHash = 0xd8ab4fca51f36de6db2efd1a7a022fef6943de8d9986ae8ca3f4db70f318b4a7

(for the canonical fixture in wire_test.go / calldata_test.go).

Tests

# Default lane (no live chain, no heavy deps) — CI-green:
SDKROOT=$(xcrun --show-sdk-path) GOWORK=off CGO_ENABLED=1 go test ./aichain/...

# Cross-spec parity vs the live chains/aivm wire (also CI-safe, no extra deps):
SDKROOT=$(xcrun --show-sdk-path) GOWORK=off CGO_ENABLED=1 go test -tags crossmodule ./aichain/

# Live transport build check (luxfi/evm; run in the lux workspace or with the tag):
go build -tags livedial ./aichain/

The crossmodule test asserts the SDK's encoders are byte-identical to the A-Chain settlement wire. It does not import chains/aivm (the SDK module does not require it, and that module's graph needs the lux go.work replace set, so an import would break the GOWORK=off lane). Instead it hand-builds the chain's exact keccak preimages and pins the live golden digests emitted by chains/aivm/quorum_wire_test.go. It is build-tagged so the default go test ./... lane carries no cross-module dependency, and it stays green in both lanes.

Status codes

InferenceReceipt.Status: 0 Unknown, 1 Pending, 2 Completed, 3 Failed, 4 Challenged. Only Completed with a non-zero CanonicalOutputHash is actionable (InferenceReceipt.Completed()); WaitReceipt enforces this.

Documentation

Overview

Package aichain is the developer-facing client SDK for the Lux A-Chain (Beluga) "Thinking Chains" architecture: on-chain LLM inference and embeddings settled by quorum.

It wraps the three on-chain entry points:

  • the AI bridge precompile (LP-5301) at 0x0300…0004 — Tier-2 large-model inference, submitted as a committed C-Chain intent and later settled by an A-Chain quorum receipt (Pattern A submit + Pattern B verify);
  • the deterministic in-consensus inference precompile (LP-0303) at 0x0300…0003 — Tier-1 small models run identically by every validator, answered inside one EVM call;
  • the model registry precompile at 0x0300…0002 — governance adoption of the model the chain treats as canonical.

wire.go pins the SHARED CROSS-CHAIN WIRE SPEC byte-for-byte. The encoders here MUST produce exactly the preimages the on-chain side hashes/decodes (chains/aivm/quorum_wire.go ComputeIntentID, chains/aivm/receipts.go AInferenceReceipt.Encode, and the LP-5301 aivmbridge calldata layout) or an intent submitted via this SDK will not re-derive to the same intent_id on A-Chain, and a receipt minted there will not verify against the SDK's recomputed hash. wire_test.go freezes golden vectors so any drift fails the build; the optional `crossmodule` build tag cross-checks against the live chains/aivm encoders.

keccak256 is luxfi/crypto.Keccak256 — the canonical keccak in the Lux stack, the same primitive the geth-side precompile and the A-Chain settlement engine use — so a digest computed here equals one computed on either chain bit-for-bit.

Index

Constants

View Source
const (
	// aivmbridge (0x0300…0004), LP-5301.
	SelectorSubmitInferenceIntent  uint32 = 0x10000000
	SelectorVerifyInferenceReceipt uint32 = 0x11000000

	// inference precompile (0x0300…0003), LP-0303 — generate(uint32,uint32[]).
	SelectorGenerate uint32 = 0x01000000

	// model registry (0x0300…0002).
	SelectorAdopt       uint32 = 0x01000000
	SelectorGetApproved uint32 = 0x02000000
	SelectorIsAdmin     uint32 = 0x03000000
	SelectorSetAdmin    uint32 = 0x04000000
)

--------------------------------------------------------------------------- Selectors (first 4 bytes of calldata), pinned to the on-chain modules. ---------------------------------------------------------------------------

View Source
const (
	DomainIntent    = "lux/aivmbridge/intent/v1"
	DomainReceipt   = "lux/aivmbridge/receipt/v1"
	DomainModelSpec = "lux/aivmbridge/modelspec/v1"
)

Domain separators (raw UTF-8 bytes, no length prefix — concatenated verbatim). They keep the intent-id, receipt-hash, and model-spec keyspaces disjoint and version the wire so a future v2 cannot collide with v1. The first two are the cross-chain contract pinned in chains/aivm/quorum_wire.go and MUST match byte-for-byte. DomainModelSpec is the SDK's canonical model-identity hash; it is the value carried as modelSpecHash in the intent (opaque to the chain, which only ever compares it for equality), so the SDK is its sole author.

View Source
const (
	StatusUnknown    uint8 = 0
	StatusPending    uint8 = 1
	StatusCompleted  uint8 = 2
	StatusFailed     uint8 = 3
	StatusChallenged uint8 = 4
)

Receipt status codes (the AInferenceReceipt.Status byte). Pinned values shared with the bridge + settlement engine. Only StatusCompleted with a non-zero CanonicalOutputHash is actionable C-side.

View Source
const MaxFanout uint16 = 256

MaxFanout is the upper bound on the intent fan-out N (LP-5301). It also caps Threshold.

View Source
const MaxProofDepth = 64

MaxProofDepth caps the Merkle path length the verifier (and the precompile) will walk — a 2^64-leaf tree is never reached in practice, but an unbounded pathLen is a calldata-DoS, so it is hard-capped (LP-5301 MaxProofDepth).

View Source
const ReceiptEncodedLen = 2 + 32 + 32 + 32 + 32 + 20 + 32 + 32 + 32 + 1 + 2 + 2 + 32 + 32 + 32 + 8 // = 355

ReceiptEncodedLen is the exact byte length of the canonical AInferenceReceipt encoding (see AInferenceReceipt.Encode). Pinned so a drift in field widths or order is a compile/test failure, not a silent cross-chain mismatch.

u16(Version)2 + IntentID32 + TaskID32 + CChainID32 + AChainID32 +
Requester20 + ModelSpecHash32 + PromptHash32 + CanonicalOutputHash32 +
u8(Status)1 + u16(N)2 + u16(Threshold)2 + WinnersRoot32 + OperatorsRoot32 +
u256(FeePaid)32 + u64(SettledAtHeight)8
View Source
const ReceiptVersion uint16 = 1

ReceiptVersion is the wire version stamped into every receipt (the u16be Version field). Bumping it is a wire change shared with the settlement engine.

Variables

View Source
var (
	AIBridgeAddress      = common.HexToAddress("0x0300000000000000000000000000000000000004")
	InferenceAddress     = common.HexToAddress("0x0300000000000000000000000000000000000003")
	ModelRegistryAddress = common.HexToAddress("0x0300000000000000000000000000000000000002")
)

Precompile addresses in the AI reserved range 0x0300…0000 .. 0x0300…00FF.

Functions

func ComputeIntentID

func ComputeIntentID(
	cChainID, aChainID, cTxHash common.Hash,
	callIndex uint32,
	caller common.Address,
	modelSpecHash, promptHash common.Hash,
	n, threshold uint16,
	fee *big.Int,
) common.Hash

ComputeIntentID derives the cross-chain intent id from the COMMITTED C-Chain intent fields, exactly the preimage chains/aivm/quorum_wire.go ComputeIntentID hashes:

keccak256( DomainIntent ||
    c_chain_id(32) || a_chain_id(32) || c_tx_hash(32) || u32be(call_index) ||
    caller(20) || model_spec_hash(32) || prompt_hash(32) ||
    u16be(N) || u16be(threshold) || u256be(fee,32) )

Every field is fixed-width in this precise order. The A-Chain importer recomputes this from the delivered fields and rejects the intent unless it equals the id the C side committed, so any altered field yields a different id.

Note: the submitInferenceIntent CALLER does not pick the intent id — the precompile derives it from the landed tx hash and call index. The SDK exposes this so a client that already knows (cTxHash, callIndex) — e.g. after the tx lands — can reproduce the id locally and correlate the eventual receipt.

func ComputeReceiptId

func ComputeReceiptId(modelID, promptHash, outputHash, paymentHash common.Hash, payer, operator common.Address) common.Hash

ComputeReceiptId reproduces the on-chain ProofOfThoughtRegistry.computeReceiptId for a paid thought:

keccak256( abi.encode(modelId, promptHash, outputHash, paymentHash, payer, operator) )

abi.encode of (bytes32,bytes32,bytes32,bytes32,address,address) is six 32-byte words (the two addresses left-padded to 32 bytes). This is the deterministic id under which the receipt is registered on-chain; the SDK computes it so a paid inference can be located/verified on-chain without a round-trip.

func DecodeGenerateResult

func DecodeGenerateResult(ret []byte) ([]uint32, error)

DecodeGenerateResult decodes the inference precompile return: a tight big-endian uint32 array (prompt+generated tokens). Errors on a non-multiple-of-4 length.

func DecodeSubmitInferenceIntentResult

func DecodeSubmitInferenceIntentResult(ret []byte) (common.Hash, error)

DecodeSubmitInferenceIntentResult decodes the aivmbridge Pattern-A return: a single bytes32 intentID.

func EncodeGenerate

func EncodeGenerate(nNew uint32, promptTokens []uint32) []byte

EncodeGenerate builds calldata for the deterministic in-consensus inference precompile (LP-0303). The frame is tight (NOT Solidity-ABI): the 4-byte selector, then u32be(nNew), then each prompt token as u32be. This matches precompile/inference/module.go Run exactly (input[4:8] = nNew, input[8:] = big-endian uint32 tokens).

func EncodeGetApproved

func EncodeGetApproved(name common.Hash) []byte

EncodeGetApproved builds calldata for getApproved(bytes32 name) -> (uint256 version, bytes32 weightHash).

func EncodeRegisterModel

func EncodeRegisterModel(spec ModelSpec) []byte

EncodeRegisterModel builds calldata for the registry's adopt(bytes32 name, uint256 version, bytes32 weightHash) op. The name is the spec's canonical id (ModelSpec.RegistryName == Hash), version is the spec version, and weightHash is the spec's weight commitment. Layout after the selector: name(32) | version(32, uint256) | weightHash(32).

func EncodeSubmitInferenceIntent

func EncodeSubmitInferenceIntent(modelSpecHash, promptHash common.Hash, n, threshold uint16, fee *big.Int, routing common.Hash) []byte

EncodeSubmitInferenceIntent builds calldata for the aivmbridge Pattern-A op (LP-5301). After the 4-byte selector, a fixed 6-word frame:

[0:32]    modelSpecHash (bytes32)
[32:64]   promptHash    (bytes32)
[64:96]   n             (uint16, right-aligned; high 30 bytes zero)
[96:128]  threshold     (uint16, right-aligned; high 30 bytes zero)
[128:160] fee           (uint256)
[160:192] routing       (bytes32, opaque transport hint)

The precompile rejects a short OR oversized frame and dirty high bytes; this encoder always emits the exact 196-byte (4+192) well-formed frame.

func EncodeVerifyInferenceReceipt

func EncodeVerifyInferenceReceipt(receipt, proof []byte) ([]byte, error)

EncodeVerifyInferenceReceipt builds calldata for the aivmbridge Pattern-B op (LP-5301). After the 4-byte selector, a tight length-prefixed frame:

[0:2]      u16be receiptLen
[2:4]      u16be proofLen
[4:4+rl]   receipt bytes (canonical 355-byte AInferenceReceipt encoding)
[..]       proof bytes   (ReceiptRoot|Index|pathLen|path*32)

`receipt` must be a canonical 355-byte encoding (InferenceReceipt.Encode) and `proof` the LP-5301 proof frame (MerkleProof.EncodeProof). Both are emitted verbatim; the precompile re-decodes them with the same exact-length discipline.

func PromptHash

func PromptHash(prompt []byte) common.Hash

PromptHash is the canonical commitment to a prompt/input: keccak256 of the raw prompt bytes. The chain treats promptHash as opaque (equality only), so any stable hash the requester and provider agree on works; this SDK standardises on keccak of the UTF-8/binary prompt so the same prompt always yields the same commitment.

func VerifyReceiptInclusion

func VerifyReceiptInclusion(receiptHash common.Hash, proof MerkleProof, root common.Hash) bool

VerifyReceiptInclusion checks that receiptHash is included under root at the proof's Index, re-applying the leaf hash and folding with the siblings, choosing left/right by the index bit at each level — exactly inverting the chains/aivm merkleProof/merkleRoot construction. Returns true iff the recomputed root equals root.

Types

type ApprovedModel

type ApprovedModel struct {
	Version      uint64
	WeightCommit common.Hash
}

ApprovedModel is the decoded getApproved return: the adopted version and weight commitment for a model name (weight == zero means none adopted).

func DecodeGetApprovedResult

func DecodeGetApprovedResult(ret []byte) (ApprovedModel, error)

DecodeGetApprovedResult decodes the 64-byte getApproved return: (uint256 version, bytes32 weightHash). Version is read from the low 8 bytes of the first word (matching modelregistry GetApproved, which reads v[24:32]).

type BudgetGuard

type BudgetGuard struct {
	// contains filtered or unexported fields
}

BudgetGuard enforces a BudgetPolicy across many GenerateWithBudget calls on one Client, holding the rolling hourly spend. Construct with NewBudgetGuard; it is safe for concurrent use. The guard is SEPARATE from the Client so the same client can serve different budgets (one and only one spend ledger per policy).

func NewBudgetGuard

func NewBudgetGuard(policy BudgetPolicy, now func() time.Time) *BudgetGuard

NewBudgetGuard builds a guard for a policy. `now` is injectable for tests (nil == time.Now). The hourly window is fixed at one hour (the policy's unit).

type BudgetPolicy

type BudgetPolicy struct {
	// MaxPerRequest caps a single request's amount (nil/zero == no per-request cap).
	MaxPerRequest *big.Int
	// MaxPerHour caps the rolling 1-hour spend (nil/zero == no hourly cap).
	MaxPerHour *big.Int
	// AllowedModels, if non-empty, whitelists model spec hashes that may be paid
	// for (a request for any other model is rejected before paying).
	AllowedModels []common.Hash
	// RequirePoT, when true, requires the paid result to carry a non-zero PoT
	// paymentHash<->settlement join (InferPaid always produces one; this asserts it).
	RequirePoT bool
}

BudgetPolicy is a CLIENT-SIDE spend guard checked BEFORE any payment is made. It caps the per-request amount and the rolling per-hour spend, optionally restricts which models may be paid for, and can require that a PoT receipt be produced. It is advisory (the client's own discipline), independent of and complementary to the on-chain escrow.

type Client

type Client struct {

	// PollInterval / PollTimeout bound the tx-receipt and settlement polling.
	PollInterval time.Duration
	PollTimeout  time.Duration
	// contains filtered or unexported fields
}

Client is the high-level A-Chain (Beluga) inference client over an EVM JSON-RPC endpoint. It signs and sends txs to the AI precompiles and reads results.

  • Tier-1 (GenerateDeterministic): the deterministic in-consensus inference precompile (0x0300…0003) answers INSIDE one EVM call — synchronous, no quorum, small models. Use it via an eth_call (no tx, no gas spent) or a tx if you want the call recorded.
  • Tier-2 (SubmitInferenceIntent / WaitReceipt / Infer): a large model on the A-Chain settled by an M-of-N quorum — ASYNCHRONOUS. Submit returns an intent id; the result arrives later as a committed A-Chain receipt the bridge can verify.

func NewClient

func NewClient(backend EVMBackend, privKeyHex string, opts ...Option) (*Client, error)

NewClient builds a Client over the given backend, signing with privKeyHex (a 0x-optional hex secp256k1 key). For a read-only client (eth_call Tier-1, or reads), pass an empty key.

func (*Client) From

func (c *Client) From() common.Address

From returns the signer address (zero for a read-only client).

func (*Client) GenerateDeterministic

func (c *Client) GenerateDeterministic(ctx context.Context, nNew uint32, promptTokens []uint32) ([]uint32, error)

GenerateDeterministic runs the Tier-1 in-consensus inference precompile (0x0300…0003) via eth_call and returns the full token sequence (prompt + generated). This is SYNCHRONOUS and answered inside the single call — no quorum, no async settlement, no gas spent (it is an eth_call). Use it for the small deterministic model; use Infer for large Tier-2 models.

func (*Client) GenerateWithBudget

func (c *Client) GenerateWithBudget(
	ctx context.Context,
	model ModelSpec,
	prompt []byte,
	n, threshold uint16,
	reqs PaymentRequirements,
	guard *BudgetGuard,
	facilitator Facilitator,
	signer *ecdsa.PrivateKey,
) (PaidResult, error)

GenerateWithBudget is the budget-gated InferPaid: it enforces `guard`'s policy (per-request cap, hourly cap, model allow-list) BEFORE building or settling any payment, then runs the full paid inference. An over-budget or disallowed request is rejected with NO payment made. If the policy RequirePoT, the result is checked to carry a valid PoT join before returning.

This is the Tier-2 entrypoint agents use to pay-per-cognition safely: the guard is the agent's spending discipline, the facilitator settles, and the PoT join makes every paid thought accountable on-chain.

func (*Client) GetModel

func (c *Client) GetModel(ctx context.Context, name common.Hash) (ApprovedModel, error)

GetModel reads the registry's currently-adopted (version, weight commitment) for a model name via eth_call (no tx). A zero weight commitment means the model is not adopted.

func (*Client) GetReceipt

func (c *Client) GetReceipt(ctx context.Context, intentID common.Hash) (receipt InferenceReceipt, proof MerkleProof, found bool, err error)

GetReceipt does a single (non-blocking) read of the committed receipt for an intent id. found==false means not yet settled.

func (*Client) Infer

func (c *Client) Infer(ctx context.Context, model ModelSpec, prompt []byte, n, threshold uint16, fee *big.Int) (InferResult, error)

Infer is the one-call Tier-2 convenience: submit an intent for `model` over `prompt`, wait for the quorum to settle, and return the canonical result.

SETTLEMENT IS ASYNC. Unlike GenerateDeterministic (Tier-1, answered in one EVM call), Infer submits a C-Chain intent and BLOCKS polling the A-Chain until an M-of-N quorum settles a receipt — seconds to minutes, bounded by PollTimeout. The returned Receipt.CanonicalOutputHash is the agreed output digest; fetch the full output bytes from your provider/DA layer keyed by that hash (the chain commits the digest, not the bytes).

func (*Client) InferPaid

func (c *Client) InferPaid(
	ctx context.Context,
	model ModelSpec,
	prompt []byte,
	n, threshold uint16,
	reqs PaymentRequirements,
	facilitator Facilitator,
	signer *ecdsa.PrivateKey,
) (PaidResult, error)

InferPaid runs the full x402 pay-per-cognition flow for a Tier-2 inference:

  1. build + sign the x402 PaymentPayload from `reqs` with `signer`;
  2. have the facilitator Settle it (verify signature, charge) -> SettlementReceipt;
  3. run the quorum inference (Infer) for (model, prompt);
  4. assemble the PoT join (paymentHash from the settlement, outputHash from the receipt) so the result can be registered on-chain.

Payment happens BEFORE inference (x402 is pay-first). The returned Settlement.PaymentHash is the on/off-chain join key; PoT.ReceiptID() is the id the result registers under on ProofOfThoughtRegistry. Errors from settle abort before any inference is requested.

func (*Client) RegisterModel

func (c *Client) RegisterModel(ctx context.Context, spec ModelSpec) (common.Hash, error)

RegisterModel adopts a model version in the registry precompile (0x0300…0002). The caller must be a registry admin (governance); a non-admin tx reverts on chain. Returns the tx hash.

func (*Client) SubmitInferenceIntent

func (c *Client) SubmitInferenceIntent(ctx context.Context, opts SubmitOptions) (intentID common.Hash, txHash common.Hash, err error)

SubmitInferenceIntent sends a Tier-2 intent tx to the aivmbridge precompile (Pattern A) and returns the deterministic intent id (re-derived from the LANDED tx hash + call index, exactly as the precompile derives it) and the tx hash.

The intent is the START of an async quorum settlement; use WaitReceipt (or the one-call Infer) to obtain the result. The single-call submit places the intent at call index 0 of its tx — the SDK derives the id on that assumption (a contract that batches multiple submits in one tx must compute ids itself).

func (*Client) VerifyPaidReceipt

func (c *Client) VerifyPaidReceipt(_ context.Context, pot PoTReceipt, settlement SettlementReceipt) (bool, error)

VerifyPaidReceipt proves the payment<->thought join: it checks that the PoT receipt's PaymentHash equals the settlement's PaymentHash (the value the signature was over and that the on-chain registry stores). A true result means "this recorded thought was paid for by exactly this settlement". It also guards against a zero paymentHash (which the on-chain registry rejects).

func (*Client) WaitReceipt

func (c *Client) WaitReceipt(ctx context.Context, intentID common.Hash) (InferenceReceipt, error)

WaitReceipt polls the A-Chain receipt store for the committed receipt of intentID until it is Completed (or the deadline / ctx fires). It returns the receipt only when actionable (Status==Completed, non-zero output); a Failed or Challenged terminal receipt returns an error carrying the receipt.

type EVMBackend

type EVMBackend interface {
	ChainID(ctx context.Context) (*big.Int, error)
	AcceptedNonceAt(ctx context.Context, account common.Address) (uint64, error)
	SuggestGasTipCap(ctx context.Context) (*big.Int, error)
	EstimateBaseFee(ctx context.Context) (*big.Int, error)
	EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
	SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error
	TransactionReceipt(ctx context.Context, txHash common.Hash) (*gethtypes.Receipt, error)
	CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error)
}

EVMBackend is the SUBSET of luxfi/evm/ethclient.Client this SDK needs: enough to read chain id / nonce / fees, send a tx, poll a receipt, and make an eth_call. luxfi/evm/ethclient.Client satisfies it directly (see DialClient), and a test can supply a mock without a live chain. Keeping the surface minimal is the decoupling: the SDK depends on these eight methods, not on the whole node client.

type Facilitator

type Facilitator interface {
	Verify(payload PaymentPayload, reqs PaymentRequirements) error
	Settle(payload PaymentPayload, reqs PaymentRequirements) (SettlementReceipt, error)
}

Facilitator is the x402 verify+settle service. A real implementation calls the facilitator HTTP endpoint (or settles on-chain itself); MockFacilitator settles deterministically in-process for tests. Verify is the cheap pre-check (valid signature, well-formed, binds the requirements, not expired); Settle performs the transfer and returns the SettlementReceipt carrying the paymentHash.

type InferResult

type InferResult struct {
	IntentID common.Hash
	TxHash   common.Hash
	Receipt  InferenceReceipt
}

InferResult is the outcome of a one-call Tier-2 Infer.

type InferenceReceipt

type InferenceReceipt struct {
	Version             uint16         `json:"version"`
	IntentID            common.Hash    `json:"intentId"` // source C-chain intent id
	TaskID              common.Hash    `json:"taskId"`   // A-chain task id
	CChainID            common.Hash    `json:"cChainId"`
	AChainID            common.Hash    `json:"aChainId"`
	Requester           common.Address `json:"requester"`
	ModelSpecHash       common.Hash    `json:"modelSpecHash"`
	PromptHash          common.Hash    `json:"promptHash"`
	CanonicalOutputHash common.Hash    `json:"canonicalOutputHash"` // zero if Failed
	Status              uint8          `json:"status"`
	N                   uint16         `json:"n"`
	Threshold           uint16         `json:"threshold"`
	WinnersRoot         common.Hash    `json:"winnersRoot"`
	OperatorsRoot       common.Hash    `json:"operatorsRoot"`
	FeePaid             *big.Int       `json:"feePaid"`
	SettledAtHeight     uint64         `json:"settledAtHeight"`
}

InferenceReceipt is the cross-chain settlement receipt produced by the A-Chain when a Tier-2 inference task settles. Its canonical encoding is PINNED byte-for-byte (Encode / ReceiptEncodedLen = 355) and identical to chains/aivm/receipts.go AInferenceReceipt; its hash is keccak(DomainReceipt || Encode()), the leaf the receipt_root commits to.

FeePaid is a non-negative *big.Int (nil == zero on the wire). Field order here is presentation; the WIRE order is fixed by Encode and MUST NOT be reordered.

func DecodeReceipt

func DecodeReceipt(b []byte) (InferenceReceipt, error)

DecodeReceipt parses a canonical 355-byte receipt encoding back into an InferenceReceipt. It is the inverse of Encode and rejects any input that is not EXACTLY ReceiptEncodedLen bytes (a corrupt/short/over-long frame is never reinterpreted into a credit — the same fail-secure discipline the precompile's DecodeReceipt applies).

func (InferenceReceipt) Completed

func (r InferenceReceipt) Completed() bool

Completed reports whether the receipt is the only actionable shape C-side: Status == StatusCompleted with a non-zero canonical output. A Pending, Failed, or Challenged receipt — or a zero output — is never credited.

func (InferenceReceipt) Encode

func (r InferenceReceipt) Encode() []byte

Encode produces the canonical fixed-width encoding (exactly ReceiptEncodedLen = 355 bytes) in the PINNED order, byte-identical to chains/aivm:

u16be(Version) || IntentID(32) || TaskID(32) || CChainID(32) || AChainID(32) ||
Requester(20) || ModelSpecHash(32) || PromptHash(32) || CanonicalOutputHash(32) ||
u8(Status) || u16be(N) || u16be(Threshold) || WinnersRoot(32) ||
OperatorsRoot(32) || u256be(FeePaid,32) || u64be(SettledAtHeight)

func (InferenceReceipt) Hash

func (r InferenceReceipt) Hash() common.Hash

Hash is the receipt commitment: keccak256(DomainReceipt || Encode()). This is the leaf value (pre-leaf-hash) that goes into the receipt_root and that an exported proof proves membership of. Identical to chains/aivm AInferenceReceipt.Hash.

type MerkleProof

type MerkleProof struct {
	ReceiptRoot common.Hash   `json:"receiptRoot"` // the committed root this proof is against
	Index       uint64        `json:"index"`       // leaf index in the tree
	Siblings    []common.Hash `json:"siblings"`    // sibling at each level, leaf->root
}

MerkleProof is an inclusion proof that a receipt_hash leaf is committed under a ReceiptRoot: the sibling hashes from leaf to root plus the leaf Index (which fixes left/right at each level). It mirrors chains/aivm MerkleProof and the LP-5301 proofBytes frame exactly so a proof exported by the A-Chain (Engine.ExportReceipt) verifies here and vice-versa.

func DecodeProof

func DecodeProof(b []byte) (MerkleProof, error)

DecodeProof parses an LP-5301 proofBytes frame back into a MerkleProof. It is the inverse of EncodeProof and is exact-length: it rejects a short frame, a frame whose declared pathLen exceeds MaxProofDepth, and a frame with trailing junk — the same calldata-hardening the precompile applies.

func (MerkleProof) EncodeProof

func (p MerkleProof) EncodeProof() ([]byte, error)

EncodeProof serializes a proof into the LP-5301 proofBytes wire frame:

ReceiptRoot(32) | u64be Index(8) | u16be pathLen(2) | pathLen*32

This is the exact bytes the aivmbridge verifyInferenceReceipt op decodes.

func (MerkleProof) Verify

func (p MerkleProof) Verify(r InferenceReceipt) bool

Verify is the receipt-aware check: it confirms the proof is against the root the proof itself carries (proof.ReceiptRoot) and that the receipt's Hash() is included under it. The CALLER must still confirm proof.ReceiptRoot is a root the C-Chain holds committed (the precompile does this against its receipt-root checkpoint; off-chain you compare against a known-good root).

type MockFacilitator

type MockFacilitator struct {
	Now func() time.Time
}

MockFacilitator is an in-process Facilitator for tests + local dev. It runs the real verification (signature recovery, binding, expiry) so tests exercise the genuine path, then "settles" by returning a deterministic receipt (no chain). Now is injectable for deterministic expiry tests (zero == time.Now).

func (*MockFacilitator) Settle

Settle verifies then returns a deterministic settlement receipt. The settled amount equals payload.Value (the mock charges the full authorized amount even for `upto`). PaymentHash is the EIP-712 digest (the PoT join key); TxHash is derived deterministically from the paymentHash so a settlement is reproducible in tests (a real facilitator returns the actual on-chain tx hash).

func (*MockFacilitator) Verify

func (m *MockFacilitator) Verify(payload PaymentPayload, reqs PaymentRequirements) error

Verify runs the genuine x402 verification: the payload binds the requirements, the signature recovers to payload.From, and the authorization is not expired.

type ModelSpec

type ModelSpec struct {
	Name         string      `json:"name"`
	Version      uint64      `json:"version"`
	WeightCommit common.Hash `json:"weightCommit"`
	Quantization string      `json:"quantization"`
}

ModelSpec is the SDK's canonical description of a model the chain can serve. On-chain (chains/aivm, modelregistry) a model is referred to ONLY by its 32-byte weight-commitment hash (an opaque equality key); ModelSpec is the human-facing identity that hashes DOWN to that key, so a requester naming a model and a provider advertising one derive the IDENTICAL modelSpecHash from the IDENTICAL fields.

Fields:

  • Name: model identity, e.g. "zenlm/zen-omni" (free-form, UTF-8).
  • Version: monotonically increasing version the registry adopts.
  • WeightCommit: the 32-byte commitment to the exact weights (a content hash of the weight file / a Merkle root over shards). This is the load-bearing field — it pins WHICH weights are canonical. If you already have the on-chain commitment, set it directly; Name/Version then only decorate it.
  • Quantization: the inference format that makes the result deterministic, e.g. "int8", "bf16-kat". Different quantizations of the same weights are different specs (they produce different canonical outputs).

func (ModelSpec) Hash

func (m ModelSpec) Hash() common.Hash

Hash is the canonical model-spec hash — the 32-byte value carried as modelSpecHash in an intent and adopted (as weightHash) in the registry:

keccak256( DomainModelSpec ||
    u32be(len(Name)) || Name ||
    u64be(Version) ||
    WeightCommit(32) ||
    u32be(len(Quantization)) || Quantization )

Variable-length fields are length-prefixed so no two distinct specs can concatenate to the same preimage (injective). The domain separator keeps this keyspace disjoint from intent ids and receipt hashes.

func (ModelSpec) RegistryName

func (m ModelSpec) RegistryName() common.Hash

RegistryName is the bytes32 model name the registry's adopt(name, version, weightHash) ABI keys on. We use the spec Hash itself as the registry name so a model is addressed by one stable id everywhere (the registry maps that id -> the adopted weight commitment + version).

type Option

type Option func(*Client)

Option configures a Client.

func WithChainID

func WithChainID(id *big.Int) Option

WithChainID pre-sets the EVM chain id, skipping the eth_chainId round-trip (useful in tests / offline signing).

func WithPolling

func WithPolling(interval, timeout time.Duration) Option

WithPolling overrides the receipt/settlement polling cadence and deadline.

func WithReceiptStore

func WithReceiptStore(s ReceiptStore) Option

WithReceiptStore wires the A-Chain receipt read path (enables WaitReceipt / Infer / GetReceipt).

type PaidResult

type PaidResult struct {
	IntentID   common.Hash
	TxHash     common.Hash
	Inference  InferenceReceipt
	Settlement SettlementReceipt
	PoT        PoTReceipt // ready to register on ProofOfThoughtRegistry (PoT.ReceiptID())
}

PaidResult is the outcome of an InferPaid call: the inference result joined to its x402 settlement and the PoT record that binds them on-chain.

type PaymentPayload

type PaymentPayload struct {
	Scheme      X402Scheme     `json:"scheme"`
	Network     *big.Int       `json:"network"`
	From        common.Address `json:"from"`        // payer (recovered-equals the signer)
	To          common.Address `json:"to"`          // payee (== requirements.PayTo)
	Value       *big.Int       `json:"value"`       // amount authorized (== requirements.MaxAmount)
	Token       common.Address `json:"token"`       // payment token
	ValidAfter  uint64         `json:"validAfter"`  // unix seconds the auth becomes valid (0 == immediately)
	ValidBefore uint64         `json:"validBefore"` // unix seconds the auth expires (== requirements.ValidUntil)
	Nonce       common.Hash    `json:"nonce"`       // == requirements.Nonce
	Signature   []byte         `json:"signature"`   // 65-byte secp256k1 [R||S||V]
}

PaymentPayload is the signed authorization the client sends in the X-PAYMENT header. It is an EIP-3009-shaped transferWithAuthorization payload: a signature over (from, to, value, validAfter, validBefore, nonce) bound to (token, chain id) via the EIP-712 domain. The facilitator recovers `from` from the signature and settles the transfer.

func BuildPaymentPayload

func BuildPaymentPayload(signer *ecdsa.PrivateKey, reqs PaymentRequirements) (PaymentPayload, error)

BuildPaymentPayload constructs the x402 authorization from a server's PaymentRequirements and EIP-712-signs it with `signer`. The payload binds from=signer-address, to=PayTo, value=MaxAmount, token, validBefore=ValidUntil, nonce, on the requirements' network. The returned payload's PaymentHash() is the EIP-712 digest the signature is over (and the PoT join key). Deterministic: the same (signer, requirements) always yields the same payload + paymentHash.

func (PaymentPayload) PaymentHash

func (p PaymentPayload) PaymentHash() common.Hash

PaymentHash is the EIP-712 signing digest of the authorization: keccak256(0x1901 || domainSeparator(network) || hashStruct(msg)). This is the value the signature is over AND the value stored on-chain as ThoughtReceipt.paymentHash, so it is the canonical x402<->PoT join key.

func (PaymentPayload) RecoverPayer

func (p PaymentPayload) RecoverPayer() (common.Address, error)

RecoverPayer recovers the address that signed the authorization from the signature over PaymentHash(). It is the facilitator's check that `from` really authorized the payment. Errors on a malformed signature or a recovered address that does not equal payload.From.

type PaymentRequirements

type PaymentRequirements struct {
	Scheme      X402Scheme     `json:"scheme"`
	Network     *big.Int       `json:"network"`     // EVM chain id the token/settlement lives on
	MaxAmount   *big.Int       `json:"maxAmount"`   // amount (exact) or cap (upto), in token base units
	Token       common.Address `json:"token"`       // ERC-20 payment token (zero == native)
	PayTo       common.Address `json:"payTo"`       // payee (resource server's receiving address)
	Resource    string         `json:"resource"`    // the resource being paid for (e.g. model id / URL)
	Description string         `json:"description"` // human-readable description
	Nonce       common.Hash    `json:"nonce"`       // server-chosen unique nonce (replay protection)
	ValidUntil  uint64         `json:"validUntil"`  // unix seconds the authorization is valid until (validBefore)
}

PaymentRequirements is what a resource server returns in its HTTP 402 response: what it costs, in what token, on what network, to whom, and until when. The client turns this into a signed PaymentPayload.

type PoTReceipt

type PoTReceipt struct {
	ModelID     common.Hash    `json:"modelId"`
	PromptHash  common.Hash    `json:"promptHash"`
	OutputHash  common.Hash    `json:"outputHash"`
	PaymentHash common.Hash    `json:"paymentHash"`
	QuorumProof common.Hash    `json:"quorumProof"`
	Payer       common.Address `json:"payer"`
	Operator    common.Address `json:"operator"`
	Cost        *big.Int       `json:"cost"`
}

PoTReceipt is the SDK's view of a Proof-of-Thought record: the join of a paid x402 settlement (PaymentHash), a model output (OutputHash), and the producing proof (QuorumProof), keyed by the on-chain ReceiptId. It is what a caller registers via ProofOfThoughtRegistry.register and what VerifyPaidReceipt checks against a SettlementReceipt.

func (PoTReceipt) ReceiptID

func (r PoTReceipt) ReceiptID() common.Hash

ReceiptID is the deterministic on-chain id for this PoT record (== ComputeReceiptId over its fields).

type ReceiptStore

type ReceiptStore interface {
	ReceiptByIntent(ctx context.Context, intentID common.Hash) (receipt InferenceReceipt, proof MerkleProof, found bool, err error)
}

ReceiptStore reads committed A-Chain receipts for the high-level WaitReceipt / GetReceipt convenience. It is intentionally SEPARATE from EVMBackend (the settlement read path is not the EVM tx path): an implementation may poll the A-Chain RPC directly, or watch the aivmbridge receipt-root checkpoint and the A->C export. ReceiptByIntent returns the committed receipt + its inclusion proof for an intent id, or (false) if not yet settled.

The SDK ships no live ReceiptStore (it depends on the node-local A-Chain RPC shape, out of scope here); callers wire their own, and tests use a fake. This keeps the SDK decoupled from any single A-Chain transport while still offering the one-call Infer convenience.

type SettlementReceipt

type SettlementReceipt struct {
	PaymentHash common.Hash    `json:"paymentHash"` // EIP-712 digest of the payload (== PoT paymentHash)
	TxHash      common.Hash    `json:"txHash"`      // settlement tx (zero if off-chain)
	Settled     bool           `json:"settled"`
	Payer       common.Address `json:"payer"`
	Payee       common.Address `json:"payee"`
	Amount      *big.Int       `json:"amount"` // amount actually settled (<= payload.Value for `upto`)
}

SettlementReceipt is what the facilitator returns after Verify+Settle — the X-PAYMENT-RESPONSE the resource server echoes. PaymentHash is the EIP-712 digest of the authorization (the x402<->PoT join key); TxHash is the on-chain settlement tx (zero for an off-chain/escrowed settlement).

type SubmitOptions

type SubmitOptions struct {
	// ModelSpecHash is the canonical model id (ModelSpec.Hash). Required, non-zero.
	ModelSpecHash common.Hash
	// PromptHash commits to the prompt (PromptHash(prompt)). Required, non-zero.
	PromptHash common.Hash
	// N is the fan-out (independent provider executions), in [1, 256].
	N uint16
	// Threshold is the M-of-N agreement, in [1, N] (the A-Chain also requires
	// floor(N/2)+1 <= Threshold).
	Threshold uint16
	// Fee funds the A escrow + burn (256-bit, non-negative).
	Fee *big.Int
	// Routing is an opaque transport hint (NOT consensus state, NOT in the id).
	Routing common.Hash
	// GasLimit overrides gas estimation when non-zero.
	GasLimit uint64
}

SubmitOptions parametrise a Tier-2 inference intent.

type VerifyResult

type VerifyResult struct {
	IntentID            common.Hash
	CanonicalOutputHash common.Hash
	Status              uint8
}

VerifyResult is the decoded aivmbridge Pattern-B return: (bytes32 intentID, bytes32 canonicalOutputHash, uint8 status) packed as 3 words (96 bytes).

func DecodeVerifyInferenceReceiptResult

func DecodeVerifyInferenceReceiptResult(ret []byte) (VerifyResult, error)

DecodeVerifyInferenceReceiptResult decodes the 96-byte Pattern-B return.

type X402Scheme

type X402Scheme string

X402Scheme is the x402 payment scheme. `exact` is a fixed per-request charge; `upto` caps a charge that the facilitator may settle for less (e.g. metered by actual tokens). The signed authorization is identical; the scheme tells the facilitator how to interpret MaxAmount vs the settled amount.

const (
	// SchemeExact: settle exactly MaxAmount.
	SchemeExact X402Scheme = "exact"
	// SchemeUpto: settle at most MaxAmount (the facilitator may charge less).
	SchemeUpto X402Scheme = "upto"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL