Skip to content

fix(ergo-nipopow): reject m == 0 instead of looping forever in best_arg - #951

Merged
kushti merged 1 commit into
ergoplatform:developfrom
Kshot3000:fix/nipopow-zero-m-hang
Oct 13, 2026
Merged

kushti merged 1 commit into
ergoplatform:developfrom
Kshot3000:fix/nipopow-zero-m-hang

Conversation

@Kshot3000

Copy link
Copy Markdown
Contributor

Summary

Fixes #950. a NipopowProof with m == 0 hangs proof comparison forever. m comes straight from the wire in scorex_parse; nothing checked it, and best_arg's level loop (args.len() >= m) is unconditionally true for m == 0.

The reference implementation guards this in three places, and this PR mirrors all three:

  • bestArg has require(m >= 1) → NipopowProof::new and NipopowAlgos::prove now return the new NipopowProofError::ZeroMParameter for m == 0 (mirroring the existing ZeroKParameter for k). The prove path additionally removes a latent panic: with m == 0 it indexed sub_chain[sub_chain.len() - m] on a non-empty sub-chain.
  • NipopowProof.isValid checks PoPowParams.isValid(m, k) (m >= 1 && k >= 1 && m + k <= Int.MaxValue) first → is_valid() now does the same via a new has_valid_params(). Parsed proofs with invalid parameters are simply invalid, so is_better_than never reaches best_arg with them (it returns self.is_valid(), matching the reference's fallback).
  • As defense in depth for direct best_arg callers, the level loop also stops when no header reaches the current level: with an empty args, every higher level is empty too. This changes nothing for m >= 1 (there args.len() >= m already implies non-empty).

Verification

Fail-first: on pristine develop a minimal m = 0 proof (single suffix-head header, empty interlinks/proof, so the other validity checks pass) never returns from is_better_than — killed after a 25 s timeout.

New regression tests (ergo-nipopow, --all-features):

  • zero_m_proof_is_invalid_and_never_better — the hanging call now returns Ok(false); the proof is invalid solely because of its parameters.
  • verifier_processes_zero_m_proofs — NipopowVerifier::process returns for a second m = 0 proof instead of hanging in the comparison.
  • new_rejects_zero_m_and_zero_k — NipopowProof::new returns ZeroMParameter / ZeroKParameter.
  • prove_rejects_zero_m — prove returns ZeroMParameter instead of panicking.
  • best_arg_zero_m_empty_chain — best_arg(&[], 0) returns 0 (the diverging-chain shape the comparison actually produces).

Suites: ergo-nipopow --all-features 6/6 (incl. the existing roundtrip proptest), ergo-lib --all-features green; clippy -D warnings clean; fmt clean.


Submitted by @Kshot9000 (https://x.com/kshot9000) — independent Ergo builder.
Tips: ERG 9fcM5RWnAjmP4vx5bnW6yohB6H9bLq8sJbaPLHtwZLtQPB32Pvy

A NipopowProof parsed from bytes carries m straight from the wire, and
nothing checked it: NipopowProof::new rejected only k == 0, is_valid()
checked connections/heights/proofs but not the parameters, and
best_arg's level loop (args.len() >= m) is unconditionally true for
m == 0, so is_better_than / NipopowVerifier::process hung forever on a
peer-supplied proof with m = 0. prove() also accepted m == 0 and then
panicked indexing sub_chain[sub_chain.len() - m].

Mirror the reference implementation (ergo node NipopowAlgos /
NipopowProof / PoPowParams):
- NipopowProof::new and NipopowAlgos::prove return the new
  NipopowProofError::ZeroMParameter for m == 0 (mirrors ZeroKParameter;
  bestArg's require(m >= 1)).
- is_valid() checks the parameters first (PoPowParams.isValid:
  m >= 1 && k >= 1 && m + k <= i32::MAX), so parsed proofs with
  invalid parameters are never compared.
- best_arg's loop also stops when no header reaches the current
  level, so it terminates for any m when called directly; no change
  for m >= 1.

Regression tests cover all five paths (invalid-and-never-better,
verifier process, new, prove, best_arg on an empty chain).
@kushti
kushti merged commit 22bb94e into ergoplatform:develop Oct 13, 2026
29 checks passed
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.

NiPoPoW proof with m == 0 hangs proof comparison forever (best_arg loop never terminates)

2 participants