|
1 | | -""" |
2 | | -Mock the Rust aggregation prover at the FFI boundary, from the test layer. |
3 | | -
|
4 | | -Most vectors do not test proofs, yet building one costs a recursive SNARK merge. |
5 | | -
|
6 | | -Mocking lets the fast scheme skip that work while leaving the spec untouched. |
7 | | -
|
8 | | -Vectors whose purpose is proof validity opt back in with the real_crypto marker. |
9 | | -""" |
| 1 | +"""Mock the Rust aggregation prover at the FFI boundary, from the test layer.""" |
10 | 2 |
|
11 | 3 | import hashlib |
12 | 4 | from collections.abc import Callable, Iterator |
@@ -52,87 +44,39 @@ def get_mode(cls) -> CryptoMode: |
52 | 44 | """Return the process-wide crypto mode.""" |
53 | 45 | return cls._mode |
54 | 46 |
|
55 | | - @staticmethod |
56 | | - def _placeholder_proof(positional_args: tuple[object, ...]) -> bytes: |
57 | | - """ |
58 | | - Build a deterministic placeholder proof bound to a call's inputs. |
59 | | -
|
60 | | - Length framing keeps distinct inputs distinct. |
61 | | -
|
62 | | - So two different blocks never share proof bytes by accident. |
63 | | - """ |
64 | | - digest = hashlib.sha256() |
65 | | - stack: list[object] = [positional_args] |
66 | | - while stack: |
67 | | - value = stack.pop() |
68 | | - if isinstance(value, bool): |
69 | | - digest.update(b"b\x01" if value else b"b\x00") |
70 | | - elif isinstance(value, int): |
71 | | - encoded = str(value).encode() |
72 | | - digest.update(b"i" + len(encoded).to_bytes(8, "big") + encoded) |
73 | | - elif isinstance(value, bytes): |
74 | | - digest.update(b"y" + len(value).to_bytes(8, "big") + value) |
75 | | - elif isinstance(value, (list, tuple)): |
76 | | - digest.update(b"s" + len(value).to_bytes(8, "big")) |
77 | | - stack.extend(reversed(value)) |
78 | | - elif value is None: |
79 | | - digest.update(b"n") |
80 | | - else: |
81 | | - raise TypeError(f"unmockable prover argument of type {type(value).__name__}") |
82 | | - return MOCK_PROOF_PREFIX + digest.digest() |
83 | | - |
84 | 47 | @classmethod |
85 | 48 | @contextmanager |
86 | 49 | def mocked(cls) -> Iterator[None]: |
87 | 50 | """ |
88 | 51 | Swap the Rust prover bindings on the spec module for in-process stubs. |
89 | 52 |
|
90 | | - - Provers return a placeholder proof; |
| 53 | + - Provers return a placeholder proof. |
91 | 54 | - Verifiers accept that placeholder. |
92 | | -
|
93 | | - The spec's own structural checks still run; only the Rust call is skipped. |
94 | | - A real proof reaching the mocked verifier falls through to the real one. |
95 | 55 | """ |
96 | 56 | originals = { |
97 | 57 | name: getattr(aggregation, name) for name in (*cls._prover_names, *cls._verifier_names) |
98 | 58 | } |
99 | 59 |
|
100 | | - def prove(*positional_args: object, **_keyword_args: object) -> tuple[None, bytes]: |
101 | | - return None, cls._placeholder_proof(positional_args) |
| 60 | + def prove( |
| 61 | + *positional_arguments: object, **_keyword_arguments: object |
| 62 | + ) -> tuple[None, bytes]: |
| 63 | + # Distinct inputs hash to distinct bytes, so two blocks never collide. |
| 64 | + fingerprint = repr(positional_arguments).encode() |
| 65 | + return None, MOCK_PROOF_PREFIX + hashlib.sha256(fingerprint).digest() |
102 | 66 |
|
103 | 67 | def make_verifier(real_verifier: Callable[..., None]) -> Callable[..., None]: |
104 | | - def verify(*positional_args: object, **keyword_args: object) -> None: |
105 | | - # A placeholder proof is recognized by its sentinel prefix alone. |
106 | | - # Acceptance is then an unconditional no-op. |
107 | | - # The message, slot, and public keys go unchecked. |
108 | | - # |
109 | | - # This is deliberately weaker than recompute-and-compare. |
110 | | - # The placeholder is content-bound when the prover builds it. |
111 | | - # But the prover hashes inputs the verifier never receives. |
112 | | - # |
113 | | - # The single-message prover folds in the raw signatures. |
114 | | - # It also folds in the child proofs and the rate exponent. |
115 | | - # The verifier only sees the public keys, message, slot, and proof. |
116 | | - # |
117 | | - # Merging and splitting reshape a proof after the fact. |
118 | | - # Its bytes then match no single prover call the verifier could replay. |
119 | | - # So reconstructing the placeholder to compare it is infeasible. |
120 | | - # |
121 | | - # Invariant: this no-op acceptance stays sound only because every |
122 | | - # vector asserting proof validity or rejection carries the |
123 | | - # real_crypto marker and runs against the real prover, not the mock. |
124 | | - # A proof-rejection vector lacking that marker would silently pass |
125 | | - # here, even though a conforming client must reject it. |
126 | | - # Never add a proof-tamper vector under the mock; mark it real_crypto. |
| 68 | + def verify(*positional_arguments: object, **keyword_arguments: object) -> None: |
| 69 | + # Accept any sentinel-prefixed placeholder unchecked. |
| 70 | + # Invariant: sound only because proof vectors carry the real-crypto marker. |
127 | 71 | carries_placeholder = any( |
128 | 72 | isinstance(argument, bytes) and argument.startswith(MOCK_PROOF_PREFIX) |
129 | | - for argument in positional_args |
| 73 | + for argument in positional_arguments |
130 | 74 | ) |
131 | 75 | if carries_placeholder: |
132 | 76 | return None |
133 | | - # A real proof can still arrive when a vector mixes real and mocked |
134 | | - # inputs, so fall through to the real verifier for those bytes. |
135 | | - return real_verifier(*positional_args, **keyword_args) |
| 77 | + # A vector mixing real and mocked inputs can still pass real bytes. |
| 78 | + # Fall through to the real verifier for those. |
| 79 | + return real_verifier(*positional_arguments, **keyword_arguments) |
136 | 80 |
|
137 | 81 | return verify |
138 | 82 |
|
|
0 commit comments