p3_binary_pcs/error.rs
1//! Typed reasons an opening proof was rejected.
2
3use core::fmt::Debug;
4
5use thiserror::Error;
6
7use crate::transcript::TranscriptFailure;
8
9/// Why an opening proof was rejected.
10///
11/// Derives [`thiserror::Error`] and [`Debug`] only — not `PartialEq`/`Eq`. [`p3_commit::Mmcs`]
12/// carries no such bound on `Error` for a general MMCS (`commit/src/mmcs.rs`), so the derive
13/// cannot apply here.
14#[derive(Debug, Error)]
15#[non_exhaustive]
16pub enum BinaryPcsError<F, MmcsError> {
17 /// The proof carries a different number of intermediate folding rounds than the config
18 /// derives.
19 ///
20 /// Checked before any indexing into `rounds`, so a malformed proof is rejected rather
21 /// than desyncing the round loop that walks it.
22 #[error("expected {expected} folding rounds, proof carries {actual}")]
23 RoundCountMismatch { expected: usize, actual: usize },
24
25 /// A round opened a different number of rows than the sampled query count demands.
26 ///
27 /// Checked before any indexing into the opened rows, so a short or padded opening list
28 /// is rejected instead of being read out of bounds or silently truncated.
29 #[error("round {round} opened {actual} rows, expected {expected}")]
30 OpeningCountMismatch {
31 round: usize,
32 expected: usize,
33 actual: usize,
34 },
35
36 /// An opened row had the wrong width.
37 ///
38 /// Every committed round holds a width-1 codeword; a row of any other width would desync
39 /// the fold if it were read as-is rather than rejected up front.
40 #[error("round {round} row {query} has width {actual}, expected {expected}")]
41 RowWidthMismatch {
42 round: usize,
43 query: usize,
44 expected: usize,
45 actual: usize,
46 },
47
48 /// The final codeword's length disagrees with the fold schedule.
49 ///
50 /// Checked before any query reads from it, so an under- or over-sized final codeword is
51 /// rejected instead of being indexed out of bounds.
52 #[error("final codeword has {actual} symbols, expected {expected}")]
53 FinalCodewordLengthMismatch { expected: usize, actual: usize },
54
55 /// A Merkle multiproof did not verify.
56 #[error("Merkle opening failed in round {round}: {source:?}")]
57 MerkleFailed {
58 round: usize,
59 #[source]
60 source: MmcsError,
61 },
62
63 /// Folding the queried pair at one round does not reproduce the value read at the next
64 /// round (or, at the last round, in the final codeword).
65 ///
66 /// This is what ties every committed round to its neighbours; without it, a prover could
67 /// commit to codewords with no relation to one another at all.
68 #[error("round {round} query {query} is not the fold of the previous round")]
69 FoldMismatch { round: usize, query: usize },
70
71 /// The final codeword does not encode the value the sumcheck ended at.
72 #[error("the final codeword does not encode the sumcheck's final value")]
73 FinalCheck,
74
75 /// The grinding witness did not meet the demanded difficulty.
76 #[error("proof-of-work witness rejected")]
77 InvalidPowWitness,
78
79 /// The proof carries a different number of opening-protocol evaluation batches than the
80 /// public protocol schedules.
81 ///
82 /// Checked before any claim is registered, so a malformed proof is rejected before it can
83 /// desync the transcript one claim at a time.
84 #[error("expected {expected} opening batches, proof carries {actual}")]
85 OpeningBatchCountMismatch { expected: usize, actual: usize },
86
87 /// The scalar claim count exceeds the remaining alpha-batching budget.
88 #[error(
89 "opening protocol has {actual} claims, but at most {max} retain the configured {security_level}-bit opening budget"
90 )]
91 OpeningClaimCountExceedsSecurityBudget {
92 actual: usize,
93 max: usize,
94 security_level: usize,
95 },
96
97 /// The table dimensions do not fit the configured stacked polynomial.
98 #[error("opening protocol dimensions do not match the configured stacked polynomial")]
99 InvalidOpeningProtocol,
100
101 /// Prescribed points must match the batches and their table arities.
102 #[error("prescribed opening points do not match the opening protocol")]
103 OpeningPointShapeMismatch,
104
105 /// Supplied evaluations must cover every batch the protocol schedules.
106 ///
107 /// Reported apart from the points, which the same protocol also prescribes, so a caller
108 /// that got one of the two lists wrong learns which.
109 #[error("expected {expected} evaluation batches, {actual} supplied")]
110 OpeningEvalCountMismatch { expected: usize, actual: usize },
111
112 /// One opening batch has the wrong number of evaluations for its column list.
113 ///
114 /// A batch's shape is the pair of side lengths, not their total, so the two sides are
115 /// reported apart: a request and a list that agree on the total can still disagree here.
116 #[error(
117 "table {table_idx} opening expected {expected_current} direct and {expected_next} successor evaluations, got {actual_current} and {actual_next}"
118 )]
119 OpeningBatchSizeMismatch {
120 table_idx: usize,
121 expected_current: usize,
122 expected_next: usize,
123 actual_current: usize,
124 actual_next: usize,
125 },
126
127 /// The committed layout runs preprocessing rounds the opening pipeline does not lay out.
128 ///
129 /// The commit phase lays out a single committed column, so no round has a per-round
130 /// residual to read. Checked before any transcript operation, like every other shape
131 /// check here, so a layout the pipeline cannot open leaves the challenger alone.
132 #[error("the committed layout runs {folding} preprocessing rounds, expected none")]
133 OpeningPreprocessingDepth { folding: usize },
134
135 /// The sumcheck transcript did not verify.
136 #[error(transparent)]
137 Sumcheck(#[from] p3_sumcheck::SumcheckError),
138
139 /// The proof's sumcheck data carries PoW witnesses.
140 ///
141 /// Every fold round runs at `pow_bits = 0` (see `prover::fold_rounds_with`), and the verifier
142 /// replays each round with a freshly built, always-empty `pow_witnesses` vector, so
143 /// whatever the proof carries here is read by nothing and bound to nothing: a third party
144 /// could mutate it and keep a valid proof. `p3_sumcheck::ring_switch` rejects a non-empty
145 /// `pow_witnesses` for the same reason.
146 #[error("the sumcheck data carries {actual} PoW witnesses, expected none")]
147 NonEmptyPowWitnesses { actual: usize },
148
149 /// The grinding witness is not the value a zero difficulty budget admits.
150 ///
151 /// Checked before any transcript operation, like every other proof-shape check here.
152 //
153 // Why: at `pow_bits = 0` neither side touches the sponge.
154 //
155 // prover : grind(0) -> returns zero, absorbs nothing
156 // verifier: check_witness(0, w) -> returns true, absorbs nothing
157 //
158 // `pow_witness` is then bound to nothing: any value rides along and still verifies.
159 // Zero is the only value an honest prover emits, so zero is the only value accepted.
160 #[error("the grinding witness is {actual} at zero difficulty, expected zero")]
161 NonCanonicalPowWitness { actual: F },
162}
163
164impl<F: Debug, E: Debug> From<TranscriptFailure<F>> for BinaryPcsError<F, E> {
165 fn from(failure: TranscriptFailure<F>) -> Self {
166 match failure {
167 TranscriptFailure::FinalCodewordLength { expected, got } => {
168 Self::FinalCodewordLengthMismatch {
169 expected,
170 actual: got,
171 }
172 }
173 TranscriptFailure::PowWitness { .. } => Self::InvalidPowWitness,
174 TranscriptFailure::NonCanonicalPowWitness { actual } => {
175 Self::NonCanonicalPowWitness { actual }
176 }
177 }
178 }
179}