Skip to content

Sequential-chain proof: make miner work unparallelisable and duration unforgeable - #16

Open
elasticLove1 wants to merge 1 commit into
mainfrom
feat/sequential-chain-proof
Open

Sequential-chain proof: make miner work unparallelisable and duration unforgeable#16
elasticLove1 wants to merge 1 commit into
mainfrom
feat/sequential-chain-proof

Conversation

@elasticLove1

@elasticLove1 elasticLove1 commented Aug 29, 2026

Copy link
Copy Markdown
Member

Part of the work in gosh-sh/acki-nacki#2591.
Pairs with the contract side in acki-nacki branch research/miner-pow-cost (Miner_V2).

Why

The v1 proof is a set of independent leaves. Three consequences, all measured
in the issue:

  • the work parallelises, so a farm amortises one CPU across every account it owns;
  • the claimed duration comes from timestamp fields the miner hashes itself and
    nothing checks against block time;
  • the reward cap is reached with a fraction of the leaves an honest client mines.

Together an attacker pays ~1/40 of what an honest client pays for the same
reward, and the ~3 cores that buys serve 98k accounts. Honest users complete
9.6% of their sessions.

Raising the difficulty does not help — it multiplies the honest client's cost 40×
harder than the farm's.

What changes

Step i consumes the state of step i-1:

h_0     = H(seed)
h_{i+1} = H(h_i ‖ x_i ‖ y_i ‖ nonce_i)

A chain cannot be parallelised, so producing it costs wall-clock time that no
amount of hardware removes — only clock speed, worth under 2×. Every account
needs its own seconds.

Keeping verification cheap

Holding every state would cost gigabytes, so the miner keeps one state per
stride steps — a checkpoint — and commits those as merkle leaves. The
spot-checked window is exactly one checkpoint interval: the verifier replays
stride steps from checkpoint k and must land on checkpoint k+1, with both
endpoints proven against the root.

So verification costs intervals × stride hashes plus two merkle paths —
independent of chain length. The miner's work can be raised arbitrarily
without making in-block verification more expensive.

Measured at stride = 256, 2 intervals: 40 µs on M3 and 93.7 µs on the
n14 box, against 42 µs / 62–103 µs for v1 on the same hardware. No regression.

Cheating

A miner that walked only a fraction f passes only if every checked
interval lands inside the part it did walk — odds f^intervals, so 1% at
f = 10% with two intervals.

Layout

bee_shared/src/miner/chain.rs shared types + the request format the contract builds
bee_miner/src/core/chain.rs building the chain with checkpoints, opening an interval
bee_verifier_v2/ the WASM component that runs in-block

bee_verifier (v1) is untouched: its binary is whitelisted on the nodes and
v1 miners keep calling it. v2 is a separate component with its own hash, so both
are live while miners migrate.

Artifact

sha256 62c3e9c1af291100969d84943d99ca3dea6899c1dd069f4ad68aa96989e27d59

Reproduced by a clean rebuild — the build is deterministic. This hash goes into
Miner_V2._wasm_hash and the node's wasm whitelist; the node has to know it
before the contract is deployed, or every proof fails to run.

Tests

26 unit tests across the three crates. The ones that matter:

  • replaying a tampered step does not reach the committed endpoint;
  • a self-consistent walk that was never committed fails the merkle check;
  • answering a different interval than the one challenged is refused;
  • an out-of-range interval index is rejected without panicking (this caught a
    real overflow on u64::MAX);
  • malformed input of every length returns a clean error rather than panicking.

Run with cargo test -p bee-shared -p bee-miner; bee-verifier-v2 needs
cdylib removed from its Cargo.toml first, same as v1.

Not in this PR — read before reviewing

The client does not use the chain yet. bee_miner/src/wasm/worker.rs still
drives MiningCore (v1 independent leaves); ChainCore is built and tested but
not wired into the mining session. Switching it over needs Miner_V2's ABI in
ackinacki-kitsetCommitData takes a checkpoint count instead of easy/hard
leaf counts, and GetInterval carries checkpoint indices — and the kit is a
tagged git dependency here (v5.1.1). So the full path is
ackinacki-kit → bee-engine → contract, and this PR is the middle piece plus the
verifier.

What this PR does give you: the proof format, the on-device builder, and the
in-block verifier, all tested, with a reproducible artifact hash the contract and
the node can already be pointed at.

  • Moving mining into a Web Worker. It currently runs as a spawn_local
    coroutine on the JS main thread, which is why worker.rs has a 10 ms timer and
    the device sits at ~2% of one core. The chain is single-core by nature, so the
    worker is what lets it use the budget without touching the UI thread.
  • Calibrating chain length N on real devices (start 15–30 M steps).

The v1 proof is a set of independent leaves, which means the work parallelises,
a farm amortises one CPU across every account it owns, and the claimed duration
is whatever the miner writes into a leaf. Measurements in acki-nacki#2591: an
attacker pays ~1/40 of what an honest client pays for the same reward, and the
~3 cores that buys serve 98k accounts.

Here step i consumes the state of step i-1, so the chain cannot be parallelised
and producing it costs wall-clock time no amount of hardware removes.

Keeping every state would cost gigabytes at the lengths that matter, so the
miner keeps one state per `stride` steps and commits those. Making the
spot-checked window exactly one checkpoint interval is what keeps verification
cheap: the verifier replays `stride` steps between two committed endpoints, so
its cost does not grow with the chain. The build cost can then be raised
without touching what runs inside a block.

A miner that computed only a fraction f of the chain passes only if every
checked interval falls inside the part it did compute, so the odds are
f^intervals -- 1% at f=10% with the two intervals the contract asks for.

bee_verifier (v1) is untouched: its binary is whitelisted on the nodes and v1
miners still call it. v2 is a separate component with its own hash, so both can
be live while miners migrate.

Artifact sha256 62c3e9c1af291100969d84943d99ca3dea6899c1dd069f4ad68aa96989e27d59,
reproduced by a clean rebuild. It goes into Miner_V2's _wasm_hash and the node's
wasm whitelist.
@elasticLove1

Copy link
Copy Markdown
Member Author

Research and measurements behind this change: gosh-sh/acki-nacki#2591. Contract side: gosh-sh/acki-nacki#2592 — the two have to ship together, the node must whitelist 62c3e9c1… before Miner_V2 is deployed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants