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