Skip to main content

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}