Deferred state semantics and API contract
DeferredState exists during execution to evaluate a deferred DAG and maintain its root commitment.
Successful execution consumes it and exports the root-reachable graph as one portable
PrecompileWitness, or None when the root is TRUE_DIGEST. A witness contains graph entries and
one root, with no evaluator or evaluation cache. Structural validity alone does not establish the
truth of its assertions.
The simplified state model is:
pub struct DeferredState {
registry: Arc<PrecompileRegistry>,
nodes: BTreeMap<Digest, Node>,
root: Digest,
remaining_elements: usize,
// evaluation results may be memoized internally, but this is not part of the public contract
}
Vocabulary
- Registered means a digest has an entry in
DeferredState.nodes. Registration can happen throughDeferredState::register, evaluation storing canonical/helper nodes,log_statementstoring frameworkANDnodes. - Evaluated means a registered input digest has been semantically reduced to a canonical node
under the installed
PrecompileRegistry. The canonical node is also stored innodesso it can be referenced by downstream nodes. - Logged or root-reachable means a registered digest contributes to
DeferredState.root. Only the root-reachable closure is exported byinto_witness; registered/evaluated orphans are dropped.
Registered nodes
nodes is the durable node store.
TRUE_DIGESTis always present and maps toNode::TRUE.Node::TRUEcosts no budget.- Every non-TRUE node is keyed by
node.digest(). - Structural nodes may reference only children already present in
nodes, except for the implicitTRUE_DIGEST:Joinhas two child digests.PairListhas one or more pairs of child digests.
- Re-registering identical content is idempotent and free.
- Reusing an existing digest for different content is rejected as a conflicting node.
Registration stores and shape-checks a node in nodes, evaluates it immediately, and stores the
canonical result. False predicates and other semantic evaluation failures are reported by
registration.
One fixed ceiling
DeferredState::new(registry) initializes one total budget from the library safety ceiling:
remaining_elements = MAX_DEFERRED_ELEMENTS
Initialization also installs the registry's init() constants, charging them against that same
budget. extend_precompiles(precompiles) merges additional precompiles into an existing state
without discarding existing nodes, evaluation results, root, or budget accounting.
Every new unique durable node inserted into nodes decrements remaining_elements by the node's
field-element footprint using checked subtraction. Duplicate insertion is free, so registering the
same data node at the exact budget limit succeeds. Evaluation results do not have a separate budget
and do not double-count canonical payloads; only canonical/helper nodes newly inserted into nodes
are charged.
The precompile's decode result is the framework shape gate:
NodeType::Dataauthorizes a non-empty data payload. For memory-backed registration, the host reads exactly the stack-suppliedn_chunks.NodeType::Joinauthorizes exactly one 8-felt payload block, interpreted as two child digests.NodeType::PairListauthorizes a non-empty list oflhs_digest || rhs_digestchunks.
After shape validation, the precompile's required validate_payload method checks any fixed or
parameter-dependent number of chunks or pairs. Rejection occurs before the node is inserted or
charged against the budget. Checks on referenced child values belong in evaluation.
Processor handlers perform a cheap deferred-budget pre-check before allocating or reading a memory-backed payload, but exact data/pair-list arity remains precompile-specific semantics.
If insertion exhausts the remaining budget, execution aborts with a budget error. The insertion path owns this accounting; processor deferred handlers do not perform post-mutation deferred budget checks.
Evaluation
Evaluation first requires the input digest to be present in nodes; evaluation state alone never
creates durable DAG membership. A call to evaluate_digest(digest) returns the digest of the
canonical node. This is a semantic operation: it may compute the result or use internal
memoization, but callers do not observe that distinction. Callers that need canonical node contents
can compose evaluate_digest with get_node.
Framework nodes evaluate as follows:
Node::TRUE => Node::TRUE
Node::AND(lhs, rhs) =>
require evaluate_digest(lhs) == TRUE_DIGEST
require evaluate_digest(rhs) == TRUE_DIGEST
Node::TRUE
Precompile-owned nodes are evaluated by PrecompileRegistry::evaluate, which dispatches to the
owning Precompile with a DeferredContext.
DeferredContext gives precompile implementations the same semantic split:
get_node(digest)queries the registered/original node by digest without evaluating it.evaluate_digest(digest)evaluates a registered child digest to its canonical digest.evaluate_digest_pair(lhs, rhs)evaluates two registered child digests to canonical digests.ensure_equal(lhs, rhs)evaluates two children and requires their canonical digests to match.register(node)inserts a freshly minted helper node and returns its original digest.
Root and portable export
root starts at TRUE_DIGEST. log_statement(stmt_digest) evaluates the current root and
statement, requires both to evaluate to Node::TRUE, then appends one framework AND node:
next_root = digest(Node::and(previous_root, stmt_digest))
Logging TRUE still records this AND node. into_witness consumes the execution state and exports
only its root-reachable closure in canonical child-first order. Index zero is implicit TRUE; data
entries carry literal chunks, joins carry two backward child indices, and pair lists carry ordered
pairs of backward child indices. A nonempty witness opens the digest of its last entry.
Checked construction and decoding reconstruct commitments, reject duplicate or conflicting entries, unsupported framework shapes, empty payloads, forward references, unreachable entries, and noncanonical traversal order. They do not interpret precompile operations or evaluate assertions. Standalone decoding rejects trailing bytes. Completed execution outputs carry this same portable representation, and release the runtime state.
Proof obligations and composition
Prover::prove proves the VM first. A TRUE_DIGEST root yields PrecompileStatus::Empty; any other
root yields PrecompileStatus::Deferred carrying its portable singleton witness. Execution-witness
construction and decoding require None exactly for a TRUE VM root, or one witness with a matching
root. Prover::prove_full proves both stages, using a one-element precompile batch.
For delegated proving, decode the transported proof and pass its witnesses directly to
Prover::prove_precompiles(Vec<PrecompileWitness>). The batch must be nonempty. Each input retains its
own indices, while one private Session shares computations across the batch. Operation support,
canonical arithmetic values, curve membership, assertion truth, MSM restrictions, and commitments
are checked during import. A root must have a transcript eval row; a bare external Keccak assertion
cannot serve as the final root and is rejected without changing its commitment.
The batch preserves exact root order and multiplicity: [A, B, A] proves AND(AND(A, B), A).
Repeated operands and root occurrences count as separate binding uses, even when their computation
is shared. Structural chunk references and point-at-infinity markers do not consume assertion
bindings. Witnesses are never merged; batching belongs entirely to proving.
The resulting PrecompileProof carries the ordered roots. Their left reduction, beginning with the
first root, is the statement verified by the aggregate precompile STARK. For one execution proof,
coverage is membership of its single VM-authenticated root in that sequence. Verification is
stateless and does not consume occurrences across calls, so one aggregate proof can complete each
compatible deferred proof independently; sequence order and duplicates still determine the STARK
statement.
VmProof and PrecompileProof are unvalidated transport records with public fields. StarkProof
retains private fields and its existing constructor and accessor interface. ExecutionProof is the
canonical binary transport. Its constructor, Serde representation, and complete method may
represent inconsistent artifacts. None of these operations establishes validity.
Verifier::verify_precompile validates the precompile proof shape, expected root membership,
ordered aggregate folding, and the precompile STARK. It can validate a precompile artifact against
an expected outstanding root and returns its authenticated security parameters. Verifier::verify
checks the proof's compatibility declaration and execution lifecycle before it verifies the VM
STARK. For deferred proofs, it evaluates the witness and requires its recomputed root to match the
VM-authenticated root. It reuses verify_precompile for complete proofs. A successful deferred
verification returns
the authenticated VM security parameters and outstanding root. A successful complete verification
has no outstanding obligation and, when it includes a precompile proof, also returns the PVM
security parameters.
Transport and limits
ExecutionProof::to_bytes is infallible. ExecutionProof::read_from_bytes checks canonical syntax,
rejects trailing bytes, and does not need a registry. The proof stores the transport format and the
compatible VM and PVM verifier root histories. Native verification requires a shared VM root and a
shared PVM root with the verifier's private support policy. Transport preserves the portable
witness without validating consistency between proof artifacts.
ExecutionProof, ExecutionWitness, and PrecompileWitness use encoding version 2.
Each canonical decoder accepts only its supported version and rejects other versions before
decoding their payloads. Previous encodings and conversion between formats are not supported.
Canonical binary decoders enforce fixed hard ceilings before allocating declared collections:
MAX_STARK_PROOF_BYTES per inner STARK, MAX_PRECOMPILE_ROOTS per ordered root list, and
MAX_DEFERRED_ELEMENTS for each portable witness. Batch import additionally enforces the root count,
total input elements, and execution work limits across all inputs, including repeated inputs, so
computation sharing does not bypass scan limits. These are library safety bounds, not configurable
protocol, whole-envelope, file, network, or ingestion policy.
Generic serialization traits are representation formats. Generic Serde deserialization is not guaranteed to apply the canonical decoder's early allocation bounds and must not be treated as a hardened untrusted-input decoder.
The canonical representational minima are 2 bytes for StarkProof, 34 bytes for VmProof, and 3
bytes for PrecompileProof, because an empty-root record remains transportable until verification.
The shortest canonical singleton PrecompileProof is 35 bytes; a vector of two such records has a
71-byte shortest encoding.
Recursive VM verification packages the unchanged proof stream under
proof_request_key(verifier_root, claim_commitment). The consumer derives the same key from the
claim commitment and vm::verify_proof procedure root, fetches the stream with
adv.push_mapval, and then invokes vm::verify_proof. Claim and kernel preimages remain
separately content-addressed; no proof values are copied into claim memory. Recursive verification
authenticates and returns the VM root but does not settle precompile work.