B1 (R15-05): the published MTP/DRSample primitive reproduced, with its complete verifier and fixtures
Blocki and Smearsoll (eprint 2025/1456 as archived, sha256 13c3646e...) Sec 2.2, 3.2 and App. C over DRSample (ABH17 Alg. 1, eprint 2017/443 as archived): CPython reference in tools/mhpow/b1 (params, drsample, mtp, attacks), the verifier's named checks in the paper's order with a known-failed test each (17 of 17 OK on build-6), five deterministic known-answer fixtures (n 4 to 20), honest rows n 12 to 24, the malicious provers A1 to A6 (Dinur-Nadler eprint 2017/497 Sec 4.1, 4.3/5, 8; the paper's Attack 1; the LuckyQuery regrind) and the k table. Encoding pinned with B2 (BLAKE2b-256, tags L/M/C/G, u64 LE, raw leaves, LSB-at-root, public graph seed). Findings: proven k with ABH17's constant exceeds N to about n 25 (F-3); one labelling yields many accepted roots (F-4); lambda 256 meets Lemma 3 only for q <= 2^31.5 (F-5). Research paths added to tools/ci/export-exclude.txt beside B2's. Registry batch NOT RUN for the steward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
206e73f70e
commit
29d7bfebd1
31 changed files with 3774 additions and 0 deletions
290
docs/analysis/mhpow/b1/README.md
Normal file
290
docs/analysis/mhpow/b1/README.md
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
# B1 (R15-05): the published MTP/DRSample primitive, reproduced
|
||||
|
||||
Track B of the 1.5x Research Programme, item B1, 9 October 2026. Lane: the cryptography lane. Box: build-6 (32 vCPU,
|
||||
CPU only). Status of every row: NOT RUN until the panel reads it. This lane writes no PASS.
|
||||
|
||||
What this is: an exact CPU reference of the Merkle Tree Proof framework of Blocki and Smearsoll (TCC 2025) over the
|
||||
DRSample graph, its complete verifier, known-answer fixtures, honest-prover rows and malicious-prover fixtures. It is a
|
||||
control and a specification. It is not an Igneum work unit, it carries no lottery, and nothing here activates.
|
||||
|
||||
## 1. Sources (full PDFs, fetched on build-6, sha256 of the bytes as fetched)
|
||||
|
||||
eprint.iacr.org answers the build boxes with a Cloudflare managed challenge. No check was bypassed: the eprint PDFs
|
||||
were taken from the Internet Archive's snapshot of the eprint URL (`web.archive.org/web/<date>id_/https://eprint.iacr.org/<id>.pdf`).
|
||||
|
||||
| key | paper | source as fetched | sha256 | pages |
|
||||
|---|---|---|---|---|
|
||||
| [BS25] | Blocki, Smearsoll, Provably Memory-Hard Proofs of Work With Memory-Easy Verification, TCC 2025 | eprint 2025/1456, archive snapshot 2025-12-31T15:20:15Z | `13c3646e7d85c1aa58a92914582caab5798d90cf2a3cad33e58d81d49c1d31db` | 46 |
|
||||
| [DN17] | Dinur, Nadler, Time-Memory Tradeoff Attacks on the MTP Proof-of-Work Scheme, CRYPTO 2017 | eprint 2017/497, archive snapshot 2026-05-09T02:34:47Z | `d2fd7774535dd3226bb4a767d4930a5c3a91ac6ed2f1421ee511b66ea37b0b03` | 30 |
|
||||
| [ABH17] | Alwen, Blocki, Harsha, Practical Graphs for Optimal Side-Channel Resistant Memory-Hard Functions, CCS 2017 | eprint 2017/443, archive snapshot 2025-12-10T21:05:18Z | `f8e08d361118120c905550d3ac63efffe2a806d68276b070c24e7aa15d8fa8eb` | 17 |
|
||||
| [BK16] | Biryukov, Khovratovich, Egalitarian computing (MTP 1.2) | arXiv 1606.03588v2 | `5dca4aef485f480cf010b744a9c0252a8460dd4a31152ffedeba70ef77399623` | 16 |
|
||||
|
||||
The [BS25] bytes were fetched twice, once by the B3 lane and once by this lane from the same snapshot. Both reads gave the
|
||||
same sha256. The copies are at `build-6:/srv/builds/mhpow-b1/papers/`. Row O-3 records that each is "the revision as
|
||||
archived" and has not been checked against eprint's current revision.
|
||||
|
||||
What each source supplies to the code:
|
||||
|
||||
| what | where in the paper | code |
|
||||
|---|---|---|
|
||||
| labelling game, local consistency, parents in ascending order | [BS25] Sec 2.2 | `mtp.label_of`, `mtp.compute_labels` |
|
||||
| the framework: Prove steps 1 to 4, Verify steps 1 to 4 | [BS25] Sec 3.2 | `mtp.prove_commit`, `mtp.respond`, `mtp.verify` |
|
||||
| Merkle construction, reveal, check | [BS25] Sec 3.2 step 2, Sec 4.1, App. C.1, Def. 15, App. C.2 | `mtp.MerkleTree`, `MerkleTree.reveal`, `mtp.merkle_tree_check` |
|
||||
| challenges c_i = H(chi, i, tau) mod N | [BS25] Sec 3.2 step 3, Sec 5 Def. 14 | `mtp.challenges` |
|
||||
| soundness chain implemented against | [BS25] Theorem 4, Theorem 6, Theorem 7, Corollary 1, Corollary 2, Fact 8, Corollary 3 | section 6 (k), section 7 (attacks) |
|
||||
| the paper's own attack | [BS25] Sec 7.1 Attack 1 | `attacks.a5_attack1` |
|
||||
| the DRSample edge rule (cited by [BS25] Sec 6.1, defined only here) | [ABH17] Sec 3, Algorithm 1 | `drsample.get_parent`, `drsample.parents` |
|
||||
| DRSample's proven depth-robustness constants | [ABH17] Theorem 3.1 | `k_table.py` |
|
||||
| the attacks on MTP-Argon2d | [DN17] Sec 4.1, 4.3, 5, 8 | `attacks.a1` to `a4` |
|
||||
|
||||
## 2. Parameters and the decisions the paper leaves open
|
||||
|
||||
[BS25] fixes the construction abstractly. It has a random oracle H with lambda-bit output, labels H(chi, v, parent
|
||||
labels), Merkle nodes H(chi, left, right) and challenges H(chi, i, tau) mod N. It names no hash, no lambda, no byte
|
||||
encoding, no domain tag, and no concrete N or k. Every such choice below is this lane's. Each is pinned in
|
||||
`tools/mhpow/b1/params.py` with its reason. The B2 lane agreed to them before the first fixture was cut and moved its instance layout to them.
|
||||
|
||||
| symbol | value | basis |
|
||||
|---|---|---|
|
||||
| H | BLAKE2b, 32-byte digest, unkeyed | B1 decision: one oracle for B1, B2 (chi) and B3 (GPU oracle) |
|
||||
| lambda | 256 | B1 decision; see row O-4 for what [BS25] Lemma 3 then covers |
|
||||
| chi | 32 bytes (B2: BLAKE2b-256('I' \|\| 328-byte instance)) | [BS25] Def. 5 input string; layout is B2's |
|
||||
| domain tags | 'L' label, 'M' Merkle node, 'C' challenge, 'G' graph draw ('I' is B2's) | B1 decision on top of the paper's salt-only separation (finding F-1) |
|
||||
| integers | node index v and challenge index i as u64 little-endian | B1 decision |
|
||||
| G | DRSample(N), [ABH17] Alg. 1, indegree 2, sampled once from a public 32-byte seed | [BS25] Sec 1.2 needs G fixed a priori (finding F-2) |
|
||||
| N | 2^n with n = 4, 8, 12, 16, 20 (fixtures) and 12 to 24 (rows) | the paper states no N (row O-2) |
|
||||
| k | 8 at n = 4, 64 otherwise | fixture sizes only, NOT a security level (row O-1) |
|
||||
| leaves | the raw 32-byte labels, not hashed | [BS25] Sec 3.2 step 2: tau_v = l_{1+bin(v)} |
|
||||
| tree layout | the bit taken at depth j from the root is bit j-1 of the leaf index | [BS25] Sec 3.2: bin(v) = sum v_i 2^(i-1) (row R-3) |
|
||||
| openings | per challenge: node c_i + 1 and each of its parents, each with its own full path | [BS25] Sec 3.2 step 3(a)(b); no path sharing |
|
||||
|
||||
Exact query byte layouts (every query is one BLAKE2b block, 41 to 105 bytes):
|
||||
|
||||
```
|
||||
label, node 1 H('L' || chi || u64(1))
|
||||
label, node v >= 2 H('L' || chi || u64(v) || l_p1 || ... ) parents ascending; node 2 has the one parent 1
|
||||
Merkle inner node H('M' || chi || left || right)
|
||||
challenge i (1..k) c_i = int_le(H('C' || chi || u64(i) || tau)) mod N opened node c_i + 1, leaf index c_i
|
||||
graph draw H('G' || graph_seed || u64(v) || u8(which) || u64(ctr)) first 8 bytes, rejection sampling
|
||||
proof container "B1MTPDR1" | n u8 | k u32le | tau 32 | per challenge: label 32 | n x 32 | count u8 | per parent: label 32 | n x 32
|
||||
```
|
||||
|
||||
graph_seed = BLAKE2b-256("igneum/mhpow/b1/drsample-graph-seed/v1") =
|
||||
`params.GRAPH_SEED` (the fixtures carry it in hex).
|
||||
|
||||
## 3. The construction in words, tied to the code
|
||||
|
||||
1. Graph ([ABH17] Alg. 1; `drsample.parents`). Nodes 1..N. Node 1 has no parent and node 2 has the parent 1. Every
|
||||
node v >= 3 has the parents v - 1 and v - r. To pick r, first draw g' uniformly from [1, floor(log2 v) + 1] and set
|
||||
g = min(v - 1, 2^g'). Then draw r uniformly from [max(ceil(g/2), 2), g]. So r >= 2 and the two parents are distinct.
|
||||
The draws come from the public seed, so the graph is a fixed public object, never a function of chi.
|
||||
2. Labels ([BS25] Sec 2.2, Sec 3.2 step 1; `mtp.compute_labels`). In topological order, l_v = H('L', chi, v, parent
|
||||
labels). This is one sequential pass of N oracle calls, holding 32N bytes.
|
||||
3. Commitment ([BS25] Sec 3.2 step 2, App. C.1; `mtp.MerkleTree`). A binary tree of depth n over the raw labels. The
|
||||
tree is salted by chi, and the leaf for node w sits at leaf index w - 1 with LSB-at-root position bits. tau is the root.
|
||||
4. Challenges ([BS25] Sec 3.2 step 3; `mtp.challenges`). c_i = H('C', chi, i, tau) mod N for i = 1..k. N divides
|
||||
2^256, so the reduction is exactly uniform.
|
||||
5. Proof ([BS25] Sec 3.2 step 3(a)(b) and step 4; `mtp.respond`, `Proof.to_bytes`). For each i, the proof carries
|
||||
l_{c_i+1} with its n siblings, then each parent's label with its n siblings. No positions are carried: the verifier
|
||||
derives every position.
|
||||
6. Verifier ([BS25] Sec 3.2 Verify; `mtp.verify`). Each check is a named function. They run in the paper's order and the
|
||||
first failure is returned by name:
|
||||
|
||||
| check | paper | refuses |
|
||||
|---|---|---|
|
||||
| `check_0_container` | B1 addition (parsing) | wrong N, wrong k, truncation, trailing bytes, bad magic |
|
||||
| `check_1_challenges` | Verify step 1 | (derives c'_i from the proof's tau; it cannot fail, it binds positions) |
|
||||
| `check_2a_parent_set` | Verify step 2 (separating L into the parents of c_i + 1) | a skipped or extra parent opening |
|
||||
| `check_2b_merkle_openings` | Verify step 2, App. C.2 MerkleTreeCheck | a wrong label, wrong path, wrong position, wrong chi, wrong Merkle tag |
|
||||
| `check_3_local_consistency` | Verify step 3 | a red challenged node (including a label made with the wrong tag) |
|
||||
| accept | Verify step 4 | |
|
||||
|
||||
Known-failed tests (`tools/mhpow/b1/test_b1.py`). The suite has 17 tests and all 17 ran OK on build-6. Each refusal is
|
||||
asserted by the name of the check that fires:
|
||||
|
||||
| test | the wrong thing | refused by |
|
||||
|---|---|---|
|
||||
| test_wrong_label | one bit of a challenged label | check_2b_merkle_openings |
|
||||
| test_wrong_parent_label | one bit of a parent label | check_2b_merkle_openings |
|
||||
| test_wrong_path | one bit of one sibling hash | check_2b_merkle_openings |
|
||||
| test_wrong_challenge | responses for i = 2..k+1 instead of 1..k | check_2a_parent_set or check_2b_merkle_openings |
|
||||
| test_skipped_parent | one parent opening dropped | check_2a_parent_set |
|
||||
| test_duplicated_parent_position | both openings for the same parent ([BK16] App. B, Bevand's attack 2 class) | check_2b_merkle_openings |
|
||||
| test_wrong_domain_tag_label | the whole array labelled with tag 'M', committed honestly | check_3_local_consistency |
|
||||
| test_wrong_domain_tag_merkle | the tree built with tag 'L' | check_2b_merkle_openings |
|
||||
| test_wrong_chi | a valid proof verified against another chi | check_2b_merkle_openings |
|
||||
| test_container | truncated, extended, wrong k, wrong n | check_0_container |
|
||||
| test_every_check_has_a_refusal | each refusing check shown to fire at least once | all four |
|
||||
| graph and Merkle tests | edge rule and bucket ranges, determinism, bucket spread, every-leaf reveal and check, LSB-at-root layout | |
|
||||
|
||||
## 4. Known-answer fixtures
|
||||
|
||||
Command (build-6, under `/srv/builds/mhpow-b1/run.pid`, mirrored to `build-1:/srv/queue/pids/build-6-b1.pid`):
|
||||
`tools/mhpow/b1/run-b1.sh /srv/builds/mhpow-b1/out-final2 all` (09:33:52Z to 09:36:50Z). The fixtures are deterministic.
|
||||
Four runs (`out`, `out-repeat`, `out-final`, `out-final2`) produced byte-identical kat files, and two runs byte-identical attack
|
||||
files (attacks-n10-k16.json sha256 `805035ebfc8d2f34441325b5d5ca46c67e81f7d0b0b86f62ba1a906964b8c51f`). chi for the fixture at n is
|
||||
BLAKE2b-256("B1-fixture" || "kat" || u64(n)).
|
||||
|
||||
| fixture | n | k | tau (first 16 hex) | proof bytes | json sha256 | proof.bin sha256 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| kat-n04 | 4 | 8 | 9f98a28d3d84dbdc | 3,893 | `9f85b22ae54785e0e078630f890036713cf3925b7e1345bd949c6945d2702f7f` | `481a39fa47ff4d3393f34e15e318ee4bda7683ee457d3b9da79d277c9ba2831d` |
|
||||
| kat-n08 | 8 | 64 | 90cb87eabf42f4c1 | 55,405 | `2c741be9a5a7c47773f902ed024c49188eacfc84f9cabdf34244f289e77f51f7` | `d45a9756b8d66357eaee34f1efa3b5fc975e25fcf7b7eb5a516746c6e078bbc7` |
|
||||
| kat-n12 | 12 | 64 | 884d36cd2df99fa9 | 79,981 | `51ccacd3030d97ec371d8cc8ea64fbb362f00bd323bd84827105c89b4eb30864` | `3c7de0539a8715e75d3d64eb11ec3af7d8a2a338c101958a0c29e300c3ad4f7f` |
|
||||
| kat-n16 | 16 | 64 | 41e05146c5819b21 | 104,557 | `240f50bc5ac5e12c7630f24c072bf291fb6ab9644530badcd1c41c7a7341440c` | `793fd4d505155f133c8acf1c657a98795f29f7d6cba2568570c8324c42fb1118` |
|
||||
| kat-n20 | 20 | 64 | e3d83489e27af44b | 129,133 | `fd0d75b66af9cbd9ef12e63d571036f41bdac0e1d21e1f4d98559deb98502392` | `a49367d9c80f0855504fe1f0994acbd5ddc4b023eaf5904a28ad5f7e71ce951c` |
|
||||
|
||||
kat-n04 and kat-n08 carry every parent, every label and the proof inline. kat-n12 to kat-n20 carry chi, tau, the
|
||||
challenges, the sha256 of the parent table (u64le), the sha256 of the label array and the proof sha256. The verifier
|
||||
accepted each one from bytes. The fixture files are in `tools/mhpow/b1/fixtures/` with their `SHA256SUMS`.
|
||||
|
||||
## 5. Honest-prover rows (no hash-rate comparison anywhere)
|
||||
|
||||
Software: `tools/mhpow/b1` at this branch, CPython 3.12.3, single thread. Hardware: build-6 (Hetzner Cloud ccx53,
|
||||
32 vCPU), shared with two other lanes' fuzz and sweep jobs at the time, so the times are loaded wall times. Boundary:
|
||||
process wall clock only, no power measured, stock clocks. These are a reference implementation's times. They are not a
|
||||
GPU figure, an optimised figure or an energy figure. Command: `run-b1.sh <out> rows` (one process per N, so each RSS is
|
||||
its own). Rows: `fixtures/row-nXX-k64.json`.
|
||||
|
||||
| n | N | prover sequential s (labels + Merkle + proof) | labels s | Merkle s | graph sample s (one-time, public) | prover state model bytes (96N - 32) | peak RSS measured | proof bytes (k 64) | verify s (from bytes) | verifier oracle calls |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 12 | 4,096 | 0.010 | 0.004 | 0.006 | 0.013 | 393,184 | 19.7 MB | 79,981 | 0.0027 | 2,432 |
|
||||
| 16 | 65,536 | 0.180 | 0.062 | 0.115 | 0.204 | 6,291,424 | 39.9 MB | 104,557 | 0.0032 | 3,200 |
|
||||
| 20 | 1,048,576 | 3.48 | 1.03 | 2.41 | 3.31 | 100,663,264 | 355.7 MB | 129,133 | 0.0040 | 3,968 |
|
||||
| 22 | 4,194,304 | 14.91 | 4.35 | 10.39 | 13.80 | 402,653,152 | 1,365.7 MB | 141,421 | 0.0483 | 4,352 |
|
||||
| 24 | 16,777,216 | 66.97 | 17.54 | 48.69 | 52.28 | 1,610,612,704 | 5,407.2 MB | 153,709 | 0.0047 | 4,736 |
|
||||
|
||||
What the rows mean:
|
||||
- Honest work is linear in N, as [BS25] Sec 3.2 "Efficiency" states (O(N + k log N) sequential). 2N - 1 + k oracle calls
|
||||
are made in one pass, and the label array (32N bytes) is held for the whole pass. The CPython RSS is about 3.4 times
|
||||
the 96N-byte model because of object overhead. A packed implementation holds 32N bytes of labels plus 64N bytes of
|
||||
tree, or 32N if the tree is streamed and the openings are recomputed. That trade-off is B3's to measure.
|
||||
- The proof grows with n: proof bytes = 45 + k (1 + 96 (n + 1)) when every challenged node has indegree 2 (the exact
|
||||
formula is `mtp.proof_bytes_formula`). Verification is k (3n + 2) oracle calls plus 2k graph draws. It needs no
|
||||
memory beyond the proof and no table.
|
||||
- The n = 22 verify time (0.047 and 0.048 s in two runs, against 0.004 to 0.005 s at n = 20 and n = 24) is not the
|
||||
verifier's work. Its oracle-call count rises smoothly (4,352). The cause inside the measuring CPython process is not
|
||||
established. A clean cold-verification budget at an admitted rate is B6's row (O-11).
|
||||
- Per user tier: a GPU miner never runs this CPython path. What carries over is the shape: one N-label sequential pass
|
||||
per instance (about 96 MiB of state at n = 20, about 1.5 GiB at n = 24 with the tree held) and a proof of 129 KB
|
||||
(n = 20, k = 64) that a validator checks with about 4,000 BLAKE2b calls. The B3 lane owns the GPU adaptation and B6
|
||||
the node budget.
|
||||
|
||||
## 6. The challenge count k: proven against attacked (code-generated)
|
||||
|
||||
Command: `python3 tools/mhpow/b1/k_table.py <out>`. Output: `fixtures/k-table.json`.
|
||||
|
||||
(a) PROVEN. [BS25] Corollary 2 gives k = ceil(2 N ln2 lambda / e) for an (e, d)-depth-robust G. [ABH17] Theorem 3.1
|
||||
gives DRSample, with high probability over the sample, (e, d, b)-block depth-robustness with e >= 2.43 x 10^-4 N / log N
|
||||
and d >= 0.03 N. Block depth-robustness implies depth-robustness. Here log is read as log2 (row R-4). At lambda = 256:
|
||||
|
||||
| n | k (Cor. 2 with [ABH17]'s e) | k / N | proof bytes at that k | proof / label array |
|
||||
|---|---|---|---|---|
|
||||
| 12 | 17,525,500 | 4,279 | 21.9 GB | 167,002 x |
|
||||
| 16 | 23,367,333 | 357 | 38.2 GB | 18,196 x |
|
||||
| 20 | 29,209,166 | 27.9 | 58.9 GB | 1,756 x |
|
||||
| 24 | 35,050,999 | 2.09 | 84.2 GB | 157 x |
|
||||
| 30 | 43,813,748 | 0.041 | 130.4 GB | 3.8 x |
|
||||
| 32 | 46,734,665 | 0.011 | 148.1 GB | 1.08 x |
|
||||
|
||||
FINDING F-3: with the only proven DRSample constant in the literature cited, the theorem's k exceeds N up to about
|
||||
n = 25. The certificate exceeds the whole label array up to n = 32. So the proven regime has no practical parameter
|
||||
today. The paper says the constants are not tight. A usable k needs a better proven constant or an accepted heuristic
|
||||
constant, and either is a decision for the panel (row O-1).
|
||||
|
||||
(b) ATTACKED (a floor on k, not a security level). [BS25] Sec 7.1 Attack 1 was run with a greedy depth-reducing set
|
||||
measured on this exact graph. The greedy rule puts v in S whenever v's depth in G - S would exceed d. The k below pushes
|
||||
that attack's acceptance (1 - |S|/N)^k under 2^-s:
|
||||
|
||||
| n | d | abs(S) / N | k for 2^-40 | k for 2^-128 | proof bytes at k for 2^-128 |
|
||||
|---|---|---|---|---|---|
|
||||
| 12 | N/4 | 0.160 | 160 | 510 | 637,035 |
|
||||
| 12 | N/256 | 0.266 | 90 | 287 | 358,508 |
|
||||
| 16 | N/4 | 0.161 | 158 | 504 | 823,077 |
|
||||
| 16 | N/256 | 0.240 | 101 | 323 | 527,504 |
|
||||
| 20 | N/4 | 0.168 | 151 | 482 | 972,239 |
|
||||
| 20 | N/256 | 0.242 | 100 | 320 | 645,485 |
|
||||
|
||||
The greedy set is a weak depth-reducing attack. Stronger ones (layered and recursive, [BS25] Sec 7.2, [ABH17] Sec 5)
|
||||
remove fewer nodes for the same d. Those move these k values UP. Row O-9 holds them.
|
||||
|
||||
## 7. Malicious-prover fixtures and verdicts
|
||||
|
||||
Command: `run-b1.sh <out> attacks`. Output: `fixtures/attacks-n10-k16.json`. N = 2^10 and k = 16 were chosen so that the
|
||||
accepting probabilities can be measured. Each adversary is implemented to win, and its verdict is measured.
|
||||
|
||||
| id | adversary (source) | what it does | measured | verdict and mechanism |
|
||||
|---|---|---|---|---|
|
||||
| A1 | [DN17] Sec 4.1 first attack | replays a valid proof made for chi' as a proof for chi | 0 of 50 accepted (check_2b, check_2a) | REFUSED. chi salts every label, Merkle node and challenge ([BS25] Sec 3.2). |
|
||||
| A2 | [DN17] Sec 4.1 second attack | commits a constant array with no memory, then (a) answers with the label the verifier would recompute or (b) opens the constant label | (a) 0 of 50, check_2b; (b) 0 of 50, check_3 | REFUSED. [BS25] Sec 3.2 opens l_{c_i+1} itself at the derived position and checks it against its opened parents. |
|
||||
| A3 | [DN17] Sec 4.3 and Sec 5, control blocks (the attack that broke MTP-Argon2d) | t = 20; grinds each control block up to 1,000 times to steer the next t - 1 parents into the stored set (Eq. 5), then stores only control blocks as red values | Eq. 5 met in 0 of 50 intervals after 1 try and 0 of 50 after 1,000; steering gain 0; red-node fallback accepted 191 of 400 = 0.478 (analytic (1 - 1/t)^k = 0.442) | STEERING REFUSED BY CONSTRUCTION. DRSample's parents are fixed a priori, so no label value moves an edge. What remains is a red-node cheat, accepted with (1 - abs(S)/N)^k: the probability [BS25] Lemma 4 bounds and k must make small. |
|
||||
| A4 | [DN17] Sec 8, self-similarity of data-independent MTP (Y2 := X2 gives Y1 = X1) | copies a label into a node that shares a long-edge parent | 0 of 200 copies reproduced a label; 189 of 200 accepted (one red node each) | SAVING REFUSED. The node index v is in every prelabel ([BS25] Sec 2.2; [DN17] Sec 8's own HAIFA countermeasure). The one red node is caught only when challenged. |
|
||||
| A5 | [BS25] Sec 7.1 Attack 1 | labels 0^lambda on a greedy depth-reducing set S, the rest in parallel | d = 64: abs(S)/N 0.222, depth of G - S 64, accepted 6 of 400 = 0.015 (analytic 0.018); d = 256: abs(S)/N 0.151, accepted 26 of 400 = 0.065 (analytic 0.072) | ACCEPTED WITH PROBABILITY (1 - abs(S)/N)^k, as the paper states. The cheater needs only d parallel rounds and O(N d lambda) cumulative memory. This is what sets k (section 6). |
|
||||
| A6 | [BS25] Sec 5 Def. 14 and Lemma 4 (LuckyQuery); B2's O-07 | one labelling; re-rolls tau by rewriting one red leaf and its path only (n + k = 26 oracle calls per try, no relabelling) | 1 red sink: 193 to 200 of 200 tries are accepted roots per labelling; 64 red: 140 to 152 of 400; first accept at try 1.2 and 1.6 on average | ACCEPTED: one labelling yields many accepted (tau, proof) pairs for one chi. The paper's soundness counts this in its q(1 - beta)^k term and still bounds cmc per accepted proof. But nothing in the primitive makes a second accepted root cost a second labelling. A lottery value derived from tau would be re-rolled for n + k calls (finding F-4, handed to B2 and B4). |
|
||||
|
||||
A1 to A4 are the Dinur and Nadler adversaries against the data-independent instantiation. The two that broke
|
||||
MTP-Argon2d (A2 and A3) have no foothold. A2 is closed by the opening rule and A3 by the fixed graph. No Dinur-Nadler
|
||||
adversary gained anything beyond the red-node probability that the theorem already prices. A5 and A6 are accepted with
|
||||
exactly the probability the paper's own bound names. They are not new breaks. They are the reason k and the lottery
|
||||
binding are open (O-1, O-8).
|
||||
|
||||
## 8. Findings (each with its consequence)
|
||||
|
||||
- F-1 Domain separation. [BS25] separates query kinds only by the chi salt and the query shape. With a common integer
|
||||
width, the challenge query H(chi, i, tau) has the shape of the prelabel of an indegree-1 node (DRSample's node 2).
|
||||
B1 pins one-byte tags (L, M, C, G). Any implementation that drops them is a different function and needs its own
|
||||
argument. Consequence: B2 and B3 pin to the tags.
|
||||
- F-2 The graph must be a public constant. [BS25] proves soundness for a graph fixed a priori. If the graph came from
|
||||
chi, a prover who varies a nonce inside chi could grind for a weak sample. Consequence: the graph seed is a constant
|
||||
and never per instance. Whether the one fixed sample is in fact depth-robust is unverified (row O-12).
|
||||
- F-3 Proven k is impractical (section 6a): about 2.9 x 10^7 challenges at n = 20 with the cited constant.
|
||||
Consequence: a Track B candidate cannot claim [BS25]'s theorem at any practical k today. A heuristic k must be
|
||||
labelled as such.
|
||||
- F-4 One labelling, many accepted roots (A6). Consequence: an Igneum lottery cannot be a hash of tau alone. B2's O-07
|
||||
and B4's composed theorem must bind the lottery to the labelling.
|
||||
- F-5 [BS25] Lemma 3 needs lambda/4 >= 2 log2(q) + log2(indeg). At lambda = 256 that covers q <= 2^31.5 oracle calls.
|
||||
q = 2^64 needs lambda >= 516. Consequence: at network scale, the theorem's extraction step does not cover lambda = 256
|
||||
as written (row O-4).
|
||||
|
||||
## 9. Open questions (BLOCKED rows)
|
||||
|
||||
| row | question | why blocked | owner | clock |
|
||||
|---|---|---|---|---|
|
||||
| O-1 | a practical k with a proof: a better proven DRSample (e, d) constant, or a panel-approved heuristic k | [ABH17] Theorem 3.1's constant gives k > N (section 6a); [BS25] states no concrete k | B4 (theory) with R15-07 | not set by B1 |
|
||||
| O-2 | "the paper's smallest stated N" | [BS25] states no concrete N, lambda, k or hash; B1's fixture sizes are its own | B1 records; the panel picks N | n/a |
|
||||
| O-3 | the archived revision against eprint's current revision of 2025/1456 and 2017/497 | eprint challenges the boxes; the snapshot dates are in section 1 | B1 or B4 when any box is served | open |
|
||||
| O-4 | lambda against Lemma 3's precondition at network q (F-5): lambda = 512 (BLAKE2b-512) or a tighter extractor | a theory question; changing lambda changes every fixture and B2's chi | B4 | open |
|
||||
| O-5 | the log base and constants in [BS25] Fact 8 and Corollary 3 (c1, c3, c4) and in [ABH17] Theorem 3.1 | the papers write "log"; B1 reads log2 (R-4) | B4 | open |
|
||||
| O-6 | [BS25] App. C.2's loop processes x_1 first from the leaf, which matches Def. 15 / Sec 3.2 only if x is read leaf-first | an erratum question for the authors; B1 pins Sec 3.2's bin(v) and Def. 15's sibling order (R-3) | B1 to the panel | open |
|
||||
| O-7 | Verify step 2 writes MerkleTreeReveal(chi, i, ...) with the challenge number i; B1 checks at the derived leaf index c_i and parent index u - 1 (R-5) | wording in the paper; B1's reading is the one under which the attacks are refused | B1 to the panel | open |
|
||||
| O-8 | lottery binding: one labelling gives many accepted roots (F-4) | the primitive has no lottery; binding is B2's spec and B4's theorem | B2, B4 | open |
|
||||
| O-9 | stronger depth-reducing attacks on this exact graph (layered, recursive, [BS25] Sec 7.2, [ABH17] Sec 5) to raise section 6b's floor | not implemented in this window | B4 / R15-07 | open |
|
||||
| O-10 | energy: [BS25] bounds cumulative memory in the parallel random oracle model (bits of state per round). It says nothing about joules, SRAM or a chip | the physical translation is B4/B5's | B4, B5 | open |
|
||||
| O-11 | a compiled verifier and cold verification at the admitted rate | only the CPython reference exists | B6 (node lane), B3 | open |
|
||||
| O-12 | is the one fixed DRSample sample (graph_seed) actually (e, d)-depth-robust? [ABH17] Theorem 3.1 holds with high probability over the sample, not for every seed | no tool here certifies a sampled graph | B4 | open |
|
||||
| O-13 | does [R4] (de Rezende, Engstrom, Reyzin 2026) touch [BS25]? [BS25]'s bound is on the PROM state size \|sigma_i\| in bits (Theorem 6), so encoded state is counted, but the reduction must be read against [R4] | a theory read | B4 | open |
|
||||
| O-14 | the quantum case | [BS25] is classical (PROM); the plan's rule keeps the claim classical | none today | n/a |
|
||||
|
||||
Resolutions B1 made where a source was ambiguous (each one pinned in code):
|
||||
|
||||
- R-1. [ABH17] "[max(g/2, 2), g]" with g = v - 1 odd: the integer range starts at ceil(g/2).
|
||||
- R-2. [ABH17]'s uniform draws: rejection sampling over 64-bit words of the 'G' oracle with the public seed.
|
||||
- R-3. Tree layout: Sec 3.2 and Sec 4.1, with LSB-at-root; Def. 15 sibling order root-first.
|
||||
- R-4. log means log2 wherever a constant is evaluated.
|
||||
- R-5. Merkle positions are always the verifier's own (c_i for the node, u - 1 for parent u).
|
||||
|
||||
## 10. Files
|
||||
|
||||
| path | what |
|
||||
|---|---|
|
||||
| `tools/mhpow/b1/params.py` | the oracle, lambda, tags, integer encoding, graph seed, with the paper section beside each |
|
||||
| `tools/mhpow/b1/drsample.py` | [ABH17] Alg. 1 DRSample, the derandomisation, depth of G - S |
|
||||
| `tools/mhpow/b1/mtp.py` | labels, Merkle tree, reveal and check, challenges, the certificate container, honest prover, verifier |
|
||||
| `tools/mhpow/b1/attacks.py` | adversaries A1 to A6 |
|
||||
| `tools/mhpow/b1/test_b1.py` | the 17-test suite (known-failed cases by check name) |
|
||||
| `tools/mhpow/b1/gen_fixtures.py`, `k_table.py`, `run-b1.sh` | fixture, row, attack and k-table generators; the box driver (pid file first) |
|
||||
| `tools/mhpow/b1/fixtures/` | every fixture, row and table above, with `SHA256SUMS` |
|
||||
| `docs/analysis/mhpow/b1/registry-batch-mhpow-b1-20261009-01.json` | the registry batch, rows NOT RUN, for the steward to record |
|
||||
|
||||
Run everything again (a build box only, never the Mac): `tools/mhpow/b1/run-b1.sh <out_dir> all`. Then compare
|
||||
`<out_dir>/SHA256SUMS` with `tools/mhpow/b1/fixtures/SHA256SUMS`. The kat, attack and k-table files must be
|
||||
byte-identical. The row files and tests.log differ in their timings only.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"run_id": "mhpow-b1-20261009-01",
|
||||
"manifest_sha": "13c3646e",
|
||||
"evidence_dir": "docs/analysis/mhpow/b1",
|
||||
"method": "model",
|
||||
"boxes": ["build-6"],
|
||||
"note": "B1 (R15-05), the cryptography lane of the 1.5x programme's Track B (9 October 2026): the exact CPU reference of Blocki and Smearsoll's MTP framework (eprint 2025/1456 as archived, sha256 13c3646e..., Sec 2.2, 3.2, App. C) over DRSample (ABH17 Alg. 1, eprint 2017/443 as archived, sha256 f8e08d36...), its complete verifier (named checks in the paper's order, each with a known-failed test, 17 of 17 OK on build-6), five deterministic known-answer fixtures (n 4 to 20), honest-prover rows n 12 to 24 (CPython, wall time only, no power) and six malicious provers (Dinur-Nadler eprint 2017/497 Sec 4.1, 4.3/5, 8; the paper's Attack 1 and LuckyQuery regrind). Dinur-Nadler's steering and unopened-block attacks are refused; red-node cheats are accepted with the (1-|S|/N)^k probability the paper prices; one labelling yields many accepted roots (F-4). The proven k with ABH17's constant exceeds N to about n 25 (F-3). NOT RUN until the panel reads it; this lane writes no PASS.",
|
||||
"cells": [
|
||||
{
|
||||
"cell": "model:mhpow-b1-reference",
|
||||
"cases": ["POW-05", "POW-06"],
|
||||
"status": "NOT RUN",
|
||||
"method": "model",
|
||||
"evidence": "docs/analysis/mhpow/b1/README.md",
|
||||
"note": "POW-05: attack fixtures A1 to A6 (tools/mhpow/b1/fixtures/attacks-n10-k16.json): A1 replay and A2 unopened block REFUSED, A3 control-block steering gain 0 with the red-node fallback accepted at (1-1/t)^k, A4 self-similarity saving refused, A5 Attack 1 accepted at (1-|S|/N)^k, A6 many accepted roots per labelling (the lottery binding BLOCKED to B2/B4, rows O-1 and O-8). POW-06: the verifier's named checks refuse malformed containers, wrong positions, skipped parents and wrong tags (test_b1.py); proof bytes and verifier oracle calls per N are exact formulas with measured rows; cold verification at rate BLOCKED to B6 (O-11).",
|
||||
"claim_impact": "none: no public figure moves; Track B's construction is not adopted and nothing activates",
|
||||
"in_progress": true
|
||||
}
|
||||
],
|
||||
"map_cell_requested": {
|
||||
"model:mhpow-b1-reference": {
|
||||
"command": "tools/mhpow/b1/run-b1.sh <out_dir> all (a build box under its pid file; kat, attack and k-table files byte-identical to tools/mhpow/b1/fixtures/SHA256SUMS)",
|
||||
"box_class": "build box, CPU only (build-6)",
|
||||
"fixtures": ["tools/mhpow/b1/fixtures/"],
|
||||
"cases": ["POW-05", "POW-06"],
|
||||
"coverage": {
|
||||
"POW-05": "partial: the published primitive's attack fixtures only; the lottery binding and the composed accepted-proof theorem are B2/B4's",
|
||||
"POW-06": "partial: verifier refusals and op counts in CPython; no compiled verifier, no admitted-rate timing"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,3 +55,5 @@ docs/analysis/proving-outcome-ledger.md
|
|||
# 9 October 2026: the 1.5x programme Track B, the B2 binding spec lane (research documents and their generator)
|
||||
docs/analysis/mhpow/b2
|
||||
tools/mhpow/b2
|
||||
docs/analysis/mhpow/b1
|
||||
tools/mhpow/b1
|
||||
|
|
|
|||
239
tools/mhpow/b1/attacks.py
Normal file
239
tools/mhpow/b1/attacks.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""Malicious provers against the B1 verifier. Each adversary is implemented honestly (it tries to win) and its verdict is
|
||||
measured, never assumed. Sources: [DN17] Dinur and Nadler, eprint 2017/497 as archived 2026-05-09 (Sec 4.1, 4.3, 8);
|
||||
[BS25] Sec 5 (LuckyQuery), Sec 7.1 (Attack 1).
|
||||
|
||||
Verdict vocabulary per adversary: REFUSED (never accepted, the refusing check named) or ACCEPTED-WITH-PROBABILITY p (measured
|
||||
over trials, beside the analytic value the paper's bound uses) with the exact mechanism.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Set, Tuple
|
||||
|
||||
import drsample
|
||||
import mtp
|
||||
import params
|
||||
from params import H, u64
|
||||
|
||||
D = params.DIGEST_BYTES
|
||||
ZERO = b"\0" * D
|
||||
|
||||
|
||||
def _chi(tag: str, j: int) -> bytes:
|
||||
return H(b"B1-attack", tag.encode(), u64(j))
|
||||
|
||||
|
||||
def _respond_from(chi: bytes, n: int, k: int, par, labels, tree) -> mtp.Proof:
|
||||
return mtp.respond(mtp.ProverState(chi, n, par, labels, tree), k)
|
||||
|
||||
|
||||
# A1 [DN17] Sec 4.1 "A First Attack": replay a proof made for another challenge (chi') -------------------------------------------
|
||||
def a1_replay(n: int, k: int, par, trials: int) -> Dict:
|
||||
acc, checks = 0, set()
|
||||
for j in range(trials):
|
||||
old = mtp.prove(_chi("a1-old", j), n, k, par)
|
||||
r = mtp.verify(_chi("a1-new", j), n, k, old)
|
||||
acc += r.accepted
|
||||
checks.add(r.check)
|
||||
return {"id": "A1", "source": "[DN17] Sec 4.1 first attack (proof replay across challenges)",
|
||||
"mechanism": "every label, Merkle node and challenge query carries chi ([BS25] Sec 3.2): a proof for chi' opens a tree "
|
||||
"whose root and challenges are bound to chi'",
|
||||
"trials": trials, "accepted": acc, "refusing_checks": sorted(checks),
|
||||
"verdict": "REFUSED" if acc == 0 else "ACCEPTED"}
|
||||
|
||||
|
||||
# A2 [DN17] Sec 4.1 "A Second Attack": constant array committed, the challenged block not truly opened -----------------------------
|
||||
def a2_unopened_block(n: int, k: int, par, trials: int) -> Dict:
|
||||
"""The cheater commits to an all-constant array (no memory), then (a) answers each challenge with the label the verifier
|
||||
would recompute from the opened parents but the constant array's path, or (b) opens the constant label honestly."""
|
||||
N = 1 << n
|
||||
res = {}
|
||||
for variant in ("a_recomputed_label", "b_constant_label"):
|
||||
acc, checks = 0, set()
|
||||
for j in range(trials):
|
||||
chi = _chi("a2", j)
|
||||
labels = [None] + [ZERO] * N
|
||||
tree = mtp.MerkleTree(chi, labels, n)
|
||||
proof = _respond_from(chi, n, k, par, labels, tree)
|
||||
if variant == "a_recomputed_label":
|
||||
for c, r in zip(mtp.challenges(chi, tree.root, k, n), proof.responses):
|
||||
r.node.label = mtp.label_of(chi, c + 1, [p.label for p in r.parents])
|
||||
v = mtp.verify(chi, n, k, proof)
|
||||
acc += v.accepted
|
||||
checks.add(v.check)
|
||||
res[variant] = {"accepted": acc, "refusing_checks": sorted(checks)}
|
||||
total = sum(x["accepted"] for x in res.values())
|
||||
return {"id": "A2", "source": "[DN17] Sec 4.1 second attack (the challenged block X[i_j] not opened in MTP 1.0)",
|
||||
"mechanism": "[BS25] Sec 3.2 step 3(a) opens l_{c_i+1} itself at the verifier-derived position and step 3 checks it "
|
||||
"against its opened parents; the constant array is red everywhere",
|
||||
"trials": trials, "variants": res, "verdict": "REFUSED" if total == 0 else "ACCEPTED"}
|
||||
|
||||
|
||||
# A3 [DN17] Sec 4.3 / Sec 5 control blocks: grind control labels to steer later parents into the stored set -----------------------
|
||||
def a3_control_block_steering(n: int, k: int, par, t: int, grind: int, trials: int) -> Dict:
|
||||
"""[DN17] Sec 5 step 2(b): for each interval s, try control-block values X[s t] until the parents of the next t-1 blocks
|
||||
fall in the stored set S_s (Eq. 5). Under Argon2d phi depends on X[i-1]; here parents() is a function of v alone, so the
|
||||
attacker's grind is run and its effect measured: the fraction of intervals meeting Eq. 5 with 1 try and with `grind` tries.
|
||||
Then the cheater does what [DN17] does next: stores only the control blocks, writes them as inconsistent (red) values, and
|
||||
computes the rest; acceptance is measured against (1 - 1/t)^k."""
|
||||
N = 1 << n
|
||||
def eq5_holds(s: int) -> bool:
|
||||
base = s * t
|
||||
stored = {s2 * t for s2 in range(0, s + 1)}
|
||||
for l in range(1, t):
|
||||
v = base + l
|
||||
if v > N:
|
||||
break
|
||||
for u in par[v]:
|
||||
if not (u in stored or base < u < v or u == v - 1):
|
||||
return False
|
||||
return True
|
||||
intervals = range(1, N // t)
|
||||
# grinding: the control block's value does not enter parents(); each "try" re-evaluates Eq. 5 on the same graph
|
||||
one_try = sum(eq5_holds(s) for s in intervals)
|
||||
many = 0
|
||||
for s in intervals:
|
||||
ok = False
|
||||
for _ in range(grind):
|
||||
if eq5_holds(s):
|
||||
ok = True
|
||||
break
|
||||
many += ok
|
||||
# the fallback cheat: control nodes red (stored as attacker-chosen values), everything else honest from them
|
||||
acc = 0
|
||||
red = set(range(t, N + 1, t))
|
||||
for j in range(trials):
|
||||
chi = _chi("a3", j)
|
||||
lab: List = [None] * (N + 1)
|
||||
for v in range(1, N + 1):
|
||||
lab[v] = H(b"attacker-control", chi, u64(v)) if v in red else mtp.label_of(chi, v, [lab[u] for u in par[v]])
|
||||
tree = mtp.MerkleTree(chi, lab, n)
|
||||
acc += mtp.verify(chi, n, k, _respond_from(chi, n, k, par, lab, tree)).accepted
|
||||
return {"id": "A3", "source": "[DN17] Sec 4.3 and Sec 5 (control blocks steering the data-dependent phi), the attack that "
|
||||
"broke MTP-Argon2d",
|
||||
"t": t, "grind_tries_per_control_block": grind, "intervals": len(intervals),
|
||||
"eq5_intervals_met_one_try": one_try, "eq5_intervals_met_after_grind": many,
|
||||
"steering_gain": many - one_try,
|
||||
"fallback_red_fraction": len(red) / N, "trials": trials, "accepted": acc,
|
||||
"accept_rate": acc / trials, "analytic_accept": (1 - len(red) / N) ** k,
|
||||
"mechanism": "DRSample's parents(v) are fixed a priori ([ABH17] Alg. 1, public seed): no label value moves an edge, so "
|
||||
"grinding has zero steering gain; what remains is a red-node cheat accepted with (1-|S|/N)^k, the "
|
||||
"probability [BS25] Lemma 4 bounds",
|
||||
"verdict": "STEERING REFUSED BY CONSTRUCTION; RED-NODE FALLBACK ACCEPTED WITH PROBABILITY (1-1/t)^k"}
|
||||
|
||||
|
||||
# A4 [DN17] Sec 8 self-similarity: Y2 := X2 so that Y1 = X1 (copy a label to another node sharing a parent) --------------------------
|
||||
def a4_self_similarity(n: int, k: int, par, trials: int) -> Dict:
|
||||
N = 1 << n
|
||||
# find pairs (x, y) of nodes sharing their long-edge parent: X1 = F(X2, X3), Y1 = F(Y2, Y3) with X3 == Y3
|
||||
by_parent: Dict[int, List[int]] = {}
|
||||
for v in range(3, N + 1):
|
||||
by_parent.setdefault(par[v][0], []).append(v)
|
||||
pairs = [(vs[0], vs[1]) for vs in by_parent.values() if len(vs) >= 2 and vs[0] + 1 < vs[1]]
|
||||
equal = 0
|
||||
acc = 0
|
||||
for j in range(trials):
|
||||
chi = _chi("a4", j)
|
||||
lab = mtp.compute_labels(chi, par)
|
||||
x, y = pairs[j % len(pairs)]
|
||||
# Y2 := X2 (the predecessor of y takes the predecessor of x's label), then y is computed from it honestly
|
||||
lab2 = list(lab)
|
||||
lab2[y - 1] = lab[x - 1]
|
||||
lab2[y] = mtp.label_of(chi, y, [lab2[u] for u in par[y]])
|
||||
equal += lab2[y] == lab[x]
|
||||
tree = mtp.MerkleTree(chi, lab2, n)
|
||||
acc += mtp.verify(chi, n, k, _respond_from(chi, n, k, par, lab2, tree)).accepted
|
||||
return {"id": "A4", "source": "[DN17] Sec 8 (self-similarity of data-independent MTP: Y2 = X2 gives Y1 = X1)",
|
||||
"pairs_available": len(pairs), "trials": trials, "copies_that_reproduced_a_label": equal,
|
||||
"accepted": acc, "accept_rate": acc / trials,
|
||||
"mechanism": "the node index v is inside every prelabel ([BS25] Sec 2.2; [DN17] Sec 8's HAIFA countermeasure): "
|
||||
"copying X2 into Y2 never reproduces X1 at y, so nothing is saved; the one red node (y-1) is caught "
|
||||
"only when challenged, as any single red node",
|
||||
"verdict": "SAVING REFUSED (0 reproduced labels); ONE RED NODE ACCEPTED WITH PROBABILITY ~ (1-1/N)^k"}
|
||||
|
||||
|
||||
# A5 [BS25] Sec 7.1 Attack 1: depth-reducing set S given label 0^lambda, the rest in parallel -------------------------------------
|
||||
def greedy_depth_reducing_set(par, d: int) -> Set[int]:
|
||||
"""Topological greedy: put v in S when its depth in G - S would exceed d. G - S then has no path longer than d."""
|
||||
n_nodes = len(par) - 1
|
||||
depth = [0] * (n_nodes + 1)
|
||||
S: Set[int] = set()
|
||||
for v in range(1, n_nodes + 1):
|
||||
dv = 1 + max((depth[u] for u in par[v] if u not in S), default=0)
|
||||
if dv > d:
|
||||
S.add(v)
|
||||
depth[v] = 0
|
||||
else:
|
||||
depth[v] = dv
|
||||
return S
|
||||
|
||||
|
||||
def a5_attack1(n: int, k: int, par, d: int, trials: int) -> Dict:
|
||||
N = 1 << n
|
||||
S = greedy_depth_reducing_set(par, d)
|
||||
depth = drsample.depth_after_removal(par, S)
|
||||
acc = 0
|
||||
for j in range(trials):
|
||||
chi = _chi("a5", j)
|
||||
lab: List = [None] * (N + 1)
|
||||
for v in range(1, N + 1):
|
||||
lab[v] = ZERO if v in S else mtp.label_of(chi, v, [lab[u] for u in par[v]])
|
||||
tree = mtp.MerkleTree(chi, lab, n)
|
||||
acc += mtp.verify(chi, n, k, _respond_from(chi, n, k, par, lab, tree)).accepted
|
||||
return {"id": "A5", "source": "[BS25] Sec 7.1 Attack 1 (labels 0^lambda on a depth-reducing set S, G - S pebbled in parallel)",
|
||||
"d_target": d, "S_size": len(S), "S_fraction": len(S) / N, "depth_G_minus_S": depth,
|
||||
"parallel_rounds_bound": depth, "trials": trials, "accepted": acc, "accept_rate": acc / trials,
|
||||
"analytic_accept": (1 - len(S) / N) ** k,
|
||||
"mechanism": "the paper's own attack: |S| red nodes escape all k challenges with probability (1-|S|/N)^k; the "
|
||||
"cheater's cumulative memory is O(N d lambda + N lambda log N) against the honest O(N^2 lambda)",
|
||||
"verdict": "ACCEPTED WITH PROBABILITY (1-|S|/N)^k (as the paper states; this is what sets k)"}
|
||||
|
||||
|
||||
# A6 [BS25] Sec 5 LuckyQuery: regrind the Merkle root cheaply until every challenge is green -----------------------------------------
|
||||
def a6_root_regrind(n: int, k: int, par, red_count: int, max_tries: int, trials: int) -> Dict:
|
||||
"""One labelling with `red_count` red nodes (the last nodes, sinks of the honest order); the cheater re-rolls tau by
|
||||
rewriting the value of one red leaf (the sink, node N) and rehashing its path only: n Merkle calls + k challenge calls per
|
||||
try. Measured: tries to the first accepted root, and how many distinct accepted roots one labelling yields (B2's O-07)."""
|
||||
N = 1 << n
|
||||
results = []
|
||||
for j in range(trials):
|
||||
chi = _chi("a6", j)
|
||||
lab = mtp.compute_labels(chi, par)
|
||||
red = set(range(N - red_count + 1, N + 1))
|
||||
for v in red:
|
||||
lab[v] = H(b"red", chi, u64(v))
|
||||
tree = mtp.MerkleTree(chi, lab, n)
|
||||
p = mtp._bitrev(N - 1, n)
|
||||
tries, accepted_roots, first = 0, 0, None
|
||||
for g in range(max_tries):
|
||||
tries += 1
|
||||
leaf = H(b"regrind", chi, u64(g))
|
||||
# rewrite leaf N-1 (node N) and its path to the root
|
||||
tree.levels[n][p] = leaf
|
||||
cur, pos = leaf, p
|
||||
for depth in range(n, 0, -1):
|
||||
sib = tree.levels[depth][pos ^ 1]
|
||||
cur = mtp.merkle_node(chi, cur, sib) if pos % 2 == 0 else mtp.merkle_node(chi, sib, cur)
|
||||
pos >>= 1
|
||||
tree.levels[depth - 1][pos] = cur
|
||||
lab[N] = leaf
|
||||
cs = mtp.challenges(chi, tree.root, k, n)
|
||||
if not any((c + 1) in red for c in cs):
|
||||
if first is None:
|
||||
first = tries
|
||||
proof = _respond_from(chi, n, k, par, lab, tree)
|
||||
assert mtp.verify(chi, n, k, proof).accepted
|
||||
accepted_roots += 1
|
||||
results.append({"first_accept_try": first, "accepted_roots": accepted_roots, "tries": tries})
|
||||
firsts = [r["first_accept_try"] for r in results if r["first_accept_try"] is not None]
|
||||
return {"id": "A6", "source": "[BS25] Sec 5 Def. 14 and Lemma 4 (LuckyQuery, the q(1-beta)^k term); B2's O-07",
|
||||
"red_count": red_count, "red_fraction": red_count / N, "k": k, "max_tries": max_tries, "trials": trials,
|
||||
"per_try_oracle_calls": n + k, "trials_with_accept": len(firsts),
|
||||
"mean_first_accept_try": (sum(firsts) / len(firsts)) if firsts else None,
|
||||
"predicted_tries": (1 - red_count / N) ** (-k),
|
||||
"accepted_roots_per_labelling": [r["accepted_roots"] for r in results],
|
||||
"mechanism": "tau is a fresh oracle output per rewrite of one red leaf (n + k calls, no relabelling): with beta = "
|
||||
"red fraction the expected tries are (1-beta)^-k, and with ONE red sink nearly every try is an "
|
||||
"accepted root, so one labelling yields many accepted (tau, proof) pairs for one chi; the theorem's "
|
||||
"cmc bound still holds per accepted proof but nothing in the primitive makes a second accepted root "
|
||||
"cost a second labelling",
|
||||
"verdict": "ACCEPTED (many accepted roots per labelling): a lottery over tau is not bound to the labelling cost"}
|
||||
87
tools/mhpow/b1/drsample.py
Normal file
87
tools/mhpow/b1/drsample.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""DRSample, [ABH17] Algorithm 1, verbatim edge rule, derandomised by a public seed.
|
||||
|
||||
[ABH17] Algorithm 1 (eprint 2017/443, Sec 3):
|
||||
Function DRSample(n): V := [n]; E := {(1, 2)}; for v in [3, n] and i in [2]: E := E u {(v, GetParent(v, i))}
|
||||
Function GetParent(v, i):
|
||||
if i = 1 then r := 1
|
||||
else
|
||||
g' <- [1, floor(log2(v)) + 1] // random range size
|
||||
g := min(v - 1, 2^g') // don't make edges too long
|
||||
r <- [max(g/2, 2), g] // random edge length
|
||||
return v - r
|
||||
Nodes are 1-indexed, node 1 is the only source, node 2 has the single parent 1, every v >= 3 has the two distinct parents
|
||||
v - 1 and v - r with r >= 2. [BS25] Sec 2.2 orders parents ascending (v1 < v2), so parents(v) = (v - r, v - 1).
|
||||
|
||||
Resolutions of what the pseudocode leaves open (README rows R-1, R-2):
|
||||
R-1 "[max(g/2, 2), g]" with g = v - 1 odd: the integer range starts at ceil(g/2).
|
||||
R-2 "<-" (uniform draw): uniform_int() below, rejection sampling over 64-bit words of H('G' || GRAPH_SEED || v || which || ctr),
|
||||
so the graph is a deterministic public function of GRAPH_SEED (fixed a priori, never chi).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
import params
|
||||
|
||||
_TWO64 = 1 << 64
|
||||
|
||||
|
||||
def uniform_int(lo: int, hi: int, v: int, which: int) -> int:
|
||||
"""Uniform integer in [lo, hi] (inclusive) for draw `which` of node v; rejection sampling, no modulo bias."""
|
||||
m = hi - lo + 1
|
||||
if m <= 0:
|
||||
raise ValueError("empty range")
|
||||
if m == 1:
|
||||
return lo
|
||||
limit = _TWO64 - (_TWO64 % m)
|
||||
ctr = 0
|
||||
while True:
|
||||
d = params.H(params.TAG_GRAPH, params.GRAPH_SEED, params.u64(v), bytes([which]), params.u64(ctr))
|
||||
x = int.from_bytes(d[:8], "little")
|
||||
if x < limit:
|
||||
return lo + (x % m)
|
||||
ctr += 1
|
||||
|
||||
|
||||
def get_parent(v: int, i: int) -> int:
|
||||
"""[ABH17] Alg. 1 GetParent(v, i) for v >= 3."""
|
||||
if v < 3:
|
||||
raise ValueError("GetParent is defined for v >= 3; node 1 has no parent and node 2 has parent 1")
|
||||
if i == 1:
|
||||
r = 1
|
||||
else:
|
||||
gp = uniform_int(1, v.bit_length(), v, 1) # floor(log2 v) + 1 == v.bit_length()
|
||||
g = min(v - 1, 1 << gp)
|
||||
r = uniform_int(max(-(-g // 2), 2), g, v, 2) # R-1: ceil(g/2)
|
||||
return v - r
|
||||
|
||||
|
||||
def parents(v: int) -> Tuple[int, ...]:
|
||||
"""parents(v, G) in ascending order ([BS25] Sec 2.2). Node 1: (); node 2: (1,); v >= 3: (v - r, v - 1)."""
|
||||
if v == 1:
|
||||
return ()
|
||||
if v == 2:
|
||||
return (1,)
|
||||
p2 = get_parent(v, 2)
|
||||
p1 = get_parent(v, 1)
|
||||
assert 1 <= p2 < p1 == v - 1
|
||||
return (p2, p1)
|
||||
|
||||
|
||||
def all_parents(n_nodes: int) -> List[Tuple[int, ...]]:
|
||||
"""Index 0 unused; all_parents(N)[v] = parents(v) for v in 1..N."""
|
||||
return [()] + [parents(v) for v in range(1, n_nodes + 1)]
|
||||
|
||||
|
||||
def depth_after_removal(par: List[Tuple[int, ...]], removed: set) -> int:
|
||||
"""Length (nodes) of the longest directed path in G - S ([BS25] Sec 7.1 uses this to bound parallel rounds)."""
|
||||
n = len(par) - 1
|
||||
d = [0] * (n + 1)
|
||||
best = 0
|
||||
for v in range(1, n + 1):
|
||||
if v in removed:
|
||||
continue
|
||||
dv = 1 + max((d[u] for u in par[v] if u not in removed), default=0)
|
||||
d[v] = dv
|
||||
best = max(best, dv)
|
||||
return best
|
||||
19
tools/mhpow/b1/fixtures/SHA256SUMS
Normal file
19
tools/mhpow/b1/fixtures/SHA256SUMS
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
805035ebfc8d2f34441325b5d5ca46c67e81f7d0b0b86f62ba1a906964b8c51f attacks-n10-k16.json
|
||||
9f85b22ae54785e0e078630f890036713cf3925b7e1345bd949c6945d2702f7f kat-n04.json
|
||||
481a39fa47ff4d3393f34e15e318ee4bda7683ee457d3b9da79d277c9ba2831d kat-n04.proof.bin
|
||||
2c741be9a5a7c47773f902ed024c49188eacfc84f9cabdf34244f289e77f51f7 kat-n08.json
|
||||
d45a9756b8d66357eaee34f1efa3b5fc975e25fcf7b7eb5a516746c6e078bbc7 kat-n08.proof.bin
|
||||
51ccacd3030d97ec371d8cc8ea64fbb362f00bd323bd84827105c89b4eb30864 kat-n12.json
|
||||
3c7de0539a8715e75d3d64eb11ec3af7d8a2a338c101958a0c29e300c3ad4f7f kat-n12.proof.bin
|
||||
240f50bc5ac5e12c7630f24c072bf291fb6ab9644530badcd1c41c7a7341440c kat-n16.json
|
||||
793fd4d505155f133c8acf1c657a98795f29f7d6cba2568570c8324c42fb1118 kat-n16.proof.bin
|
||||
fd0d75b66af9cbd9ef12e63d571036f41bdac0e1d21e1f4d98559deb98502392 kat-n20.json
|
||||
a49367d9c80f0855504fe1f0994acbd5ddc4b023eaf5904a28ad5f7e71ce951c kat-n20.proof.bin
|
||||
1ceca69b04d46083bf7d6d6fa51e21b7ecd49e71eb6051324a439ebcb519336a k-table.json
|
||||
06f50be652157fb685e17d65853c95628c336980174b238ca8738ebe0ac0de5d k-table.log
|
||||
0e9f907ed4ffca6f558988ac91066475dcf60da21a9ba3106b79d014db268178 row-n12-k64.json
|
||||
0dbd2103af742b3f9bb38e679615de34f128db30baa8dd7bdc1b1eaa6c4732bc row-n16-k64.json
|
||||
23e1cd2749ca933abcde77602557b6228cefc76c6776bc8370720b8bcca833dc row-n20-k64.json
|
||||
21cf88c3151cb5007304bf1ba8d75764878626c2384b5471e6cc5ceb5f8aa4f4 row-n22-k64.json
|
||||
48d890a92c5bda6a55ddf65e2780fec63afc7f7adc8d6143f6b6270705b68e7c row-n24-k64.json
|
||||
55c663b3ab539741aad0ff97fa11a2a52308f715e0ed5047b2c2add82243f5bf tests.log
|
||||
177
tools/mhpow/b1/fixtures/attacks-n10-k16.json
Normal file
177
tools/mhpow/b1/fixtures/attacks-n10-k16.json
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
[
|
||||
{
|
||||
"accepted": 0,
|
||||
"id": "A1",
|
||||
"mechanism": "every label, Merkle node and challenge query carries chi ([BS25] Sec 3.2): a proof for chi' opens a tree whose root and challenges are bound to chi'",
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"refusing_checks": [
|
||||
"check_2a_parent_set",
|
||||
"check_2b_merkle_openings"
|
||||
],
|
||||
"source": "[DN17] Sec 4.1 first attack (proof replay across challenges)",
|
||||
"trials": 50,
|
||||
"verdict": "REFUSED"
|
||||
},
|
||||
{
|
||||
"id": "A2",
|
||||
"mechanism": "[BS25] Sec 3.2 step 3(a) opens l_{c_i+1} itself at the verifier-derived position and step 3 checks it against its opened parents; the constant array is red everywhere",
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"source": "[DN17] Sec 4.1 second attack (the challenged block X[i_j] not opened in MTP 1.0)",
|
||||
"trials": 50,
|
||||
"variants": {
|
||||
"a_recomputed_label": {
|
||||
"accepted": 0,
|
||||
"refusing_checks": [
|
||||
"check_2b_merkle_openings"
|
||||
]
|
||||
},
|
||||
"b_constant_label": {
|
||||
"accepted": 0,
|
||||
"refusing_checks": [
|
||||
"check_3_local_consistency"
|
||||
]
|
||||
}
|
||||
},
|
||||
"verdict": "REFUSED"
|
||||
},
|
||||
{
|
||||
"accept_rate": 0.4775,
|
||||
"accepted": 191,
|
||||
"analytic_accept": 0.44157668828816804,
|
||||
"eq5_intervals_met_after_grind": 0,
|
||||
"eq5_intervals_met_one_try": 0,
|
||||
"fallback_red_fraction": 0.0498046875,
|
||||
"grind_tries_per_control_block": 1000,
|
||||
"id": "A3",
|
||||
"intervals": 50,
|
||||
"mechanism": "DRSample's parents(v) are fixed a priori ([ABH17] Alg. 1, public seed): no label value moves an edge, so grinding has zero steering gain; what remains is a red-node cheat accepted with (1-|S|/N)^k, the probability [BS25] Lemma 4 bounds",
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"source": "[DN17] Sec 4.3 and Sec 5 (control blocks steering the data-dependent phi), the attack that broke MTP-Argon2d",
|
||||
"steering_gain": 0,
|
||||
"t": 20,
|
||||
"trials": 400,
|
||||
"verdict": "STEERING REFUSED BY CONSTRUCTION; RED-NODE FALLBACK ACCEPTED WITH PROBABILITY (1-1/t)^k"
|
||||
},
|
||||
{
|
||||
"accept_rate": 0.945,
|
||||
"accepted": 189,
|
||||
"copies_that_reproduced_a_label": 0,
|
||||
"id": "A4",
|
||||
"mechanism": "the node index v is inside every prelabel ([BS25] Sec 2.2; [DN17] Sec 8's HAIFA countermeasure): copying X2 into Y2 never reproduces X1 at y, so nothing is saved; the one red node (y-1) is caught only when challenged, as any single red node",
|
||||
"pairs_available": 253,
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"source": "[DN17] Sec 8 (self-similarity of data-independent MTP: Y2 = X2 gives Y1 = X1)",
|
||||
"trials": 200,
|
||||
"verdict": "SAVING REFUSED (0 reproduced labels); ONE RED NODE ACCEPTED WITH PROBABILITY ~ (1-1/N)^k"
|
||||
},
|
||||
{
|
||||
"S_fraction": 0.2216796875,
|
||||
"S_size": 227,
|
||||
"accept_rate": 0.015,
|
||||
"accepted": 6,
|
||||
"analytic_accept": 0.01813567966356165,
|
||||
"d_target": 64,
|
||||
"depth_G_minus_S": 64,
|
||||
"id": "A5",
|
||||
"mechanism": "the paper's own attack: |S| red nodes escape all k challenges with probability (1-|S|/N)^k; the cheater's cumulative memory is O(N d lambda + N lambda log N) against the honest O(N^2 lambda)",
|
||||
"parallel_rounds_bound": 64,
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"source": "[BS25] Sec 7.1 Attack 1 (labels 0^lambda on a depth-reducing set S, G - S pebbled in parallel)",
|
||||
"trials": 400,
|
||||
"verdict": "ACCEPTED WITH PROBABILITY (1-|S|/N)^k (as the paper states; this is what sets k)"
|
||||
},
|
||||
{
|
||||
"S_fraction": 0.1513671875,
|
||||
"S_size": 155,
|
||||
"accept_rate": 0.065,
|
||||
"accepted": 26,
|
||||
"analytic_accept": 0.07236309223391428,
|
||||
"d_target": 256,
|
||||
"depth_G_minus_S": 256,
|
||||
"id": "A5",
|
||||
"mechanism": "the paper's own attack: |S| red nodes escape all k challenges with probability (1-|S|/N)^k; the cheater's cumulative memory is O(N d lambda + N lambda log N) against the honest O(N^2 lambda)",
|
||||
"parallel_rounds_bound": 256,
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"source": "[BS25] Sec 7.1 Attack 1 (labels 0^lambda on a depth-reducing set S, G - S pebbled in parallel)",
|
||||
"trials": 400,
|
||||
"verdict": "ACCEPTED WITH PROBABILITY (1-|S|/N)^k (as the paper states; this is what sets k)"
|
||||
},
|
||||
{
|
||||
"accepted_roots_per_labelling": [
|
||||
193,
|
||||
199,
|
||||
198,
|
||||
200,
|
||||
195
|
||||
],
|
||||
"id": "A6",
|
||||
"k": 16,
|
||||
"max_tries": 200,
|
||||
"mean_first_accept_try": 1.2,
|
||||
"mechanism": "tau is a fresh oracle output per rewrite of one red leaf (n + k calls, no relabelling): with beta = red fraction the expected tries are (1-beta)^-k, and with ONE red sink nearly every try is an accepted root, so one labelling yields many accepted (tau, proof) pairs for one chi; the theorem's cmc bound still holds per accepted proof but nothing in the primitive makes a second accepted root cost a second labelling",
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"per_try_oracle_calls": 26,
|
||||
"predicted_tries": 1.0157554632052712,
|
||||
"red_count": 1,
|
||||
"red_fraction": 0.0009765625,
|
||||
"source": "[BS25] Sec 5 Def. 14 and Lemma 4 (LuckyQuery, the q(1-beta)^k term); B2's O-07",
|
||||
"trials": 5,
|
||||
"trials_with_accept": 5,
|
||||
"verdict": "ACCEPTED (many accepted roots per labelling): a lottery over tau is not bound to the labelling cost"
|
||||
},
|
||||
{
|
||||
"accepted_roots_per_labelling": [
|
||||
140,
|
||||
146,
|
||||
140,
|
||||
151,
|
||||
152
|
||||
],
|
||||
"id": "A6",
|
||||
"k": 16,
|
||||
"max_tries": 400,
|
||||
"mean_first_accept_try": 1.6,
|
||||
"mechanism": "tau is a fresh oracle output per rewrite of one red leaf (n + k calls, no relabelling): with beta = red fraction the expected tries are (1-beta)^-k, and with ONE red sink nearly every try is an accepted root, so one labelling yields many accepted (tau, proof) pairs for one chi; the theorem's cmc bound still holds per accepted proof but nothing in the primitive makes a second accepted root cost a second labelling",
|
||||
"params": {
|
||||
"N": 1024,
|
||||
"k": 16,
|
||||
"n": 10
|
||||
},
|
||||
"per_try_oracle_calls": 26,
|
||||
"predicted_tries": 2.808403965576447,
|
||||
"red_count": 64,
|
||||
"red_fraction": 0.0625,
|
||||
"source": "[BS25] Sec 5 Def. 14 and Lemma 4 (LuckyQuery, the q(1-beta)^k term); B2's O-07",
|
||||
"trials": 5,
|
||||
"trials_with_accept": 5,
|
||||
"verdict": "ACCEPTED (many accepted roots per labelling): a lottery over tau is not bound to the labelling cost"
|
||||
}
|
||||
]
|
||||
249
tools/mhpow/b1/fixtures/k-table.json
Normal file
249
tools/mhpow/b1/fixtures/k-table.json
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
[
|
||||
{
|
||||
"N": 4096,
|
||||
"d_ABH17": 122.88,
|
||||
"e_ABH17": 0.082944,
|
||||
"k_cor2": 17525500,
|
||||
"k_over_N": 4278.6865234375,
|
||||
"label_array_bytes": 131072,
|
||||
"lambda": 256,
|
||||
"n": 12,
|
||||
"proof_bytes": 21889349545,
|
||||
"proof_over_label_array": 167002.48371124268,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 65536,
|
||||
"d_ABH17": 1966.08,
|
||||
"e_ABH17": 0.995328,
|
||||
"k_cor2": 23367333,
|
||||
"k_over_N": 356.5572052001953,
|
||||
"label_array_bytes": 2097152,
|
||||
"lambda": 256,
|
||||
"n": 16,
|
||||
"proof_bytes": 38158854834,
|
||||
"proof_over_label_array": 18195.55989933014,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 1048576,
|
||||
"d_ABH17": 31457.28,
|
||||
"e_ABH17": 12.7401984,
|
||||
"k_cor2": 29209166,
|
||||
"k_over_N": 27.85603141784668,
|
||||
"label_array_bytes": 33554432,
|
||||
"lambda": 256,
|
||||
"n": 20,
|
||||
"proof_bytes": 58914887867,
|
||||
"proof_over_label_array": 1755.800481647253,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 4194304,
|
||||
"d_ABH17": 125829.12,
|
||||
"e_ABH17": 46.327994181818184,
|
||||
"k_cor2": 32130082,
|
||||
"k_over_N": 7.6604084968566895,
|
||||
"label_array_bytes": 134217728,
|
||||
"lambda": 256,
|
||||
"n": 22,
|
||||
"proof_bytes": 70975351183,
|
||||
"proof_over_label_array": 528.8075743839145,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 16777216,
|
||||
"d_ABH17": 503316.48,
|
||||
"e_ABH17": 169.869312,
|
||||
"k_cor2": 35050999,
|
||||
"k_over_N": 2.089202344417572,
|
||||
"label_array_bytes": 536870912,
|
||||
"lambda": 256,
|
||||
"n": 24,
|
||||
"proof_bytes": 84157448644,
|
||||
"proof_over_label_array": 156.75546348839998,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 1073741824,
|
||||
"d_ABH17": 32212254.72,
|
||||
"e_ABH17": 8697.3087744,
|
||||
"k_cor2": 43813748,
|
||||
"k_over_N": 0.04080473259091377,
|
||||
"label_array_bytes": 34359738368,
|
||||
"lambda": 256,
|
||||
"n": 30,
|
||||
"proof_bytes": 130433527841,
|
||||
"proof_over_label_array": 3.7961152801581193,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 4294967296,
|
||||
"d_ABH17": 128849018.88,
|
||||
"e_ABH17": 32614.907904,
|
||||
"k_cor2": 46734665,
|
||||
"k_over_N": 0.010881262132897973,
|
||||
"label_array_bytes": 137438953472,
|
||||
"lambda": 256,
|
||||
"n": 32,
|
||||
"proof_bytes": 148102153430,
|
||||
"proof_over_label_array": 1.0775849909259705,
|
||||
"table": "proven"
|
||||
},
|
||||
{
|
||||
"N": 4096,
|
||||
"S": 655,
|
||||
"beta": 0.159912109375,
|
||||
"cheater_parallel_rounds": 1024,
|
||||
"d": 1024,
|
||||
"k_for_2^-128": 510,
|
||||
"k_for_2^-40": 160,
|
||||
"n": 12,
|
||||
"proof_bytes_2^-128": 637035,
|
||||
"proof_bytes_2^-40": 199885,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 4096,
|
||||
"S": 900,
|
||||
"beta": 0.2197265625,
|
||||
"cheater_parallel_rounds": 256,
|
||||
"d": 256,
|
||||
"k_for_2^-128": 358,
|
||||
"k_for_2^-40": 112,
|
||||
"n": 12,
|
||||
"proof_bytes_2^-128": 447187,
|
||||
"proof_bytes_2^-40": 139933,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 4096,
|
||||
"S": 964,
|
||||
"beta": 0.2353515625,
|
||||
"cheater_parallel_rounds": 64,
|
||||
"d": 64,
|
||||
"k_for_2^-128": 331,
|
||||
"k_for_2^-40": 104,
|
||||
"n": 12,
|
||||
"proof_bytes_2^-128": 413464,
|
||||
"proof_bytes_2^-40": 129941,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 4096,
|
||||
"S": 1090,
|
||||
"beta": 0.26611328125,
|
||||
"cheater_parallel_rounds": 16,
|
||||
"d": 16,
|
||||
"k_for_2^-128": 287,
|
||||
"k_for_2^-40": 90,
|
||||
"n": 12,
|
||||
"proof_bytes_2^-128": 358508,
|
||||
"proof_bytes_2^-40": 112455,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 65536,
|
||||
"S": 10582,
|
||||
"beta": 0.161468505859375,
|
||||
"cheater_parallel_rounds": 16384,
|
||||
"d": 16384,
|
||||
"k_for_2^-128": 504,
|
||||
"k_for_2^-40": 158,
|
||||
"n": 16,
|
||||
"proof_bytes_2^-128": 823077,
|
||||
"proof_bytes_2^-40": 258059,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 65536,
|
||||
"S": 14404,
|
||||
"beta": 0.21978759765625,
|
||||
"cheater_parallel_rounds": 4096,
|
||||
"d": 4096,
|
||||
"k_for_2^-128": 358,
|
||||
"k_for_2^-40": 112,
|
||||
"n": 16,
|
||||
"proof_bytes_2^-128": 584659,
|
||||
"proof_bytes_2^-40": 182941,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 65536,
|
||||
"S": 15375,
|
||||
"beta": 0.2346038818359375,
|
||||
"cheater_parallel_rounds": 1024,
|
||||
"d": 1024,
|
||||
"k_for_2^-128": 332,
|
||||
"k_for_2^-40": 104,
|
||||
"n": 16,
|
||||
"proof_bytes_2^-128": 542201,
|
||||
"proof_bytes_2^-40": 169877,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 65536,
|
||||
"S": 15745,
|
||||
"beta": 0.2402496337890625,
|
||||
"cheater_parallel_rounds": 256,
|
||||
"d": 256,
|
||||
"k_for_2^-128": 323,
|
||||
"k_for_2^-40": 101,
|
||||
"n": 16,
|
||||
"proof_bytes_2^-128": 527504,
|
||||
"proof_bytes_2^-40": 164978,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 1048576,
|
||||
"S": 176424,
|
||||
"beta": 0.16825103759765625,
|
||||
"cheater_parallel_rounds": 262144,
|
||||
"d": 262144,
|
||||
"k_for_2^-128": 482,
|
||||
"k_for_2^-40": 151,
|
||||
"n": 20,
|
||||
"proof_bytes_2^-128": 972239,
|
||||
"proof_bytes_2^-40": 304612,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 1048576,
|
||||
"S": 232446,
|
||||
"beta": 0.2216777801513672,
|
||||
"cheater_parallel_rounds": 65536,
|
||||
"d": 65536,
|
||||
"k_for_2^-128": 355,
|
||||
"k_for_2^-40": 111,
|
||||
"n": 20,
|
||||
"proof_bytes_2^-128": 716080,
|
||||
"proof_bytes_2^-40": 223932,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 1048576,
|
||||
"S": 249184,
|
||||
"beta": 0.237640380859375,
|
||||
"cheater_parallel_rounds": 16384,
|
||||
"d": 16384,
|
||||
"k_for_2^-128": 327,
|
||||
"k_for_2^-40": 103,
|
||||
"n": 20,
|
||||
"proof_bytes_2^-128": 659604,
|
||||
"proof_bytes_2^-40": 207796,
|
||||
"table": "attack1-greedy"
|
||||
},
|
||||
{
|
||||
"N": 1048576,
|
||||
"S": 253922,
|
||||
"beta": 0.2421588897705078,
|
||||
"cheater_parallel_rounds": 4096,
|
||||
"d": 4096,
|
||||
"k_for_2^-128": 320,
|
||||
"k_for_2^-40": 100,
|
||||
"n": 20,
|
||||
"proof_bytes_2^-128": 645485,
|
||||
"proof_bytes_2^-40": 201745,
|
||||
"table": "attack1-greedy"
|
||||
}
|
||||
]
|
||||
19
tools/mhpow/b1/fixtures/k-table.log
Normal file
19
tools/mhpow/b1/fixtures/k-table.log
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{"table": "attack1-greedy", "n": 12, "N": 4096, "d": 1024, "S": 655, "beta": 0.159912109375, "k_for_2^-40": 160, "proof_bytes_2^-40": 199885, "k_for_2^-128": 510, "proof_bytes_2^-128": 637035, "cheater_parallel_rounds": 1024}
|
||||
{"table": "attack1-greedy", "n": 12, "N": 4096, "d": 256, "S": 900, "beta": 0.2197265625, "k_for_2^-40": 112, "proof_bytes_2^-40": 139933, "k_for_2^-128": 358, "proof_bytes_2^-128": 447187, "cheater_parallel_rounds": 256}
|
||||
{"table": "attack1-greedy", "n": 12, "N": 4096, "d": 64, "S": 964, "beta": 0.2353515625, "k_for_2^-40": 104, "proof_bytes_2^-40": 129941, "k_for_2^-128": 331, "proof_bytes_2^-128": 413464, "cheater_parallel_rounds": 64}
|
||||
{"table": "attack1-greedy", "n": 12, "N": 4096, "d": 16, "S": 1090, "beta": 0.26611328125, "k_for_2^-40": 90, "proof_bytes_2^-40": 112455, "k_for_2^-128": 287, "proof_bytes_2^-128": 358508, "cheater_parallel_rounds": 16}
|
||||
{"table": "attack1-greedy", "n": 16, "N": 65536, "d": 16384, "S": 10582, "beta": 0.161468505859375, "k_for_2^-40": 158, "proof_bytes_2^-40": 258059, "k_for_2^-128": 504, "proof_bytes_2^-128": 823077, "cheater_parallel_rounds": 16384}
|
||||
{"table": "attack1-greedy", "n": 16, "N": 65536, "d": 4096, "S": 14404, "beta": 0.21978759765625, "k_for_2^-40": 112, "proof_bytes_2^-40": 182941, "k_for_2^-128": 358, "proof_bytes_2^-128": 584659, "cheater_parallel_rounds": 4096}
|
||||
{"table": "attack1-greedy", "n": 16, "N": 65536, "d": 1024, "S": 15375, "beta": 0.2346038818359375, "k_for_2^-40": 104, "proof_bytes_2^-40": 169877, "k_for_2^-128": 332, "proof_bytes_2^-128": 542201, "cheater_parallel_rounds": 1024}
|
||||
{"table": "attack1-greedy", "n": 16, "N": 65536, "d": 256, "S": 15745, "beta": 0.2402496337890625, "k_for_2^-40": 101, "proof_bytes_2^-40": 164978, "k_for_2^-128": 323, "proof_bytes_2^-128": 527504, "cheater_parallel_rounds": 256}
|
||||
{"table": "attack1-greedy", "n": 20, "N": 1048576, "d": 262144, "S": 176424, "beta": 0.16825103759765625, "k_for_2^-40": 151, "proof_bytes_2^-40": 304612, "k_for_2^-128": 482, "proof_bytes_2^-128": 972239, "cheater_parallel_rounds": 262144}
|
||||
{"table": "attack1-greedy", "n": 20, "N": 1048576, "d": 65536, "S": 232446, "beta": 0.2216777801513672, "k_for_2^-40": 111, "proof_bytes_2^-40": 223932, "k_for_2^-128": 355, "proof_bytes_2^-128": 716080, "cheater_parallel_rounds": 65536}
|
||||
{"table": "attack1-greedy", "n": 20, "N": 1048576, "d": 16384, "S": 249184, "beta": 0.237640380859375, "k_for_2^-40": 103, "proof_bytes_2^-40": 207796, "k_for_2^-128": 327, "proof_bytes_2^-128": 659604, "cheater_parallel_rounds": 16384}
|
||||
{"table": "attack1-greedy", "n": 20, "N": 1048576, "d": 4096, "S": 253922, "beta": 0.2421588897705078, "k_for_2^-40": 100, "proof_bytes_2^-40": 201745, "k_for_2^-128": 320, "proof_bytes_2^-128": 645485, "cheater_parallel_rounds": 4096}
|
||||
{"table": "proven", "n": 12, "N": 4096, "lambda": 256, "e_ABH17": 0.082944, "d_ABH17": 122.88, "k_cor2": 17525500, "k_over_N": 4278.6865234375, "proof_bytes": 21889349545, "label_array_bytes": 131072, "proof_over_label_array": 167002.48371124268}
|
||||
{"table": "proven", "n": 16, "N": 65536, "lambda": 256, "e_ABH17": 0.995328, "d_ABH17": 1966.08, "k_cor2": 23367333, "k_over_N": 356.5572052001953, "proof_bytes": 38158854834, "label_array_bytes": 2097152, "proof_over_label_array": 18195.55989933014}
|
||||
{"table": "proven", "n": 20, "N": 1048576, "lambda": 256, "e_ABH17": 12.7401984, "d_ABH17": 31457.28, "k_cor2": 29209166, "k_over_N": 27.85603141784668, "proof_bytes": 58914887867, "label_array_bytes": 33554432, "proof_over_label_array": 1755.800481647253}
|
||||
{"table": "proven", "n": 22, "N": 4194304, "lambda": 256, "e_ABH17": 46.327994181818184, "d_ABH17": 125829.12, "k_cor2": 32130082, "k_over_N": 7.6604084968566895, "proof_bytes": 70975351183, "label_array_bytes": 134217728, "proof_over_label_array": 528.8075743839145}
|
||||
{"table": "proven", "n": 24, "N": 16777216, "lambda": 256, "e_ABH17": 169.869312, "d_ABH17": 503316.48, "k_cor2": 35050999, "k_over_N": 2.089202344417572, "proof_bytes": 84157448644, "label_array_bytes": 536870912, "proof_over_label_array": 156.75546348839998}
|
||||
{"table": "proven", "n": 30, "N": 1073741824, "lambda": 256, "e_ABH17": 8697.3087744, "d_ABH17": 32212254.72, "k_cor2": 43813748, "k_over_N": 0.04080473259091377, "proof_bytes": 130433527841, "label_array_bytes": 34359738368, "proof_over_label_array": 3.7961152801581193}
|
||||
{"table": "proven", "n": 32, "N": 4294967296, "lambda": 256, "e_ABH17": 32614.907904, "d_ABH17": 128849018.88, "k_cor2": 46734665, "k_over_N": 0.010881262132897973, "proof_bytes": 148102153430, "label_array_bytes": 137438953472, "proof_over_label_array": 1.0775849909259705}
|
||||
118
tools/mhpow/b1/fixtures/kat-n04.json
Normal file
118
tools/mhpow/b1/fixtures/kat-n04.json
Normal file
File diff suppressed because one or more lines are too long
BIN
tools/mhpow/b1/fixtures/kat-n04.proof.bin
Normal file
BIN
tools/mhpow/b1/fixtures/kat-n04.proof.bin
Normal file
Binary file not shown.
1374
tools/mhpow/b1/fixtures/kat-n08.json
Normal file
1374
tools/mhpow/b1/fixtures/kat-n08.json
Normal file
File diff suppressed because one or more lines are too long
BIN
tools/mhpow/b1/fixtures/kat-n08.proof.bin
Normal file
BIN
tools/mhpow/b1/fixtures/kat-n08.proof.bin
Normal file
Binary file not shown.
93
tools/mhpow/b1/fixtures/kat-n12.json
Normal file
93
tools/mhpow/b1/fixtures/kat-n12.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"N": 4096,
|
||||
"challenges_c_i": [
|
||||
2331,
|
||||
739,
|
||||
401,
|
||||
182,
|
||||
691,
|
||||
3144,
|
||||
301,
|
||||
3840,
|
||||
1473,
|
||||
3907,
|
||||
3027,
|
||||
782,
|
||||
1184,
|
||||
3316,
|
||||
331,
|
||||
3084,
|
||||
2937,
|
||||
3059,
|
||||
3432,
|
||||
2573,
|
||||
1944,
|
||||
2975,
|
||||
591,
|
||||
3706,
|
||||
556,
|
||||
3167,
|
||||
2343,
|
||||
3262,
|
||||
3890,
|
||||
4081,
|
||||
2659,
|
||||
3382,
|
||||
756,
|
||||
2746,
|
||||
1108,
|
||||
3817,
|
||||
1788,
|
||||
2129,
|
||||
972,
|
||||
3805,
|
||||
3363,
|
||||
245,
|
||||
1375,
|
||||
3444,
|
||||
2029,
|
||||
1672,
|
||||
3403,
|
||||
43,
|
||||
635,
|
||||
94,
|
||||
2323,
|
||||
2054,
|
||||
1986,
|
||||
1239,
|
||||
2175,
|
||||
3712,
|
||||
1677,
|
||||
2346,
|
||||
2707,
|
||||
560,
|
||||
4095,
|
||||
2662,
|
||||
3468,
|
||||
592
|
||||
],
|
||||
"chi": "286bb44f63729d7b19b40f08071e3a9cb42e812edf4079e5a0189ff7c92fa030",
|
||||
"construction": "[BS25] Sec 3.2 over [ABH17] Alg. 1 DRSample; encoding params.py",
|
||||
"fixture": "kat-n12",
|
||||
"graph_parents_u64le_sha256": "a12e9e1e0a063833687df3a7486cff329593079b6b63f24198c7cd4ddc2f8aed",
|
||||
"graph_seed": "9f25395581b8a72f3e17a812fede366102eba1dbd75cb3a02ad8054d5d79af2d",
|
||||
"hash": "BLAKE2b-256 unkeyed",
|
||||
"k": 64,
|
||||
"labels_concat_sha256": "5b2188573c4143fa1c69a262ebd656fc1bd3afa38e96c44ceca6533506cd5c75",
|
||||
"lambda_bits": 256,
|
||||
"n": 12,
|
||||
"proof_bytes": 79981,
|
||||
"proof_sha256": "3c7de0539a8715e75d3d64eb11ec3af7d8a2a338c101958a0c29e300c3ad4f7f",
|
||||
"tags": {
|
||||
"challenge": "C",
|
||||
"graph": "G",
|
||||
"label": "L",
|
||||
"merkle": "M"
|
||||
},
|
||||
"tau": "884d36cd2df99fa94e49bc0c358d7cdd3663ebac944d21fee983c3176dfe2687",
|
||||
"verifier": {
|
||||
"accepted": true,
|
||||
"check": "accept",
|
||||
"oracle_calls": 2432
|
||||
}
|
||||
}
|
||||
BIN
tools/mhpow/b1/fixtures/kat-n12.proof.bin
Normal file
BIN
tools/mhpow/b1/fixtures/kat-n12.proof.bin
Normal file
Binary file not shown.
93
tools/mhpow/b1/fixtures/kat-n16.json
Normal file
93
tools/mhpow/b1/fixtures/kat-n16.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"N": 65536,
|
||||
"challenges_c_i": [
|
||||
37058,
|
||||
36166,
|
||||
7395,
|
||||
13143,
|
||||
29525,
|
||||
58720,
|
||||
10284,
|
||||
57346,
|
||||
21195,
|
||||
33217,
|
||||
45923,
|
||||
15399,
|
||||
22255,
|
||||
13950,
|
||||
48909,
|
||||
16634,
|
||||
46736,
|
||||
59559,
|
||||
55581,
|
||||
47879,
|
||||
50708,
|
||||
22007,
|
||||
32888,
|
||||
48509,
|
||||
12067,
|
||||
49112,
|
||||
3385,
|
||||
7094,
|
||||
46049,
|
||||
54059,
|
||||
18450,
|
||||
51765,
|
||||
50610,
|
||||
40490,
|
||||
7132,
|
||||
59844,
|
||||
18264,
|
||||
22098,
|
||||
40888,
|
||||
29136,
|
||||
43203,
|
||||
45899,
|
||||
26164,
|
||||
5304,
|
||||
48542,
|
||||
37977,
|
||||
41471,
|
||||
49209,
|
||||
1718,
|
||||
22943,
|
||||
46330,
|
||||
30881,
|
||||
22914,
|
||||
10602,
|
||||
15776,
|
||||
52395,
|
||||
2205,
|
||||
27808,
|
||||
6684,
|
||||
38160,
|
||||
32810,
|
||||
39169,
|
||||
48933,
|
||||
15339
|
||||
],
|
||||
"chi": "1dd81aac8403c3b65c7da0b52eadb1a08104a988e73e3aa92c955ca16fa67a9a",
|
||||
"construction": "[BS25] Sec 3.2 over [ABH17] Alg. 1 DRSample; encoding params.py",
|
||||
"fixture": "kat-n16",
|
||||
"graph_parents_u64le_sha256": "811895268cb0e8f37e22e8762de44b4e5e5f0297a79207227580a6bf43ee4314",
|
||||
"graph_seed": "9f25395581b8a72f3e17a812fede366102eba1dbd75cb3a02ad8054d5d79af2d",
|
||||
"hash": "BLAKE2b-256 unkeyed",
|
||||
"k": 64,
|
||||
"labels_concat_sha256": "b8cf70b7a10c82dee36dda8ea0fef39308819d24611c9051b104ba704322d97b",
|
||||
"lambda_bits": 256,
|
||||
"n": 16,
|
||||
"proof_bytes": 104557,
|
||||
"proof_sha256": "793fd4d505155f133c8acf1c657a98795f29f7d6cba2568570c8324c42fb1118",
|
||||
"tags": {
|
||||
"challenge": "C",
|
||||
"graph": "G",
|
||||
"label": "L",
|
||||
"merkle": "M"
|
||||
},
|
||||
"tau": "41e05146c5819b21487fc485ac9de87ff0521b8d4e921d8655c5a1cb7c99385e",
|
||||
"verifier": {
|
||||
"accepted": true,
|
||||
"check": "accept",
|
||||
"oracle_calls": 3200
|
||||
}
|
||||
}
|
||||
BIN
tools/mhpow/b1/fixtures/kat-n16.proof.bin
Normal file
BIN
tools/mhpow/b1/fixtures/kat-n16.proof.bin
Normal file
Binary file not shown.
93
tools/mhpow/b1/fixtures/kat-n20.json
Normal file
93
tools/mhpow/b1/fixtures/kat-n20.json
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"N": 1048576,
|
||||
"challenges_c_i": [
|
||||
478328,
|
||||
234763,
|
||||
962086,
|
||||
987402,
|
||||
136367,
|
||||
302234,
|
||||
907558,
|
||||
945947,
|
||||
443172,
|
||||
200341,
|
||||
196554,
|
||||
615131,
|
||||
1032010,
|
||||
24137,
|
||||
31777,
|
||||
462029,
|
||||
299644,
|
||||
270093,
|
||||
905498,
|
||||
969459,
|
||||
16578,
|
||||
368025,
|
||||
859298,
|
||||
419728,
|
||||
841167,
|
||||
1025362,
|
||||
687240,
|
||||
176569,
|
||||
580698,
|
||||
957290,
|
||||
784586,
|
||||
978457,
|
||||
36129,
|
||||
577448,
|
||||
392226,
|
||||
711776,
|
||||
949535,
|
||||
343819,
|
||||
281553,
|
||||
809444,
|
||||
857405,
|
||||
904463,
|
||||
334337,
|
||||
577019,
|
||||
794312,
|
||||
477162,
|
||||
351630,
|
||||
104684,
|
||||
479418,
|
||||
685136,
|
||||
782054,
|
||||
333943,
|
||||
78240,
|
||||
784632,
|
||||
143051,
|
||||
787678,
|
||||
84897,
|
||||
883533,
|
||||
614079,
|
||||
250175,
|
||||
999091,
|
||||
869462,
|
||||
825523,
|
||||
955417
|
||||
],
|
||||
"chi": "842efbdf693296a28d46e967fe9930bacd9c48cb06b518a4e9529308fabc2560",
|
||||
"construction": "[BS25] Sec 3.2 over [ABH17] Alg. 1 DRSample; encoding params.py",
|
||||
"fixture": "kat-n20",
|
||||
"graph_parents_u64le_sha256": "a3c61becd7b467711f5ca3e44ec1faf51905f221d3e8be71ea8515b02523d5d5",
|
||||
"graph_seed": "9f25395581b8a72f3e17a812fede366102eba1dbd75cb3a02ad8054d5d79af2d",
|
||||
"hash": "BLAKE2b-256 unkeyed",
|
||||
"k": 64,
|
||||
"labels_concat_sha256": "02e355ae74233acbe451012ad58c438cee307ab5821204231e14e64da31daa96",
|
||||
"lambda_bits": 256,
|
||||
"n": 20,
|
||||
"proof_bytes": 129133,
|
||||
"proof_sha256": "a49367d9c80f0855504fe1f0994acbd5ddc4b023eaf5904a28ad5f7e71ce951c",
|
||||
"tags": {
|
||||
"challenge": "C",
|
||||
"graph": "G",
|
||||
"label": "L",
|
||||
"merkle": "M"
|
||||
},
|
||||
"tau": "e3d83489e27af44b199e2a73db33ee2b9492b38e777e778b694bba152b53786b",
|
||||
"verifier": {
|
||||
"accepted": true,
|
||||
"check": "accept",
|
||||
"oracle_calls": 3968
|
||||
}
|
||||
}
|
||||
BIN
tools/mhpow/b1/fixtures/kat-n20.proof.bin
Normal file
BIN
tools/mhpow/b1/fixtures/kat-n20.proof.bin
Normal file
Binary file not shown.
24
tools/mhpow/b1/fixtures/row-n12-k64.json
Normal file
24
tools/mhpow/b1/fixtures/row-n12-k64.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"N": 4096,
|
||||
"boundary": "process wall time, no power measured",
|
||||
"graph_sample_s": 0.012868257996160537,
|
||||
"host": "igneum-build-6",
|
||||
"k": 64,
|
||||
"labels_bytes_model": 131072,
|
||||
"labels_s": 0.003818661003606394,
|
||||
"merkle_bytes_model": 262112,
|
||||
"merkle_s": 0.005726456998672802,
|
||||
"n": 12,
|
||||
"oracle_calls_prover": 8255,
|
||||
"peak_rss_bytes_measured": 19660800,
|
||||
"proof_bytes": 79981,
|
||||
"prover_sequential_s_excl_graph": 0.01040476000343915,
|
||||
"prover_state_bytes_model": 393184,
|
||||
"python": "3.12.3",
|
||||
"respond_s": 0.0008596420011599548,
|
||||
"row": "honest-n12-k64",
|
||||
"setting": "stock",
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)",
|
||||
"verifier_oracle_calls": 2432,
|
||||
"verify_s": 0.002651128997968044
|
||||
}
|
||||
24
tools/mhpow/b1/fixtures/row-n16-k64.json
Normal file
24
tools/mhpow/b1/fixtures/row-n16-k64.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"N": 65536,
|
||||
"boundary": "process wall time, no power measured",
|
||||
"graph_sample_s": 0.20376398300140863,
|
||||
"host": "igneum-build-6",
|
||||
"k": 64,
|
||||
"labels_bytes_model": 2097152,
|
||||
"labels_s": 0.06238005399791291,
|
||||
"merkle_bytes_model": 4194272,
|
||||
"merkle_s": 0.11455441799625987,
|
||||
"n": 16,
|
||||
"oracle_calls_prover": 131135,
|
||||
"peak_rss_bytes_measured": 39870464,
|
||||
"proof_bytes": 104557,
|
||||
"prover_sequential_s_excl_graph": 0.17990126999939093,
|
||||
"prover_state_bytes_model": 6291424,
|
||||
"python": "3.12.3",
|
||||
"respond_s": 0.002966798005218152,
|
||||
"row": "honest-n16-k64",
|
||||
"setting": "stock",
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)",
|
||||
"verifier_oracle_calls": 3200,
|
||||
"verify_s": 0.003200609004124999
|
||||
}
|
||||
24
tools/mhpow/b1/fixtures/row-n20-k64.json
Normal file
24
tools/mhpow/b1/fixtures/row-n20-k64.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"N": 1048576,
|
||||
"boundary": "process wall time, no power measured",
|
||||
"graph_sample_s": 3.311251960003574,
|
||||
"host": "igneum-build-6",
|
||||
"k": 64,
|
||||
"labels_bytes_model": 33554432,
|
||||
"labels_s": 1.0262025019983412,
|
||||
"merkle_bytes_model": 67108832,
|
||||
"merkle_s": 2.4088434259974747,
|
||||
"n": 20,
|
||||
"oracle_calls_prover": 2097215,
|
||||
"peak_rss_bytes_measured": 355725312,
|
||||
"proof_bytes": 129133,
|
||||
"prover_sequential_s_excl_graph": 3.4809705539955758,
|
||||
"prover_state_bytes_model": 100663264,
|
||||
"python": "3.12.3",
|
||||
"respond_s": 0.04592462599975988,
|
||||
"row": "honest-n20-k64",
|
||||
"setting": "stock",
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)",
|
||||
"verifier_oracle_calls": 3968,
|
||||
"verify_s": 0.004032391007058322
|
||||
}
|
||||
24
tools/mhpow/b1/fixtures/row-n22-k64.json
Normal file
24
tools/mhpow/b1/fixtures/row-n22-k64.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"N": 4194304,
|
||||
"boundary": "process wall time, no power measured",
|
||||
"graph_sample_s": 13.798183551996772,
|
||||
"host": "igneum-build-6",
|
||||
"k": 64,
|
||||
"labels_bytes_model": 134217728,
|
||||
"labels_s": 4.345659284001158,
|
||||
"merkle_bytes_model": 268435424,
|
||||
"merkle_s": 10.386853066003823,
|
||||
"n": 22,
|
||||
"oracle_calls_prover": 8388671,
|
||||
"peak_rss_bytes_measured": 1365721088,
|
||||
"proof_bytes": 141421,
|
||||
"prover_sequential_s_excl_graph": 14.914240817000973,
|
||||
"prover_state_bytes_model": 402653152,
|
||||
"python": "3.12.3",
|
||||
"respond_s": 0.18172846699599177,
|
||||
"row": "honest-n22-k64",
|
||||
"setting": "stock",
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)",
|
||||
"verifier_oracle_calls": 4352,
|
||||
"verify_s": 0.04833257199788932
|
||||
}
|
||||
24
tools/mhpow/b1/fixtures/row-n24-k64.json
Normal file
24
tools/mhpow/b1/fixtures/row-n24-k64.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"N": 16777216,
|
||||
"boundary": "process wall time, no power measured",
|
||||
"graph_sample_s": 52.28400299400528,
|
||||
"host": "igneum-build-6",
|
||||
"k": 64,
|
||||
"labels_bytes_model": 536870912,
|
||||
"labels_s": 17.542553989995213,
|
||||
"merkle_bytes_model": 1073741792,
|
||||
"merkle_s": 48.69316720600182,
|
||||
"n": 24,
|
||||
"oracle_calls_prover": 33554495,
|
||||
"peak_rss_bytes_measured": 5407219712,
|
||||
"proof_bytes": 153709,
|
||||
"prover_sequential_s_excl_graph": 66.96936401799758,
|
||||
"prover_state_bytes_model": 1610612704,
|
||||
"python": "3.12.3",
|
||||
"respond_s": 0.7336428220005473,
|
||||
"row": "honest-n24-k64",
|
||||
"setting": "stock",
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)",
|
||||
"verifier_oracle_calls": 4736,
|
||||
"verify_s": 0.004654051001125481
|
||||
}
|
||||
22
tools/mhpow/b1/fixtures/tests.log
Normal file
22
tools/mhpow/b1/fixtures/tests.log
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
test_bucket_spread (test_b1.GraphTests.test_bucket_spread) ... ok
|
||||
test_deterministic (test_b1.GraphTests.test_deterministic) ... ok
|
||||
test_edge_rule (test_b1.GraphTests.test_edge_rule) ... ok
|
||||
test_layout_lsb_at_root (test_b1.MerkleTests.test_layout_lsb_at_root) ... ok
|
||||
test_reveal_check_every_leaf (test_b1.MerkleTests.test_reveal_check_every_leaf) ... ok
|
||||
test_container (test_b1.VerifierTests.test_container) ... ok
|
||||
test_duplicated_parent_position (test_b1.VerifierTests.test_duplicated_parent_position) ... ok
|
||||
test_every_check_has_a_refusal (test_b1.VerifierTests.test_every_check_has_a_refusal) ... ok
|
||||
test_honest_accepts_object_and_bytes (test_b1.VerifierTests.test_honest_accepts_object_and_bytes) ... ok
|
||||
test_skipped_parent (test_b1.VerifierTests.test_skipped_parent) ... ok
|
||||
test_wrong_challenge (test_b1.VerifierTests.test_wrong_challenge) ... ok
|
||||
test_wrong_chi (test_b1.VerifierTests.test_wrong_chi) ... ok
|
||||
test_wrong_domain_tag_label (test_b1.VerifierTests.test_wrong_domain_tag_label) ... ok
|
||||
test_wrong_domain_tag_merkle (test_b1.VerifierTests.test_wrong_domain_tag_merkle) ... ok
|
||||
test_wrong_label (test_b1.VerifierTests.test_wrong_label) ... ok
|
||||
test_wrong_parent_label (test_b1.VerifierTests.test_wrong_parent_label) ... ok
|
||||
test_wrong_path (test_b1.VerifierTests.test_wrong_path) ... ok
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Ran 17 tests in 0.086s
|
||||
|
||||
OK
|
||||
139
tools/mhpow/b1/gen_fixtures.py
Normal file
139
tools/mhpow/b1/gen_fixtures.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""Known-answer fixtures, honest-prover rows and attack fixtures for B1. Deterministic; run on a build box only.
|
||||
|
||||
python3 gen_fixtures.py kat <out_dir> known-answer fixtures n = 4, 8, 12, 16, 20 (k = 8 at n = 4, else 64)
|
||||
python3 gen_fixtures.py row <out_dir> <n> <k> one honest-prover row (own process, so its peak RSS is its own)
|
||||
python3 gen_fixtures.py attacks <out_dir> the malicious-prover fixtures
|
||||
Every file written is listed with its sha256 in <out_dir>/SHA256SUMS by the driver (run-b1.sh).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import resource
|
||||
import sys
|
||||
import time
|
||||
|
||||
import attacks
|
||||
import drsample
|
||||
import mtp
|
||||
import params
|
||||
from params import H
|
||||
|
||||
D = params.DIGEST_BYTES
|
||||
|
||||
|
||||
def kat_chi(n: int) -> bytes:
|
||||
return H(b"B1-fixture", b"kat", params.u64(n))
|
||||
|
||||
|
||||
def sha(b: bytes) -> str:
|
||||
import hashlib
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def kat(out: str) -> None:
|
||||
for n, k in ((4, 8), (8, 64), (12, 64), (16, 64), (20, 64)):
|
||||
N = 1 << n
|
||||
chi = kat_chi(n)
|
||||
par = drsample.all_parents(N)
|
||||
st = mtp.prove_commit(chi, n, par)
|
||||
proof = mtp.respond(st, k)
|
||||
pb = proof.to_bytes()
|
||||
v = mtp.verify(chi, n, k, pb)
|
||||
assert v.accepted, v
|
||||
graph_bytes = b"".join(params.u64(u) for vv in range(1, N + 1) for u in par[vv])
|
||||
label_bytes = b"".join(st.labels[1:])
|
||||
cs = mtp.challenges(chi, st.tree.root, k, n)
|
||||
fx = {
|
||||
"fixture": f"kat-n{n:02d}", "construction": "[BS25] Sec 3.2 over [ABH17] Alg. 1 DRSample; encoding params.py",
|
||||
"lambda_bits": params.LAMBDA_BITS, "hash": "BLAKE2b-256 unkeyed", "tags": {"label": "L", "merkle": "M",
|
||||
"challenge": "C", "graph": "G"}, "graph_seed": params.GRAPH_SEED.hex(),
|
||||
"n": n, "N": N, "k": k, "chi": chi.hex(),
|
||||
"graph_parents_u64le_sha256": sha(graph_bytes),
|
||||
"labels_concat_sha256": sha(label_bytes),
|
||||
"tau": st.tree.root.hex(), "challenges_c_i": cs,
|
||||
"proof_bytes": len(pb), "proof_sha256": sha(pb),
|
||||
"verifier": {"accepted": v.accepted, "check": v.check, "oracle_calls": v.hashes},
|
||||
}
|
||||
if n <= 8:
|
||||
fx["parents"] = {str(vv): list(par[vv]) for vv in range(1, N + 1)}
|
||||
fx["labels"] = {str(vv): st.labels[vv].hex() for vv in range(1, N + 1)}
|
||||
fx["proof_hex"] = pb.hex()
|
||||
with open(os.path.join(out, f"kat-n{n:02d}.json"), "w") as f:
|
||||
json.dump(fx, f, indent=1, sort_keys=True)
|
||||
with open(os.path.join(out, f"kat-n{n:02d}.proof.bin"), "wb") as f:
|
||||
f.write(pb)
|
||||
print(f"kat n={n} tau={st.tree.root.hex()[:16]} proof={len(pb)}B accepted={v.accepted}", flush=True)
|
||||
|
||||
|
||||
def row(out: str, n: int, k: int) -> None:
|
||||
N = 1 << n
|
||||
chi = kat_chi(n)
|
||||
t0 = time.perf_counter()
|
||||
par = drsample.all_parents(N)
|
||||
t1 = time.perf_counter()
|
||||
labels = mtp.compute_labels(chi, par)
|
||||
t2 = time.perf_counter()
|
||||
tree = mtp.MerkleTree(chi, labels, n)
|
||||
t3 = time.perf_counter()
|
||||
st = mtp.ProverState(chi, n, par, labels, tree)
|
||||
proof = mtp.respond(st, k)
|
||||
pb = proof.to_bytes()
|
||||
t4 = time.perf_counter()
|
||||
rss_kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
del st, tree, labels
|
||||
# cold verification: a fresh process state is not available here, so the verifier is timed from bytes with the graph
|
||||
# parents re-derived on demand (no table), which is the network verifier's real path
|
||||
t5 = time.perf_counter()
|
||||
v = mtp.verify(chi, n, k, pb)
|
||||
t6 = time.perf_counter()
|
||||
assert v.accepted
|
||||
r = {
|
||||
"row": f"honest-n{n:02d}-k{k}", "n": n, "N": N, "k": k,
|
||||
"graph_sample_s": t1 - t0, "labels_s": t2 - t1, "merkle_s": t3 - t2, "respond_s": t4 - t3,
|
||||
"prover_sequential_s_excl_graph": t4 - t1,
|
||||
"labels_bytes_model": N * D, "merkle_bytes_model": (2 * N - 1) * D,
|
||||
"prover_state_bytes_model": N * D + (2 * N - 1) * D,
|
||||
"peak_rss_bytes_measured": rss_kb * 1024,
|
||||
"proof_bytes": len(pb), "verify_s": t6 - t5, "verifier_oracle_calls": v.hashes,
|
||||
"oracle_calls_prover": N + (N - 1) + k,
|
||||
"software": "tools/mhpow/b1 (CPython reference, single thread)", "python": platform.python_version(),
|
||||
"host": platform.node(), "boundary": "process wall time, no power measured", "setting": "stock",
|
||||
}
|
||||
with open(os.path.join(out, f"row-n{n:02d}-k{k}.json"), "w") as f:
|
||||
json.dump(r, f, indent=1, sort_keys=True)
|
||||
print(json.dumps(r), flush=True)
|
||||
|
||||
|
||||
def run_attacks(out: str) -> None:
|
||||
n, k = 10, 16
|
||||
par = drsample.all_parents(1 << n)
|
||||
res = [
|
||||
attacks.a1_replay(n, k, par, trials=50),
|
||||
attacks.a2_unopened_block(n, k, par, trials=50),
|
||||
attacks.a3_control_block_steering(n, k, par, t=20, grind=1000, trials=400),
|
||||
attacks.a4_self_similarity(n, k, par, trials=200),
|
||||
attacks.a5_attack1(n, k, par, d=64, trials=400),
|
||||
attacks.a5_attack1(n, k, par, d=256, trials=400),
|
||||
attacks.a6_root_regrind(n, k, par, red_count=1, max_tries=200, trials=5),
|
||||
attacks.a6_root_regrind(n, k, par, red_count=64, max_tries=400, trials=5),
|
||||
]
|
||||
for r in res:
|
||||
r["params"] = {"n": n, "N": 1 << n, "k": k}
|
||||
print(json.dumps({x: r[x] for x in r if x not in ("mechanism",)}), flush=True)
|
||||
with open(os.path.join(out, "attacks-n10-k16.json"), "w") as f:
|
||||
json.dump(res, f, indent=1, sort_keys=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd, out = sys.argv[1], sys.argv[2]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
if cmd == "kat":
|
||||
kat(out)
|
||||
elif cmd == "row":
|
||||
row(out, int(sys.argv[3]), int(sys.argv[4]))
|
||||
elif cmd == "attacks":
|
||||
run_attacks(out)
|
||||
else:
|
||||
raise SystemExit("usage")
|
||||
57
tools/mhpow/b1/k_table.py
Normal file
57
tools/mhpow/b1/k_table.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""The challenge count k two ways, code-generated (README section 6):
|
||||
|
||||
(a) PROVEN: [BS25] Corollary 2, k = ceil(2 N ln2 lambda / e) for an (e, d)-depth-robust G, with e from [ABH17] Theorem 3.1
|
||||
(DRSample is (e, d, b)-block depth-robust w.h.p. with e >= 2.43e-4 N / log N, d >= 0.03 N; log read as log2, README R-4).
|
||||
(b) ATTACK-DRIVEN (a lower bound on k, not a security claim): for [BS25] Sec 7.1 Attack 1 with the greedy depth-reducing set S
|
||||
measured on THIS graph at depth targets d = N / 2^j, the k that pushes the attack's acceptance (1 - |S|/N)^k under 2^-s.
|
||||
Proof bytes follow mtp.proof_bytes_formula with every challenged node of indegree 2.
|
||||
|
||||
python3 k_table.py <out_dir>
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import attacks
|
||||
import drsample
|
||||
|
||||
LAMBDA = 256
|
||||
|
||||
|
||||
def proof_bytes(n: int, k: int) -> int:
|
||||
return 45 + k * (1 + 32 * (n + 1) * 3)
|
||||
|
||||
|
||||
def main(out: str) -> None:
|
||||
rows = []
|
||||
for n in (12, 16, 20, 22, 24, 30, 32):
|
||||
N = 1 << n
|
||||
e = 2.43e-4 * N / n
|
||||
k = math.ceil(2 * N * math.log(2) * LAMBDA / e)
|
||||
rows.append({"table": "proven", "n": n, "N": N, "lambda": LAMBDA, "e_ABH17": e, "d_ABH17": 0.03 * N,
|
||||
"k_cor2": k, "k_over_N": k / N, "proof_bytes": proof_bytes(n, k),
|
||||
"label_array_bytes": 32 * N, "proof_over_label_array": proof_bytes(n, k) / (32 * N)})
|
||||
for n in (12, 16, 20):
|
||||
N = 1 << n
|
||||
par = drsample.all_parents(N)
|
||||
for j in (2, 4, 6, 8):
|
||||
d = N >> j
|
||||
S = attacks.greedy_depth_reducing_set(par, d)
|
||||
beta = len(S) / N
|
||||
row = {"table": "attack1-greedy", "n": n, "N": N, "d": d, "S": len(S), "beta": beta}
|
||||
for s in (40, 128):
|
||||
row[f"k_for_2^-{s}"] = math.ceil(s * math.log(2) / -math.log1p(-beta)) if beta > 0 else None
|
||||
row[f"proof_bytes_2^-{s}"] = proof_bytes(n, row[f"k_for_2^-{s}"]) if beta > 0 else None
|
||||
row["cheater_parallel_rounds"] = drsample.depth_after_removal(par, S)
|
||||
rows.append(row)
|
||||
print(json.dumps(row), flush=True)
|
||||
with open(os.path.join(out, "k-table.json"), "w") as f:
|
||||
json.dump(rows, f, indent=1, sort_keys=True)
|
||||
for r in rows:
|
||||
if r["table"] == "proven":
|
||||
print(json.dumps(r))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1])
|
||||
304
tools/mhpow/b1/mtp.py
Normal file
304
tools/mhpow/b1/mtp.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
"""The Merkle Tree Proof framework of [BS25] Sec 3.2 over DRSample: honest prover, proof container, complete verifier.
|
||||
|
||||
Every step names the paragraph it implements. Section numbers are [BS25] = eprint 2025/1456 as archived 2025-12-31.
|
||||
|
||||
Merkle layout ([BS25] Sec 3.2 step 2, Sec 4.1, App. C.1). The tree position of a leaf is a bit string x = x_1 ... x_n read from
|
||||
the root (x_1 picks the root's child), and the leaf holds tau_x = l_{1 + bin(x)} with bin(x) = sum_i x_i 2^(i-1). So the bit
|
||||
chosen at depth j is bit j-1 of the leaf index (the root splits even and odd indices). Leaves are the raw labels (the paper
|
||||
does not hash them). Inner nodes tau_x = H(chi, tau_x0, tau_x1). README row R-3 records App. C.2's printed loop order, which
|
||||
walks x_1 first from the leaf and is consistent with this layout only if x is read leaf-first; B1 follows Sec 3.2 / 4.1 /
|
||||
Def. 15's x(j) = x_1 ... x_{j-1} not(x_j) (sibling at depth j from the root).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Sequence, Tuple
|
||||
|
||||
import drsample
|
||||
import params
|
||||
from params import H, u64
|
||||
|
||||
D = params.DIGEST_BYTES
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# Labels: [BS25] Sec 2.2 "The Labeling Game" and Sec 3.2 Prove step 1.
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def label_of(chi: bytes, v: int, parent_labels: Sequence[bytes], tag: bytes = params.TAG_LABEL) -> bytes:
|
||||
"""l_v = H(chi, v, l_v1, ..., l_vk) with parents ascending; l_1 = H(chi, 1). `tag` is overridable only for the
|
||||
wrong-domain-tag test adversary."""
|
||||
return H(tag, chi, u64(v), *parent_labels)
|
||||
|
||||
|
||||
def compute_labels(chi: bytes, par: List[Tuple[int, ...]], tag: bytes = params.TAG_LABEL) -> List[bytes]:
|
||||
"""Prove step 1: every label in topological order 1..N. Returns a list indexed 0..N with index 0 unused (None)."""
|
||||
n_nodes = len(par) - 1
|
||||
lab: List[Optional[bytes]] = [None] * (n_nodes + 1)
|
||||
for v in range(1, n_nodes + 1):
|
||||
lab[v] = H(tag, chi, u64(v), *[lab[u] for u in par[v]])
|
||||
return lab
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# Merkle commitment: [BS25] Sec 3.2 Prove step 2, App. C.1 (construction), Def. 15 (reveal), App. C.2 (check).
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def _bitrev(x: int, n: int) -> int:
|
||||
r = 0
|
||||
for _ in range(n):
|
||||
r = (r << 1) | (x & 1)
|
||||
x >>= 1
|
||||
return r
|
||||
|
||||
|
||||
def merkle_node(chi: bytes, left: bytes, right: bytes, tag: bytes = params.TAG_MERKLE) -> bytes:
|
||||
return H(tag, chi, left, right)
|
||||
|
||||
|
||||
class MerkleTree:
|
||||
"""levels[j] holds the 2^j tree nodes at depth j, indexed by their root-first position bits read as a binary number;
|
||||
levels[0] = [tau]."""
|
||||
|
||||
def __init__(self, chi: bytes, labels: List[bytes], n: int, tag: bytes = params.TAG_MERKLE):
|
||||
N = 1 << n
|
||||
assert len(labels) == N + 1
|
||||
self.n = n
|
||||
leaves = [labels[_bitrev(p, n) + 1] for p in range(N)] # tree position p holds leaf index bitrev(p)
|
||||
levels = [leaves]
|
||||
cur = leaves
|
||||
for _ in range(n):
|
||||
cur = [merkle_node(chi, cur[2 * q], cur[2 * q + 1], tag) for q in range(len(cur) // 2)]
|
||||
levels.append(cur)
|
||||
levels.reverse()
|
||||
self.levels = levels
|
||||
|
||||
@property
|
||||
def root(self) -> bytes:
|
||||
return self.levels[0][0]
|
||||
|
||||
def reveal(self, leaf_index: int) -> List[bytes]:
|
||||
"""Def. 15: the sibling labels tau_{x(j)} for j = 1..n (root-first), x = the tree position of leaf `leaf_index`."""
|
||||
p = _bitrev(leaf_index, self.n)
|
||||
return [self.levels[j][(p >> (self.n - j)) ^ 1] for j in range(1, self.n + 1)]
|
||||
|
||||
|
||||
def merkle_tree_check(chi: bytes, leaf_index: int, leaf: bytes, path: Sequence[bytes], tau: bytes, n: int,
|
||||
counter: Optional[list] = None) -> bool:
|
||||
"""App. C.2 MerkleTreeCheck(chi, i, l_i, mu_i, tau): recompute the root from the leaf up and compare with tau."""
|
||||
if len(path) != n or not (0 <= leaf_index < (1 << n)) or len(leaf) != D or any(len(s) != D for s in path):
|
||||
return False
|
||||
sigma = leaf
|
||||
for j in range(n, 0, -1): # from the leaf (depth n) to the root (depth 1)
|
||||
bit = (leaf_index >> (j - 1)) & 1 # x_j = bit j-1 of the index (bin(x) = sum x_j 2^(j-1))
|
||||
sib = path[j - 1]
|
||||
sigma = merkle_node(chi, sigma, sib) if bit == 0 else merkle_node(chi, sib, sigma)
|
||||
if counter is not None:
|
||||
counter[0] += 1
|
||||
return sigma == tau
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# Challenges: [BS25] Sec 3.2 Prove step 3 / Verify step 1; Sec 5 Def. 14.
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
def challenges(chi: bytes, tau: bytes, k: int, n: int, tag: bytes = params.TAG_CHALLENGE) -> List[int]:
|
||||
"""c_i = H(chi, i, tau) mod N for i = 1..k (0-based leaf index; the challenged node is c_i + 1). N = 2^n divides
|
||||
2^256, so the reduction is exactly uniform."""
|
||||
N = 1 << n
|
||||
return [int.from_bytes(H(tag, chi, u64(i), tau), "little") % N for i in range(1, k + 1)]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# The certificate: [BS25] Sec 3.2 Prove step 4, c = (tau, {(l_i, mu_i, L_i)}_{i=1..k}), L_i = {(l_v, mu_{i,v})}_{v in parents}.
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class Opening:
|
||||
label: bytes
|
||||
path: List[bytes]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChallengeResponse:
|
||||
node: Opening # (l_i, mu_i) for node c_i + 1
|
||||
parents: List[Opening] = field(default_factory=list) # L_i in ascending parent order
|
||||
|
||||
|
||||
@dataclass
|
||||
class Proof:
|
||||
n: int
|
||||
tau: bytes
|
||||
responses: List[ChallengeResponse]
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""B1 container: magic(8) | n(u8) | k(u32 le) | tau(32) | per challenge: node label(32) | n siblings(32 n) |
|
||||
parent count(u8) | per parent: label(32) | n siblings(32 n). Positions are never carried: the verifier derives them."""
|
||||
out = [params.PROOF_MAGIC, bytes([self.n]), struct.pack("<I", len(self.responses)), self.tau]
|
||||
for r in self.responses:
|
||||
out.append(r.node.label)
|
||||
out.extend(r.node.path)
|
||||
out.append(bytes([len(r.parents)]))
|
||||
for p in r.parents:
|
||||
out.append(p.label)
|
||||
out.extend(p.path)
|
||||
return b"".join(out)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(b: bytes) -> "Proof":
|
||||
"""Parser; raises ValueError on any malformed container (truncation, trailing bytes, bad magic)."""
|
||||
if len(b) < 8 + 1 + 4 + D or b[:8] != params.PROOF_MAGIC:
|
||||
raise ValueError("bad magic or short header")
|
||||
n = b[8]
|
||||
k = struct.unpack("<I", b[9:13])[0]
|
||||
tau = b[13:13 + D]
|
||||
off = 13 + D
|
||||
|
||||
def take(m: int) -> bytes:
|
||||
nonlocal off
|
||||
if off + m > len(b):
|
||||
raise ValueError("truncated")
|
||||
s = b[off:off + m]
|
||||
off += m
|
||||
return s
|
||||
|
||||
def opening() -> Opening:
|
||||
lab = take(D)
|
||||
return Opening(lab, [take(D) for _ in range(n)])
|
||||
|
||||
resps = []
|
||||
for _ in range(k):
|
||||
node = opening()
|
||||
cnt = take(1)[0]
|
||||
resps.append(ChallengeResponse(node, [opening() for _ in range(cnt)]))
|
||||
if off != len(b):
|
||||
raise ValueError("trailing bytes")
|
||||
return Proof(n, tau, resps)
|
||||
|
||||
|
||||
def proof_bytes_formula(n: int, k: int, indegs: Sequence[int]) -> int:
|
||||
"""Exact container size: 45 + sum_i (1 + 32(n+1) (1 + indeg(c_i + 1)))."""
|
||||
return 8 + 1 + 4 + D + sum(1 + D * (n + 1) * (1 + d) for d in indegs)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# Honest prover: [BS25] Sec 3.2 Prove^H(chi, N, k), steps 1 to 4.
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class ProverState:
|
||||
chi: bytes
|
||||
n: int
|
||||
par: List[Tuple[int, ...]]
|
||||
labels: List[bytes]
|
||||
tree: MerkleTree
|
||||
|
||||
|
||||
def prove_commit(chi: bytes, n: int, par: List[Tuple[int, ...]]) -> ProverState:
|
||||
labels = compute_labels(chi, par) # step 1
|
||||
tree = MerkleTree(chi, labels, n) # step 2
|
||||
return ProverState(chi, n, par, labels, tree)
|
||||
|
||||
|
||||
def respond(st: ProverState, k: int) -> Proof:
|
||||
resps = []
|
||||
for c in challenges(st.chi, st.tree.root, k, st.n): # step 3: c_i
|
||||
v = c + 1
|
||||
node = Opening(st.labels[v], st.tree.reveal(c)) # step 3(a): mu_i for l_{c_i + 1} at bin(c_i)
|
||||
pars = [Opening(st.labels[u], st.tree.reveal(u - 1)) for u in st.par[v]] # step 3(b): mu_{i,v} at bin(v - 1)
|
||||
resps.append(ChallengeResponse(node, pars))
|
||||
return Proof(st.n, st.tree.root, resps) # step 4
|
||||
|
||||
|
||||
def prove(chi: bytes, n: int, k: int, par: Optional[List[Tuple[int, ...]]] = None) -> Proof:
|
||||
if par is None:
|
||||
par = drsample.all_parents(1 << n)
|
||||
return respond(prove_commit(chi, n, par), k)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
# The complete verifier: [BS25] Sec 3.2 Verify^H(chi, R, c', k), every check a named function, run in the paper's order.
|
||||
# ----------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
class Refused(Exception):
|
||||
def __init__(self, check: str, detail: str):
|
||||
super().__init__(f"{check}: {detail}")
|
||||
self.check = check
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def check_0_container(proof: Proof, n: int, k: int) -> None:
|
||||
"""B1 addition (parsing): the certificate is for this N and carries exactly k responses of the right widths."""
|
||||
if proof.n != n:
|
||||
raise Refused("check_0_container", f"n {proof.n} != {n}")
|
||||
if len(proof.responses) != k:
|
||||
raise Refused("check_0_container", f"{len(proof.responses)} responses != k {k}")
|
||||
if len(proof.tau) != D:
|
||||
raise Refused("check_0_container", "tau width")
|
||||
|
||||
|
||||
def check_1_challenges(chi: bytes, proof: Proof, k: int, n: int, counter: list) -> List[int]:
|
||||
"""Verify step 1: identify tau from c' and compute every challenge c'_i = H(chi, i, tau) mod N."""
|
||||
counter[0] += k
|
||||
return challenges(chi, proof.tau, k, n)
|
||||
|
||||
|
||||
def check_2a_parent_set(cs: List[int], proof: Proof) -> List[Tuple[int, ...]]:
|
||||
"""Verify step 2, the separation of L into {(l_j, mu_j)}_{j in parents(c_i + 1)}: the response must open exactly the
|
||||
parents of c_i + 1 in G (the verifier derives them; a skipped or extra parent is refused). Returns the parent tuples."""
|
||||
out = []
|
||||
for i, (c, r) in enumerate(zip(cs, proof.responses), 1):
|
||||
ps = drsample.parents(c + 1)
|
||||
if len(r.parents) != len(ps):
|
||||
raise Refused("check_2a_parent_set", f"challenge {i}: node {c + 1} has {len(ps)} parents, response opens {len(r.parents)}")
|
||||
out.append(ps)
|
||||
return out
|
||||
|
||||
|
||||
def check_2b_merkle_openings(chi: bytes, cs: List[int], pss: List[Tuple[int, ...]], proof: Proof, n: int, counter: list) -> None:
|
||||
"""Verify step 2: MerkleTreeCheck on every reveal, each at the position the verifier derived (node c_i + 1 at index c_i,
|
||||
parent u at index u - 1), never at a position the prover names (the MTP 1.2 / Bevand 'Attack 2' class)."""
|
||||
for i, (c, ps, r) in enumerate(zip(cs, pss, proof.responses), 1):
|
||||
if not merkle_tree_check(chi, c, r.node.label, r.node.path, proof.tau, n, counter):
|
||||
raise Refused("check_2b_merkle_openings", f"challenge {i}: node {c + 1} opening")
|
||||
for u, op in zip(ps, r.parents):
|
||||
if not merkle_tree_check(chi, u - 1, op.label, op.path, proof.tau, n, counter):
|
||||
raise Refused("check_2b_merkle_openings", f"challenge {i}: parent {u} of node {c + 1} opening")
|
||||
|
||||
|
||||
def check_3_local_consistency(chi: bytes, cs: List[int], proof: Proof, counter: list) -> None:
|
||||
"""Verify step 3: l_v = H(chi, v, l_v1, ..., l_vk) for v = c_i + 1 with the committed parent labels."""
|
||||
for i, (c, r) in enumerate(zip(cs, proof.responses), 1):
|
||||
counter[0] += 1
|
||||
if label_of(chi, c + 1, [p.label for p in r.parents]) != r.node.label:
|
||||
raise Refused("check_3_local_consistency", f"challenge {i}: node {c + 1} is red")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Verdict:
|
||||
accepted: bool
|
||||
check: str # "accept" or the first refusing check
|
||||
detail: str
|
||||
hashes: int # oracle calls made by the verifier (challenges + Merkle + local consistency; graph draws counted apart)
|
||||
|
||||
|
||||
def verify(chi: bytes, n: int, k: int, proof_or_bytes) -> Verdict:
|
||||
"""[BS25] Sec 3.2 Verify, steps 1 to 4, in order; returns the first refusing check by name."""
|
||||
counter = [0]
|
||||
try:
|
||||
if isinstance(proof_or_bytes, (bytes, bytearray)):
|
||||
try:
|
||||
proof = Proof.from_bytes(bytes(proof_or_bytes))
|
||||
except ValueError as e:
|
||||
raise Refused("check_0_container", str(e))
|
||||
else:
|
||||
proof = proof_or_bytes
|
||||
check_0_container(proof, n, k)
|
||||
cs = check_1_challenges(chi, proof, k, n, counter)
|
||||
pss = check_2a_parent_set(cs, proof)
|
||||
check_2b_merkle_openings(chi, cs, pss, proof, n, counter)
|
||||
check_3_local_consistency(chi, cs, proof, counter)
|
||||
except Refused as r:
|
||||
return Verdict(False, r.check, r.detail, counter[0])
|
||||
return Verdict(True, "accept", "", counter[0]) # step 4
|
||||
62
tools/mhpow/b1/params.py
Normal file
62
tools/mhpow/b1/params.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"""Pinned constants of the B1 reference (R15-05): the MTP framework instantiated with DRSample.
|
||||
|
||||
Sources (full PDFs, sha256 recorded in docs/analysis/mhpow/b1/README.md):
|
||||
[BS25] Blocki and Smearsoll, "Provably Memory-Hard Proofs of Work With Memory-Easy Verification", TCC 2025,
|
||||
IACR eprint 2025/1456 as archived 2025-12-31T15:20:15Z (46 pp).
|
||||
[ABH17] Alwen, Blocki and Harsha, "Practical Graphs for Optimal Side-Channel Resistant Memory-Hard Functions",
|
||||
CCS 2017, IACR eprint 2017/443 as archived 2025-12-10T21:05:18Z; Algorithm 1 (DRSample, GetParent).
|
||||
|
||||
[BS25] fixes the construction abstractly: a random oracle H : {0,1}* -> {0,1}^lambda, labels H(chi, v, parents' labels)
|
||||
(Sec 2.2 "The Labeling Game", Sec 3.2 step 1), Merkle inner nodes H(chi, left, right) with chi as the salt (Sec 3.2 step 2,
|
||||
Sec 4.1, App. C.1), challenges H(chi, i, tau) mod N (Sec 3.2 step 3). It names no concrete hash, lambda, byte encoding or
|
||||
domain tag; the paper's only domain separation is the salt chi and the shape of each query. Everything in this file that the
|
||||
paper leaves open is a B1 decision, stated here with its reason, agreed with the B2 lane (instance layout) before the first
|
||||
fixture.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
# lambda (bits) and the oracle. B1 decision: BLAKE2b with a 32-byte digest, lambda = 256. Reason: one primitive across B1, B2
|
||||
# (chi = BLAKE2b('I' || instance)) and B3's GPU oracle; a 256-bit output keeps [BS25] Corollary 1's collision term
|
||||
# 2^-lambda * C(q,2) negligible for any q a network could make. NOTE (README row O-4): [BS25] Lemma 3 also needs
|
||||
# lambda/4 >= 2 log2(q) + log2(indeg), which lambda = 256 meets only for q <= 2^31.5 oracle calls.
|
||||
LAMBDA_BITS = 256
|
||||
DIGEST_BYTES = LAMBDA_BITS // 8
|
||||
|
||||
|
||||
def H(*parts: bytes) -> bytes:
|
||||
"""The random oracle instantiation: BLAKE2b-256 over the plain concatenation of fixed-width fields."""
|
||||
h = hashlib.blake2b(digest_size=DIGEST_BYTES)
|
||||
for p in parts:
|
||||
h.update(p)
|
||||
return h.digest()
|
||||
|
||||
|
||||
# Domain tags: one leading byte per query kind. B1 decision on top of [BS25]'s salt-only separation. Reason: without a tag the
|
||||
# challenge query H(chi, i, tau) (Sec 3.2 step 3) has exactly the shape of the prelabel H(chi, v, l_u) of an indegree-1 node
|
||||
# (node 2 of DRSample, [ABH17] Alg. 1 E := {(1,2)}) when i and v share a width; the tags make the four query families disjoint
|
||||
# by their first byte. The paper's proof (Sec 4.2, the extractor reads a parent label at offset |chi| + log2 N + h*lambda)
|
||||
# is unaffected by a fixed one-byte prefix.
|
||||
TAG_LABEL = b"L" # [BS25] Sec 2.2 labeling game; Sec 3.2 step 1: l_1 = H(chi, 1), l_v = H(chi, v, l_v1, ..., l_vk)
|
||||
TAG_MERKLE = b"M" # [BS25] Sec 3.2 step 2; App. C.1: tau_x = H(chi, tau_x0, tau_x1), tau = H(chi, tau_0, tau_1)
|
||||
TAG_CHALLENGE = b"C" # [BS25] Sec 3.2 step 3; Sec 5 Def. 14: c_i = H(chi, i, tau) mod N, i = 1..k
|
||||
TAG_GRAPH = b"G" # [ABH17] Alg. 1 random draws (g', r): B1's derandomisation of DRSample, see drsample.py
|
||||
TAG_INSTANCE = b"I" # owned by B2 (chi = BLAKE2b('I' || 328-byte instance)); listed so B1 never reuses it
|
||||
|
||||
ALL_TAGS = (TAG_LABEL, TAG_MERKLE, TAG_CHALLENGE, TAG_GRAPH, TAG_INSTANCE)
|
||||
assert len(set(ALL_TAGS)) == len(ALL_TAGS)
|
||||
|
||||
CHI_BYTES = 32 # chi is the 32-byte BLAKE2b digest of B2's instance; B1's own fixtures use chi = H(b"B1-fixture", tag)
|
||||
|
||||
|
||||
def u64(x: int) -> bytes:
|
||||
"""Node indices v (1..N) and challenge indices i (1..k): unsigned 64-bit little-endian, fixed width."""
|
||||
return int(x).to_bytes(8, "little")
|
||||
|
||||
|
||||
# The DRSample graph is sampled ONCE from a public constant seed, never from chi. Reason: [BS25] proves soundness for a graph
|
||||
# fixed a priori (Sec 1.2: "sound as long as the underlying graph is fixed a priori (i.e., cannot be adversarially modified)");
|
||||
# a chi-derived graph would let a prover that chooses chi (a nonce) grind for a weak graph.
|
||||
GRAPH_SEED = hashlib.blake2b(b"igneum/mhpow/b1/drsample-graph-seed/v1", digest_size=32).digest()
|
||||
|
||||
# Proof serialisation magic (B1's container; the paper defines the certificate c = (tau, {(l_i, mu_i, L_i)}) abstractly).
|
||||
PROOF_MAGIC = b"B1MTPDR1"
|
||||
23
tools/mhpow/b1/run-b1.sh
Executable file
23
tools/mhpow/b1/run-b1.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# B1 driver for a build box (never the Mac). Writes its pid file first, runs the tests, the known-answer fixtures, the honest
|
||||
# rows and the attack fixtures, then SHA256SUMS. Stop it only by its pid file: kill "$(cut -d' ' -f1 "$PIDFILE")".
|
||||
# tools/mhpow/b1/run-b1.sh <out_dir> [tests|kat|rows|attacks|ktable|all]
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT="${1:?out dir}"; WHAT="${2:-all}"
|
||||
PIDFILE="${B1_PIDFILE:-/srv/builds/mhpow-b1/run.pid}"
|
||||
printf '%s %s b1-%s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$WHAT" > "$PIDFILE"
|
||||
mkdir -p "$OUT"
|
||||
cd "$HERE"
|
||||
if [ "$WHAT" = tests ] || [ "$WHAT" = all ]; then
|
||||
python3 -m unittest -v test_b1 2>&1 | tee "$OUT/tests.log"
|
||||
grep -q '^OK' "$OUT/tests.log"
|
||||
fi
|
||||
if [ "$WHAT" = kat ] || [ "$WHAT" = all ]; then python3 gen_fixtures.py kat "$OUT"; fi
|
||||
if [ "$WHAT" = rows ] || [ "$WHAT" = all ]; then
|
||||
for n in 12 16 20 22 24; do python3 gen_fixtures.py row "$OUT" "$n" 64; done
|
||||
fi
|
||||
if [ "$WHAT" = attacks ] || [ "$WHAT" = all ]; then python3 gen_fixtures.py attacks "$OUT"; fi
|
||||
if [ "$WHAT" = ktable ] || [ "$WHAT" = all ]; then python3 k_table.py "$OUT" | tee "$OUT/k-table.log"; fi
|
||||
(cd "$OUT" && sha256sum $(ls | grep -v '^SHA256SUMS$') > SHA256SUMS)
|
||||
echo "B1 DONE $WHAT $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
162
tools/mhpow/b1/test_b1.py
Normal file
162
tools/mhpow/b1/test_b1.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""B1 tests: the honest path accepts, and every verifier check refuses its own known-failed case by name.
|
||||
|
||||
Run on a build box only (the founder's rule): cd tools/mhpow/b1 && python3 -m unittest -v test_b1
|
||||
"""
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
import drsample
|
||||
import mtp
|
||||
import params
|
||||
|
||||
CHI = params.H(b"B1-test", b"chi-0")
|
||||
CHI2 = params.H(b"B1-test", b"chi-1")
|
||||
|
||||
|
||||
class GraphTests(unittest.TestCase):
|
||||
def test_edge_rule(self):
|
||||
par = drsample.all_parents(1 << 12)
|
||||
self.assertEqual(par[1], ())
|
||||
self.assertEqual(par[2], (1,))
|
||||
for v in range(3, len(par)):
|
||||
u, w = par[v]
|
||||
self.assertEqual(w, v - 1)
|
||||
r = v - u
|
||||
self.assertGreaterEqual(r, 2)
|
||||
self.assertLessEqual(r, v - 1)
|
||||
# r lies in some bucket [max(ceil(g/2),2), g] with g = min(v-1, 2^g'), 1 <= g' <= floor(log2 v)+1
|
||||
ok = any(max(-(-min(v - 1, 1 << gp) // 2), 2) <= r <= min(v - 1, 1 << gp) for gp in range(1, v.bit_length() + 1))
|
||||
self.assertTrue(ok, (v, r))
|
||||
|
||||
def test_deterministic(self):
|
||||
self.assertEqual(drsample.all_parents(512), drsample.all_parents(512))
|
||||
|
||||
def test_bucket_spread(self):
|
||||
# [ABH17] Sec 3 intuition: each bucket B_i (2^(i-1) <= dist <= 2^i) is hit; a sanity check that the derandomisation
|
||||
# did not collapse the distribution.
|
||||
par = drsample.all_parents(1 << 14)
|
||||
buckets = set((v - par[v][0]).bit_length() for v in range(3, len(par)))
|
||||
self.assertGreaterEqual(len(buckets), 13)
|
||||
|
||||
|
||||
class MerkleTests(unittest.TestCase):
|
||||
def test_reveal_check_every_leaf(self):
|
||||
n = 5
|
||||
par = drsample.all_parents(1 << n)
|
||||
lab = mtp.compute_labels(CHI, par)
|
||||
t = mtp.MerkleTree(CHI, lab, n)
|
||||
for x in range(1 << n):
|
||||
self.assertTrue(mtp.merkle_tree_check(CHI, x, lab[x + 1], t.reveal(x), t.root, n))
|
||||
self.assertFalse(mtp.merkle_tree_check(CHI, x ^ 1, lab[x + 1], t.reveal(x), t.root, n))
|
||||
|
||||
def test_layout_lsb_at_root(self):
|
||||
# [BS25] Sec 3.2: tau_v = l_{1+bin(v)}, bin(v) = sum v_i 2^(i-1): the root's left subtree holds even leaf indices.
|
||||
n = 3
|
||||
lab = [None] + [bytes([v]) * 32 for v in range(1, 9)]
|
||||
t = mtp.MerkleTree(CHI, lab, n)
|
||||
left = mtp.merkle_node(CHI, mtp.merkle_node(CHI, lab[1], lab[5]), mtp.merkle_node(CHI, lab[3], lab[7]))
|
||||
self.assertEqual(t.levels[1][0], left)
|
||||
|
||||
|
||||
class VerifierTests(unittest.TestCase):
|
||||
n, k = 8, 16
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.par = drsample.all_parents(1 << cls.n)
|
||||
cls.st = mtp.prove_commit(CHI, cls.n, cls.par)
|
||||
cls.proof = mtp.respond(cls.st, cls.k)
|
||||
|
||||
def v(self, proof, chi=CHI):
|
||||
return mtp.verify(chi, self.n, self.k, proof)
|
||||
|
||||
def test_honest_accepts_object_and_bytes(self):
|
||||
self.assertTrue(self.v(self.proof).accepted)
|
||||
b = self.proof.to_bytes()
|
||||
self.assertTrue(self.v(b).accepted)
|
||||
self.assertEqual(mtp.Proof.from_bytes(b).to_bytes(), b)
|
||||
indeg = [len(self.par[c + 1]) for c in mtp.challenges(CHI, self.proof.tau, self.k, self.n)]
|
||||
self.assertEqual(len(b), mtp.proof_bytes_formula(self.n, self.k, indeg))
|
||||
|
||||
def test_wrong_label(self):
|
||||
p = copy.deepcopy(self.proof)
|
||||
p.responses[3].node.label = bytes([p.responses[3].node.label[0] ^ 1]) + p.responses[3].node.label[1:]
|
||||
self.assertEqual(self.v(p).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_wrong_parent_label(self):
|
||||
p = copy.deepcopy(self.proof)
|
||||
lab = p.responses[2].parents[0].label
|
||||
p.responses[2].parents[0].label = bytes([lab[0] ^ 0x80]) + lab[1:]
|
||||
self.assertEqual(self.v(p).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_wrong_path(self):
|
||||
p = copy.deepcopy(self.proof)
|
||||
s = p.responses[0].node.path[4]
|
||||
p.responses[0].node.path[4] = s[:-1] + bytes([s[-1] ^ 1])
|
||||
self.assertEqual(self.v(p).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_wrong_challenge(self):
|
||||
# answer the challenges for i = 2..k+1 instead of 1..k (each response opens a node the verifier did not derive)
|
||||
cs = mtp.challenges(CHI, self.st.tree.root, self.k + 1, self.n)[1:]
|
||||
resps = []
|
||||
for c in cs:
|
||||
v = c + 1
|
||||
resps.append(mtp.ChallengeResponse(mtp.Opening(self.st.labels[v], self.st.tree.reveal(c)),
|
||||
[mtp.Opening(self.st.labels[u], self.st.tree.reveal(u - 1)) for u in self.par[v]]))
|
||||
r = self.v(mtp.Proof(self.n, self.st.tree.root, resps))
|
||||
self.assertFalse(r.accepted)
|
||||
self.assertIn(r.check, ("check_2a_parent_set", "check_2b_merkle_openings"))
|
||||
|
||||
def test_skipped_parent(self):
|
||||
p = copy.deepcopy(self.proof)
|
||||
i = next(j for j, r in enumerate(p.responses) if len(r.parents) == 2)
|
||||
p.responses[i].parents = p.responses[i].parents[1:]
|
||||
self.assertEqual(self.v(p).check, "check_2a_parent_set")
|
||||
|
||||
def test_duplicated_parent_position(self):
|
||||
# the Bevand 'Attack 2' class: both openings for the same parent; positions are bound by the verifier
|
||||
p = copy.deepcopy(self.proof)
|
||||
i = next(j for j, r in enumerate(p.responses) if len(r.parents) == 2)
|
||||
p.responses[i].parents[0] = copy.deepcopy(p.responses[i].parents[1])
|
||||
self.assertEqual(self.v(p).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_wrong_domain_tag_label(self):
|
||||
# a prover that labels with the Merkle tag instead of the label tag, consistently, and commits honestly to that array
|
||||
lab = mtp.compute_labels(CHI, self.par, tag=params.TAG_MERKLE)
|
||||
st = mtp.ProverState(CHI, self.n, self.par, lab, mtp.MerkleTree(CHI, lab, self.n))
|
||||
self.assertEqual(self.v(mtp.respond(st, self.k)).check, "check_3_local_consistency")
|
||||
|
||||
def test_wrong_domain_tag_merkle(self):
|
||||
tree = mtp.MerkleTree(CHI, self.st.labels, self.n, tag=params.TAG_LABEL)
|
||||
st = mtp.ProverState(CHI, self.n, self.par, self.st.labels, tree)
|
||||
self.assertEqual(self.v(mtp.respond(st, self.k)).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_wrong_chi(self):
|
||||
self.assertEqual(self.v(self.proof, chi=CHI2).check, "check_2b_merkle_openings")
|
||||
|
||||
def test_container(self):
|
||||
b = self.proof.to_bytes()
|
||||
self.assertEqual(self.v(b[:-1]).check, "check_0_container")
|
||||
self.assertEqual(self.v(b + b"\0").check, "check_0_container")
|
||||
self.assertEqual(mtp.verify(CHI, self.n, self.k + 1, b).check, "check_0_container")
|
||||
self.assertEqual(mtp.verify(CHI, self.n + 1, self.k, b).check, "check_0_container")
|
||||
|
||||
def test_every_check_has_a_refusal(self):
|
||||
# the named checks of the verifier, each shown to fire above (a watcher is trusted only after it fires)
|
||||
names = {"check_0_container", "check_2a_parent_set", "check_2b_merkle_openings", "check_3_local_consistency"}
|
||||
fired = {
|
||||
self.v(self.proof.to_bytes()[:-1]).check,
|
||||
self.v(self.proof, chi=CHI2).check,
|
||||
}
|
||||
p = copy.deepcopy(self.proof)
|
||||
i = next(j for j, r in enumerate(p.responses) if len(r.parents) == 2)
|
||||
p.responses[i].parents = p.responses[i].parents[1:]
|
||||
fired.add(self.v(p).check)
|
||||
lab = mtp.compute_labels(CHI, self.par, tag=params.TAG_MERKLE)
|
||||
st = mtp.ProverState(CHI, self.n, self.par, lab, mtp.MerkleTree(CHI, lab, self.n))
|
||||
fired.add(self.v(mtp.respond(st, self.k)).check)
|
||||
self.assertEqual(fired, names)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue