Skip to content

ergo-nipopow: add prove_with_reader for db-backed proof construction#851

Open
mwaddip wants to merge 1 commit into
ergoplatform:developfrom
mwaddip:feat/nipopow-prove-with-reader
Open

ergo-nipopow: add prove_with_reader for db-backed proof construction#851
mwaddip wants to merge 1 commit into
ergoplatform:developfrom
mwaddip:feat/nipopow-prove-with-reader

Conversation

@mwaddip
Copy link
Copy Markdown

@mwaddip mwaddip commented Apr 8, 2026

Adds prove_with_reader, a chain-reader-based port of JVM NipopowProverWithDbAlgs.prove. Walks interlinks via a PopowHeaderReader callback instead of materializing the whole chain — O(m + k + m·log N) fetches vs O(N). Without it, Rust nodes can't serve GetNipopowProof over P2P on long chains.

Additive; existing prove untouched.

Does NOT implement params.continuous (no field on NipopowProof yet) — follow-up.

Direct port of JVM
`org.ergoplatform.modifiers.history.popow.NipopowProverWithDbAlgs.prove`,
the production-side prover used by Ergo nodes to serve NiPoPoW proofs
to peers. Unlike the existing `NipopowAlgos::prove(chain: &[PoPowHeader])`
(which is paper-style and requires the caller to materialize every
block as a `PoPowHeader` up front), `prove_with_reader` walks the
interlink hierarchy via a new `PopowHeaderReader` callback and only
requests `PoPowHeader`s for blocks the walk actually visits.

