Claims & the backward pass
how operators compose into one proof
The algorithm
The glue. A whole-model proof is not one monolithic circuit — it is a backward pass that threads small claims through the operator DAG. Each layer converts claims about its outputs into claims about its inputs; run from the output layer to the input layer, the entire model collapses to a handful of claims on the boundary polynomials (weights and inputs), which the commitment scheme then opens.
Why it resists a proof
Our docs called these "virtual claims." The code shows they are concrete Claim{point,eval} structs — owned, stored in a register, and (across distributed chunks) backed by polynomial commitments. And the layer role is not one trait but five — OpInfo, Evaluate, ProveInfo, ProvableOp, VerifiableCtx — with prove(last_claims) → input claims as the load-bearing method. Non-provable layers (Flatten, Reshape) propagate claims unchanged.
Cryptographic toolbox
- The five-trait layer anatomy
- A claim register: HashMap
- flatten_and_merge_claims for fan-out
- Committed boundary edges across distributed chunks
How each scheme proves it
A claim is one MLE evaluation. The backward pass over the DAG turns each layer's output claims into input claims; across chunk boundaries, claims are threaded by committing the edge MLEs, not passed inline.
Prover::prove_chunkwalkschunk.subgraph.backward_iter().Node::Outputseeds a registerHashMap<NodeInput, Claim>; each provable node reads the claims its successors deposited on its output ports, merges fan-out withflatten_and_merge_claims, callsop.prove(...), and writes the returned input claims back for its predecessors.- Non-provable layers don't prove anything — they propagate the claims unchanged, because they don't alter values. The recursion bottoms out at model inputs, where the verifier checks
input.into_mle().evaluate(claim.point) == claim.evaldirectly. - Distributed proving cuts the model into
ModelChunks; claims crossing a boundary are not passed inline. Each chunk commits its boundary-edge MLEs, keys claims by a SHA256-derived commitment id, and a consistency check verifies an input-edge commitment equals the matching output-edge commitment in the source chunk. - The verifier context is stored already reversed, because proving runs last-layer-to-first.
pub struct Claim<F> {
#[serde(with = "dp_crypto::serialization")]
pub point: Vec<F>,
#[serde(with = "dp_crypto::serialization")]
pub eval: F,
}let my_claims = if op.is_provable() {
op.prove(
node_id,
ctx,
claims_for_prove.iter().collect::<Vec<_>>(),
section,
&mut self,
)
.with_context(|| format!("proving {}: {}", node_id, op.describe()))?
} else {
// we only propagate the claims, without changing them, as a non-provable layer
// shouldn't change the input values
claims_for_prove
};