diff --git a/reports/block-proposal/README.md b/reports/block-proposal/README.md new file mode 100644 index 0000000..7b61ffb --- /dev/null +++ b/reports/block-proposal/README.md @@ -0,0 +1,14 @@ +# Block proposal compression + +Research supporting the compressed block proposal, [logos-lips#389]. + +| Report | Question it answers | +|---|---| +| [reference-prefix-length.md](reference-prefix-length.md) | How many bytes of the transaction hash must a block proposal carry to refer to a transaction? Prices the birthday-collision attack against measured generation rates, and reconstruction cost against a measured latency curve on the target validator. | + +Benchmark suite: [`tools/benchmarks/block-proposal/reference-prefix-length`](../../tools/benchmarks/block-proposal/reference-prefix-length/). + +Start with the report's **Notation and terms** section if you are picking this up +cold — every symbol used (`L`, `k`, `b`, `n`, `R_gen`) is defined there. + +[logos-lips#389]: https://github.com/logos-co/logos-lips/pull/389 diff --git a/reports/block-proposal/figures/reconstruction-latency.png b/reports/block-proposal/figures/reconstruction-latency.png new file mode 100644 index 0000000..2364fef Binary files /dev/null and b/reports/block-proposal/figures/reconstruction-latency.png differ diff --git a/reports/block-proposal/reference-prefix-length.md b/reports/block-proposal/reference-prefix-length.md new file mode 100644 index 0000000..3917d5f --- /dev/null +++ b/reports/block-proposal/reference-prefix-length.md @@ -0,0 +1,729 @@ +# Choosing `REFERENCE_PREFIX_LENGTH` from measurement + +**Subject:** the reference prefix length for the compressed block proposal, [logos-lips#389]. +**Question:** is 16 bytes too conservative, and what does the measured data say the parameter should be? +**Answer in one line:** the data rules out 8, 10 and 12 outright; **14 is defensible but thin, 16 is comfortable, and the difference costs 2 KB** — so 16 is recommended as insurance, not because 14 is broken. + +> **Where this is a measurement and where it is a judgement.** That 8, 10 and 12 +> fail is not a judgement call: at those lengths a *day* of sustained disruption +> costs between $0.07 and $4,554. The choice between **14 and 16 is a +> judgement**, argued as such in [§8](#8-recommendation). The reviewer's instinct that 16 looks conservative is +> not wrong; the argument for 16 is about margin over the parameter's lifetime, +> not about 14 being exploitable today. + +Everything below is measured against the real `logos-blockchain` code at commit +[`40e76c8`], not a model of it. The benchmark suite is in +[`tools/benchmarks/block-proposal/reference-prefix-length`](../../tools/benchmarks/block-proposal/reference-prefix-length/), +and every number in this report can be re-derived by running it. + +> **Status of the RPi5 columns: complete.** macOS (Apple M3) is the development +> baseline; the Raspberry Pi 5 is the target validator class, and its numbers +> are now measured and filled in below (run of 2026-08-15, byte-identical code, +> same pinned toolchain, no throttling). The headline: the RPi5 changes **no +> conclusion**. Its reconstruction is ~1.7× slower than the M3's, but its slot +> crossover is the **same k = 10**, so the cost tables are identical between +> the two machines — see [what the RPi5 data changed](#what-the-rpi5-data-changed). + +--- + +## Notation and terms + +Everything in this report is expressed in a handful of symbols. They are defined +once here so no section has to re-introduce them. + +| Symbol | Means | Typical values here | +|---|---|---| +| **`L`** | **Reference prefix length in bytes** — the parameter being chosen. A block proposal refers to each transaction by the first `L` bytes of its hash instead of all 32. | 8, 10, 12, 14, 16 | +| **`b`** | The same quantity **in bits**, `b = 8L`. Used when quoting cryptographic costs, which are conventionally powers of two. | 64 … 128 | +| **`k`** | **Colliding pairs present in the mempool** — how many of a proposal's references are ambiguous, i.e. resolve to two candidate transactions instead of one. This is the attacker's dial. | 0 … 15 | +| **`n`** | **Transactions in the block** being reconstructed. Matters because the validator's per-attempt cost is proportional to it. | 128, 1024 | +| **`N`** | **Candidates an attacker must generate** to find a collision. Not to be confused with `n`. | ~10⁹ … ~10¹⁹ | +| **`R_gen`** | **Candidate-generation rate** — how many candidate transactions per second an attacker can build, encode, hash and reduce to a prefix. | ~10⁶/s per core | + +And the domain terms, in the order a reader meets them: + +- **Reference** — one entry in a block proposal, identifying a transaction the + validator should already hold in its mempool. Shortening references from 32 + bytes to `L` is what this whole change is about. +- **Reconstruction** — the validator's job on receiving a proposal: turn each + reference back into the transaction it names, and rebuild the block. It + succeeds when the rebuilt block reproduces `block_root`. +- **Prefix collision** — two different transactions whose hashes agree in their + first `L` bytes, so one reference matches both and the validator cannot tell + which was meant without trying. +- **Birthday collision** — finding *any* two colliding transactions among a set + the attacker generates. Much cheaper than hitting one *specific* + target: ~2^(b/2) attempts rather than ~2^b. This distinction is the entire + reason the prefix is 16 bytes rather than 8. +- **Slot** — the block-production interval, **1 second** in Cryptarchia. This is + the deadline reconstruction must fit inside; exceeding it is the failure this + report measures against. +- **Grinding** — repeatedly generating candidate transactions and hashing them, + looking for a collision. Cheap here because it needs no signatures or proofs. + +**Two conventions used throughout.** Costs are given for an attacker on *strong* +hardware and a validator on *weak* hardware, because that pairing is the +conservative direction for a security margin. And every figure is labelled as +measured or assumed — there is exactly one assumed input, the GPU hash rate, and +it is flagged wherever it is used. + +--- + +## 0. What the review asked for, and where it is answered + +| Review point | Where | Status | +|---|---|---| +| "Set a threat model and start the analysis from there" | [§1](#1-threat-model) | Done — stated before any measurement, with generation and propagation priced separately | +| Hansie's mempool flood reached ~0.3 tx/s | [§1b](#1b-propagation-and-why-hansies-03-txs-does-not-bound-this) | Addressed directly: the attack needs **20 transactions**, not volume, so 0.3 tx/s is ~67 seconds of injection, not a bound. Also measured against the current code — admission costs **1.44 µs/tx**, so 0.3 tx/s is ~2 million× off and cannot be a protocol limit | +| "How many valid transactions we can generate locally per second on a single core" | [§4](#4-candidate-generation-rate-r_gen) | Measured: **6.22 × 10⁶ /s** on one M3 core, via the real encode + Blake2b path | +| "How many cores (or GPUs) one needs to make the reconstruction fail" | [§7](#how-much-hardware-in-the-units-the-question-was-asked-in) | Table in cores and GPUs, against a 1-hour and 1-day deadline | +| "Show how reconstruction time grows on a single machine with the number of colliding transactions" | [§6](#6-reconstruction-latency--the-defenders-side) | Measured curve + plot, k = 0…15, at two block sizes | +| "Hard stop when we define the max number of permutations" | [§6](#the-two-policies-differ-in-failure-mode-not-in-whether-they-fail) | Both policies measured; the merged cap refuses at **k = 6** | +| "…or the reconstruction time exceeds the block production time on a single machine (RPi5)" | [§6](#6-reconstruction-latency--the-defenders-side) | Slot = 1 s; **both** the M3 and the RPi5 cross at **k = 10** (measured) | +| "The decision is too cautious" | [§8](#8-recommendation) | Partly conceded: L ≤ 12 is ruled out by measurement, but 14-vs-16 is a judgement about margin, not a demonstration that 14 is broken | + +--- + +## 1. Threat model + +Two costs set this parameter and they must be priced separately, because they +differ by many orders of magnitude and only one of them is a real bound. + +### 1a. Generation — the cost that sets the prefix length + +An adversary grinds candidate transactions locally and computes +`prefix(mantle_txhash(tx), L)` for each. This is **offline** and needs no +signature, proof, network, or stake, because of a specific property of the hash: + +```rust +// core/src/mantle/transactions/mantle_tx.rs +impl Hashable for RawMantleTx { + const HASHER: hashable::Hasher = |tx| { + let bytes: [u8; 32] = Hasher::digest(tx.as_signing()).into(); + TxHash::from(bytes) + }; + fn as_signing(&self) -> Vec { + let mut buffer = MANTLE_TX_HASH_V1_BYTES.to_vec(); // b"MANTLE_TXHASH_V1" + buffer.extend(self.encode()); // the MantleTx only + buffer + } +} +``` + +`mantle_txhash` covers the `MantleTx` and **not** the `op_proofs`. So the cost +of one candidate is *encode + Blake2b-256* — no cryptography beyond a hash. +Measured below at **6.22 × 10⁶ candidates/s on a single M3 core**, which is +within 3% of the machine's raw Blake2b rate. Grinding candidates *is* hashing. + +The relevant event is a **birthday self-collision**: any two of the adversary's +own candidates sharing a prefix. They need no fixed target, so this costs +≈ 2^(b/2), not the ≈ 2^b of a targeted match. This is the quantity that governs +`L`, and [§3](#3-the-birthday-model-is-measured-not-assumed) measures that the +model holds. + +### 1b. Propagation, and why Hansie's 0.3 tx/s does not bound this + +Hansie's mempool-flood experiment reached **~0.3 tx/s**, and it is reasonable to +ask whether that already caps the attack. It does not, and the reason is structural. + +**The attack does not need volume. It needs 20 specific transactions.** + +Grinding is offline. To manufacture k = 10 colliding pairs at L = 16 the +adversary hashes ~10¹⁹ candidates — but they never transmit them. They discard +every candidate except the ones that collide, and what has to reach a mempool is +just **2 transactions per pair, 20 in total**. The 10¹⁹ figure is a *local +compute* cost; the propagation requirement is 20 transactions of ~76 bytes. + +The two rates apply to different quantities: + +| quantity | rate | what it applies to | +|---|---|---| +| candidate generation | 6.22 × 10⁶ /s (one core) | ~10¹⁹ candidates, never transmitted | +| mempool ingest (Hansie) | ~0.3 tx/s | **20** transactions | + +At 0.3 tx/s, injecting those 20 transactions from a **single** node takes +**~67 seconds**. That is not a barrier — it is comfortably inside any mempool +retention window, and it has to happen once per stalled slot, not once per +candidate. + +For a *sustained* stall the arithmetic is barely worse. Holding a stall open +needs k = 10 pairs per 1-second slot, i.e. 20 tx/s of injection. At 0.3 tx/s per +node that is **~67 nodes** — a trivial number of rented VMs, and an adversary +who can afford the grinding cost in [§5](#5-the-cost-of-manufacturing-a-collision) +can certainly afford 67 VMs. + +So 0.3 tx/s is a real measurement of one node under one set of conditions, but +it composes into nothing: it is a *per-node* figure against an adversary who +parallelises across nodes, applied to a payload of 20 transactions rather than +to the grinding. **Generation is the binding constraint, and this report prices +it on its own.** Propagation only ever makes the attack easier than generation +alone implies. + +#### Can 0.3 tx/s be reproduced against the current code? + +Not the experiment itself — that needed a node, a network and a harness, none of +which are reconstructable from the figure alone, and it is not known whether it +measured submission, admission, or inclusion. What *can* be measured is the +protocol's own per-transaction cost, which bounds any explanation of it. On the +same M3, against the same pinned commit: + +| step | where it runs | cost | single-core ceiling | +|---|---|---|---| +| mempool admission — decode + `preverify` | on every incoming tx | **1.44 µs** | ~693,000 tx/s | +| ZK multi-signature verify | block application, **not** admission | **4.69 ms** | ~213 tx/s | + +Admission is cheap because signature verification is not on that path: the +mempool's item type is `SignedMantleTx`, whose `Deserialize` runs +`preverify()`, and for a `Transfer` that only checks structure. The ZK proof is +checked later by the stateful `verify` (`ZkPublicKey::verify_multi`, a Groth16 +verification via `lb_zksign::verify`), which needs the UTXO set. + +So the observed 0.3 tx/s sits **~700× below** even the most expensive +per-transaction cryptography in the pipeline, and **~2 million× below** the +admission path it would most naturally be attributed to. Whatever produced that +figure, it was not the protocol's CPU cost — it points at the harness or the +transport (sequential submission round-trips, an RPC path, rate limiting, or +input funding), not at a limit an adversary would inherit. That is the concrete +reason it cannot be carried into a security argument. + +> **A finding outside this study's scope, worth raising separately.** At 4.69 ms +> per transaction, verifying a *full* 1024-transaction block costs ~4.8 s of +> single-core work against a 1 s slot. Whatever that implies for throughput, it +> also means realistic blocks are likely far smaller than `MAX_BLOCK_TXS` — which +> makes the **n = 128** reconstruction curve in [§6](#6-reconstruction-latency--the-defenders-side) +> the more representative one (crossover k = 13, not k = 10), and correspondingly +> raises the attacker's bar. This is stated as an observation, not folded into +> the recommendation: it is single-core and the node may parallelise or batch, +> which was not investigated here. + +**The decisive reason, though, is not that propagation is fast — it is that `L` +does not affect it.** Whatever the prefix length, a colliding pair is still two +transactions and `k` pairs are still `2k` transactions to inject. Doubling `L` +multiplies the *generation* cost by 2^(8L/2) and leaves the *distribution* cost +exactly where it was. Distribution is therefore not a lever this parameter can +pull, at any value, which is why no propagation measurement can substitute for a +generation one. + +### 1c. Success condition + +Cause reconstruction ambiguity on an **honest** proposal: place two +transactions sharing a referenced prefix into a validator's mempool, so one +reference resolves to two candidates. Then scale it until the validator either + +* exceeds the block-production interval (the slot) while searching, or +* trips a cap and drops the honest proposal outright. + +Both are liveness failures. Neither is a safety failure: `header.block_root` +commits to the **full** 32-byte hashes, so a wrong resolution cannot be accepted +as a valid block — it can only waste time or fail. + +### 1d. A colliding pair is spent after one slot + +This mechanic matters more than anything else in the cost model, and it is easy +to miss. + +A self-collision gives the adversary two transactions, A and B, sharing a +prefix. Both go into mempools. The reference only becomes ambiguous once an +honest proposer includes **one of them** in a block — a reference is just a +prefix, so ambiguity requires that prefix to be *referenced*. Say the proposer +includes A. The reference now matches both A and B, and reconstruction has two +candidates to try. + +But once that block is applied, **A is removed from the mempool**. B is left +alone, and every later reference to B resolves to exactly one transaction. The +pair is consumed. + +So `k` colliding pairs buy **one stalled slot**, not a stalled chain. Holding a +stall open across `N` slots needs `k · N` pairs. Cost still grows only as the +square root of the total — bulk collisions are discounted — but at a 1-second +slot the discount does not keep up, and this is what separates "annoying" from +"unaffordable" at the lengths in question ([§7](#7-decision-table)). + +The adversary must also get those transactions *selected* by the proposer, +paying fees and competing for block space. That is a further linear overhead, +and it is deliberately left unpriced so the generation bound stands unaided. + +--- + +## 2. What the code actually does + +Four facts from the implementation shape everything that follows. + +**The hash is Blake2b-256** (`core/src/crypto.rs`: `pub type Hasher = blake2::Blake2b`). + +**The merged implementation is at 8 bytes, not 16.** + +```rust +// core/src/mantle/transactions/hash.rs +const REFERENCE_PREFIX_BYTES: usize = 8; +``` + +The specification PR argues for 16; the code on `master` is at 8. That gap is +the most urgent finding here, because [§5](#5-the-cost-of-manufacturing-a-collision) +prices an 8-byte prefix at **under a second of GPU time** per colliding pair. + +**Reconstruction is a cartesian-product search.** From +`services/chain/chain-network/src/lib.rs`: + +```rust +candidates + .into_iter() + .multi_cartesian_product() + .find_map(try_rebuild_with_txs) + .ok_or(Error::NoMatchingReconstruction) +``` + +with two caps applied before the search starts: + +```rust +// core/src/block/mod.rs +pub const MAX_CANDIDATES_PER_REFERENCE: usize = 8; +pub const MAX_RECONSTRUCTION_COMBINATIONS: usize = 32; +``` + +Note that #389 v3 argues these can be **deleted**, on the grounds that at 16 +bytes ambiguity cannot be manufactured at all. Both policies are measured +separately in [§6](#6-reconstruction-latency--the-defenders-side). + +**Each combination re-hashes the whole block.** `Block::reconstruct` validates +the size of every transaction and then recomputes the Merkle root, and +`calculate_block_root` hashes every transaction from scratch on every call: + +```rust +// core/src/utils/merkle.rs +let mut leaves: Vec<_> = transactions.iter().map(Hashable::hash).collect(); +``` + +So per-combination cost is O(n) in the block's transaction count, not O(1). +This is why the defender's curve is so much steeper than the attacker's. + +**The slot is 1 second** (`DEFAULT_SLOT_TIME_IN_SECS = 1`, matching +`slot_duration: '1.000000000'` in the standalone deployment config). That is +the budget §6 measures reconstruction against. + +--- + +## 3. The birthday model is measured, not assumed + +The whole parameter choice rests on the claim that *any* two colliding +candidates cost ≈ 2^(b/2). Rather than assert it, the harness grinds real +`mantle_txhash` output at prefix lengths short enough to collide in seconds and +compares the observed count against the prediction +sqrt(π/2) · 2^(b/2) ≈ 1.2533 · 2^(b/2). + +**mac** — 24 trials per row: + +| prefix | b (bits) | predicted N | measured N | ratio ± SE | +|---|---|---|---|---| +| 2 B | 16 | 321 | 349 | 1.088 ± 0.092 | +| 3 B | 24 | 5,134 | 4,948 | 0.964 ± 0.100 | +| 4 B | 32 | 82,137 | 84,013 | 1.023 ± 0.104 | +| 5 B | 40 | 1,314,195 | 1,104,775 | 0.841 ± 0.107 | + +Every ratio is consistent with 1.0 within ~1.5 standard errors, across 24 bits +of doubling. The first-collision distribution is strongly right-skewed, so this +spread is expected, so the standard error is what matters. **The 2^(b/2) model holds on real transaction hashes**, so +extrapolating to b = 64 and b = 128 needs only the grinding rate. + +**rpi5** — the measured-N column is **identical to the mac run at every prefix +length** (349 / 4,948 / 84,013 / 1,104,775), as it must be: the nonce sequence +and the hashes are deterministic, so only the rate column may differ between +machines. The cross-check called for in [§7](#what-the-rpi5-data-changed) +therefore passes — the harness behaves identically on both machines. + +--- + +## 4. Candidate-generation rate (R_gen) + +Three rates, because the difference between them is itself the point. + +| machine | node path | attacker (patched) | raw Blake2b | aggregate (all cores) | +|---|---|---|---|---| +| **mac** (Apple M3, 4P+4E) | 2.40 × 10⁶ /s | **6.22 × 10⁶ /s** | 6.01 × 10⁶ /s | **3.34 × 10⁷ /s** (8 threads, 5.39×) | +| **rpi5** (Cortex-A76, 4 cores) | 1.77 × 10⁶ /s | **4.31 × 10⁶ /s** | 4.10 × 10⁶ /s | **1.72 × 10⁷ /s** (4 threads, 3.98×) | + +Sample transaction: the smallest valid `MantleTx` — a single `Transfer`, one +input, one output — **76 bytes encoded, 92 bytes hashed** including the +16-byte domain prefix. Larger transactions hash slightly slower, so this is the +adversary-favourable choice; at 92 bytes the preimage still fits in a single +Blake2b compression, so a 2× larger transaction would cost roughly 2×. + +* **node path** builds the operation structure and reallocates per candidate — + what a node does per transaction. +* **attacker (patched)** encodes once and overwrites only the varying bytes. + This is the rate the security margin is computed from, because no adversary + would do more work than this. `cargo test` asserts byte-for-byte that it + produces the same hashes as the real path. +* **raw Blake2b** is the bare hash over a buffer of the same size, with no + transaction work at all. + +The last two are the substantive result: **grinding candidates is pure +hashing.** They land within 5% of each other on both machines, and the ordering +inverts — the attacker path measures marginally *faster* than the bare hash, +which cannot be true, since it does the same hash plus an 8-byte copy. A +difference that changes sign is at or below this measurement's noise floor, so +the two are indistinguishable rather than merely close. The transaction +machinery contributes nothing measurable once the encoding is hoisted out of the +loop, and there is no software headroom left for a defender to rely on. + +Aggregate scaling is **measured, not multiplied**: 8 threads on the M3 give +**5.39×**, not 8×, because four of those cores are efficiency cores. Assuming +linear scaling would have credited the adversary with 48% more throughput than +the machine delivers, which is the wrong direction to be wrong in. The RPi5's +four identical cores scale essentially perfectly (3.98×), and one Pi core runs +at ~70% of one M3 core. + +--- + +## 5. The cost of manufacturing a collision + +Candidates needed for one colliding pair, and the wall-clock at each rate. +Parallel collision search (van Oorschot–Wiener) is memoryless and embarrassingly +parallel, so aggregate hash rate is the honest cost basis and a 100-GPU farm +really is 100× faster. + +> **The GPU rate is the one assumed input, and it is not generous.** The tables +> use **10¹⁰ H/s**, the figure #389 argues from. #389 does not name a device; +> attaching one shows the assumption is, if anything, slightly *optimistic for +> the defender*: published hashcat benchmarks put a single **RTX 4090** at +> **~1.25 × 10¹⁰ H/s** on BLAKE2b, so the tables understate one current +> consumer GPU by ~20%. Re-pricing at the measured rate scales every cost and +> duration below by 0.8× — sustaining a stall for a day at L = 14 becomes ~$933k +> instead of $1.2M, and at L = 16 ~$239M instead of $298.5M. **No conclusion +> changes**, which is why 10¹⁰ is kept for comparability with #389 rather than +> silently diverged from. +> +> The direction matters more than the magnitude. This assumption was previously +> described in this report as "roughly 2× above" a 4090 — that was wrong, and +> wrong in the unsafe direction. It is a concrete instance of the assumption +> risk that [§8](#8-recommendation) rests part of its argument on: one careless +> factor moves L = 14 by 20% before any future hardware is considered. + +| L (bytes) | b (bits) | candidates N | 1 core (mac) | 1 machine (mac) | 1 GPU (RTX 4090) | 100 GPUs | +|---|---|---|---|---|---|---| +| **8** | 64 | 5.38 × 10⁹ | 14.4 min | 2.7 min | **0.54 s** | 5 ms | +| **10** | 80 | 1.38 × 10¹² | 3 days | 11.5 h | 2.3 min | 1.4 s | +| **12** | 96 | 3.53 × 10¹⁴ | 657 days | 122 days | 9.8 h | 5.9 min | +| **14** | 112 | 9.03 × 10¹⁶ | 460 years | 86 years | 105 days | 25.1 h | +| **16** | 128 | 2.31 × 10¹⁹ | 118,000 years | 22,000 years | **73 years** | 268 days | + +_(RPi5 columns are omitted deliberately: an adversary is not going to grind on a +Pi. Pricing the generating side with strong hardware is the conservative +direction, and the RPi5's role in this report is as the **defender**.)_ + +### Consistency with #389 + +#389 states 16 bytes gives "about 58 years on one GPU at 10¹⁰ H/s, ~214 days +against a 100× adversary". Those are 2^64 / rate exactly. This report gets +**73 years** and **268 days** because it includes the sqrt(π/2) ≈ 1.2533 factor +in the expected first-collision count — validated empirically in §3. + +**This is a refinement in the safe direction, not a contradiction**: #389's +figures are ~22% *lower* than the corrected ones, i.e. #389 slightly +understates the attacker's cost. No revision to #389's conclusion is needed, but +if those two numbers are quoted anywhere normative they should become 73 years +and 268 days. + +--- + +## 6. Reconstruction latency — the defender's side + +Manufacturing `k` colliding pairs costs the +attacker only ~sqrt(k) times one pair, because collisions accumulate as N²/2^(b+1) +as the search runs. An **uncapped** validator must walk ∏|Cᵢ| = 2^k combinations, +each of which re-encodes and re-hashes every transaction in the block. + +![Reconstruction latency vs. collision multiplicity](figures/reconstruction-latency.png) + +**mac** (Apple M3), full block of 1024 transactions, uncapped policy: + +| k | combinations | median | vs. 1 s slot | +|---|---|---|---| +| 0 | 1 | 1.4 ms | within | +| 4 | 16 | 17 ms | within | +| 6 | 64 | 68 ms | within | +| 8 | 256 | 286 ms | within | +| 9 | 512 | 559 ms | within | +| **10** | **1,024** | **1.19 s** | **over slot** | +| 12 | 4,096 | 4.5 s | over slot | +| 15 | 32,768 | 35.5 s | over slot | + +Per-combination cost ≈ **1.2 ms** at n = 1024 and ≈ **190 µs** at n = 128, +scaling with block size as the O(n) re-hash predicts. A 128-transaction block +crosses the slot at k = 13 instead of k = 10. Note what the k = 0 row means in +isolation: with no ambiguity at all, reconstruction costs 1.4 ms against a +1,000 ms budget, so normal operation has roughly **700× of headroom**. The +entire risk is in how fast that headroom is consumed — one doubling per +colliding pair. + +**rpi5** (Cortex-A76), full block of 1024 transactions, uncapped policy: + +| k | combinations | median | vs. 1 s slot | +|---|---|---|---| +| 0 | 1 | 2.6 ms | within | +| 4 | 16 | 30 ms | within | +| 6 | 64 | 116 ms | within | +| 8 | 256 | 462 ms | within | +| 9 | 512 | 925 ms | within | +| **10** | **1,024** | **1.87 s** | **over slot** | +| 12 | 4,096 | 7.7 s | over slot | +| 15 | 32,768 | 62.7 s | over slot | + +Per-combination cost ≈ **1.8 ms** at n = 1024 and ≈ **210 µs** at n = 128. The +expectation before the run was a crossover at a *lower* k. That did not happen: +**the crossover is unchanged at k = 10**, and at **k = 13** for n = 128, both +matching the M3. The reason is visible in the k = 9 row. The Pi lands at 925 ms, +1.65× the M3's 559 ms, where pushing k = 9 over the slot would have needed +1.79×. The Pi's slowdown falls just short of that, so it crosses at the same k +and the attacker gains nothing. At k = 0 the Pi reconstructs in 2.6 ms against +the 1,000 ms budget, **~380× of headroom** in normal operation on the target +validator class. (Run conditions: `performance` governor, ~56 °C, +`get_throttled=0x0` before and after — no thermal throttling.) + +### The two policies differ in failure mode, not in whether they fail + +| | uncapped (#389 v3) | capped (merged today) | +|---|---|---| +| k ≤ 5 | searches, ≤ 34 ms | identical — searches | +| k ≥ 6 | searches, cost doubles per k | **refuses instantly**, drops the proposal | +| failure at scale | slot overrun → block production stalls | honest proposal discarded → block lost | + +The caps bound CPU cost but do not remove the liveness failure — they convert a +slow reconstruction into a dropped honest block, and they do so at **k = 6**, +which is *cheaper for the attacker* than the k = 10 needed to stall the M3. +So the caps do not soften the requirement on `L`; if anything they tighten it. + +Either way the conclusion is the same: **`L` must be large enough that +manufacturing ~6–10 collisions is infeasible.** That requirement is robust to +which policy ships, which is what makes it a sound basis for the parameter. + +--- + +## 7. Decision table + +Cost to manufacture enough colliding pairs to break one validator, at +$0.50/GPU-hour on the RTX 4090 assumption above. `k` is taken from the measured +crossover in §6; the sqrt(k) scaling means the choice of `k` moves these numbers +far less than the choice of `L` does. + +| L (bytes) | proposal max | vs. master | GPU-hours, 1 pair | $ for 1 pair | $ to stall **mac** (k=10) | $ to stall **rpi5** (k=10) | pairs $10k buys | +|---|---|---|---|---|---|---|---| +| **8** | 8,555 B | 3.87× | ~0 | <$0.01 | **<$0.01** | **<$0.01** | 1.8 × 10¹⁶ | +| **10** | 10,603 B | 3.12× | 0.04 | $0.02 | **$0.06** | **$0.06** | 2.7 × 10¹¹ | +| **12** | 12,651 B | 2.62× | 9.8 | $4.90 | **$15.49** | **$15.49** | 4.2 × 10⁶ | +| **14** | 14,699 B | 2.25× | 2,509 | $1,254 | **$3,966** | **$3,966** | **64** | +| **16** | 16,747 B | 1.98× | 642,210 | $321,105 | **$1.0M** | **$1.0M** | **0.001 — not even one** | + +_The two stall columns are identical because the measured crossover is the same +k = 10 on both machines ([§6](#6-reconstruction-latency--the-defenders-side)) — +the attacker's cost depends only on k and L, not on the defender's hash rate._ + +The last column states the single-event margin directly: how many +colliding pairs a $10,000 adversary can afford, against the **10** they need. +L = 16 is the first row where a serious budget cannot buy even a single +collision; L = 14 is the last row where it buys six times more than one stalled +slot requires. + +### How much hardware, in the units the question was asked in + +Cost in dollars is one way to read the margin; "how many machines do I need to +point at this" is the other, and it is the one the review asked for. To +manufacture the k = 10 pairs that stall one slot, within a fixed deadline: + +| L (bytes) | GPUs in 1 hour | GPUs in 1 day | cores in 1 hour | cores in 1 day | +|---|---|---|---|---| +| **8** | <1 | <1 | <1 | <1 | +| **10** | <1 | <1 | 195 | 8 | +| **12** | 31 | **1** | 50,040 | 2,085 | +| **14** | 7,933 | **331** | 1.3 × 10⁷ | 533,758 | +| **16** | 2.0 × 10⁶ | **84,619** | 3.3 × 10⁹ | 1.4 × 10⁸ | + +In the "GPUs in 1 day" column: at L = 12 **one** GPU does it +overnight. At L = 14 it takes a **331-GPU** farm — large, but a real datacentre +rents that. At L = 16 it takes **84,619 GPUs**, which is not a rental; it is a +hyperscaler. + +Core counts assume the measured 6.22 × 10⁶ candidates/s per M3 core; they are +included because the review asked in cores, though no serious adversary would +grind Blake2b on CPUs when GPUs are ~1,600× more cost-effective per hash here. + +### Sustaining the stall is the number that decides it + +Because a pair is spent after one slot ([§1d](#1d-a-colliding-pair-is-spent-after-one-slot)), +the honest question is not "what does one missed slot cost" but "what does +holding the chain down cost". At k = 10 pairs per slot and 1-second slots: + +| L (bytes) | one slot | one hour | one day | +|---|---|---|---| +| **8** | <$0.01 | $0.01 | **$0.07** | +| **10** | $0.06 | $3.63 | **$17.79** | +| **12** | $15.49 | $929.65 | **$4,554** | +| **14** | $3,966 | $237,990 | **$1.2M** | +| **16** | $1.0M | $60.9M | **$298.5M** | + +This table is what makes 8, 10 and 12 indefensible, and not marginally: +**a full day of stalled block production costs seven cents at L = 8** and under +$5,000 at L = 12. It is also the table that makes L = 14 arguable — $1.2M/day is +a genuine deterrent, and anyone claiming 14 is broken has to explain away that +figure. + +What L = 14 does *not* survive well is the one-off case: $3,966 to burn a slot +network-wide is cheap enough for intermittent griefing, and unlike the sustained +case it does not benefit the defender that pairs are consumed. + +Two independent checks that the size column is right: at L = 8 it reproduces +**8,555 bytes**, the value pinned in the implementation's own +`maximum_proposal_matches_the_specified_size` test; at L = 16 it reproduces +**16,747 bytes**, the figure in #389. The compression ratio is against master's +33,129-byte fixed proposal. + +### What the RPi5 data changed + +Before the run, this section argued the pending cells could move the numbers +only slightly and only in the attacker's favour: a lower crossover `k` shrinks +the attacker's cost as √k, while every 2-byte step in `L` multiplies it by 256, +so even a crossover at k = 5 would have made the attack just 1.41× cheaper and +could not move the decision between 14 and 16. + +**The measured outcome is at the conservative end of that range: the crossover +did not move at all** ([§6](#6-reconstruction-latency--the-defenders-side)). The +`$ to stall rpi5` column is therefore *identical* to the mac column at every +`L`, and the attack got no cheaper on the target validator class than on the +development baseline. The √k-vs-256× argument above stands, but it turned out +not even to be needed. + +**Do not over-read the coincidence, though.** The Pi's k = 9 sits at 925 ms +against a 1,000 ms slot — **92.5% of the deadline, a margin of 7.5%**. The +crossovers match, but they match narrowly: a marginally slower board, a +validator doing anything else at the same time, a cooler-limited unit that +throttles, or a slightly larger block would all report **k = 9** instead. That +would be a correct measurement, not a contradiction of this one. It is also +immaterial to the decision — k = 9 makes the attack √(9/10) ≈ 5% cheaper, which +is invisible next to the 256× per 2-byte step of `L` — but anyone reproducing +this should expect the crossover to sit at 9 or 10 depending on the machine, and +should not treat a k = 9 result as a failure to replicate. + +What the Pi run establishes positively is that neither anticipated surprise +materialised: the slowdown was mild, where even ~10× would not have shifted the +conclusion, and the board did not throttle. + +The pre-registered cross-check also passed: **`birthday.csv` is identical +between `mac/` and `rpi5/` except for the rate column**, exactly as the +deterministic nonce sequence requires ([§3](#3-the-birthday-model-is-measured-not-assumed)). +Had it differed, no other number from the run could have been trusted. + +--- + +## 8. Recommendation + +**Recommended: `REFERENCE_PREFIX_LENGTH = 16`. Required regardless: raise the +implementation off 8.** These are two different strengths of claim and should +not be conflated. + +**What the data proves.** L ≤ 12 is indefensible, and not marginally so. A full +day of stalled block production costs **$0.07 at L = 8** — the value merged +today — **$17.79 at L = 10, and $4,554 at L = 12**. No assumption in this report +has to hold very tightly for those to be disqualifying; they would survive a GPU +rate ten times slower and a price ten times higher. + +**What the data leaves open.** The choice between 14 and 16 is a judgement, and +the measurements do not settle it: + +* At **L = 14** a one-off network-wide missed slot costs ~$4k, but *sustaining* + the stall costs **$1.2M/day**. That is a real deterrent. Anyone arguing 14 is + broken has to get past that number, and it cannot be done with this data. +* At **L = 16** even a single missed slot costs ~$1M, and one colliding pair is + 73 GPU-years. + +So the case for 16 rests on three things that are about *margin*, not +exploitability: + +1. **Assumption risk.** The whole table pivots on one unmeasured input, the GPU + hash rate. At 14 the one-off cost is a few thousand dollars, so a factor of a + few in that assumption moves it across the line that matters. At 16 a factor + of a few changes nothing. +2. **Lifetime.** This is a consensus constant that will outlive several hardware + generations. Every 4× in hash rate divides the attacker's cost by four; L = 14 + spends its margin against hardware that does not exist yet; L = 16 does not. +3. **Asymmetric cost of being wrong — but not for the usual reason.** It is + tempting to say "ship 16, shorten it later if the data allows". Be precise + about what that costs: `L` sets the `References` layout, so changing it in + *either* direction is a breaking wire-format change requiring a coordinated + upgrade, exactly as #389 says of the original change. Shortening is not a + free knob. + + The real asymmetry is **urgency and information, not cost**. Discovering that + 16 was generous is something you learn at leisure, from analysis, and can + bundle into a scheduled upgrade. Discovering that 14 was thin is something you + learn while someone is stalling the chain — and you then have to ship a hard + fork under pressure, against a mempool already seeded with colliding + transactions that do not expire on their own. Same mechanism, very different + conditions. + + The corollary cuts against over-relying on revisability: since shortening is + a hard fork for a 2 KB saving, realistically nobody will ever spend one on + it. **The value shipped is, in practice, the value forever.** That is an + argument for getting it right now, not for treating it as easily revisable + later. + +**And the price of that margin is small:** 16,747 bytes versus 14,699 — **2 KB +on the maximum proposal**, still a 1.98× reduction against master's 33,129. Two +kilobytes is cheap insurance against re-litigating this every time GPUs get +faster. + +### On "16 is too conservative" + +The reviewer is right that the *targeted* 2^128 framing overstates the threat, +and right that 14 is not absurd — this report should not be read as saying +otherwise. Where the disagreement actually lands is on how much margin a +consensus constant deserves when the margin costs 2 KB and the downside is a +breaking change under pressure. + +If the team prefers 14, that is a defensible position and this data supports it +against a *sustained* adversary. It should then be adopted deliberately, with +the one-off griefing cost (~$4k per burned slot) and the hardware-lifetime +exposure written down as accepted risks, rather than arrived at by treating 16 +as merely over-cautious. + +### Action items + +1. **Raise `REFERENCE_PREFIX_BYTES` from 8 to 16** in + `core/src/mantle/transactions/hash.rs`. The merged implementation is + currently at the one value this analysis rules out outright. This is the + highest-priority item in this report. +2. **Correct the two figures in #389** from 58 years / 214 days to **73 years / + 268 days** (§5). The conclusion is unchanged. +3. **Decide the cap question explicitly.** #389 v3 deletes + `MAX_RECONSTRUCTION_COMBINATIONS` and `MAX_CANDIDATES_PER_REFERENCE`; they are + still in the merged code. At L = 16 either is defensible, but the spec and the + implementation should not disagree about which. Note that keeping the caps + means a `k = 6` collision set drops honest proposals, which is *cheaper* to + provoke than the `k = 10` stall — so if they stay, they should be documented + as a liveness trade rather than a DoS defence. +4. **Re-run on the RPi5** — **done** (2026-08-15, `results/rpi5/`). All pending + columns above are filled from that run: crossover unchanged at k = 10, the + birthday cross-check passed, and no deviation from the pinned code or + toolchain was needed. + +--- + +## Appendix — reproducing this + +```bash +cd tools/benchmarks/block-proposal/reference-prefix-length +make run # the full suite; MACHINE is detected, override with MACHINE=rpi5 +make analyse # regenerates every table and the figure above +``` + +Assumptions, all changeable at the top of `scripts/analyse.py`: GPU model and +hash rate (`RTX 4090`, 10¹⁰ H/s), GPU price ($0.50/hour), and the proposal +layout constants. Hardware and toolchain for each run are recorded in +`results//machine.txt` and `toolchain.txt`. + +Measured on: Apple M3, 8 cores (4P + 4E), 16 GB, macOS 25.3.0, rustc 1.97.1; +and Raspberry Pi 5 Model B Rev 1.1 (Cortex-A76, 4 cores, 8 GB), Debian, kernel +6.18.34+rpt-rpi-2712, rustc 1.97.1, governor `performance`, no thermal +throttling (`get_throttled=0x0`). + +[logos-lips#389]: https://github.com/logos-co/logos-lips/pull/389 +[`40e76c8`]: https://github.com/logos-blockchain/logos-blockchain/commit/40e76c8e32934f14c3370621db9bda9f14d50dc7 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/.gitignore b/tools/benchmarks/block-proposal/reference-prefix-length/.gitignore new file mode 100644 index 0000000..8eb26b2 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/.gitignore @@ -0,0 +1,2 @@ +target/ +.venv/ diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.lock b/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.lock new file mode 100644 index 0000000..77e6f9e --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.lock @@ -0,0 +1,3119 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "archery" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca55ee147b1926dbea904f50fe4902494e97bc742205abbbf10c709e43815f" +dependencies = [ + "triomphe", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-crypto-primitives" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0c292754729c8a190e50414fd1a37093c786c709899f29c9f7daccecfa855e" +dependencies = [ + "ahash", + "ark-crypto-primitives-macros", + "ark-ec", + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-snark", + "ark-std", + "blake2", + "derivative", + "digest", + "fnv", + "merlin", + "sha2", +] + +[[package]] +name = "ark-crypto-primitives-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e89fe77d1f0f4fe5b96dfc940923d88d17b6a773808124f21e764dfb063c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-groth16" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f1d0f3a534bb54188b8dcc104307db6c56cdae574ddc3212aec0625740fc7e" +dependencies = [ + "ark-crypto-primitives", + "ark-ec", + "ark-ff", + "ark-poly", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-relations" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" +dependencies = [ + "ark-ff", + "ark-std", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-snark" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d368e2848c2d4c129ce7679a7d0d2d612b6a274d3ea6a13bad4445d61b381b88" +dependencies = [ + "ark-ff", + "ark-relations", + "ark-serialize", + "ark-std", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "astro-float" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cf6ecd69dd6fbe3763187ff2fa2b0e475b79f04ed86f631a7bc6b04ede0355e" +dependencies = [ + "astro-float-macro", + "astro-float-num", +] + +[[package]] +name = "astro-float-macro" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c007205ae7076600c8a59022866990afac02ff186f9031839f67ecce4df2146" +dependencies = [ + "astro-float-num", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "astro-float-num" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7f6a0312e8f5845f84241b21bd2394d49940b6bc333c6a84f992e6dc9ebb85" +dependencies = [ + "itertools 0.10.5", + "lazy_static", +] + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + +[[package]] +name = "base45" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240e56f4d3c453c36faacb695c535a4d5f8c7d23dac175014f32eb0a71012a03" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chkstk_stub" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "047f6ab2f3b9bcaf23b593d1580898e4244d27eadf1a1fae99212ee5735d3d1c" +dependencies = [ + "cc", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 3.0.3", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "generic-array" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" +dependencies = [ + "rustversion", + "serde_core", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jf-crhf" +version = "0.2.0" +source = "git+https://github.com/EspressoSystems/jellyfish?tag=jf-crhf-v0.2.0#f1538793f7f0e391495cb17bbb0c8703ec5f689d" +dependencies = [ + "ark-serialize", + "ark-std", +] + +[[package]] +name = "jf-poseidon2" +version = "0.2.0" +source = "git+https://github.com/EspressoSystems/jellyfish.git?rev=8d80230358e900f8d63765a937f63f4978ca1daa#8d80230358e900f8d63765a937f63f4978ca1daa" +dependencies = [ + "ark-bn254", + "ark-ff", + "ark-std", + "displaydoc", + "hex", + "jf-crhf", + "lazy_static", + "spongefish", + "zeroize", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libp2p-identity" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c" +dependencies = [ + "bs58", + "hkdf", + "multihash", + "sha2", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "libredox" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +dependencies = [ + "libc", +] + +[[package]] +name = "light-poseidon" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a1ccadd0bb5a32c196da536fd72c59183de24a055f6bf0513bf845fefab862" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint", + "thiserror 1.0.69", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "logos-blockchain-blend-crypto" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "blake2", + "logos-blockchain-groth16", + "logos-blockchain-poq", + "logos-blockchain-poseidon2", + "logos-blockchain-utils", + "rs-merkle-tree", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-blend-proofs" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ed25519-dalek", + "generic-array 1.4.5", + "hex", + "logos-blockchain-blend-crypto", + "logos-blockchain-codec", + "logos-blockchain-groth16", + "logos-blockchain-pol", + "logos-blockchain-poq", + "logos-blockchain-poseidon2", + "logos-blockchain-utils", + "num-bigint", + "serde", + "thiserror 2.0.20", + "zeroize", +] + +[[package]] +name = "logos-blockchain-circuits-build" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "dirs", + "fd-lock", + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "logos-blockchain-circuits-common" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poc-sys" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-pol-sys" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poq-sys" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-prover" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "rust-rapidsnark", +] + +[[package]] +name = "logos-blockchain-circuits-signature-sys" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-types" +version = "0.5.6" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c439356435eb6a2f1f4a8daa973bd04bd0b088" +dependencies = [ + "bytes", + "libc", +] + +[[package]] +name = "logos-blockchain-codec" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "hex", + "logos-blockchain-codec-macros", + "logos-blockchain-groth16", + "logos-blockchain-utils", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-codec-macros" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "logos-blockchain-core" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ark-ff", + "bincode", + "blake2", + "bytes", + "const-hex", + "hex", + "logos-blockchain-blend-proofs", + "logos-blockchain-codec", + "logos-blockchain-cryptarchia-engine", + "logos-blockchain-groth16", + "logos-blockchain-key-management-system-keys", + "logos-blockchain-log-targets", + "logos-blockchain-mmr", + "logos-blockchain-poc", + "logos-blockchain-pol", + "logos-blockchain-poseidon2", + "logos-blockchain-utils", + "logos-blockchain-utxotree", + "multiaddr", + "num-bigint", + "rpds", + "serde", + "strum", + "thiserror 2.0.20", + "time", + "tracing", +] + +[[package]] +name = "logos-blockchain-cryptarchia-engine" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-codec", + "logos-blockchain-pol", + "logos-blockchain-utils", + "rpds", + "serde", + "serde_with", + "thiserror 2.0.20", + "time", + "tracing", +] + +[[package]] +name = "logos-blockchain-dynamic-merkle" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "rpds", + "serde", +] + +[[package]] +name = "logos-blockchain-groth16" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-groth16", + "ark-serialize", + "generic-array 1.4.5", + "hex", + "num-bigint", + "rand 0.8.7", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-key-management-system-keys" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "async-trait", + "bytes", + "ed25519-dalek", + "generic-array 1.4.5", + "hex", + "logos-blockchain-codec", + "logos-blockchain-groth16", + "logos-blockchain-key-management-system-macros", + "logos-blockchain-log-targets", + "logos-blockchain-poseidon2", + "logos-blockchain-utils", + "logos-blockchain-zksign", + "num-bigint", + "rand_core 0.6.4", + "serde", + "subtle", + "thiserror 2.0.20", + "tokio", + "tracing", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "logos-blockchain-key-management-system-macros" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "logos-blockchain-log-targets" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-log-targets-macros", +] + +[[package]] +name = "logos-blockchain-log-targets-macros" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "logos-blockchain-merkle-tree" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-dynamic-merkle", + "rpds", + "serde", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-mmr" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ark-ff", + "logos-blockchain-groth16", + "logos-blockchain-poseidon2", + "logos-blockchain-utils", + "rpds", + "serde", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-poc" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-circuits-poc-sys", + "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "logos-blockchain-log-targets", + "logos-blockchain-proofs-error", + "num-bigint", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "logos-blockchain-pol" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "astro-float", + "logos-blockchain-circuits-pol-sys", + "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "logos-blockchain-log-targets", + "logos-blockchain-proofs-error", + "logos-blockchain-utils", + "num-bigint", + "num-traits", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "logos-blockchain-poq" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-circuits-poq-sys", + "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "logos-blockchain-log-targets", + "logos-blockchain-pol", + "logos-blockchain-proofs-error", + "num-bigint", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "logos-blockchain-poseidon2" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ark-bn254", + "ark-ff", + "jf-poseidon2", + "num-bigint", +] + +[[package]] +name = "logos-blockchain-proofs-error" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "logos-blockchain-utils" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "async-trait", + "blake2", + "cipher", + "const-hex", + "humantime", + "logos-blockchain-log-targets", + "multiaddr", + "overwatch", + "rand 0.8.7", + "serde", + "serde_ignored", + "serde_with", + "serde_yaml", + "thiserror 2.0.20", + "time", + "tracing", +] + +[[package]] +name = "logos-blockchain-utxotree" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "ark-ff", + "logos-blockchain-dynamic-merkle", + "logos-blockchain-merkle-tree", + "logos-blockchain-poseidon2", + "rpds", + "serde", +] + +[[package]] +name = "logos-blockchain-zksign" +version = "0.0.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e32934f14c3370621db9bda9f14d50dc7" +dependencies = [ + "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-signature-sys", + "logos-blockchain-circuits-types", + "logos-blockchain-groth16", + "logos-blockchain-log-targets", + "logos-blockchain-poseidon2", + "logos-blockchain-proofs-error", + "num-bigint", + "serde", + "serde-big-array", + "serde_json", + "thiserror 2.0.20", + "tracing", +] + +[[package]] +name = "match-lookup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiaddr" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "libp2p-identity", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", +] + +[[package]] +name = "multibase" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0e4a371cbf1dfd666b658ba137763edb23c45beb43cfe369b5593cd6b437b6" +dependencies = [ + "base-x", + "base256emoji", + "base45", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" +dependencies = [ + "unsigned-varint", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overwatch" +version = "0.1.0" +source = "git+https://github.com/logos-co/Overwatch?rev=ae887f41f5a626c341179026ad7f03953ff2072e#ae887f41f5a626c341179026ad7f03953ff2072e" +dependencies = [ + "async-trait", + "futures", + "thiserror 2.0.20", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "reference-prefix-bench" +version = "0.1.0" +dependencies = [ + "blake2", + "criterion", + "itertools 0.14.0", + "logos-blockchain-codec", + "logos-blockchain-core", + "logos-blockchain-cryptarchia-engine", + "logos-blockchain-groth16", + "logos-blockchain-key-management-system-keys", + "logos-blockchain-pol", + "num-bigint", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpds" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e025feb26210bc196b908e72deb063b1b4000754304341cbc168a1e72c857ebc" +dependencies = [ + "archery", + "serde", + "smallvec", +] + +[[package]] +name = "rs-merkle-tree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a3ef170810c387d31b64c0b59734abb0839dac2a8d137909e271bfdec9b1e0" +dependencies = [ + "ark-bn254", + "ark-ff", + "byteorder", + "futures", + "light-poseidon", + "quote", + "rand 0.9.5", + "syn 1.0.109", + "thiserror 2.0.20", + "tiny-keccak", + "tokio", +] + +[[package]] +name = "rust-rapidsnark" +version = "0.1.3" +source = "git+https://github.com/logos-blockchain/logos-blockchain-rust-rapidsnark.git?rev=e91187f8ccb5bbfc7bb00dac88169112428da78f#e91187f8ccb5bbfc7bb00dac88169112428da78f" +dependencies = [ + "anyhow", + "cc", + "chkstk_stub", + "num-bigint", + "num-traits", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_ignored" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115dffd5f3853e06e746965a20dcbae6ee747ae30b543d91b0e089668bb07798" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spongefish" +version = "0.2.0" +source = "git+https://github.com/arkworks-rs/spongefish.git?rev=3ded547f7f56d7f8a1fc4c9a5c0ce965310bba5f#3ded547f7f56d7f8a1fc4c9a5c0ce965310bba5f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-serialize", + "digest", + "hex", + "keccak", + "rand 0.8.7", + "sha3", + "thiserror 2.0.20", + "zerocopy", + "zeroize", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "tracing-core", +] + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unsigned-varint" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.toml b/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.toml new file mode 100644 index 0000000..f12d805 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "reference-prefix-bench" +version = "0.1.0" +edition = "2024" +publish = false +description = "Security-margin measurements for REFERENCE_PREFIX_LENGTH in the compressed block proposal (logos-lips#389)" + +# Every measurement below runs the real logos-blockchain code. The pin is a +# commit on `master`, not a branch, so the Mac and the RPi5 build byte-identical +# sources however far apart in time they are run. +[dependencies] +lb-core = { package = "logos-blockchain-core", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } +lb-codec = { package = "logos-blockchain-codec", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } +lb-cryptarchia-engine = { package = "logos-blockchain-cryptarchia-engine", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } +lb-groth16 = { package = "logos-blockchain-groth16", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } +lb-key-management-system-keys = { package = "logos-blockchain-key-management-system-keys", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } +lb-pol = { package = "logos-blockchain-pol", git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "40e76c8e32934f14c3370621db9bda9f14d50dc7" } + +blake2 = "0.10" +itertools = "0.14" +num-bigint = "0.4" + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +harness = false +name = "candidate_generation" + +# `release` is what both `cargo bench` and `cargo run --release` use here. The +# settings mirror logos-blockchain's own release profile so the code under +# measurement is compiled the way a validator would ship it. +[profile.release] +codegen-units = 1 +lto = "fat" + +[profile.bench] +inherits = "release" diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/Makefile b/tools/benchmarks/block-proposal/reference-prefix-length/Makefile new file mode 100644 index 0000000..a8fac19 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/Makefile @@ -0,0 +1,101 @@ +# ============================================================================= +# reference-prefix-length — measurements behind REFERENCE_PREFIX_LENGTH for the +# compressed block proposal (logos-lips#389). Executable documentation: +# `make help` lists everything, and these targets are the supported way to +# check, build, test and run the suite on any machine. +# +# Design rules: +# * `check` is READ-ONLY. It reports what is missing and how to install it; +# it never installs anything itself. +# * Every measuring target depends on `build`, not on `test`, so a failing +# hygiene check can never block a long run. Run `make test` before +# trusting numbers — it is the gate that proves the harness still agrees +# with the real logos-blockchain code path. +# * MACHINE labels the run and nothing else. The code executed is identical +# everywhere; the label only selects the results directory, so a Mac run +# and an RPi5 run stay separable and comparable. +# * Results are committed. `run` overwrites results/$(MACHINE)/ in place, so +# a re-run is a deliberate act with a visible diff, not a silent drift. +# * The toolchain is pinned in rust-toolchain.toml and the dependency is +# pinned by commit in Cargo.toml. Neither is overridable here — that is +# what makes two machines comparable. +# ============================================================================= +SHELL := /bin/bash +.DEFAULT_GOAL := help + +# Label for this run: selects results/$(MACHINE)/. Defaults to the platform, +# so `make run` on the Pi lands in results/rpi5/ without being told. +MACHINE ?= $(shell if [ "$$(uname -s)" = "Darwin" ]; then echo mac; \ + elif grep -qi raspberry /proc/cpuinfo 2>/dev/null; then echo rpi5; \ + else echo "$$(uname -m)-$$(uname -s | tr A-Z a-z)"; fi) + +RESULTS := results/$(MACHINE) +REPORT := ../../../../reports/block-proposal + +# Python for the analysis step. PEP-668 systems refuse a global install, so +# `make venv` builds one here and every python target prefers it when present. +VENV := .venv +PYTHON := $(shell if [ -x "$(VENV)/bin/python" ]; then echo "$(VENV)/bin/python"; else echo python3; fi) + +.PHONY: help where check build test bench run analyse venv figures clean distclean + +help: ## list targets + @grep -E '^[a-z-]+:.*##' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " make %-10s %s\n", $$1, $$2}' + @echo + @echo " MACHINE=$(MACHINE) (override to label a run: make run MACHINE=rpi5)" + +where: ## print what this tool reads and writes + @echo " results $(abspath $(RESULTS))" + @echo " report $(abspath $(REPORT))" + @echo " machine $(MACHINE)" + @echo " toolchain $$(grep -o '"[0-9.]*"' rust-toolchain.toml | tr -d '"') (pinned)" + +check: ## verify the environment (read-only; prints install hints) + @fail=0; \ + if command -v cargo >/dev/null; then \ + echo " ok cargo $$(cargo --version)"; \ + else \ + echo " MISS cargo install rustup: https://rustup.rs"; fail=1; \ + fi; \ + if command -v cc >/dev/null || command -v gcc >/dev/null; then \ + echo " ok cc $$(command -v cc || command -v gcc)"; \ + else \ + echo " MISS cc apt-get install build-essential"; fail=1; \ + fi; \ + if $(PYTHON) -c 'import matplotlib' 2>/dev/null; then \ + echo " ok matplotlib ($(PYTHON))"; \ + else \ + echo " warn matplotlib missing — 'make analyse' prints tables but no plot; 'make venv' fixes it"; \ + fi; \ + exit $$fail + +build: ## compile the harness and benches (release, pinned toolchain) + cargo build --release --all-targets + +test: build ## the gate: assert the harness still matches the real code path + cargo test --release + +bench: build ## criterion: candidate generation and mempool admission + cargo bench --bench candidate_generation + +run: build ## full suite -> results// (~15-30 min after the build) + ./scripts/run_all.sh $(MACHINE) + +analyse: ## regenerate the report's tables and figure from results/ + $(PYTHON) scripts/analyse.py + +venv: ## create .venv with matplotlib, for machines that refuse a global install + python3 -m venv $(VENV) + $(VENV)/bin/pip install --quiet --upgrade pip matplotlib + @echo " created $(VENV) — 'make analyse' will now use it" + +figures: analyse ## copy the regenerated figure into the report + @mkdir -p $(REPORT)/figures + cp results/reconstruction-latency.png $(REPORT)/figures/ + @echo " copied into $(REPORT)/figures/" + +clean: ## remove build artifacts (keeps results) + cargo clean + +distclean: clean ## additionally remove the local venv + rm -rf $(VENV) diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/README.md b/tools/benchmarks/block-proposal/reference-prefix-length/README.md new file mode 100644 index 0000000..f85654f --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/README.md @@ -0,0 +1,169 @@ +# Block proposal compression — benchmark suite + +Measurements behind the choice of `REFERENCE_PREFIX_LENGTH` for the compressed +block proposal ([logos-lips#389]). The report that reads these results is +[`reports/block-proposal/reference-prefix-length.md`](../../../../reports/block-proposal/reference-prefix-length.md), +and its **Notation and terms** section defines every symbol used below +(`L`, `k`, `b`, `n`, `R_gen`). + +The crate is named `reference-prefix-bench` because that is what it measures; +the directory is named for the wider topic, so further block-proposal +tools can sit beside it later. + +Everything here runs the **real `logos-blockchain` code**, pinned by commit in +`Cargo.toml` — the Mantle transaction encoding, `mantle_txhash` (Blake2b-256), +the Merkle `block_root`, and `Block::reconstruct`. Nothing is reimplemented, so +a rate measured here is a rate the protocol actually achieves. The two places +where the harness supplies its own code rather than calling the node's are +documented inline in `src/lib.rs` and repeated under +[What is real and what is not](#what-is-real-and-what-is-not). + +## What gets measured + +| Binary / bench | Question it answers | +|---|---| +| `bench candidate_generation` | **R_gen** — how fast can one core turn out candidate transactions reduced to a prefix? | +| `bin throughput` | How does that rate scale across a whole machine? (measured, not multiplied) | +| `bin birthday` | Does the 2^(b/2) birthday model actually hold on real `mantle_txhash` output? | +| `bin reconstruction` | How does a validator's reconstruction latency grow with the number of ambiguous references, and where does it cross the slot deadline? | + +## Requirements + +* Rust **1.97.1** — pinned in `rust-toolchain.toml`, installed automatically by + `rustup` on first use. +* A C toolchain and `git` (transitive crates build C). +* Roughly 3 GB of disk for the dependency build, and ~10 minutes for the first + compile on an RPi5. + +No binaries are shipped. Each machine builds from source, from the same pinned +sources, so the comparison is like-for-like. + +## (a) macOS — development and validation + +```bash +brew install rustup git # if not already present +rustup-init -y # then restart the shell + +cd tools/benchmarks/block-proposal/reference-prefix-length +make check # read-only: what's installed, what's missing +make test # the gate — asserts the harness matches the real code path +make run # the full suite -> results/mac/ +``` + +Results land in `results/mac/`. + +## (b) Raspberry Pi 5 — the numbers that decide the parameter + +The RPi5 is the target validator class, so its reconstruction latency is what +the recommendation is based on. macOS is a development baseline only. + +```bash +sudo apt-get update +sudo apt-get install -y build-essential git curl + +# rustup, not the distro's rustc: the toolchain must match the pin. +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" + +git clone https://github.com/logos-blockchain/research.git +cd research/tools/benchmarks/block-proposal/reference-prefix-length # or: git pull, if already cloned + +make check +make run # MACHINE is detected as rpi5; results land in results/rpi5/ +``` + +Results land in `results/rpi5/`. Commit that directory and the report tables +pick the numbers up. + +For a quieter measurement, pin the governor to `performance` first and let the +board settle — an RPi5 under a passive heatsink will thermally throttle during +the longer reconstruction runs, which shows up as a widening gap between the +`min_s` and `max_s` columns: + +```bash +echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor +vcgencmd measure_temp # keep an eye on this across the run +``` + +## Reading the results + +```bash +make analyse # tables + plot, from whatever is in results/ +make figures # analyse, then copy the plot into the report +``` + +The analysis needs `matplotlib` for the plot and is meant to run on the +development machine, not the Pi. Recent Pythons refuse a global `pip install` +(PEP 668), so `make venv` builds a local one; every later `make analyse` picks +it up automatically: + +```bash +make venv +make analyse +``` + +Without it the tables still print and only the figure is skipped. + +`make help` lists every target, and `make where` prints the exact results and +report directories this tool reads and writes. + +It writes the report's cost tables to stdout and the latency plot to +`results/reconstruction-latency.png`. Without `matplotlib` it still prints every +table and simply skips the figure. + +## Output layout + +``` +results/ + mac/ + machine.txt label, date, CPU, core count + toolchain.txt rustc / cargo versions actually used + candidate_generation.txt raw criterion output + throughput.csv aggregate candidates/s vs thread count + birthday.csv predicted vs measured first-collision counts + reconstruction.csv latency vs k, both policies, both block sizes + rpi5/ + ...same files... +``` + +## What is real and what is not + +Real, called directly from `logos-blockchain`: + +* `RawMantleTx` construction and its canonical encoding. +* `mantle_txhash` — `blake2b-256(b"MANTLE_TXHASH_V1" || encode(tx))`. +* `merkle::calculate_block_root` and `Block::reconstruct`, including the + per-combination re-encode and re-hash of every transaction in the block. +* `Block::create` and `to_proposal`, so proposal sizes are the real sizes. + +Supplied by the harness, and why: + +* **The reconstruction search loop.** `reconstruct_block_from_proposal` in + `services/chain/chain-network/src/lib.rs` is a private `async fn` reachable + only through a running service. `search_reconstruction` reproduces its + cartesian-product loop and its two caps; the per-combination work it performs + is the real `Block::reconstruct`, which is where essentially all the time + goes. The reproduction is asserted against the caps' documented constants in + the crate's tests. +* **A faster-than-the-node grinding loop** (`AttackerHasher`). The node rebuilds + and reallocates a transaction per hash; an attacker would encode once and + patch the varying bytes. Measuring the attacker's real cost means measuring + the attacker's real loop, and pricing them generously is the conservative + direction for a security margin. `cargo test` asserts byte-for-byte that this + shortcut produces the same hashes as the real path, at nine nonces spanning + the range; if it ever diverges, the suite fails rather than reporting a + wrong rate. +* **Proofs are constructed, not proved.** The proof of leadership is decoded + from its wire form and the per-op proof is signed once and cloned. + `Block::reconstruct` never verifies either, so this changes no measured + quantity — it only avoids minutes of Groth16 setup per run. Sizes are the + genuine encoded sizes, so the size check costs what it costs in production. + +## Determinism + +Every transaction is derived from a fixed key and a `u64` nonce, so both +machines grind byte-identical candidates and the birthday trials are +reproducible. The only machine-dependent inputs are the CPU and the toolchain, +both recorded in `results//`. + +[logos-lips#389]: https://github.com/logos-co/logos-lips/pull/389 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/benches/candidate_generation.rs b/tools/benchmarks/block-proposal/reference-prefix-length/benches/candidate_generation.rs new file mode 100644 index 0000000..f04ba00 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/benches/candidate_generation.rs @@ -0,0 +1,100 @@ +//! R_gen — the rate at which an adversary can turn out candidate +//! transactions reduced to a reference prefix. +//! +//! Three rates are measured, and the difference between them is the point: +//! +//! * `node_path` — build a valid `MantleTx`, encode it, hash it. This is what a +//! node does per transaction, and it is the *slowest* honest estimate. +//! * `attacker_patched` — encode once, then per candidate overwrite the varying +//! bytes and hash. No attacker would do less than this, so it is the rate the +//! security margin should be computed from. +//! * `blake2b_only` — Blake2b-256 over the same fixed preimage, with no +//! transaction work at all. This is the floor: no implementation of this +//! attack, on this CPU, can beat it. +//! +//! Truncating the 32-byte hash to `L` bytes is a slice and costs nothing +//! measurable, so R_gen does not depend on `L`. `L` enters the model only +//! through the number of candidates needed, 2^(8L/2). + +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use reference_prefix_bench::{ + AttackerHasher, admit_tx, mantle_txhash, minimal_transfer_tx, sample_preimage_len, + sample_tx_encoded_len, signature_verification_inputs, verify_signature, wire_encoded_tx, +}; +use std::hint::black_box; + +fn candidate_generation(c: &mut Criterion) { + // Print the sizes the rates are relative to, so a reviewer reading the + // criterion output alone can re-derive them. + eprintln!( + "sample MantleTx: {} bytes encoded, {} bytes hashed (b\"MANTLE_TXHASH_V1\" || encode(tx))", + sample_tx_encoded_len(), + sample_preimage_len() + ); + + let mut group = c.benchmark_group("candidate_generation"); + group.throughput(Throughput::Elements(1)); + + group.bench_function("node_path", |b| { + let mut nonce = 0u64; + b.iter(|| { + nonce = nonce.wrapping_add(1); + let tx = minimal_transfer_tx(black_box(nonce)); + black_box(mantle_txhash(&tx)) + }); + }); + + group.bench_function("attacker_patched", |b| { + let mut hasher = AttackerHasher::new(); + hasher.verify_against_real_path(); + let max = hasher.max_nonce(); + let mut nonce = 0u64; + b.iter(|| { + nonce = if nonce >= max { 0 } else { nonce + 1 }; + black_box(hasher.hash_candidate(black_box(nonce))) + }); + }); + + group.bench_function("blake2b_only", |b| { + use lb_core::crypto::{Digest as _, Hasher}; + let preimage = vec![0u8; sample_preimage_len()]; + b.iter(|| { + let digest: [u8; 32] = Hasher::digest(black_box(&preimage)).into(); + black_box(digest) + }); + }); + + group.finish(); +} + +/// What a validator actually pays to admit one transaction to its mempool. +/// +/// This exists to test a specific claim rather than to set the parameter: an +/// earlier experiment measured mempool flooding at ~0.3 tx/s, and it is worth +/// knowing whether the protocol's own per-transaction cost can account for +/// that. It cannot be a reproduction of that experiment — no node, network or +/// harness is involved here — but it does bound the CPU side of it. +fn mempool_admission(c: &mut Criterion) { + let bytes = wire_encoded_tx(); + eprintln!("wire-encoded SignedMantleTx: {} bytes", bytes.len()); + + let mut group = c.benchmark_group("mempool_admission"); + group.throughput(Throughput::Elements(1)); + + group.bench_function("deserialize_and_preverify", |b| { + b.iter(|| black_box(admit_tx(black_box(&bytes)))); + }); + + // Not on the admission path — this runs at block application — but it is + // the most expensive per-transaction cryptography in the pipeline, so it + // bounds the protocol's per-transaction cost from above. + let (pks, hash, sig) = signature_verification_inputs(); + group.bench_function("zk_multisig_verify", |b| { + b.iter(|| black_box(verify_signature(black_box(&pks), &hash, &sig))); + }); + + group.finish(); +} + +criterion_group!(benches, candidate_generation, mempool_admission); +criterion_main!(benches); diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/birthday.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/birthday.csv new file mode 100644 index 0000000..7504a7f --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/birthday.csv @@ -0,0 +1,5 @@ +machine,prefix_bits,prefix_bytes,trials,predicted_n,measured_n_mean,measured_n_stderr,ratio,ratio_stderr,search_hashes_per_second +mac,16,2,24,320.8,349.2,29.4,1.0883,0.0917,3290893.4 +mac,24,3,24,5133.6,4947.6,512.1,0.9638,0.0998,3923945.2 +mac,32,4,24,82137.2,84013.4,8582.1,1.0228,0.1045,4714564.5 +mac,40,5,24,1314195.1,1104775.0,140594.8,0.8406,0.1070,3795708.6 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/candidate_generation.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/candidate_generation.txt new file mode 100644 index 0000000..9184389 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/candidate_generation.txt @@ -0,0 +1,73 @@ + Finished `bench` profile [optimized] target(s) in 0.22s + Running benches/candidate_generation.rs (target/release/deps/candidate_generation-4dc49b8274b424c2) +Gnuplot not found, using plotters backend +sample MantleTx: 76 bytes encoded, 92 bytes hashed (b"MANTLE_TXHASH_V1" || encode(tx)) +Benchmarking candidate_generation/node_path +Benchmarking candidate_generation/node_path: Warming up for 3.0000 s +Benchmarking candidate_generation/node_path: Collecting 100 samples in estimated 5.0004 s (12M iterations) +Benchmarking candidate_generation/node_path: Analyzing +candidate_generation/node_path + time: [416.19 ns 417.37 ns 418.48 ns] + thrpt: [2.3896 Melem/s 2.3960 Melem/s 2.4028 Melem/s] + change: + time: [+0.8332% +1.1710% +1.5005%] (p = 0.00 < 0.05) + thrpt: [-1.4784% -1.1575% -0.8264%] + Change within noise threshold. +Benchmarking candidate_generation/attacker_patched +Benchmarking candidate_generation/attacker_patched: Warming up for 3.0000 s +Benchmarking candidate_generation/attacker_patched: Collecting 100 samples in estimated 5.0001 s (31M iterations) +Benchmarking candidate_generation/attacker_patched: Analyzing +candidate_generation/attacker_patched + time: [160.84 ns 160.87 ns 160.90 ns] + thrpt: [6.2152 Melem/s 6.2163 Melem/s 6.2174 Melem/s] + change: + time: [-3.6911% -1.5381% -0.3568%] (p = 0.10 > 0.05) + thrpt: [+0.3581% +1.5621% +3.8326%] + No change in performance detected. +Found 9 outliers among 100 measurements (9.00%) + 1 (1.00%) low mild + 6 (6.00%) high mild + 2 (2.00%) high severe +Benchmarking candidate_generation/blake2b_only +Benchmarking candidate_generation/blake2b_only: Warming up for 3.0000 s +Benchmarking candidate_generation/blake2b_only: Collecting 100 samples in estimated 5.0003 s (30M iterations) +Benchmarking candidate_generation/blake2b_only: Analyzing +candidate_generation/blake2b_only + time: [166.21 ns 166.27 ns 166.36 ns] + thrpt: [6.0111 Melem/s 6.0143 Melem/s 6.0166 Melem/s] + change: + time: [-2.0247% -0.9269% -0.2714%] (p = 0.02 < 0.05) + thrpt: [+0.2722% +0.9356% +2.0666%] + Change within noise threshold. +Found 4 outliers among 100 measurements (4.00%) + 2 (2.00%) low mild + 2 (2.00%) high severe + +wire-encoded SignedMantleTx: 212 bytes +Benchmarking mempool_admission/deserialize_and_preverify +Benchmarking mempool_admission/deserialize_and_preverify: Warming up for 3.0000 s +Benchmarking mempool_admission/deserialize_and_preverify: Collecting 100 samples in estimated 5.0024 s (3.5M iterations) +Benchmarking mempool_admission/deserialize_and_preverify: Analyzing +mempool_admission/deserialize_and_preverify + time: [1.4409 µs 1.4431 µs 1.4450 µs] + thrpt: [692.03 Kelem/s 692.97 Kelem/s 693.99 Kelem/s] + change: + time: [-1.5532% -1.3170% -1.0839%] (p = 0.00 < 0.05) + thrpt: [+1.0958% +1.3345% +1.5777%] + Performance has improved. +Found 3 outliers among 100 measurements (3.00%) + 3 (3.00%) low mild +Benchmarking mempool_admission/zk_multisig_verify +Benchmarking mempool_admission/zk_multisig_verify: Warming up for 3.0000 s +Benchmarking mempool_admission/zk_multisig_verify: Collecting 100 samples in estimated 5.2969 s (1100 iterations) +Benchmarking mempool_admission/zk_multisig_verify: Analyzing +mempool_admission/zk_multisig_verify + time: [4.5831 ms 4.6866 ms 4.8594 ms] + thrpt: [205.79 elem/s 213.37 elem/s 218.19 elem/s] + change: + time: [+2.2500% +4.9189% +8.6080%] (p = 0.00 < 0.05) + thrpt: [-7.9258% -4.6883% -2.2005%] + Performance has regressed. +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high severe + diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/machine.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/machine.txt new file mode 100644 index 0000000..f6dad63 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/machine.txt @@ -0,0 +1,5 @@ +label: mac +date: 2026-08-14T17:37:32Z +uname: Darwin mehmetmac.local 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:56:34 PST 2026; root:xnu-12377.91.3~2/RELEASE_ARM64_T8122 arm64 +cpu: Apple M3 +cores: 8 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/reconstruction.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/reconstruction.csv new file mode 100644 index 0000000..3c3cc77 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/reconstruction.csv @@ -0,0 +1,65 @@ +machine,block_txs,k,policy,combinations,attempts,outcome,median_s,min_s,max_s,repeats,over_slot +mac,1024,0,uncapped,1,1,rebuilt,0.001420000,0.001400125,0.001600083,5,false +mac,1024,0,capped,1,0,rebuilt,0.001411500,0.001411500,0.001411500,1,false +mac,1024,1,uncapped,2,2,rebuilt,0.002485083,0.002381250,0.002543792,5,false +mac,1024,1,capped,2,0,rebuilt,0.002312083,0.002312083,0.002312083,1,false +mac,1024,2,uncapped,4,4,rebuilt,0.004406875,0.004396458,0.004434917,5,false +mac,1024,2,capped,4,0,rebuilt,0.004432667,0.004432667,0.004432667,1,false +mac,1024,3,uncapped,8,8,rebuilt,0.008659083,0.008630333,0.008672625,5,false +mac,1024,3,capped,8,0,rebuilt,0.008655166,0.008655166,0.008655166,1,false +mac,1024,4,uncapped,16,16,rebuilt,0.017102125,0.017071125,0.017179792,5,false +mac,1024,4,capped,16,0,rebuilt,0.017008625,0.017008625,0.017008625,1,false +mac,1024,5,uncapped,32,32,rebuilt,0.034102042,0.033834667,0.034533542,5,false +mac,1024,5,capped,32,0,rebuilt,0.034161250,0.034161250,0.034161250,1,false +mac,1024,6,uncapped,64,64,rebuilt,0.068338916,0.067973417,0.068891792,5,false +mac,1024,6,capped,64,0,refused_combinations,0.000100500,0.000100500,0.000100500,1,false +mac,1024,7,uncapped,128,128,rebuilt,0.135836750,0.134199666,0.141206667,5,false +mac,1024,7,capped,128,0,refused_combinations,0.000097708,0.000097708,0.000097708,1,false +mac,1024,8,uncapped,256,256,rebuilt,0.285755833,0.273285583,0.360758125,5,false +mac,1024,8,capped,256,0,refused_combinations,0.000102292,0.000102292,0.000102292,1,false +mac,1024,9,uncapped,512,512,rebuilt,0.558886625,0.551156417,0.586189208,5,false +mac,1024,9,capped,512,0,refused_combinations,0.000099584,0.000099584,0.000099584,1,false +mac,1024,10,uncapped,1024,1024,rebuilt,1.186574709,1.119725833,1.309987666,5,true +mac,1024,10,capped,1024,0,refused_combinations,0.000111042,0.000111042,0.000111042,1,false +mac,1024,11,uncapped,2048,2048,rebuilt,2.189018750,2.189018750,2.189018750,1,true +mac,1024,11,capped,2048,0,refused_combinations,0.000098625,0.000098625,0.000098625,1,false +mac,1024,12,uncapped,4096,4096,rebuilt,4.539128833,4.539128833,4.539128833,1,true +mac,1024,12,capped,4096,0,refused_combinations,0.000100041,0.000100041,0.000100041,1,false +mac,1024,13,uncapped,8192,8192,rebuilt,9.016253000,9.016253000,9.016253000,1,true +mac,1024,13,capped,8192,0,refused_combinations,0.000098833,0.000098833,0.000098833,1,false +mac,1024,14,uncapped,16384,16384,rebuilt,17.803907584,17.803907584,17.803907584,1,true +mac,1024,14,capped,16384,0,refused_combinations,0.000103416,0.000103416,0.000103416,1,false +mac,1024,15,uncapped,32768,32768,rebuilt,35.475198250,35.475198250,35.475198250,1,true +mac,1024,15,capped,32768,0,refused_combinations,0.000103792,0.000103792,0.000103792,1,false +mac,128,0,uncapped,1,1,rebuilt,0.000192791,0.000191417,0.000207459,5,false +mac,128,0,capped,1,0,rebuilt,0.000191917,0.000191917,0.000191917,1,false +mac,128,1,uncapped,2,2,rebuilt,0.000322375,0.000321625,0.000323166,5,false +mac,128,1,capped,2,0,rebuilt,0.000320875,0.000320875,0.000320875,1,false +mac,128,2,uncapped,4,4,rebuilt,0.000580583,0.000580292,0.000581000,5,false +mac,128,2,capped,4,0,rebuilt,0.000588000,0.000588000,0.000588000,1,false +mac,128,3,uncapped,8,8,rebuilt,0.001098291,0.001097000,0.001106167,5,false +mac,128,3,capped,8,0,rebuilt,0.001099000,0.001099000,0.001099000,1,false +mac,128,4,uncapped,16,16,rebuilt,0.002133708,0.002132459,0.002139083,5,false +mac,128,4,capped,16,0,rebuilt,0.002179791,0.002179791,0.002179791,1,false +mac,128,5,uncapped,32,32,rebuilt,0.004189500,0.004182458,0.004192708,5,false +mac,128,5,capped,32,0,rebuilt,0.004187500,0.004187500,0.004187500,1,false +mac,128,6,uncapped,64,64,rebuilt,0.008285292,0.008273917,0.008304208,5,false +mac,128,6,capped,64,0,refused_combinations,0.000011916,0.000011916,0.000011916,1,false +mac,128,7,uncapped,128,128,rebuilt,0.016442000,0.016426583,0.016469291,5,false +mac,128,7,capped,128,0,refused_combinations,0.000012166,0.000012166,0.000012166,1,false +mac,128,8,uncapped,256,256,rebuilt,0.032817875,0.032667833,0.032845583,5,false +mac,128,8,capped,256,0,refused_combinations,0.000011959,0.000011959,0.000011959,1,false +mac,128,9,uncapped,512,512,rebuilt,0.064845584,0.064739292,0.064984542,5,false +mac,128,9,capped,512,0,refused_combinations,0.000011750,0.000011750,0.000011750,1,false +mac,128,10,uncapped,1024,1024,rebuilt,0.128638375,0.128328875,0.128896750,5,false +mac,128,10,capped,1024,0,refused_combinations,0.000012291,0.000012291,0.000012291,1,false +mac,128,11,uncapped,2048,2048,rebuilt,0.260206959,0.256379125,0.261368667,5,false +mac,128,11,capped,2048,0,refused_combinations,0.000012584,0.000012584,0.000012584,1,false +mac,128,12,uncapped,4096,4096,rebuilt,0.516718500,0.514156166,0.530193167,5,false +mac,128,12,capped,4096,0,refused_combinations,0.000012542,0.000012542,0.000012542,1,false +mac,128,13,uncapped,8192,8192,rebuilt,1.057197417,1.041655750,1.126215208,5,true +mac,128,13,capped,8192,0,refused_combinations,0.000013042,0.000013042,0.000013042,1,false +mac,128,14,uncapped,16384,16384,rebuilt,2.084088958,2.084088958,2.084088958,1,true +mac,128,14,capped,16384,0,refused_combinations,0.000012458,0.000012458,0.000012458,1,false +mac,128,15,uncapped,32768,32768,rebuilt,4.160033542,4.160033542,4.160033542,1,true +mac,128,15,capped,32768,0,refused_combinations,0.000013334,0.000013334,0.000013334,1,false diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/throughput.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/throughput.csv new file mode 100644 index 0000000..44ceaa5 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/throughput.csv @@ -0,0 +1,9 @@ +machine,threads,candidates_per_second,per_thread_candidates_per_second,scaling_vs_one_thread,seconds +mac,1,6188189.3,6188189.3,1.000,5 +mac,2,12353718.0,6176859.0,1.996,5 +mac,3,18713230.5,6237743.5,3.024,5 +mac,4,24751944.5,6187986.1,4.000,5 +mac,5,25812602.6,5162520.5,4.171,5 +mac,6,27053390.5,4508898.4,4.372,5 +mac,7,31380830.7,4482975.8,5.071,5 +mac,8,33371226.1,4171403.3,5.393,5 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/toolchain.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/toolchain.txt new file mode 100644 index 0000000..c5452c6 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/mac/toolchain.txt @@ -0,0 +1,2 @@ +rustc 1.97.1 (8bab26f4f 2026-07-14) +cargo 1.97.1 (c980f4866 2026-06-30) diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/reconstruction-latency.png b/tools/benchmarks/block-proposal/reference-prefix-length/results/reconstruction-latency.png new file mode 100644 index 0000000..85e472b Binary files /dev/null and b/tools/benchmarks/block-proposal/reference-prefix-length/results/reconstruction-latency.png differ diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/README.md b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/README.md new file mode 100644 index 0000000..dc25ca1 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/README.md @@ -0,0 +1,25 @@ +# RPi5 results + +Collected 2026-08-15 on a Raspberry Pi 5 Model B Rev 1.1 (Cortex-A76, 4 cores), +Debian kernel 6.18.34+rpt-rpi-2712, rustc 1.97.1 (the pinned toolchain), with +the CPU governor set to `performance` and no thermal throttling before or after +the run (`get_throttled=0x0`, ~56 °C). Produced by: + +```bash +cd tools/benchmarks/block-proposal/reference-prefix-length +make run # MACHINE is detected as rpi5 +``` + +with no modification to sources, scripts, profile, or toolchain. The +`birthday.csv` determinism cross-check against `../mac/birthday.csv` passes: +all columns identical except the rate column. + +To regenerate the report tables and the latency figure from these files: + +```bash +make analyse +``` + +The corresponding cells in +[`reports/block-proposal/reference-prefix-length.md`](../../../../../../reports/block-proposal/reference-prefix-length.md) +are filled from this run. diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/birthday.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/birthday.csv new file mode 100644 index 0000000..978b3c1 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/birthday.csv @@ -0,0 +1,5 @@ +machine,prefix_bits,prefix_bytes,trials,predicted_n,measured_n_mean,measured_n_stderr,ratio,ratio_stderr,search_hashes_per_second +rpi5,16,2,24,320.8,349.2,29.4,1.0883,0.0917,3693992.8 +rpi5,24,3,24,5133.6,4947.6,512.1,0.9638,0.0998,3696266.8 +rpi5,32,4,24,82137.2,84013.4,8582.1,1.0228,0.1045,3578534.3 +rpi5,40,5,24,1314195.1,1104775.0,140594.8,0.8406,0.1070,3207984.4 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/candidate_generation.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/candidate_generation.txt new file mode 100644 index 0000000..bdfb2e7 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/candidate_generation.txt @@ -0,0 +1,67 @@ + Compiling logos-blockchain-circuits-pol-sys v0.5.6 (https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c43935) + Compiling logos-blockchain-circuits-poq-sys v0.5.6 (https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c43935) + Compiling logos-blockchain-circuits-signature-sys v0.5.6 (https://github.com/logos-blockchain/logos-blockchain-circuits.git?tag=v0.5.6#07c43935) + Compiling logos-blockchain-zksign v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-pol v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-key-management-system-keys v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-poq v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-blend-crypto v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-cryptarchia-engine v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-blend-proofs v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling logos-blockchain-core v0.0.0 (https://github.com/logos-blockchain/logos-blockchain.git?rev=40e76c8e32934f14c3370621db9bda9f14d50dc7#40e76c8e) + Compiling reference-prefix-bench v0.1.0 (/home/mem/research/tools/benchmarks/reference-prefix) + Finished `bench` profile [optimized] target(s) in 1m 18s + Running benches/candidate_generation.rs (target/release/deps/candidate_generation-6f4815aa941f7403) +Gnuplot not found, using plotters backend +sample MantleTx: 76 bytes encoded, 92 bytes hashed (b"MANTLE_TXHASH_V1" || encode(tx)) +Benchmarking candidate_generation/node_path +Benchmarking candidate_generation/node_path: Warming up for 3.0000 s +Benchmarking candidate_generation/node_path: Collecting 100 samples in estimated 5.0012 s (8.8M iterations) +Benchmarking candidate_generation/node_path: Analyzing +candidate_generation/node_path + time: [565.61 ns 566.41 ns 567.55 ns] + thrpt: [1.7620 Melem/s 1.7655 Melem/s 1.7680 Melem/s] +Found 3 outliers among 100 measurements (3.00%) + 3 (3.00%) high severe +Benchmarking candidate_generation/attacker_patched +Benchmarking candidate_generation/attacker_patched: Warming up for 3.0000 s +Benchmarking candidate_generation/attacker_patched: Collecting 100 samples in estimated 5.0006 s (22M iterations) +Benchmarking candidate_generation/attacker_patched: Analyzing +candidate_generation/attacker_patched + time: [231.78 ns 231.91 ns 232.05 ns] + thrpt: [4.3095 Melem/s 4.3120 Melem/s 4.3145 Melem/s] +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild +Benchmarking candidate_generation/blake2b_only +Benchmarking candidate_generation/blake2b_only: Warming up for 3.0000 s +Benchmarking candidate_generation/blake2b_only: Collecting 100 samples in estimated 5.0010 s (21M iterations) +Benchmarking candidate_generation/blake2b_only: Analyzing +candidate_generation/blake2b_only + time: [243.62 ns 243.66 ns 243.70 ns] + thrpt: [4.1034 Melem/s 4.1041 Melem/s 4.1048 Melem/s] +Found 6 outliers among 100 measurements (6.00%) + 2 (2.00%) low severe + 3 (3.00%) low mild + 1 (1.00%) high severe + +wire-encoded SignedMantleTx: 212 bytes +Benchmarking mempool_admission/deserialize_and_preverify +Benchmarking mempool_admission/deserialize_and_preverify: Warming up for 3.0000 s +Benchmarking mempool_admission/deserialize_and_preverify: Collecting 100 samples in estimated 5.0075 s (1.8M iterations) +Benchmarking mempool_admission/deserialize_and_preverify: Analyzing +mempool_admission/deserialize_and_preverify + time: [2.7438 µs 2.7446 µs 2.7455 µs] + thrpt: [364.23 Kelem/s 364.36 Kelem/s 364.46 Kelem/s] +Found 1 outliers among 100 measurements (1.00%) + 1 (1.00%) high mild +Benchmarking mempool_admission/zk_multisig_verify +Benchmarking mempool_admission/zk_multisig_verify: Warming up for 3.0000 s +Benchmarking mempool_admission/zk_multisig_verify: Collecting 100 samples in estimated 5.9621 s (400 iterations) +Benchmarking mempool_admission/zk_multisig_verify: Analyzing +mempool_admission/zk_multisig_verify + time: [14.886 ms 14.887 ms 14.888 ms] + thrpt: [67.166 elem/s 67.173 elem/s 67.179 elem/s] +Found 12 outliers among 100 measurements (12.00%) + 11 (11.00%) high mild + 1 (1.00%) high severe + diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/machine.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/machine.txt new file mode 100644 index 0000000..6719922 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/machine.txt @@ -0,0 +1,6 @@ +label: rpi5 +date: 2026-08-15T21:28:49Z +uname: Linux rasberrypi5 6.18.34+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.18.34-1+rpt1 (2026-06-09) aarch64 GNU/Linux +cpu: Raspberry Pi 5 Model B Rev 1.1 +cores: 4 +governor: performance diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/reconstruction.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/reconstruction.csv new file mode 100644 index 0000000..bcfd353 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/reconstruction.csv @@ -0,0 +1,65 @@ +machine,block_txs,k,policy,combinations,attempts,outcome,median_s,min_s,max_s,repeats,over_slot +rpi5,1024,0,uncapped,1,1,rebuilt,0.002648003,0.002573001,0.003162385,5,false +rpi5,1024,0,capped,1,0,rebuilt,0.002629003,0.002629003,0.002629003,1,false +rpi5,1024,1,uncapped,2,2,rebuilt,0.004512248,0.004479951,0.004571453,5,false +rpi5,1024,1,capped,2,0,rebuilt,0.004553008,0.004553008,0.004553008,1,false +rpi5,1024,2,uncapped,4,4,rebuilt,0.008077531,0.007995418,0.008108809,5,false +rpi5,1024,2,capped,4,0,rebuilt,0.008031937,0.008031937,0.008031937,1,false +rpi5,1024,3,uncapped,8,8,rebuilt,0.015311672,0.015233466,0.015342191,5,false +rpi5,1024,3,capped,8,0,rebuilt,0.015405285,0.015405285,0.015405285,1,false +rpi5,1024,4,uncapped,16,16,rebuilt,0.029585321,0.029490764,0.029706305,5,false +rpi5,1024,4,capped,16,0,rebuilt,0.029605156,0.029605156,0.029605156,1,false +rpi5,1024,5,uncapped,32,32,rebuilt,0.058482517,0.058063415,0.059364518,5,false +rpi5,1024,5,capped,32,0,rebuilt,0.058801542,0.058801542,0.058801542,1,false +rpi5,1024,6,uncapped,64,64,rebuilt,0.115825564,0.115722414,0.116487265,5,false +rpi5,1024,6,capped,64,0,refused_combinations,0.000184319,0.000184319,0.000184319,1,false +rpi5,1024,7,uncapped,128,128,rebuilt,0.231877455,0.231006918,0.232705110,5,false +rpi5,1024,7,capped,128,0,refused_combinations,0.000139892,0.000139892,0.000139892,1,false +rpi5,1024,8,uncapped,256,256,rebuilt,0.461668892,0.458452644,0.466281555,5,false +rpi5,1024,8,capped,256,0,refused_combinations,0.000143800,0.000143800,0.000143800,1,false +rpi5,1024,9,uncapped,512,512,rebuilt,0.925414524,0.922552884,0.933206648,5,false +rpi5,1024,9,capped,512,0,refused_combinations,0.000141706,0.000141706,0.000141706,1,false +rpi5,1024,10,uncapped,1024,1024,rebuilt,1.866991188,1.852895942,1.875008675,5,true +rpi5,1024,10,capped,1024,0,refused_combinations,0.000187023,0.000187023,0.000187023,1,false +rpi5,1024,11,uncapped,2048,2048,rebuilt,3.802968232,3.802968232,3.802968232,1,true +rpi5,1024,11,capped,2048,0,refused_combinations,0.000140059,0.000140059,0.000140059,1,false +rpi5,1024,12,uncapped,4096,4096,rebuilt,7.691364538,7.691364538,7.691364538,1,true +rpi5,1024,12,capped,4096,0,refused_combinations,0.000142948,0.000142948,0.000142948,1,false +rpi5,1024,13,uncapped,8192,8192,rebuilt,15.635065615,15.635065615,15.635065615,1,true +rpi5,1024,13,capped,8192,0,refused_combinations,0.000186986,0.000186986,0.000186986,1,false +rpi5,1024,14,uncapped,16384,16384,rebuilt,32.031760669,32.031760669,32.031760669,1,true +rpi5,1024,14,capped,16384,0,refused_combinations,0.000141355,0.000141355,0.000141355,1,false +rpi5,1024,15,uncapped,32768,32768,rebuilt,62.715830888,62.715830888,62.715830888,1,true +rpi5,1024,15,capped,32768,0,refused_combinations,0.000142244,0.000142244,0.000142244,1,false +rpi5,128,0,uncapped,1,1,rebuilt,0.000445752,0.000443771,0.000568478,5,false +rpi5,128,0,capped,1,0,rebuilt,0.000454827,0.000454827,0.000454827,1,false +rpi5,128,1,uncapped,2,2,rebuilt,0.000658572,0.000654887,0.000659850,5,false +rpi5,128,1,capped,2,0,rebuilt,0.000674091,0.000674091,0.000674091,1,false +rpi5,128,2,uncapped,4,4,rebuilt,0.001078175,0.001078083,0.001083324,5,false +rpi5,128,2,capped,4,0,rebuilt,0.001111713,0.001111713,0.001111713,1,false +rpi5,128,3,uncapped,8,8,rebuilt,0.001921438,0.001912752,0.001940272,5,false +rpi5,128,3,capped,8,0,rebuilt,0.001916641,0.001916641,0.001916641,1,false +rpi5,128,4,uncapped,16,16,rebuilt,0.003594906,0.003588776,0.003601703,5,false +rpi5,128,4,capped,16,0,rebuilt,0.003595813,0.003595813,0.003595813,1,false +rpi5,128,5,uncapped,32,32,rebuilt,0.006915120,0.006904397,0.006939657,5,false +rpi5,128,5,capped,32,0,rebuilt,0.006945991,0.006945991,0.006945991,1,false +rpi5,128,6,uncapped,64,64,rebuilt,0.013623697,0.013563492,0.013682625,5,false +rpi5,128,6,capped,64,0,refused_combinations,0.000018093,0.000018093,0.000018093,1,false +rpi5,128,7,uncapped,128,128,rebuilt,0.026896107,0.026867255,0.026983498,5,false +rpi5,128,7,capped,128,0,refused_combinations,0.000018093,0.000018093,0.000018093,1,false +rpi5,128,8,uncapped,256,256,rebuilt,0.053616932,0.053321980,0.053822308,5,false +rpi5,128,8,capped,256,0,refused_combinations,0.000018427,0.000018427,0.000018427,1,false +rpi5,128,9,uncapped,512,512,rebuilt,0.107277255,0.107065047,0.107378852,5,false +rpi5,128,9,capped,512,0,refused_combinations,0.000018816,0.000018816,0.000018816,1,false +rpi5,128,10,uncapped,1024,1024,rebuilt,0.214644979,0.214341456,0.215396146,5,false +rpi5,128,10,capped,1024,0,refused_combinations,0.000018723,0.000018723,0.000018723,1,false +rpi5,128,11,uncapped,2048,2048,rebuilt,0.429937732,0.429038408,0.431485991,5,false +rpi5,128,11,capped,2048,0,refused_combinations,0.000019112,0.000019112,0.000019112,1,false +rpi5,128,12,uncapped,4096,4096,rebuilt,0.862403860,0.860548418,0.864353434,5,false +rpi5,128,12,capped,4096,0,refused_combinations,0.000019279,0.000019279,0.000019279,1,false +rpi5,128,13,uncapped,8192,8192,rebuilt,1.722517485,1.719127014,1.724525453,5,true +rpi5,128,13,capped,8192,0,refused_combinations,0.000019353,0.000019353,0.000019353,1,false +rpi5,128,14,uncapped,16384,16384,rebuilt,3.444473233,3.444473233,3.444473233,1,true +rpi5,128,14,capped,16384,0,refused_combinations,0.000019649,0.000019649,0.000019649,1,false +rpi5,128,15,uncapped,32768,32768,rebuilt,6.877156777,6.877156777,6.877156777,1,true +rpi5,128,15,capped,32768,0,refused_combinations,0.000019352,0.000019352,0.000019352,1,false diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/throughput.csv b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/throughput.csv new file mode 100644 index 0000000..b06bc7c --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/throughput.csv @@ -0,0 +1,5 @@ +machine,threads,candidates_per_second,per_thread_candidates_per_second,scaling_vs_one_thread,seconds +rpi5,1,4336610.6,4336610.6,1.000,5 +rpi5,2,8638303.8,4319151.9,1.992,5 +rpi5,3,12939790.5,4313263.5,2.984,5 +rpi5,4,17245129.1,4311282.3,3.977,5 diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/toolchain.txt b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/toolchain.txt new file mode 100644 index 0000000..c5452c6 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/results/rpi5/toolchain.txt @@ -0,0 +1,2 @@ +rustc 1.97.1 (8bab26f4f 2026-07-14) +cargo 1.97.1 (c980f4866 2026-06-30) diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/rust-toolchain.toml b/tools/benchmarks/block-proposal/reference-prefix-length/rust-toolchain.toml new file mode 100644 index 0000000..a08cd2b --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/rust-toolchain.toml @@ -0,0 +1,5 @@ +# Pinned to the same toolchain logos-blockchain pins, so the Mac and the RPi5 +# compile the measured code identically. rustup installs this on first `cargo` +# invocation in this directory. +[toolchain] +channel = "1.97.1" diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/scripts/analyse.py b/tools/benchmarks/block-proposal/reference-prefix-length/scripts/analyse.py new file mode 100755 index 0000000..d29c7b3 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/scripts/analyse.py @@ -0,0 +1,550 @@ +#!/usr/bin/env python3 +"""Turn the measured CSVs into the report's cost tables and latency plot. + +Reads results// and prints markdown. Every derived number is computed +here from a measured input, so a reviewer can change an assumption (GPU hash +rate, GPU price) at the top of this file and re-derive the whole table. + + python3 scripts/analyse.py + python3 scripts/analyse.py --machines mac rpi5 +""" + +from __future__ import annotations + +import argparse +import csv +import math +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +RESULTS = ROOT / "results" + +# --- assumptions, all stated so they can be re-priced ----------------------- + +# Expected candidates before the first birthday repeat is sqrt(pi/2) * 2^(b/2). +SQRT_HALF_PI = math.sqrt(math.pi / 2) + +# GPU hash rate used for the adversary's strong-hardware column. This is the one +# input in this whole model that is assumed rather than measured, and every cost +# and duration below scales inversely with it. +# +# 10^10 H/s is the figure logos-lips#389 argues from, kept here for +# comparability with it. Note it is NOT generous to the attacker: published +# hashcat benchmarks put a single RTX 4090 at ~1.25e10 H/s on BLAKE2b, so this +# understates one current consumer GPU by ~20%. Re-pricing at 1.25e10 scales +# every figure by 0.8x and changes no conclusion. +# +# Set GPU_HASH_RATE to 1.25e10 to re-derive everything at the measured rate. +GPU_MODEL = "RTX 4090" +GPU_HASH_RATE = 1e10 + +# Spot-ish rental price for one such GPU. Generous to the attacker. +GPU_USD_PER_HOUR = 0.50 + +# A reference adversary budget, used to express the margin as "how many +# colliding pairs does this much money buy?" -- which is directly comparable to +# the number of pairs needed to stall a validator. +BUDGET_USD = 10_000.0 + +# Proposal layout, from logos-lips#389 and core/src/block/mod.rs: +# Proposal = header(297) + references(2 + L*n) + signature(64) +HEADER_BYTES = 297 +SIGNATURE_BYTES = 64 +COUNT_PREFIX_BYTES = 2 +MAX_BLOCK_TXS = 1024 +# master's fixed 1024-entry array of full 32-byte hashes +MASTER_PROPOSAL_BYTES = 33129 + +PREFIX_LENGTHS = [8, 10, 12, 14, 16] + +SECONDS_PER_DAY = 86400.0 +SECONDS_PER_YEAR = 365.25 * SECONDS_PER_DAY + + +# --- loading ---------------------------------------------------------------- + + +def load_csv(machine: str, name: str): + path = RESULTS / machine / f"{name}.csv" + if not path.exists(): + return None + with path.open() as handle: + return list(csv.DictReader(handle)) + + +def load_rgen(machine: str) -> dict[str, float] | None: + """Parse single-core candidate rates out of the saved criterion output. + + Falls back to the 1-thread row of throughput.csv when the criterion log is + absent, so a partially-collected machine still yields a table. + """ + path = RESULTS / machine / "candidate_generation.txt" + rates: dict[str, float] = {} + if path.exists(): + text = path.read_text() + # criterion prints: name \n time: [lo mid hi] \n thrpt: [lo mid hi] + pattern = re.compile( + r"candidate_generation/(\w+)\s*\n\s*time:\s*\[[\d.]+ \w+ ([\d.]+) (\w+)", + re.MULTILINE, + ) + unit = {"ns": 1e-9, "µs": 1e-6, "us": 1e-6, "ms": 1e-3, "s": 1.0} + for name, value, suffix in pattern.findall(text): + seconds = float(value) * unit[suffix] + rates[name] = 1.0 / seconds + + if not rates: + rows = load_csv(machine, "throughput") + if rows: + one = next((r for r in rows if int(r["threads"]) == 1), None) + if one: + rates["attacker_patched"] = float(one["candidates_per_second"]) + return rates or None + + +def aggregate_rate(machine: str) -> tuple[float, int] | None: + rows = load_csv(machine, "throughput") + if not rows: + return None + best = max(rows, key=lambda r: int(r["threads"])) + return float(best["candidates_per_second"]), int(best["threads"]) + + +# --- formatting ------------------------------------------------------------- + + +def human_time(seconds: float) -> str: + if seconds < 1e-3: + return f"{seconds * 1e6:.0f} µs" + if seconds < 1.0: + return f"{seconds * 1e3:.0f} ms" + if seconds < 120: + return f"{seconds:.1f} s" + if seconds < 7200: + return f"{seconds / 60:.1f} min" + if seconds < 2 * SECONDS_PER_DAY: + return f"{seconds / 3600:.1f} h" + if seconds < 2 * SECONDS_PER_YEAR: + return f"{seconds / SECONDS_PER_DAY:.0f} days" + years = seconds / SECONDS_PER_YEAR + if years < 1e4: + return f"{years:,.0f} years" + return f"{years:.2e} years" + + +def human_usd(usd: float) -> str: + if usd < 0.01: + return "<$0.01" + if usd < 1000: + return f"${usd:,.2f}" + if usd < 1e6: + return f"${usd:,.0f}" + return f"${usd / 1e6:,.1f}M" + + +def candidates_for_pairs(prefix_len: int, pairs: int) -> float: + """Candidates needed for `pairs` colliding pairs at an L-byte prefix. + + One pair needs sqrt(pi/2) * 2^(b/2). Collisions accumulate as N^2 / 2^(b+1), + so k pairs need sqrt(k) times as many -- the sqrt(k) scaling that makes the + attacker's side of the asymmetry so much flatter than the defender's. + """ + bits = 8 * prefix_len + return SQRT_HALF_PI * math.sqrt(pairs) * (2.0 ** (bits / 2.0)) + + +# --- tables ----------------------------------------------------------------- + + +def table_birthday(machine: str) -> str: + rows = load_csv(machine, "birthday") + if not rows: + return f"_no birthday data for `{machine}`_\n" + + out = [ + f"**{machine}** — measured vs. predicted first-collision counts", + "", + "| prefix | b (bits) | predicted N | measured N | ratio ± SE |", + "|---|---|---|---|---|", + ] + for r in rows: + out.append( + f"| {r['prefix_bytes']} B | {r['prefix_bits']} | " + f"{float(r['predicted_n']):,.0f} | {float(r['measured_n_mean']):,.0f} | " + f"{float(r['ratio']):.3f} ± {float(r['ratio_stderr']):.3f} |" + ) + return "\n".join(out) + "\n" + + +def table_rgen(machines: list[str]) -> str: + out = [ + "| machine | node path | attacker (patched) | raw Blake2b | aggregate (all cores) |", + "|---|---|---|---|---|", + ] + for machine in machines: + rates = load_rgen(machine) + if not rates: + out.append(f"| {machine} | _(pending)_ | _(pending)_ | _(pending)_ | _(pending)_ |") + continue + agg = aggregate_rate(machine) + agg_text = f"{agg[0]:.3e} /s ({agg[1]} threads)" if agg else "_(pending)_" + out.append( + f"| {machine} | " + f"{rates.get('node_path', float('nan')):.3e} /s | " + f"{rates.get('attacker_patched', float('nan')):.3e} /s | " + f"{rates.get('blake2b_only', float('nan')):.3e} /s | " + f"{agg_text} |" + ) + return "\n".join(out) + "\n" + + +def table_generation_cost(machines: list[str]) -> str: + """Cost of manufacturing ONE colliding pair, at each prefix length.""" + columns: list[tuple[str, float]] = [] + for machine in machines: + rates = load_rgen(machine) + if rates and "attacker_patched" in rates: + columns.append((f"1 core ({machine})", rates["attacker_patched"])) + for machine in machines: + agg = aggregate_rate(machine) + if agg: + columns.append((f"1 machine ({machine}, {agg[1]}t)", agg[0])) + columns.append((f"1 GPU ({GPU_MODEL})", GPU_HASH_RATE)) + columns.append((f"100 GPUs", GPU_HASH_RATE * 100)) + + header = "| L (bytes) | b (bits) | candidates N | " + " | ".join( + name for name, _ in columns + ) + " |" + sep = "|---" * (3 + len(columns)) + "|" + out = [header, sep] + + for prefix_len in PREFIX_LENGTHS: + need = candidates_for_pairs(prefix_len, 1) + cells = [human_time(need / rate) for _, rate in columns] + out.append( + f"| {prefix_len} | {8 * prefix_len} | {need:.3e} | " + " | ".join(cells) + " |" + ) + return "\n".join(out) + "\n" + + +def pairs_affordable(prefix_len: int, budget_usd: float) -> float: + """How many colliding pairs `budget_usd` buys at this prefix length. + + Cost grows as sqrt(k), so inverting gives k = (budget / cost_of_one)^2. + Compare against the measured number of pairs needed to stall a validator: + the crossover L is the first one where this drops below that. + """ + one_pair = candidates_for_pairs(prefix_len, 1) + cost_of_one = one_pair / GPU_HASH_RATE / 3600.0 * GPU_USD_PER_HOUR + return (budget_usd / cost_of_one) ** 2 + + +def format_pairs(count: float) -> str: + if count < 1.0: + return f"**{count:.3g}** (not even one)" + if count < 1e5: + return f"{count:,.0f}" + return f"{count:.2e}" + + +def table_machines_needed(pairs: int, rgen_single_core: float | None) -> str: + """How many GPUs / cores it takes to manufacture `pairs` within a deadline. + + This is the question in the review as asked -- "how many cores (or GPUs) one + needs" -- rather than a wall-clock or a dollar figure. + """ + horizons = [("within 1 hour", 3600.0), ("within 1 day", 86_400.0)] + header = "| L (bytes) | " + " | ".join( + f"GPUs {name}" for name, _ in horizons + ) + if rgen_single_core: + header += " | " + " | ".join(f"cores {name}" for name, _ in horizons) + out = [header + " |", "|---" * (1 + len(horizons) * (2 if rgen_single_core else 1)) + "|"] + + def count(need: float, rate: float, seconds: float) -> str: + units = need / (rate * seconds) + if units < 1: + return f"<1 ({units:.2g})" + if units < 1e6: + return f"{units:,.0f}" + return f"{units:.2e}" + + for prefix_len in PREFIX_LENGTHS: + need = candidates_for_pairs(prefix_len, pairs) + cells = [count(need, GPU_HASH_RATE, seconds) for _, seconds in horizons] + if rgen_single_core: + cells += [count(need, rgen_single_core, seconds) for _, seconds in horizons] + out.append(f"| {prefix_len} | " + " | ".join(cells) + " |") + return "\n".join(out) + "\n" + + +def table_sustained(pairs_per_slot: int) -> str: + """Cost of a one-off stalled slot vs. sustaining the stall. + + A colliding pair buys ambiguity only while BOTH of its transactions sit in + the mempool. Once the proposer includes one, the block is applied and that + transaction is removed, leaving the other unambiguous. So a pair is spent + after roughly one slot, and stalling N consecutive slots needs + pairs_per_slot * N pairs in total. + + Cost still grows as sqrt of the total, so bulk is discounted -- but the + discount does not keep up with a 1-second slot. + """ + horizons = [ + ("one slot", 1), + ("one hour", 3600), + ("one day", 86_400), + ] + out = [ + "| L (bytes) | " + " | ".join(name for name, _ in horizons) + " |", + "|---" * (1 + len(horizons)) + "|", + ] + for prefix_len in PREFIX_LENGTHS: + cells = [] + for _, slots in horizons: + total_pairs = pairs_per_slot * slots + need = candidates_for_pairs(prefix_len, total_pairs) + usd = need / GPU_HASH_RATE / 3600.0 * GPU_USD_PER_HOUR + cells.append(human_usd(usd)) + out.append(f"| {prefix_len} | " + " | ".join(cells) + " |") + return "\n".join(out) + "\n" + + +def table_summary(machines: list[str], pairs_needed: dict[str, int]) -> str: + """The decision table: what it costs to stall each machine, per L.""" + out = [ + "| L (bytes) | proposal max | vs. master | GPU-hours for 1 pair | $ for 1 pair | " + + " | ".join(f"$ to stall {m}" for m in machines) + + f" | pairs for ${BUDGET_USD:,.0f} |", + "|---" * (6 + len(machines)) + "|", + ] + for prefix_len in PREFIX_LENGTHS: + proposal = ( + HEADER_BYTES + + COUNT_PREFIX_BYTES + + prefix_len * MAX_BLOCK_TXS + + SIGNATURE_BYTES + ) + ratio = MASTER_PROPOSAL_BYTES / proposal + + one_pair = candidates_for_pairs(prefix_len, 1) + gpu_hours = one_pair / GPU_HASH_RATE / 3600.0 + one_pair_usd = gpu_hours * GPU_USD_PER_HOUR + + stall_cells = [] + for machine in machines: + k = pairs_needed.get(machine) + if k is None: + stall_cells.append("_(pending)_") + continue + need = candidates_for_pairs(prefix_len, k) + usd = need / GPU_HASH_RATE / 3600.0 * GPU_USD_PER_HOUR + stall_cells.append(human_usd(usd)) + + out.append( + f"| {prefix_len} | {proposal:,} B | {ratio:.2f}× | {gpu_hours:,.1f} | " + f"{human_usd(one_pair_usd)} | " + + " | ".join(stall_cells) + + f" | {format_pairs(pairs_affordable(prefix_len, BUDGET_USD))} |" + ) + return "\n".join(out) + "\n" + + +def reconstruction_summary(machine: str) -> str: + rows = load_csv(machine, "reconstruction") + if not rows: + return f"_no reconstruction data for `{machine}`_\n" + + out = [] + for block_txs in sorted({int(r["block_txs"]) for r in rows}, reverse=True): + subset = [ + r + for r in rows + if int(r["block_txs"]) == block_txs and r["policy"] == "uncapped" + ] + subset.sort(key=lambda r: int(r["k"])) + baseline = float(subset[0]["median_s"]) if subset else float("nan") + + out.append(f"**{machine}**, block of {block_txs} transactions") + out.append("") + out.append("| k | combinations | median | vs. 1 s slot |") + out.append("|---|---|---|---|") + for r in subset: + median = float(r["median_s"]) + verdict = "**over slot**" if r["over_slot"] == "true" else "within" + out.append( + f"| {r['k']} | {int(r['combinations']):,} | {human_time(median)} | {verdict} |" + ) + first_over = next((int(r["k"]) for r in subset if r["over_slot"] == "true"), None) + out.append("") + out.append( + f"Per-combination cost ≈ {human_time(baseline)}; " + + ( + f"the 1 s slot is first exceeded at **k = {first_over}**." + if first_over is not None + else "the slot was not exceeded within the measured range." + ) + ) + out.append("") + return "\n".join(out) + + +def pairs_to_stall(machine: str) -> int | None: + """Smallest k whose uncapped reconstruction exceeds the slot, at a full block.""" + rows = load_csv(machine, "reconstruction") + if not rows: + return None + full = max(int(r["block_txs"]) for r in rows) + candidates = [ + int(r["k"]) + for r in rows + if r["policy"] == "uncapped" + and int(r["block_txs"]) == full + and r["over_slot"] == "true" + ] + return min(candidates) if candidates else None + + +# --- plot ------------------------------------------------------------------- + + +def plot(machines: list[str]) -> None: + try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + print( + "\n_(matplotlib not installed — skipping the plot; " + "`python3 -m pip install matplotlib` to enable it)_", + file=sys.stderr, + ) + return + + fig, ax = plt.subplots(figsize=(9, 5.5)) + colours = {"mac": "#4C72B0", "rpi5": "#C44E52"} + + plotted = False + for machine in machines: + rows = load_csv(machine, "reconstruction") + if not rows: + continue + for block_txs, style in ((1024, "-o"), (128, "--s")): + subset = [ + r + for r in rows + if r["policy"] == "uncapped" and int(r["block_txs"]) == block_txs + ] + if not subset: + continue + subset.sort(key=lambda r: int(r["k"])) + ax.plot( + [int(r["k"]) for r in subset], + [float(r["median_s"]) for r in subset], + style, + color=colours.get(machine, None), + markersize=4, + linewidth=1.6, + label=f"{machine}, {block_txs} txs", + ) + plotted = True + + if not plotted: + return + + ax.axhline(1.0, color="#555555", linestyle=":", linewidth=1.5) + ax.text( + 0.02, + 1.15, + "1 s slot — block production stalls above this line", + transform=ax.get_yaxis_transform(), + fontsize=9, + color="#555555", + ) + + ax.set_yscale("log") + ax.set_xlabel("k — ambiguous references in the proposal (colliding pairs in the mempool)") + ax.set_ylabel("reconstruction time (s, log scale)") + ax.set_title("Uncapped reconstruction cost doubles with every colliding pair") + ax.grid(True, which="both", alpha=0.25, linewidth=0.6) + ax.legend(frameon=False) + fig.tight_layout() + + out = RESULTS / "reconstruction-latency.png" + fig.savefig(out, dpi=160) + print(f"\n_wrote {out.relative_to(ROOT)}_", file=sys.stderr) + + +# --- main ------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--machines", + nargs="+", + default=None, + help="machine labels to include (default: whatever exists under results/)", + ) + args = parser.parse_args() + + machines = args.machines + if machines is None: + machines = sorted( + d.name for d in RESULTS.iterdir() if d.is_dir() and any(d.iterdir()) + ) + if not machines: + print("no results found under results/", file=sys.stderr) + raise SystemExit(1) + + pairs_needed = {m: k for m in machines if (k := pairs_to_stall(m)) is not None} + + print("## Candidate-generation rate (R_gen)\n") + print(table_rgen(machines)) + + print("\n## Birthday model, measured against prediction\n") + for machine in machines: + print(table_birthday(machine)) + + print("\n## Cost of manufacturing one colliding pair\n") + print(table_generation_cost(machines)) + + print("\n## Reconstruction latency\n") + for machine in machines: + print(reconstruction_summary(machine)) + + print("\n## Decision table\n") + print( + f"_Assumes {GPU_MODEL} at {GPU_HASH_RATE:.1e} H/s, " + f"${GPU_USD_PER_HOUR:.2f}/GPU-hour._\n" + ) + for machine, k in pairs_needed.items(): + print(f"_Stalling **{machine}** needs k = {k} colliding pairs (measured)._") + print() + print(table_summary(machines, pairs_needed)) + + if pairs_needed: + k = max(pairs_needed.values()) + rates = load_rgen(machines[0]) if machines else None + one_core = rates.get("attacker_patched") if rates else None + print( + f"\n### Hardware needed to manufacture k = {k} colliding pairs\n" + ) + print(table_machines_needed(k, one_core)) + print( + f"\n### Cost of sustaining the stall (k = {k} pairs per slot, " + "1 s slots)\n" + ) + print( + "A pair is spent once the proposer includes one of its two " + "transactions, so holding a stall open costs pairs *per slot*.\n" + ) + print(table_sustained(k)) + + plot(machines) + + +if __name__ == "__main__": + main() diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/scripts/run_all.sh b/tools/benchmarks/block-proposal/reference-prefix-length/scripts/run_all.sh new file mode 100755 index 0000000..e456135 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/scripts/run_all.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Run the whole reference-prefix measurement suite and drop every result under +# results//. +# +# ./scripts/run_all.sh mac +# ./scripts/run_all.sh rpi5 +# +# The machine name is only a label for the output directory and the CSV rows; +# the code executed is identical on every machine. + +set -euo pipefail + +MACHINE="${1:-}" +if [[ -z "$MACHINE" ]]; then + echo "usage: $0 (e.g. mac, rpi5)" >&2 + exit 1 +fi + +cd "$(dirname "$0")/.." +OUT="results/${MACHINE}" +mkdir -p "$OUT" + +echo "==> toolchain" +rustc --version | tee "$OUT/toolchain.txt" +cargo --version | tee -a "$OUT/toolchain.txt" + +echo +echo "==> machine" +{ + echo "label: ${MACHINE}" + echo "date: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "uname: $(uname -a)" + if [[ -r /proc/cpuinfo ]]; then + echo "cpu: $(grep -m1 -E 'model name|Model' /proc/cpuinfo | cut -d: -f2- | sed 's/^ *//')" + echo "cores: $(nproc)" + if [[ -r /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]]; then + echo "governor: $(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor)" + fi + elif command -v sysctl >/dev/null; then + echo "cpu: $(sysctl -n machdep.cpu.brand_string)" + echo "cores: $(sysctl -n hw.ncpu)" + fi +} | tee "$OUT/machine.txt" + +echo +echo "==> harness self-check (the shortcut must agree with the real hash path)" +cargo test --release --quiet + +echo +echo "==> 1/4 R_gen, single core (criterion)" +cargo bench --bench candidate_generation 2>&1 | tee "$OUT/candidate_generation.txt" + +echo +echo "==> 2/4 aggregate generation throughput vs. thread count" +cargo run --release --quiet --bin throughput -- \ + --machine "$MACHINE" --seconds 5 --out "$OUT/throughput.csv" + +echo +echo "==> 3/4 birthday model validation on real mantle_txhash output" +cargo run --release --quiet --bin birthday -- \ + --machine "$MACHINE" --trials 24 --out "$OUT/birthday.csv" + +echo +echo "==> 4/4 reconstruction latency vs. collision multiplicity" +# k-max 15 is 32768 combinations; on a slow machine the budget stops it sooner. +cargo run --release --quiet --bin reconstruction -- \ + --machine "$MACHINE" --block-sizes 1024,128 --k-max 15 --repeats 5 \ + --budget-secs 60 --out "$OUT/reconstruction.csv" + +echo +echo "==> done. results in $OUT/" +ls -la "$OUT" diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/birthday.rs b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/birthday.rs new file mode 100644 index 0000000..49029d0 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/birthday.rs @@ -0,0 +1,196 @@ +//! Empirical check of the birthday model the prefix length is chosen against, +//! plus the measured grinding rate that turns it into a cost. +//! +//! The security argument rests on one claim: an adversary who wants *any* two +//! transactions sharing a reference prefix needs about 2^(b/2) candidates, not +//! 2^b. Rather than assert that, this harness measures it — it grinds real +//! `mantle_txhash` outputs at prefix lengths short enough to collide in +//! seconds, and reports the observed number of candidates against the +//! prediction. +//! +//! For a uniform b-bit output the expected number of draws before the first +//! repeat is sqrt(pi/2) * 2^(b/2) ≈ 1.2533 * 2^(b/2). If the measured ratio +//! sits near 1.0 across several values of b, the extrapolation to b = 64 and +//! b = 128 is sound, and the only remaining input is the grinding rate. +//! +//! ```text +//! cargo run --release --bin birthday -- --machine mac +//! ``` + +use std::{ + collections::HashMap, + fmt::Write as _, + time::{Duration, Instant}, +}; + +use reference_prefix_bench::{AttackerHasher, sample_preimage_len, sample_tx_encoded_len}; + +/// sqrt(pi / 2), the constant in the expected number of draws before the first +/// birthday repeat. +const SQRT_HALF_PI: f64 = 1.253_314_137_315_500_3; + +struct Args { + machine: String, + prefix_bits: Vec, + trials: usize, + out: Option, +} + +impl Args { + fn parse() -> Self { + let mut args = Self { + machine: "unknown".to_owned(), + // 2, 3, 4 and 5 byte prefixes. Long enough to span 24 bits of + // doubling, short enough that every trial finishes in seconds. + prefix_bits: vec![16, 24, 32, 40], + trials: 8, + out: None, + }; + + let mut argv = std::env::args().skip(1); + while let Some(flag) = argv.next() { + let mut value = || { + argv.next() + .unwrap_or_else(|| panic!("{flag} needs a value")) + }; + match flag.as_str() { + "--machine" => args.machine = value(), + "--prefix-bits" => { + args.prefix_bits = value() + .split(',') + .map(|s| s.trim().parse().expect("prefix bits must be a number")) + .collect(); + } + "--trials" => args.trials = value().parse().expect("trials must be a number"), + "--out" => args.out = Some(value()), + "--help" | "-h" => { + eprintln!( + "birthday --machine [--prefix-bits 16,24,32,40] \ + [--trials 8] [--out FILE]" + ); + std::process::exit(0); + } + other => panic!("unknown flag {other}"), + } + } + args + } +} + +/// Grind candidates until two share their leading `bits` bits, starting the +/// nonce sequence at `start` so repeated trials are independent. +/// +/// Returns the number of candidates drawn and how long it took. +fn first_collision(hasher: &mut AttackerHasher, bits: u32, start: u64) -> (u64, Duration) { + let mut seen: HashMap = HashMap::new(); + let shift = 64 - bits; + let mut nonce = start; + let max = hasher.max_nonce(); + + let began = Instant::now(); + let mut drawn = 0u64; + loop { + let hash = hasher.hash_candidate(nonce); + // The prefix is the leading bytes of the hash; read them big-endian so + // that "leading `bits` bits" means what it says. + let leading = u64::from_be_bytes(hash[..8].try_into().expect("8 bytes")) >> shift; + drawn += 1; + + if seen.insert(leading, nonce).is_some() { + return (drawn, began.elapsed()); + } + + nonce = if nonce >= max { 0 } else { nonce + 1 }; + } +} + +fn main() { + let args = Args::parse(); + + let mut hasher = AttackerHasher::new(); + // If this diverges, every number below is meaningless, so it is checked + // before anything is measured. + hasher.verify_against_real_path(); + + println!( + "birthday self-collision on real mantle_txhash output\n\ + machine : {}\n\ + sample tx : {} bytes encoded, {} bytes hashed\n\ + expected : sqrt(pi/2) * 2^(b/2) candidates before the first repeat\n", + args.machine, + sample_tx_encoded_len(), + sample_preimage_len() + ); + + println!( + "{:>5} {:>6} {:>13} {:>13} {:>16} {:>12}", + "bits", "bytes", "predicted N", "measured N", "ratio +- SE", "search H/s" + ); + + let mut csv = String::from( + "machine,prefix_bits,prefix_bytes,trials,predicted_n,measured_n_mean,\ + measured_n_stderr,ratio,ratio_stderr,search_hashes_per_second\n", + ); + + for &bits in &args.prefix_bits { + let predicted = SQRT_HALF_PI * 2f64.powf(f64::from(bits) / 2.0); + + let mut draws: Vec = Vec::with_capacity(args.trials); + let mut elapsed = Duration::ZERO; + for trial in 0..args.trials { + // Offset each trial so it draws a different region of the nonce + // space, making the trials independent samples. + let start = (trial as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) % hasher.max_nonce(); + let (drawn, took) = first_collision(&mut hasher, bits, start); + draws.push(drawn as f64); + elapsed += took; + } + + let n = draws.len() as f64; + let mean = draws.iter().sum::() / n; + // The first-collision distribution is strongly right-skewed, so the + // spread across trials is large by nature. The standard error of the + // mean is what says whether the ratio is consistent with 1.0. + let variance = draws.iter().map(|d| (d - mean).powi(2)).sum::() / (n - 1.0); + let stderr = (variance / n).sqrt(); + + let ratio = mean / predicted; + let ratio_stderr = stderr / predicted; + let rate = draws.iter().sum::() / elapsed.as_secs_f64(); + + println!( + "{bits:>5} {:>6} {predicted:>13.0} {mean:>13.0} \ + {:>16} {rate:>12.3e}", + bits / 8, + format!("{ratio:.3} +- {ratio_stderr:.3}") + ); + + writeln!( + csv, + "{},{bits},{},{},{predicted:.1},{mean:.1},{stderr:.1},{ratio:.4},{ratio_stderr:.4},{rate:.1}", + args.machine, + bits / 8, + args.trials + ) + .expect("writing to a String cannot fail"); + } + + println!( + "\nA ratio consistent with 1.0 means the 2^(b/2) model holds on real \ + transaction hashes,\nso the cost at b = 64 and b = 128 follows from \ + the grinding rate alone.\n\n\ + The 'search H/s' column is NOT R_gen: it includes the hash-table \ + insert this harness\nuses to detect a repeat. A real parallel \ + collision search (van Oorschot-Wiener) is\nmemoryless and does not pay \ + that cost. Take R_gen from the criterion benchmark." + ); + + let path = args + .out + .unwrap_or_else(|| format!("results/{}/birthday.csv", args.machine)); + if let Some(parent) = std::path::Path::new(&path).parent() { + std::fs::create_dir_all(parent).expect("results directory must be creatable"); + } + std::fs::write(&path, csv).expect("results file must be writable"); + println!("wrote {path}"); +} diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/reconstruction.rs b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/reconstruction.rs new file mode 100644 index 0000000..59ab3d3 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/reconstruction.rs @@ -0,0 +1,267 @@ +//! Reconstruction latency as a function of `k`, the number of references in a +//! proposal that resolve ambiguously in the validator's mempool. +//! +//! This is the defender's side of the asymmetry. Manufacturing `k` colliding +//! pairs costs the attacker roughly sqrt(k) times one collision, because the +//! birthday search keeps finding pairs as it goes. An *uncapped* validator, by +//! contrast, must walk the product of the candidate-set sizes — 2^k +//! combinations for `k` two-way collisions — and each combination re-encodes +//! and re-hashes every transaction in the block. +//! +//! Two policies are measured: +//! +//! * `uncapped` — the deterministic lookup logos-lips#389 v3 argues for, on the +//! grounds that at a long enough prefix ambiguity cannot be manufactured. +//! * `capped` — what `logos-blockchain` merged: refuse the proposal outright +//! once the combination product passes `MAX_RECONSTRUCTION_COMBINATIONS`. +//! Cheap, but refusing an *honest* proposal is itself the liveness failure. +//! +//! Output is a CSV; `scripts/analyse.py` turns it into the report's table and +//! plot. +//! +//! ```text +//! cargo run --release --bin reconstruction -- --machine mac +//! ``` + +use std::{ + fmt::Write as _, + time::{Duration, Instant}, +}; + +use reference_prefix_bench::{ + MAX_BLOCK_TRANSACTIONS, Reconstruction, SLOT_DURATION, candidate_sets, + honest_block_and_proposal, sample_op_proof, sample_tx_encoded_len, search_reconstruction, +}; + +struct Args { + machine: String, + block_sizes: Vec, + k_max: usize, + repeats: usize, + budget: Duration, + out: Option, +} + +impl Args { + fn parse() -> Self { + let mut args = Self { + machine: "unknown".to_owned(), + block_sizes: vec![MAX_BLOCK_TRANSACTIONS], + k_max: 16, + repeats: 5, + budget: Duration::from_secs(60), + out: None, + }; + + let mut argv = std::env::args().skip(1); + while let Some(flag) = argv.next() { + let mut value = || { + argv.next() + .unwrap_or_else(|| panic!("{flag} needs a value")) + }; + match flag.as_str() { + "--machine" => args.machine = value(), + "--block-sizes" => { + args.block_sizes = value() + .split(',') + .map(|s| s.trim().parse().expect("block size must be a number")) + .collect(); + } + "--k-max" => args.k_max = value().parse().expect("k-max must be a number"), + "--repeats" => args.repeats = value().parse().expect("repeats must be a number"), + "--budget-secs" => { + args.budget = + Duration::from_secs(value().parse().expect("budget must be a number")); + } + "--out" => args.out = Some(value()), + "--help" | "-h" => { + eprintln!( + "reconstruction --machine [--block-sizes 1024,128] \ + [--k-max 16] [--repeats 5] [--budget-secs 60] [--out FILE]" + ); + std::process::exit(0); + } + other => panic!("unknown flag {other}"), + } + } + + for &n in &args.block_sizes { + assert!( + n <= MAX_BLOCK_TRANSACTIONS, + "block size {n} exceeds MAX_BLOCK_TRANSACTIONS ({MAX_BLOCK_TRANSACTIONS})" + ); + } + args + } +} + +fn median(mut samples: Vec) -> Duration { + samples.sort_unstable(); + samples[samples.len() / 2] +} + +fn main() { + let args = Args::parse(); + + println!( + "reconstruction latency vs. mempool collision multiplicity\n\ + machine : {}\n\ + slot (the deadline): {:?}\n\ + sample tx : {} bytes encoded\n", + args.machine, + SLOT_DURATION, + sample_tx_encoded_len() + ); + + let mut csv = String::from( + "machine,block_txs,k,policy,combinations,attempts,outcome,\ + median_s,min_s,max_s,repeats,over_slot\n", + ); + + for &n in &args.block_sizes { + let (transactions, proposal, header) = honest_block_and_proposal(n); + let signature = *proposal.signature(); + let proof = sample_op_proof(); + + println!("--- block of {n} transactions ---"); + println!( + "{:>3} {:>12} {:>10} {:>12} {}", + "k", "combinations", "attempts", "median", "verdict" + ); + + let mut first_over_slot: Option = None; + + for k in 0..=args.k_max.min(n) { + // The uncapped policy: what #389 v3 specifies. + let mut samples = Vec::new(); + let mut outcome = Reconstruction::Failed { attempts: 0 }; + + for _ in 0..args.repeats { + // Candidate sets are rebuilt outside the timed region: a real + // validator receives them from the mempool, it does not clone + // them into existence. + let candidates = candidate_sets(&transactions, k, &proof); + + let start = Instant::now(); + outcome = search_reconstruction(&header, &signature, candidates, false); + samples.push(start.elapsed()); + + // Stop repeating once a single run is already expensive; the + // spread at that scale is far smaller than the effect measured. + if samples[0] > Duration::from_secs(2) { + break; + } + } + + let repeats = samples.len(); + let med = median(samples.clone()); + let min = *samples.iter().min().expect("at least one sample"); + let max = *samples.iter().max().expect("at least one sample"); + let combinations = 1u64 << k; + let attempts = match outcome { + Reconstruction::Rebuilt { attempts } | Reconstruction::Failed { attempts } => { + attempts + } + _ => 0, + }; + let over_slot = med > SLOT_DURATION; + if over_slot && first_over_slot.is_none() { + first_over_slot = Some(k); + } + + println!( + "{k:>3} {combinations:>12} {attempts:>10} {:>12} {}", + format!("{:.3?}", med), + if over_slot { + "OVER SLOT — block production stalls" + } else { + "within slot" + } + ); + + writeln!( + csv, + "{},{n},{k},uncapped,{combinations},{attempts},{},{:.9},{:.9},{:.9},{repeats},{}", + args.machine, + match outcome { + Reconstruction::Rebuilt { .. } => "rebuilt", + Reconstruction::Failed { .. } => "failed", + Reconstruction::RefusedTooManyCombinations { .. } => "refused_combinations", + Reconstruction::RefusedAmbiguousReference => "refused_ambiguous", + }, + med.as_secs_f64(), + min.as_secs_f64(), + max.as_secs_f64(), + over_slot, + ) + .expect("writing to a String cannot fail"); + + // The capped policy, for the same k. + let (capped_outcome, capped_time) = + capped_sample(&header, &signature, &transactions, k, &proof); + writeln!( + csv, + "{},{n},{k},capped,{combinations},0,{},{:.9},{:.9},{:.9},1,false", + args.machine, + match capped_outcome { + Reconstruction::Rebuilt { .. } => "rebuilt", + Reconstruction::Failed { .. } => "failed", + Reconstruction::RefusedTooManyCombinations { .. } => "refused_combinations", + Reconstruction::RefusedAmbiguousReference => "refused_ambiguous", + }, + capped_time.as_secs_f64(), + capped_time.as_secs_f64(), + capped_time.as_secs_f64(), + ) + .expect("writing to a String cannot fail"); + + if med > args.budget { + println!( + " stopping at k={k}: a single run exceeded the {} s measurement budget", + args.budget.as_secs() + ); + break; + } + } + + match first_over_slot { + Some(k) => println!( + "\n uncapped reconstruction first exceeds the {:?} slot at k = {k} \ + ({} combinations)\n", + SLOT_DURATION, + 1u64 << k + ), + None => println!( + "\n uncapped reconstruction stayed within the {:?} slot for every k measured\n", + SLOT_DURATION + ), + } + } + + let path = args + .out + .unwrap_or_else(|| format!("results/{}/reconstruction.csv", args.machine)); + if let Some(parent) = std::path::Path::new(&path).parent() { + std::fs::create_dir_all(parent).expect("results directory must be creatable"); + } + std::fs::write(&path, csv).expect("results file must be writable"); + println!("wrote {path}"); +} + +/// Time the capped policy — the merged node's behaviour — at the same `k`. +/// +/// Below the cap this is the same search the uncapped policy runs. At and above +/// it, the node refuses before searching, which is fast but drops an honest +/// proposal: the liveness failure the report prices. +fn capped_sample( + header: &lb_core::header::Header, + signature: &lb_key_management_system_keys::keys::Ed25519Signature, + transactions: &[reference_prefix_bench::Tx], + k: usize, + proof: &lb_core::mantle::OpProof, +) -> (Reconstruction, Duration) { + let candidates = candidate_sets(transactions, k, proof); + let start = Instant::now(); + let outcome = search_reconstruction(header, signature, candidates, true); + (outcome, start.elapsed()) +} diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/throughput.rs b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/throughput.rs new file mode 100644 index 0000000..5964dd4 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/src/bin/throughput.rs @@ -0,0 +1,182 @@ +//! Aggregate candidate-generation throughput as a function of thread count. +//! +//! The security margin is a function of the adversary's *aggregate* rate, not +//! their single-core rate, and the two are not related by a clean multiple. +//! On a heterogeneous CPU — Apple Silicon's performance and efficiency cores, +//! or the RPi5's shared memory bandwidth — the last few threads are worth less +//! than the first. So this measures the whole-machine rate directly rather +//! than multiplying a one-core number by the core count. +//! +//! Parallel collision search (van Oorschot–Wiener) is memoryless and +//! embarrassingly parallel: `m` machines find a collision about `m` times +//! sooner, with no communication and no shared table. Aggregate throughput is +//! therefore the honest cost basis, which is why it is measured here. +//! +//! ```text +//! cargo run --release --bin throughput -- --machine mac +//! ``` + +use std::{ + fmt::Write as _, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, + thread, + time::{Duration, Instant}, +}; + +use reference_prefix_bench::{AttackerHasher, sample_preimage_len, sample_tx_encoded_len}; + +struct Args { + machine: String, + threads: Vec, + seconds: u64, + out: Option, +} + +impl Args { + fn parse() -> Self { + let available = thread::available_parallelism().map_or(1, std::num::NonZero::get); + + let mut args = Self { + machine: "unknown".to_owned(), + threads: (1..=available).collect(), + seconds: 3, + out: None, + }; + + let mut argv = std::env::args().skip(1); + while let Some(flag) = argv.next() { + let mut value = || { + argv.next() + .unwrap_or_else(|| panic!("{flag} needs a value")) + }; + match flag.as_str() { + "--machine" => args.machine = value(), + "--threads" => { + args.threads = value() + .split(',') + .map(|s| s.trim().parse().expect("thread count must be a number")) + .collect(); + } + "--seconds" => args.seconds = value().parse().expect("seconds must be a number"), + "--out" => args.out = Some(value()), + "--help" | "-h" => { + eprintln!( + "throughput --machine [--threads 1,2,4,8] [--seconds 3] [--out FILE]" + ); + std::process::exit(0); + } + other => panic!("unknown flag {other}"), + } + } + args + } +} + +/// Grind for `duration` on `threads` threads, returning total candidates hashed. +fn measure(threads: usize, duration: Duration) -> (u64, Duration) { + let stop = Arc::new(AtomicBool::new(false)); + let began = Instant::now(); + + let handles: Vec<_> = (0..threads) + .map(|t| { + let stop = Arc::clone(&stop); + thread::spawn(move || { + let mut hasher = AttackerHasher::new(); + let max = hasher.max_nonce(); + let mut nonce = (t as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) % max; + let mut count = 0u64; + // Check the flag every 4096 hashes: often enough to stop + // promptly, rarely enough that the atomic load is not itself + // part of what is being measured. + while !stop.load(Ordering::Relaxed) { + for _ in 0..4096 { + std::hint::black_box(hasher.hash_candidate(nonce)); + nonce = if nonce >= max { 0 } else { nonce + 1 }; + } + count += 4096; + } + count + }) + }) + .collect(); + + thread::sleep(duration); + stop.store(true, Ordering::Relaxed); + + let total: u64 = handles + .into_iter() + .map(|h| h.join().expect("grinding thread must not panic")) + .sum(); + + (total, began.elapsed()) +} + +fn main() { + let args = Args::parse(); + + // Validate the shortcut before trusting any rate it produces. + AttackerHasher::new().verify_against_real_path(); + + println!( + "aggregate candidate-generation throughput\n\ + machine : {}\n\ + cores : {} reported available\n\ + sample tx : {} bytes encoded, {} bytes hashed\n", + args.machine, + thread::available_parallelism().map_or(1, std::num::NonZero::get), + sample_tx_encoded_len(), + sample_preimage_len() + ); + + println!( + "{:>8} {:>16} {:>14} {:>10}", + "threads", "candidates/s", "per thread", "scaling" + ); + + let mut csv = String::from( + "machine,threads,candidates_per_second,per_thread_candidates_per_second,\ + scaling_vs_one_thread,seconds\n", + ); + + let mut one_thread_rate = 0.0f64; + + for &threads in &args.threads { + let (total, elapsed) = measure(threads, Duration::from_secs(args.seconds)); + let rate = total as f64 / elapsed.as_secs_f64(); + if threads == 1 { + one_thread_rate = rate; + } + let per_thread = rate / threads as f64; + let scaling = if one_thread_rate > 0.0 { + rate / one_thread_rate + } else { + f64::NAN + }; + + println!("{threads:>8} {rate:>16.4e} {per_thread:>14.4e} {scaling:>9.2}x"); + + writeln!( + csv, + "{},{threads},{rate:.1},{per_thread:.1},{scaling:.3},{}", + args.machine, args.seconds + ) + .expect("writing to a String cannot fail"); + } + + println!( + "\n'scaling' is measured, not assumed. Use the highest-thread row as \ + the per-machine\naggregate rate when pricing an adversary." + ); + + let path = args + .out + .unwrap_or_else(|| format!("results/{}/throughput.csv", args.machine)); + if let Some(parent) = std::path::Path::new(&path).parent() { + std::fs::create_dir_all(parent).expect("results directory must be creatable"); + } + std::fs::write(&path, csv).expect("results file must be writable"); + println!("wrote {path}"); +} diff --git a/tools/benchmarks/block-proposal/reference-prefix-length/src/lib.rs b/tools/benchmarks/block-proposal/reference-prefix-length/src/lib.rs new file mode 100644 index 0000000..99cb8b6 --- /dev/null +++ b/tools/benchmarks/block-proposal/reference-prefix-length/src/lib.rs @@ -0,0 +1,617 @@ +//! Fixtures and harness code for the `REFERENCE_PREFIX_LENGTH` security-margin +//! study. +//! +//! Everything measured here runs the real `logos-blockchain` code, pinned by +//! commit in `Cargo.toml`. The transaction encoding, the transaction hash, the +//! Merkle block root and the block-reconstruction check are all called, never +//! reimplemented. What this crate supplies is the scaffolding: how to build a +//! smallest valid transaction, how to arrange a mempool that contains prefix +//! collisions, and how to drive the reconstruction search. +//! +//! Two things *are* reproduced rather than called, and both are called out +//! where they appear: +//! +//! 1. [`search_reconstruction`] reproduces the candidate-combination loop of +//! `reconstruct_block_from_proposal` +//! (`services/chain/chain-network/src/lib.rs`), because that function is a +//! private `async fn` reachable only through a running service. The +//! per-combination work it performs — `Block::reconstruct` — is the real +//! function, and that is where essentially all of the time goes. +//! 2. [`AttackerHasher`] is a deliberately *faster-than-the-node* grinding +//! loop, used to price the attacker generously. See its documentation. + +use std::{sync::LazyLock, time::Duration}; + +use lb_codec::{BinaryDecode as _, BinaryEncode as _}; +use lb_core::{ + block::{Block, BlockTransactions, Proposal}, + codec::{DeserializeOp as _, SerializeOp as _}, + crypto::{Digest as _, Hasher}, + header::Header, + mantle::{ + Note, NoteId, Op, OpProof, SignedMantleTx, TxHash, Utxo, + ledger::{Inputs, Outputs}, + ops::transfer::TransferOp, + traits::Hashable as _, + transactions::{mantle_tx::RawMantleTx, states::Preverified}, + }, + proofs::leader_proof::Groth16LeaderProof, +}; +use lb_cryptarchia_engine::Slot; +use lb_key_management_system_keys::keys::{ + Ed25519Key, Ed25519Signature, ZkKey, ZkPublicKey, ZkSignature, +}; + +/// `COMPRESSED_PROOF_SIZE` — the compressed Groth16 proof inside a +/// `Groth16LeaderProof`. +const POL_PROOF_SIZE: usize = 128; + +/// The concrete mempool transaction type used throughout the harness. +pub type Tx = SignedMantleTx; + +/// Cryptarchia's default slot time — `DEFAULT_SLOT_TIME_IN_SECS` in +/// `tools/config/src/time.rs`, matching the `slot_duration` in +/// `nodes/node/standalone-deployment-config.yaml`. +/// +/// This is the deadline reconstruction has to fit inside: a validator that +/// takes longer than one slot to turn a proposal back into a block has already +/// lost the slot. +pub const SLOT_DURATION: Duration = Duration::from_secs(1); + +/// `MAX_BLOCK_TRANSACTIONS` in `core/src/block/mod.rs`, and therefore the +/// largest number of references a proposal can carry. +pub const MAX_BLOCK_TRANSACTIONS: usize = 1024; + +/// `MAX_CANDIDATES_PER_REFERENCE` in `core/src/block/mod.rs`. +pub const MAX_CANDIDATES_PER_REFERENCE: usize = 8; + +/// `MAX_RECONSTRUCTION_COMBINATIONS` in `core/src/block/mod.rs`. +pub const MAX_RECONSTRUCTION_COMBINATIONS: usize = 32; + +/// The prefix lengths, in bytes, that the study prices. +pub const PREFIX_LENGTHS: [usize; 5] = [8, 10, 12, 14, 16]; + +// --------------------------------------------------------------------------- +// The candidate transaction +// --------------------------------------------------------------------------- + +/// The secret key whose note the sample transaction spends. Fixed so that every +/// run on every machine grinds byte-identical transactions. +fn input_key() -> ZkKey { + ZkKey::from(num_bigint::BigUint::from(1u8)) +} + +/// The UTXO the sample transaction consumes. +/// +/// A `Transfer` must consume at least one note — `logos-blockchain` rejects +/// empty inputs in every note-consuming operation — so this is what makes the +/// transaction below the *smallest valid* one rather than merely the smallest. +/// +/// The note id is cached because deriving it is a Poseidon2 hash, and it is the +/// *same* input note for every candidate. An adversary grinding candidates +/// derives it once; charging it per candidate would overstate their cost by +/// more than two orders of magnitude, which is the wrong direction for a +/// security margin. +static INPUT_NOTE_ID: LazyLock = + LazyLock::new(|| Utxo::new([1u8; 32], 0, Note::new(1_000_000, input_key().to_public_key())).id()); + +/// The smallest valid `MantleTx`: a single `Transfer` operation, one input and +/// one output. +/// +/// `nonce` varies the output note's value. That is the cheapest edit an +/// attacker can make that still leaves a valid, differently-hashing +/// transaction, so grinding over it is the attacker-favourable choice. +#[must_use] +pub fn minimal_transfer_tx(nonce: u64) -> RawMantleTx { + RawMantleTx( + [Op::Transfer(TransferOp::new( + Inputs::new([*INPUT_NOTE_ID]), + Outputs::new([Note::new( + // Never zero: zero-valued outputs are rejected. + nonce.wrapping_add(1), + ZkPublicKey::zero(), + )]), + ))] + .into(), + ) +} + +/// `mantle_txhash(tx)` — the real hash, via `Hashable` on `RawMantleTx`. +/// +/// This is `blake2b-256(b"MANTLE_TXHASH_V1" || encode(tx))`, and it covers the +/// `MantleTx` only, never the `op_proofs`. That is precisely why an attacker can +/// grind candidates offline without ever producing a signature or a proof. +#[must_use] +pub fn mantle_txhash(tx: &RawMantleTx) -> TxHash { + tx.hash() +} + +/// `prefix(mantle_txhash(tx), length)`, as the proposal would carry it. +#[must_use] +pub fn reference_prefix(tx: &RawMantleTx, length: usize) -> Vec { + mantle_txhash(tx).0[..length].to_vec() +} + +/// The encoded size, in bytes, of the sample transaction — reported alongside +/// every generation rate so a reviewer can scale the numbers to other sizes. +#[must_use] +pub fn sample_tx_encoded_len() -> usize { + minimal_transfer_tx(0).encode().len() +} + +/// The size of the full hash preimage, `b"MANTLE_TXHASH_V1" || encode(tx)`. +#[must_use] +pub fn sample_preimage_len() -> usize { + minimal_transfer_tx(0).as_signing().len() +} + +// --------------------------------------------------------------------------- +// Pricing the attacker generously +// --------------------------------------------------------------------------- + +/// A grinding loop that is strictly faster than anything the node does, used so +/// that the attack is priced in the attacker's favour. +/// +/// The node's own path — [`minimal_transfer_tx`] followed by [`mantle_txhash`] — +/// rebuilds the operation structure and reallocates the encoding buffer for +/// every candidate. An attacker has no reason to do either. Since the nonce +/// only varies a `u64` inside an otherwise fixed encoding, they can encode +/// once, then per candidate overwrite those bytes and hash the buffer. +/// +/// The byte range to patch is *discovered* rather than hardcoded, by encoding +/// two transactions that differ only in the nonce and diffing them. If the +/// Mantle encoding ever changes shape this keeps working, or panics loudly; it +/// cannot silently grind the wrong bytes. +pub struct AttackerHasher { + preimage: Vec, + patch_offset: usize, + patch_len: usize, +} + +impl Default for AttackerHasher { + fn default() -> Self { + Self::new() + } +} + +impl AttackerHasher { + /// # Panics + /// + /// If the nonce does not map to a single contiguous byte range in the + /// encoding, which would mean the transaction layout has changed in a way + /// this shortcut no longer models. + #[must_use] + pub fn new() -> Self { + // The probes are chosen so that every byte of the varying field + // differs: value 1 (0x01,0,0,0,0,0,0,0) against value u64::MAX + // (0xFF x8). Probing with a small value instead would leave the high + // bytes equal in both encodings and silently under-detect the field. + let a = minimal_transfer_tx(0).as_signing(); + let b = minimal_transfer_tx(u64::MAX - 1).as_signing(); + assert_eq!( + a.len(), + b.len(), + "nonce must not change the encoded length, or the shortcut is invalid" + ); + + let first = (0..a.len()) + .find(|&i| a[i] != b[i]) + .expect("the nonce must change the encoding"); + let last = (0..a.len()) + .rfind(|&i| a[i] != b[i]) + .expect("the nonce must change the encoding"); + let patch_len = last - first + 1; + assert!( + patch_len <= 8, + "the nonce must occupy one contiguous u64 field; it spans {patch_len} bytes, \ + so the transaction layout has changed and this shortcut no longer models it" + ); + + Self { + preimage: a, + patch_offset: first, + patch_len, + } + } + + /// Hash one candidate, reusing the buffer. Returns the full 32-byte hash; + /// truncating it to `L` bytes is free and so is not modelled separately. + #[must_use] + pub fn hash_candidate(&mut self, nonce: u64) -> [u8; 32] { + let value = nonce.wrapping_add(1).to_le_bytes(); + self.preimage[self.patch_offset..self.patch_offset + self.patch_len] + .copy_from_slice(&value[..self.patch_len]); + Hasher::digest(&self.preimage).into() + } + + /// The preimage this hashes, for reporting. + #[must_use] + pub fn preimage_len(&self) -> usize { + self.preimage.len() + } + + /// Cross-check that the shortcut agrees with the real code path over a + /// spread of nonces. Any divergence would invalidate the generation rate, + /// so this runs as a test *and* at the start of the grinding harness. + /// + /// # Panics + /// + /// If the shortcut and the real path disagree on any nonce. + pub fn verify_against_real_path(&mut self) { + let top = self.max_nonce(); + for nonce in [ + 0u64, + 1, + 2, + 255, + 4096, + u64::from(u32::MAX) - 1, + top / 2, + top - 1, + top, + ] { + let shortcut = self.hash_candidate(nonce); + let real = mantle_txhash(&minimal_transfer_tx(nonce)).0; + assert_eq!( + shortcut, real, + "attacker shortcut diverged from the real hash path at nonce {nonce}" + ); + } + } + + /// The largest nonce the shortcut can represent without overflowing the + /// patched field. + #[must_use] + pub fn max_nonce(&self) -> u64 { + if self.patch_len >= 8 { + u64::MAX + } else { + (1u64 << (self.patch_len * 8)) - 2 + } + } +} + +// --------------------------------------------------------------------------- +// Blocks, proposals and the mempool +// --------------------------------------------------------------------------- + +/// The block producer's signing key, which must match the leader key inside the +/// proof of leadership. +fn leader_key() -> Ed25519Key { + Ed25519Key::from_bytes(&[7u8; 32]) +} + +/// A structurally valid proof of leadership carrying our leader key. +/// +/// Decoded from its wire form rather than proved. `Block::reconstruct` never +/// verifies the Groth16 proof — it checks the slot, the total transaction size, +/// the block root and the header signature — so proving here would add minutes +/// of setup to every run without changing a single measured nanosecond. The +/// proof is still the real 224-byte structure produced by the real decoder, so +/// header encoding and signing cost exactly what they cost in production. +/// +/// The wire layout is the one `BinaryEncode for Groth16LeaderProof` writes +/// (`core/src/proofs/leader_proof.rs`): `proof (128) || entropy_contribution +/// (32) || leader_key (32) || voucher_cm (32)`. +/// +/// # Panics +/// +/// If the encoding no longer decodes, i.e. the layout above has changed. +fn proof_of_leadership() -> Groth16LeaderProof { + let mut bytes = Vec::with_capacity(POL_PROOF_SIZE + 32 * 3); + bytes.extend_from_slice(&[0u8; POL_PROOF_SIZE]); // proof + bytes.extend_from_slice(&[0u8; 32]); // entropy_contribution = 0 + bytes.extend_from_slice(leader_key().public_key().as_bytes()); // leader_key + bytes.extend_from_slice(&[0u8; 32]); // voucher_cm = 0 + + let (rest, proof) = Groth16LeaderProof::decode(&bytes, &()) + .expect("the proof-of-leadership wire layout must still decode"); + assert!(rest.is_empty(), "proof of leadership must consume exactly"); + proof +} + +/// One genuine `ZkSig` over a sample transaction, to be cloned into every +/// mempool entry. +/// +/// # Panics +/// +/// If signing fails. +#[must_use] +pub fn sample_op_proof() -> OpProof { + let tx = minimal_transfer_tx(0); + let signature = ZkKey::multi_sign(&[input_key()], &tx.hash().to_fr()) + .expect("signing the sample transaction must succeed"); + OpProof::ZkSig(signature) +} + +/// A mempool transaction: the sample `MantleTx` plus a proof, as a validator +/// holds it. +/// +/// The proof is real in shape and size — produced once by [`sample_op_proof`] +/// and cloned — but is not re-verified per transaction. Reconstruction never +/// inspects `op_proofs`, so this affects no measurement. It does affect +/// `storage_size`, which the block's size check reads, and that is identical +/// either way because the proof is the genuine encoded structure. +#[must_use] +pub fn signed_tx(nonce: u64, proof: &OpProof) -> Tx { + SignedMantleTx::new_trusted(minimal_transfer_tx(nonce), [proof.clone()].into()) +} + +/// An honest block over `n` distinct transactions, and the proposal a producer +/// would broadcast for it. +/// +/// # Panics +/// +/// If `n` exceeds `MAX_BLOCK_TRANSACTIONS`, or block construction fails. +#[must_use] +pub fn honest_block_and_proposal(n: usize) -> (Vec, Proposal, Header) { + let proof = sample_op_proof(); + let transactions: Vec<_> = (0..n as u64).map(|nonce| signed_tx(nonce, &proof)).collect(); + + let block = Block::create( + [0u8; 32].into(), + Slot::from(42u64), + proof_of_leadership(), + BlockTransactions::::try_from(transactions.clone()).expect("n within MAX_BLOCK_TRANSACTIONS"), + &leader_key(), + ) + .expect("valid block"); + + let header = block.header().clone(); + let proposal = block.to_proposal(); + + (transactions, proposal, header) +} + +// --------------------------------------------------------------------------- +// Mempool admission — what a node pays per incoming transaction +// --------------------------------------------------------------------------- + +/// One sample transaction serialized exactly as it crosses the wire. +/// +/// # Panics +/// +/// If serialization fails. +#[must_use] +pub fn wire_encoded_tx() -> Vec { + let tx = signed_tx(0, &sample_op_proof()); + tx.to_bytes() + .expect("a sample transaction must serialize") + .to_vec() +} + +/// The pieces needed to verify the sample transaction's ZK multi-signature. +/// +/// # Panics +/// +/// If signing fails. +#[must_use] +pub fn signature_verification_inputs() -> (Vec, lb_groth16::Fr, ZkSignature) { + let tx = minimal_transfer_tx(0); + let signature = ZkKey::multi_sign(&[input_key()], &tx.hash().to_fr()) + .expect("signing the sample transaction must succeed"); + (vec![input_key().to_public_key()], tx.hash().to_fr(), signature) +} + +/// Verify the sample transaction's ZK multi-signature — the most expensive +/// per-transaction cryptography in the pipeline. +/// +/// This is what `TransferOp::verify` calls +/// (`ZkPublicKey::verify_multi`). It runs at block application rather than at +/// mempool admission, so it is not part of [`admit_tx`]; it is measured +/// separately so that the *upper* bound on per-transaction protocol cost is +/// known, not just the admission cost. +#[must_use] +pub fn verify_signature(pks: &[ZkPublicKey], hash: &lb_groth16::Fr, sig: &ZkSignature) -> bool { + ZkPublicKey::verify_multi(pks, hash, sig) +} + +/// Admit one transaction to the mempool, from wire bytes. +/// +/// This is the real ingest path: the mempool's item type is +/// `SignedMantleTx`, and that type's `Deserialize` impl decodes the +/// transaction and then runs `preverify()` — which checks proof/op arity, +/// computes `mantle_txhash`, and runs each operation's stateless checks. +/// +/// Worth being precise about what this does *not* include, because it bounds +/// what the number can be used to argue. Signature verification is **not** on +/// this path: for a `Transfer`, `preverify` only validates structure, and the +/// ZK multi-signature is checked later by the stateful `verify` +/// (`ZkPublicKey::verify_multi`), which needs the UTXO set and runs at block +/// application. So this measures admission cost, not total validation cost. +/// +/// # Panics +/// +/// If the bytes do not deserialize, which would mean the wire format changed. +pub fn admit_tx(bytes: &[u8]) -> Tx { + Tx::from_bytes(bytes).expect("a sample transaction must deserialize and preverify") +} + +// --------------------------------------------------------------------------- +// Reconstruction +// --------------------------------------------------------------------------- + +/// The candidate sets a validator's mempool would hand back for a proposal of +/// `n` references, of which the first `k` are ambiguous. +/// +/// Each ambiguous reference resolves to two transactions: a decoy, and the one +/// the proposer actually meant. The decoy is placed **first**, so the search +/// has to exhaust every wrong combination before reaching the right one. That +/// is the worst case, and it is the case an attacker gets to choose, since the +/// mempool stream is explicitly documented as unordered. +/// +/// How the collisions came to exist does not affect what reconstruction costs — +/// only how many candidates each reference has does. So the sets are built +/// directly, which lets the latency curve be measured at any `k` without first +/// spending 2^(b/2) work to manufacture real collisions at the deployed prefix +/// length. The `birthday` harness measures that manufacturing cost separately. +#[must_use] +pub fn candidate_sets( + transactions: &[Tx], + k: usize, + proof: &OpProof, +) -> Vec> { + transactions + .iter() + .enumerate() + .map(|(index, tx)| { + if index < k { + // A decoy that is not in the block, tried before the real one. + let decoy = signed_tx(u64::MAX - index as u64, proof); + vec![decoy, tx.clone()] + } else { + vec![tx.clone()] + } + }) + .collect() +} + +/// The outcome of a reconstruction attempt. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Reconstruction { + /// The block was rebuilt, after trying `attempts` combinations. + Rebuilt { attempts: u64 }, + /// No combination reproduced `block_root`. + Failed { attempts: u64 }, + /// The combination product exceeded `MAX_RECONSTRUCTION_COMBINATIONS`, so + /// the node refused to search at all and the proposal is dropped. + RefusedTooManyCombinations { combinations: u64 }, + /// Some reference had more than `MAX_CANDIDATES_PER_REFERENCE` candidates. + RefusedAmbiguousReference, +} + +/// Whether the merged node would even begin the search, applying the two caps +/// from `candidates_for_proposal` in +/// `services/chain/chain-network/src/lib.rs`. +#[must_use] +pub fn caps_verdict(candidates: &[Vec]) -> Option { + let mut combinations: u64 = 1; + for set in candidates { + if set.len() > MAX_CANDIDATES_PER_REFERENCE { + return Some(Reconstruction::RefusedAmbiguousReference); + } + combinations = combinations.saturating_mul(set.len() as u64); + if combinations > MAX_RECONSTRUCTION_COMBINATIONS as u64 { + return Some(Reconstruction::RefusedTooManyCombinations { combinations }); + } + } + None +} + +/// Run the reconstruction search over `candidates`. +/// +/// This reproduces the loop in `reconstruct_block_from_proposal` +/// (`services/chain/chain-network/src/lib.rs`): take the cartesian product of +/// the per-reference candidate sets and return the first combination that +/// `Block::reconstruct` accepts. `Block::reconstruct` is the real function, and +/// it is where the cost is — per combination it re-encodes every transaction +/// for the size check and re-hashes every transaction for the Merkle root. +/// +/// `enforce_caps` selects which policy is measured: +/// +/// * `true` — the caps as merged in `logos-blockchain` today. +/// * `false` — the uncapped deterministic lookup that logos-lips#389 v3 +/// specifies, on the argument that at a long enough prefix collisions cannot +/// be manufactured and so the caps are unnecessary. +pub fn search_reconstruction( + header: &Header, + signature: &Ed25519Signature, + candidates: Vec>, + enforce_caps: bool, +) -> Reconstruction { + use itertools::Itertools as _; + + if enforce_caps && let Some(verdict) = caps_verdict(&candidates) { + return verdict; + } + + let mut attempts: u64 = 0; + for combination in candidates.into_iter().multi_cartesian_product() { + attempts += 1; + let Ok(transactions) = BlockTransactions::::try_from(combination) else { + continue; + }; + if Block::reconstruct(header.clone(), transactions, *signature).is_ok() { + return Reconstruction::Rebuilt { attempts }; + } + } + + Reconstruction::Failed { attempts } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_transaction_is_the_smallest_valid_transfer() { + let tx = minimal_transfer_tx(0); + assert_eq!(tx.0.len(), 1, "one operation"); + match &tx.0[0] { + Op::Transfer(transfer) => { + assert_eq!(transfer.inputs.len(), 1); + assert_eq!(transfer.outputs.len(), 1); + } + other => panic!("expected a Transfer, got {other:?}"), + } + } + + #[test] + fn distinct_nonces_give_distinct_hashes() { + assert_ne!( + mantle_txhash(&minimal_transfer_tx(0)), + mantle_txhash(&minimal_transfer_tx(1)) + ); + } + + #[test] + fn attacker_shortcut_agrees_with_the_real_hash_path() { + AttackerHasher::new().verify_against_real_path(); + } + + #[test] + fn honest_proposal_reconstructs_when_unambiguous() { + let (transactions, proposal, header) = honest_block_and_proposal(8); + let proof = sample_op_proof(); + let candidates = candidate_sets(&transactions, 0, &proof); + + assert_eq!( + search_reconstruction(&header, proposal.signature(), candidates, true), + Reconstruction::Rebuilt { attempts: 1 } + ); + } + + #[test] + fn ambiguity_forces_the_search_to_exhaust_wrong_combinations_first() { + let (transactions, proposal, header) = honest_block_and_proposal(8); + let proof = sample_op_proof(); + let k = 3; + let candidates = candidate_sets(&transactions, k, &proof); + + // Decoys are tried first at every ambiguous position, so the correct + // combination is the last of the 2^k. + assert_eq!( + search_reconstruction(&header, proposal.signature(), candidates, false), + Reconstruction::Rebuilt { attempts: 1 << k } + ); + } + + #[test] + fn the_merged_caps_refuse_the_proposal_past_five_collisions() { + let (transactions, proposal, header) = honest_block_and_proposal(8); + let proof = sample_op_proof(); + // 2^6 = 64 > MAX_RECONSTRUCTION_COMBINATIONS = 32. + let candidates = candidate_sets(&transactions, 6, &proof); + + assert!(matches!( + search_reconstruction(&header, proposal.signature(), candidates, true), + Reconstruction::RefusedTooManyCombinations { .. } + )); + } + + #[test] + fn proposal_carries_one_reference_per_transaction() { + let (transactions, proposal, _) = honest_block_and_proposal(16); + assert_eq!(proposal.mempool_transactions().len(), transactions.len()); + } +}