Skip to content

feat: serialize trusted trace proving inputs - #3284

Closed
huitseeker wants to merge 12 commits into
nextfrom
trace-proving-inputs-serialization
Closed

feat: serialize trusted trace proving inputs#3284
huitseeker wants to merge 12 commits into
nextfrom
trace-proving-inputs-serialization

Conversation

@huitseeker

@huitseeker huitseeker commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

The transaction executor can now serialize TraceProvingInputs and send them to a trusted prover. The prover can deserialize the payload, build the trace, prove it, and return a proof.

This adds binary serialization for the trace replay state, sparse MAST replay data, TraceBuildInputs, ProvingOptions, and TraceProvingInputs.

Sparse MAST serialization is a trusted replay format. It preserves the source node IDs, roots, advice data, digest entries, and source forest commitment. It does not implement the untrusted hashless MAST read path (and does not verify node hashes).

Tests cover sparse round trips, malformed sparse payloads, bad serialized forest IDs, trace summary equality after TraceBuildInputs round trip, proof generation after TraceProvingInputs round trip, and fuzz smoke runs for the new deserialize targets.

Closes #3235

@huitseeker
huitseeker requested a review from plafer June 23, 2026 22:59
@huitseeker huitseeker changed the title Trace proving inputs serialization serialize trusted trace proving inputs Jun 23, 2026
@huitseeker huitseeker changed the title serialize trusted trace proving inputs feat: serialize trusted trace proving inputs Jun 23, 2026
@huitseeker
huitseeker marked this pull request as ready for review June 23, 2026 23:22

@plafer plafer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall really LGTM!

Comment thread core/src/mast/mod.rs
Comment thread core/src/mast/serialization/tests.rs
@huitseeker

huitseeker commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

@plafer Thanks, both points were right.

  • I made testing enable arbitrary (as it does in core) for every crate that has arbitrary backed serde tests. make test now builds and runs those generated tests.
  • I also changed both trailing byte tests so the budget covers the full invalid payload. They now reach the trailing byte checks instead of the budget check.

@huitseeker
huitseeker force-pushed the trace-proving-inputs-serialization branch from c8d1bda to b661a6f Compare June 25, 2026 14:36
@huitseeker huitseeker added this to the v0.25.0 milestone Jun 25, 2026
@huitseeker
huitseeker force-pushed the trace-proving-inputs-serialization branch from 54df782 to cf221e4 Compare June 25, 2026 20:00
@bobbinth

Copy link
Copy Markdown
Contributor

Sparse MAST serialization is a trusted replay format. It preserves the source node IDs, roots, advice data, digest entries, and source forest commitment. It does not implement the untrusted hashless MAST read path (and does not verify node hashes).

Question: what would happen if someone sends a malformed MAST forest (i.e., with incorrect hashes) - would the prover error out or crush?

The reason for the question is that we have two separate settings for this:

  • Block/batch proof generation - where we trust that the block producer is not malicious and so a remote prover doesn't need to verify the inputs.
  • Transaction proof generation - here, any external user could send a request to prove a transaction to a remote prover. So, in theory, the proving inputs could be serialized incorrectly. Though, maybe this is something that could be caught at a higher level because we do need to enforce some limits (or collect some fees) from users who'd like to use remote provers.

@huitseeker

huitseeker commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

@bobbinth

Question: what would happen if someone sends a malformed MAST forest (i.e., with incorrect hashes) - would the prover error out or crush?

The prover would not crash. The bad digest is accepted into the sparse forest. build_trace may still succeed, because trace replay uses those stored digests. The STARK proof path does not call ExecutionTrace::check_constraints() before proving The likely developer-visible outcome is an invalid proof that fails verification, or a constraint failure only if they explicitly run check_constraints().

From the PR description:

Sparse MAST serialization is a trusted replay format. It preserves the source node IDs, roots, advice data, digest entries, and source forest commitment. It does not implement the untrusted hashless MAST read path (and does not verify node hashes).

The closest signal for this scope is code documentation, not an API boundary strong enough to prevent misuse.

I opened #3303 for the untrusted serialization.

@huitseeker
huitseeker force-pushed the trace-proving-inputs-serialization branch from cf221e4 to 0c4c6f4 Compare June 29, 2026 14:45

@bobbinth bobbinth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Looks good. Not a full review but I left some comments inline.

Comment thread prover/src/lib.rs
"TraceProvingInputs byte budget is smaller than payload length".into(),
));
}
let allocation_budget = budget.min(bytes.len().saturating_mul(4));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add a comment why multiplying by 4 is appropriate/sufficient here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #3314, which names the multiplier and documents the split between the byte budget and the allocation budget. It's the same pattern as the MastForest, UntrustedMastForest, Package, etc.

Comment thread prover/src/lib.rs
Comment on lines +86 to +90
if reader.has_more_bytes() {
return Err(SerdeDeserializationError::InvalidValue(
"TraceProvingInputs payload has trailing bytes".into(),
));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this check? Having it implies that we wouldn't be able to serialize TraceProvingInputs as a part of some bigger object, right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is in read_from_bytes() because that function reads one full standalone payload. A larger wrapper should call TraceProvingInputs::read_from(reader) inside its own reader, then the wrapper owns the final end-of-input check. #3314 documents that, and it is the same pattern used e.g. by serde_json or bincode for whole-slice vs reader-based deserialization.

Comment thread processor/src/trace/chiplets/ace/trace.rs
Comment thread processor/src/continuation_stack.rs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has grown too large - let's split it up and move the content under src/trace/trace_state directory. I would split it up along the component lines. For example:

  • src/trace/trace_state/system.rs
  • src/trace/trace_state/stack.rs
  • src/trace/trace_state/block_stack.rs
  • etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that trace_state.rs is large. I have found large mechanical diffs have a negative impact on some reviewers, so I would rather keep that split out of this stack. Happy to have a component split as a separate cleanup PR.

Comment on lines 51 to 63
pub struct TraceBuildInputs {
trace_output: TraceBuildOutput,
trace_generation_context: TraceGenerationContext,
program_info: ProgramInfo,
}

#[derive(Debug)]
pub(crate) struct TraceBuildOutput {
stack_outputs: StackOutputs,
final_precompile_transcript: PrecompileTranscript,
precompile_requests: Vec<PrecompileRequest>,
precompile_requests_digest: [u8; 32],
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but I find the structure of TraceBuildInputs and related structs pretty confusing. For example:

Do we actually need TraceBuildOutput? It seems like 3 out of 4 fields there are about precompiles and probably should be put into a dedicated struct. And once this is done, maybe it would make sense to dissolve this struct entirely?

Also, TraceBuildOutput doesn't need to be pub(crate) as it is used only within this module (and its submodules).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#3314 makes TraceBuildOutput private. I would leave the larger shape change for a follow-up because it is not needed for serialization.

Comment thread processor/src/trace/execution_tracer.rs
Comment thread processor/src/trace/execution_tracer.rs
Comment thread core/src/lib.rs
///
/// This uses the same wire shape as `Vec<T>`: a length prefix followed by items in iteration
/// order.
pub struct SerializableVecDeque<'a, T>(pub &'a VecDeque<T>);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are doing this because we don't have have serialization implemented for VecDeque in miden-crypto? If so, should we just implement it it there?

If we'd rather keep it here for now, I'd move it src/utils/mod.rs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#3314 moves the VecDeque helper to core::utils. I did not add it to miden-crypto for now because (besides release propagation delays) the only current caller is trace replay.

Comment thread core/src/mast/mod.rs
fn read_from<R: crate::serde::ByteReader>(
source: &mut R,
) -> Result<Self, DeserializationError> {
Ok(Self(<u32 as Deserializable>::read_from(source)?))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is safe. I remember previously we've always avoided deserializing MAST node IDs directly and preferred using from_u32_with_node_count(). I believe this was because MAST node ID values were limited to $2^{30} - 1$ and so a full u32 value could result in an invalid MAST node ID.

If this is not a concern now, we should explicitly document this. But also, I'm a bit weary of adding this serialization option as it could be easily misused.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #3313/#3314:

@huitseeker

huitseeker commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Closing this version because the change is too large as one PR. It has been replaced by three smaller PRs:

These keep the same code changes, split by scope.

@huitseeker huitseeker closed this Jun 30, 2026
@huitseeker
huitseeker deleted the trace-proving-inputs-serialization branch July 21, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement serialization for TraceProvingInputs

3 participants