Asymptotic win: ~`m + k + m * log2(N)` `popow_header_by_id` calls per
proof, vs `O(N)` for the in-memory variant. At the JVM P2P defaults
`m = 6, k = 10` on a `N ~= 270k` testnet chain that's roughly 120
fetches versus 270k — three orders of magnitude fewer. The in-memory
`prove` makes serving NiPoPoW proofs over P2P unviable on long chains
(it stalls sync, mining, and mempool for the duration of a single
peer's `GetNipopowProof`); `prove_with_reader` is the foundation for
making the Rust ergo node a viable NiPoPoW serving peer.

Changes (all in `ergo-nipopow`, plus a test in `ergo-chain-generation`):

* New `PopowHeaderReader` trait (`ergo-nipopow/src/popow_header_reader.rs`)
  with five methods: `headers_height`, `popow_header_by_id`,
  `popow_header_at_height`, `last_headers`, `best_headers_after`.
  Re-exported from `lib.rs`. The trait is the minimal Rust analogue of
  the subset of `ErgoHistoryReader` that the JVM `prove` requires.

* New `NipopowAlgos::prove_with_reader<R: PopowHeaderReader + ?Sized>`,
  with private helpers `links_with_indexes`,
  `previous_header_id_at_level`, `collect_level`, and `prove_prefix`
  ported line-by-line from the JVM source. The Scala `@tailrec`
  recursion in `collectLevel` is translated to an explicit loop. The
  `mutable.TreeMap[ModifierId, PoPowHeader]` accumulator becomes a
  `BTreeMap<BlockId, PoPowHeader>`.

* New `NipopowProofError::MissingPopowHeader` variant. The JVM source
  uses `Try` and `.get` on every `popowHeader` lookup; the Rust port
  returns `Err(MissingPopowHeader)` instead, signalling a reader that
  is inconsistent with the chain it claims to expose.

The existing `NipopowAlgos::prove(&[PoPowHeader])` is left untouched —
it remains in use by `ergo-chain-generation`'s tests and is still the
canonical paper-style implementation.

Known divergence from the JVM source: the JVM `prove` accepts a
`params.continuous` flag that, when set, embeds extra epoch-boundary
popow headers into the prefix so peers can validate difficulty for
blocks past the suffix without further sync. This Rust port does NOT
implement that mode: sigma-rust's `NipopowProof` currently has no
`continuous` field, so adding it requires coordinated changes to the
struct, the serializer, and the on-wire format — out of scope for
this PR. `prove_with_reader` always produces non-continuous proofs.
JVM peers applying non-continuous proofs still succeed
(`applyPopowProof` does not strictly require the flag), they just
cannot self-validate difficulty for blocks beyond the suffix until
they sync more headers — fine for the P2P serve use case. Continuous
mode is tracked as a follow-up.

Testing:

* `test_nipopow_prove_with_reader_matches_in_memory`
  (`ergo-chain-generation/src/fake_pow_scheme.rs`) asserts byte-for-byte
  equivalence between `prove(&chain)` and
  `prove_with_reader(&reader, None, k, m)` on a 100-block synthetic
  chain at the JVM P2P defaults `m = 6, k = 10`. This is the Rust
  counterpart to the JVM
  `PoPowAlgosWithDBSpec`-"proof(chain) is equivalent to proof(histReader)"
  test. The test is in `fake_pow_scheme.rs` (rather than
  `chain_generation.rs`) because byte-for-byte equivalence only holds
  when every non-genesis block has `max_level_of >= 1`: the in-memory
  `prove` adds blocks at its level-0 iteration that the db-backed
  interlink walk never visits, so on a chain with level-0-only blocks
  (which the real-Autolykos `chain_generation.rs` generator
  produces) the prefixes legitimately diverge. The fake pow scheme
  forces `d = order / (height + 1)`, guaranteeing positive level for
  every block — exactly mirroring `DefaultFakePowScheme` in the JVM
  spec, which uses `d = q / (height + 10)` for the same reason.

* `test_nipopow_prove_with_reader_valid_on_real_autolykos_chain`
  (`ergo-chain-generation/src/chain_generation.rs`) is a sanity check
  on the real-Autolykos generator: the resulting proof has valid
  connections, the suffix has length `k`, and the db-backed prefix is
  a subset of the in-memory prefix (since the db-backed walk only
  visits level-1+ blocks via interlinks).

* `test_nipopow_prove_with_reader_explicit_header_id`
  (`ergo-chain-generation/src/chain_generation.rs`) covers the
  `header_id_opt = Some(..)` branch: the resulting proof's suffix
  head matches the requested header, the suffix tail contains the
  next `k - 1` headers in ascending-height order, and the proof has
  valid connections.

Verification:

  cargo test --workspace -p ergo-nipopow -p ergo-chain-generation
    883 passed; 0 failed
  cargo clippy --workspace -p ergo-nipopow --all-targets
    No issues
  cargo clippy --workspace -p ergo-chain-generation --all-targets
    No issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mwaddip added a commit to mwaddip/enr-chain that referenced this pull request Apr 8, 2026
Replace the O(N) full-chain materialization in build_nipopow_proof with
a thin adapter over ergo_nipopow::NipopowAlgos::prove_with_reader, the
direct port of JVM NipopowProverWithDbAlgs.prove. The reader walks the
interlink hierarchy on demand and only fetches the popow headers the
proof actually visits — O(m + k + m * log2 N) per call instead of O(N).

For the standard m=6, k=10 defaults at N=270k that's ~120 fetches vs
~270 000 with the old in-memory prove path: three orders of magnitude
fewer. Per-request cost on testnet drops from ~5 minutes (single-threaded
under the chain mutex) to subsecond, eliminating the GetNipopowProof DoS
window that stalled sync, mining, and mempool for the duration.

The genesis (h=1) special case moves out of build_nipopow_proof and into
the new private ChainPopowReader::popow_header_at_height path: real
testnet/mainnet genesis extensions are empty and would yield wrong
interlinks via the loader, so the reader synthesizes the canonical
[genesis_id] interlinks vector in-process. The loader is never called
for h=1, matching the contract invariant in facts/chain.md.

Bumps the four sigma-rust git deps (ergo-chain-types, ergo-lib,
ergo-nipopow, sigma-ser) to rev 79558180 (ergoplatform/sigma-rust#851)
for the new PopowHeaderReader trait, prove_with_reader entry point, and
NipopowProofError::MissingPopowHeader variant.

The public signature of build_nipopow_proof is unchanged. All 10
existing nipopow_proof.rs unit tests pass without modification,
including build_proof_skips_loader_for_genesis (the regression guard
that the genesis path never queries the loader).

Pulls facts submodule to 4feccee, which updates the Phase 6 contract:
prove_with_reader algorithm, O(m + k + m * log2 N) cost, the
non-byte-equivalent in-memory prove divergence note, the genesis-in-
reader requirement, and the m+k precondition fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant