Deferred state semantics and API contract
DeferredState is the host-side witness for deferred DAG verification and the deferred root
commitment. It is not serialized directly: partial proofs carry DeferredStateWire; final non-empty
proofs carry a precompile VM STARK proof for the exact deferred root.
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, or wire rehydration rebuilding entries. - 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 serialized byto_wire; 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 remaining budget
DeferredState::new(registry, max_elements) initializes one total budget:
remaining_elements = max_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; precompile evaluation checks any tag-derived semantic data length.NodeType::Joinauthorizes exactly one 8-felt payload block, interpreted as two child digests.NodeType::PairListauthorizes a non-empty list oflhs_digest || rhs_digestchunks. Precompile evaluation checks any tag-derived semantic pair count.
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 wire
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))
to_wire serializes only the root-reachable closure in canonical child-first order:
- data entries carry literal data chunks;
- join entries emit two child indices;
- pair-list entries emit pairs of child indices.
The wire root is implicit: empty wire opens TRUE_DIGEST, otherwise the root is the digest of the
final entry. from_wire(registry, wire, max_elements) decodes untrusted wire, rejects non-canonical
or dangling wire by requiring state.to_wire() == wire, then evaluates the implicit wire root to
Node::TRUE. Evaluation may insert canonical/helper nodes in addition to the wire nodes.
Deferred proof root resolution
DeferredProof carries the material needed to bind VM proof verification to a deferred root:
Emptyis final and resolves toTRUE_DIGEST.Wireis partial/delegable material backed by canonicalDeferredStateWire.Starkis final: verification checks the precompile STARK proof against its embedded public root, then returns that root.
The public final verifier accepts only final deferred proof forms. It rejects Wire; wire-backed
partial proofs are handled by the explicit Verifier::verify_partial path. Partial verification
rehydrates the wire into a DeferredState with the standard precompile registry, verifies the VM
STARK against the hydrated state's root, and returns the hydrated state. It rejects Empty and
Stark because neither carries wire material to hydrate.
The VM STARK public inputs are built with the resolved final root or hydrated partial root.
High-level miden-prover proving APIs and the miden-vm proving facade produce final proofs by
default: Empty when no precompile claims were logged, or Stark for non-empty deferred roots. The
miden-vm facade exposes only final proving; callers that intentionally delegate or batch precompile
proving use the explicit miden-prover::prove_partial* APIs to preserve Wire proof material, then
verify_partial to validate and rehydrate that material before producing a final deferred proof.
The current STARK-backed variant is exact-root only; batch-backed proofs need a separate proof form.
Low-level framework API
The preferred low-level miden_core::deferred::DeferredState surface is small. These APIs are
framework APIs, not the public proof-verifier policy surface:
DeferredState::new(registry, max_elements)for a state booted with precompile constantsextend_precompiles(precompiles)for additive setupregistry()root()remaining_elements()get_node(digest)andnodes() -> &BTreeMap<Digest, Node>for registered-node inspectiondecode(tag)for structural tag decodingregister(node)for inserting concrete node contentevaluate_digest(digest)for the canonical digestlog_statement(stmt_digest)to_wire()from_wire(registry, wire, max_elements)
Callers that have a concrete node should explicitly register it; they may call
evaluate_digest on the returned digest when they need the canonical result, and then get_node if
they need canonical node contents. Raw evaluation memoization and direct root mutation are not part
of the public contract.
Scope note
Deferred state is the proof-bound precompile witness model. VM execution accumulates a deferred
root for logged precompile claims; DeferredProof explains why that root should be accepted.
Final public verification accepts only final proof forms, resolving Empty or a verified Stark
root before VM STARK verification. Wire-backed material is partial/delegable:
Verifier::verify_partial rehydrates Wire under the built-in miden_precompiles::registry(),
verifies the VM STARK against the hydrated root, and returns the hydrated state.
The lower-level DeferredState APIs remain parameterized by PrecompileRegistry for framework
construction, tests, and non-verifier wire validation. The default registry is empty, but the public
VM/prover/verifier path installs the standard miden_precompiles::registry() policy and does not
accept caller-supplied precompile registries.