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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
//! Public construction helpers for operation chains.
//!
//! Every `ChainSpec` carries a [`ProofToken`] that encodes which algebraic
//! laws survive the composition. The token is load-bearing: the dispatch
//! pipeline refuses to lower any chain whose attached token does not
//! match one recomputed from the chain's own specs.
//!
//! # The trust boundary
//!
//! The blessed constructor is [`ChainSpec::from_specs`] — it derives the
//! proof token internally from the provided specs, so a caller cannot
//! attach a bogus token that claims preserved laws the specs do not
//! actually compose to. Every in-crate call site and every downstream
//! user should prefer `from_specs`.
//!
//! [`ChainSpec::new`] is kept as a legacy constructor so pre-existing
//! callers do not break, but it now returns `Err` when the passed token
//! does not match the derived one. The pipeline's runtime
//! `pipeline::proof::validate_chain` still catches any mismatch before
//! dispatch, so there is no path from a chain with a corrupted proof to
//! a GPU dispatch — the construction-time check is the early-warning
//! system, and the pipeline check is the floor.
use crate::spec::types::{
ChainSpec, ConstructionTime, CpuReferenceFn, OpSignature, OpSpec, ProofToken, ProofTokenError,
};
impl ChainSpec {
/// Build a chain specification, deriving the composition proof token
/// from the provided specs. This is the blessed constructor: it is
/// impossible for a caller to attach a bogus proof because the token
/// is computed here from the authoritative source (`specs`).
///
/// Prefer this over [`ChainSpec::new`] in every new call site.
#[inline]
pub fn from_specs(
id: String,
ops: Vec<&'static str>,
signature: OpSignature,
specs: Vec<OpSpec>,
cpu_chain: Option<CpuReferenceFn>,
) -> Result<Self, ProofTokenError> {
validate_ops_match_specs(&id, &ops, &specs)?;
let proof_token = ProofToken::from_specs(&specs, ConstructionTime::Manual)?;
Ok(Self {
id,
ops,
signature,
specs,
cpu_chain,
proof_token,
})
}
/// Construct a chain with an explicit, possibly-wrong, proof token
/// — bypassing the construction-time check entirely.
///
/// **This is an escape hatch.** It exists for two narrow use cases:
///
/// 1. **Testing the runtime gate.** A test that wants to construct
/// a deliberately-wrong chain to verify the dispatch-time
/// `pipeline::proof::validate_chain` catches it must bypass the
/// construction-time assert; otherwise the assert fires before
/// the runtime gate has a chance to run.
/// 2. **Forensic replay.** When loading a serialized chain whose
/// proof token may legitimately differ from a recomputed one
/// (e.g., the spec set has changed since the chain was frozen),
/// the loader needs to reconstruct the chain as-was without
/// asserting on a token that no longer matches.
///
/// Production code paths and contributors writing new ops MUST use
/// [`ChainSpec::from_specs`]. Reaching for `new_unchecked` outside
/// the two cases above is a code smell — it deliberately disables
/// the construction-time discipline that makes the proof system
/// honest.
#[must_use]
#[inline]
pub fn new_unchecked(
id: String,
ops: Vec<&'static str>,
signature: OpSignature,
specs: Vec<OpSpec>,
cpu_chain: Option<CpuReferenceFn>,
proof_token: ProofToken,
) -> Self {
Self {
id,
ops,
signature,
specs,
cpu_chain,
proof_token,
}
}
/// Legacy constructor: take an explicit construction-time proof.
///
/// This verifies the passed token equals `ProofToken::from_specs(&specs, _)`.
/// A caller that constructs a wrong token fails fast at the call site
/// rather than shipping a chain that would silently fail the pipeline's
/// proof check later.
///
/// New code should use [`ChainSpec::from_specs`] instead.
#[inline]
pub fn new(
id: String,
ops: Vec<&'static str>,
signature: OpSignature,
specs: Vec<OpSpec>,
cpu_chain: Option<CpuReferenceFn>,
proof_token: ProofToken,
) -> Result<Self, ProofTokenError> {
validate_ops_match_specs(&id, &ops, &specs)?;
let expected = ProofToken::from_specs(&specs, proof_token.computed_at)?;
if !proof_token.theorem_claims_match(&expected) {
return Err(ProofTokenError::VerificationFailed(format!(
"ChainSpec::new: proof token mismatch for chain `{id}`. Fix: use \
ChainSpec::from_specs which derives the token from specs, or \
recompute ProofToken::from_specs(&specs, _) before passing."
)));
}
Ok(Self {
id,
ops,
signature,
specs,
cpu_chain,
proof_token,
})
}
}
fn validate_ops_match_specs(
chain_id: &str,
ops: &[&'static str],
specs: &[OpSpec],
) -> Result<(), ProofTokenError> {
if ops.len() != specs.len() {
return Err(ProofTokenError::VerificationFailed(format!(
"ChainSpec `{chain_id}` has {} op ids but {} specs. Fix: pass one op id for each spec, in the same order.",
ops.len(),
specs.len()
)));
}
for (index, (op_id, spec)) in ops.iter().zip(specs.iter()).enumerate() {
if *op_id != spec.id {
return Err(ProofTokenError::VerificationFailed(format!(
"ChainSpec `{chain_id}` op id mismatch at index {index}: ops has `{op_id}` but specs has `{}`. Fix: build chains with ops[i] == specs[i].id.",
spec.id
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::spec::types::{ChainSpec, ConstructionTime, OpSignature, OpSpec, ProofToken};
fn empty_signature() -> OpSignature {
OpSignature {
inputs: Vec::new(),
output: crate::spec::types::DataType::U32,
}
}
fn dummy_cpu(_input: &[u8]) -> Vec<u8> {
vec![0, 0, 0, 0]
}
fn dummy_wgsl() -> String {
String::new()
}
#[test]
fn from_specs_derives_proof_token_internally() {
let chain = ChainSpec::from_specs(
"empty-chain".to_string(),
Vec::new(),
empty_signature(),
Vec::<OpSpec>::new(),
None,
)
.unwrap();
let expected = ProofToken::from_specs(&chain.specs, ConstructionTime::Manual).unwrap();
assert!(
chain.proof_token.theorem_claims_match(&expected),
"ChainSpec::from_specs must attach a token equal to \
ProofToken::from_specs(&chain.specs, _); otherwise the gate can \
be bypassed by a wrong construction-time token."
);
}
#[test]
fn new_matches_from_specs_on_correct_token() {
let specs = Vec::<OpSpec>::new();
let token = ProofToken::from_specs(&specs, ConstructionTime::Manual).unwrap();
let chain = ChainSpec::new(
"correct-token".to_string(),
Vec::new(),
empty_signature(),
specs,
None,
token.clone(),
)
.expect("ChainSpec::new must accept a correctly-derived proof token");
assert!(chain.proof_token.theorem_claims_match(&token));
}
#[test]
fn from_specs_rejects_mismatched_op_ids() {
let spec = OpSpec::builder("primitive.math.add")
.signature(empty_signature())
.cpu_fn(dummy_cpu)
.wgsl_fn(dummy_wgsl)
.category(crate::Category::A {
composition_of: vec!["primitive.math.add"],
})
.laws(Vec::new())
.strictness(crate::spec::types::Strictness::Strict)
.version(1)
.build()
.expect("Fix: test OpSpec fixture must satisfy builder invariants.");
let result = ChainSpec::from_specs(
"bad-chain".to_string(),
vec!["primitive.math.sub"],
empty_signature(),
vec![spec],
None,
);
let err = match result {
Ok(_) => panic!("Fix: mismatched ChainSpec ops/specs must fail loudly."),
Err(err) => err,
};
assert!(
err.to_string().contains("op id mismatch"),
"Fix: mismatched ChainSpec ops/specs must fail loudly, got {err}"
);
}
}