Files
coracle-rust/book/plan/proof-of-work.md
T
Jon Staab c8f6bc1652 Add pow
2026-04-21 06:18:49 -07:00

6.0 KiB
Raw Blame History

Plan: Proof of Work

Topic Summary

Full implementation of NIP-13 proof-of-work: validation and generation. The mining function receives an OwnedEvent reference with a target difficulty and hashes until the difficulty is achieved, returning a HashedEvent. The API is batch-based and pure — no threading, no async, no platform-specific code. Callers control the loop and can parallelize or cancel by managing batch ranges externally.

Chapter Outline

  1. Introduction — What PoW means in nostr: leading zero bits on the SHA-256 event id. Why it exists (spam resistance, relay filtering, signal of effort). Reference NIP-13.

  2. Module setup — Add pub mod pow; to coracle-lib/src/lib.rs. Module imports.

  3. Counting leading zero bitsget_leading_zero_bits(hash: &[u8]) -> u8. Pure utility that iterates bytes: full zero byte = +8 bits, partial byte uses leading_zeros(). This is the foundation everything else builds on.

  4. The nonce tag — NIP-13 specifies a ["nonce", "<counter>", "<difficulty>"] tag. Explain that the nonce is part of the event content that gets hashed — changing it changes the id. Show how to add/read the tag using the existing Tags type.

  5. Validationcheck_pow(event: &HashedEvent, difficulty: u8) -> bool. Checks that the event id has at least difficulty leading zero bits AND that the nonce tag's claimed difficulty is at least difficulty. Both checks are needed: the id check proves the work was done, the tag check proves the miner intended at least this difficulty.

  6. Miningmine_pow(event: &OwnedEvent, difficulty: u8, start: Option<u64>, count: Option<u64>) -> Option<HashedEvent>. Clones the event's tags, appends/updates a nonce tag, hashes, checks difficulty. Iterates nonces from start (default 0) for up to count attempts (default unbounded). Returns Some(HashedEvent) on success, None if the batch is exhausted.

  7. Usage patterns — Brief prose (not tangled code) showing:

    • Simple: mine_pow(&event, 20, None, None) — blocks until done
    • Batched: loop calling with Some(cursor) and Some(100_000)
    • Parallel: N threads/workers with non-overlapping ranges
    • WASM: worker runs batches, main thread stops sending to cancel
  8. What's next — Transition to filters chapter.

API Design

/// Count the leading zero bits in a byte slice.
pub fn get_leading_zero_bits(hash: &[u8]) -> u8

/// Check that an event meets a minimum proof-of-work difficulty.
/// Returns true if the event id has at least `difficulty` leading zero bits
/// AND the nonce tag claims at least `difficulty`.
pub fn check_pow(event: &HashedEvent, difficulty: u8) -> bool

/// Mine proof-of-work for an event.
///
/// Tries nonces starting from `start` (default 0) for up to `count` attempts
/// (default unbounded). Returns `Some(HashedEvent)` with the nonce tag included
/// if a valid nonce is found, `None` if the batch is exhausted.
pub fn mine_pow(
    event: &OwnedEvent,
    difficulty: u8,
    start: Option<u64>,
    count: Option<u64>,
) -> Option<HashedEvent>

Code Organization

  • Crate: coracle-lib
  • Module: coracle-lib/src/pow.rs
  • Exports: get_leading_zero_bits, check_pow, mine_pow
  • Dependencies on existing code: OwnedEvent, HashedEvent from events, Tags from tags
  • Add pub mod pow; to coracle-lib/src/lib.rs

Dependencies

No new external crates. SHA-256 (sha2) and hex encoding are already available from the events chapter. Leading zero bit counting uses only u8::leading_zeros() from std.

Narrative Notes

  • Why both id check and tag check: An event with 25 leading zeros but a nonce tag claiming difficulty 10 only committed to 10 bits of work — the extra zeros are luck. Relays that require difficulty 20 should reject it. Explain this clearly.

  • Why batch-based: The chapter should explain the design choice. A blocking loop until found function can't be cancelled and can't be parallelized. By exposing start/count, the library stays pure and platform-agnostic while giving callers full control. Show how this maps to threads (native) and workers (WASM).

  • Nonce tag placement: The tag must be part of the serialized event before hashing. The mining loop adds the tag, hashes, checks, and either returns or tries the next nonce. Work on a clone of the input's tags — don't mutate the original.

  • Difficulty semantics: Difficulty is measured in leading zero bits, not bytes or hex characters. Difficulty 20 means the first 20 bits of the 256-bit hash are zero. This allows fine-grained difficulty that isn't locked to 4-bit (hex) or 8-bit (byte) boundaries.

Design Decisions

  1. Batch-based API over blocking loop: Enables cancellation and parallelization without platform-specific code. Callers who want simple blocking behavior pass None for both start and count. Research showed rust-nostr uses a tight blocking loop with optional multi-threading via feature flags; welshman uses Web Workers. Our approach avoids both by pushing the concurrency concern to the caller.

  2. No prefix generation: get_prefixes_for_difficulty() (seen in rust-nostr and nostr-tools) converts bit difficulty to hex prefixes for relay querying. Deferred — it's orthogonal to mining/validation and can be added later if needed.

  3. Nonce as u64: rust-nostr uses u128, but u64 gives 1.8×10¹⁹ attempts — more than enough for any practical difficulty. Keeps the tag string shorter and matches JS number limits for WASM interop.

  4. Clone, don't mutate: mine_pow takes &OwnedEvent and clones internally. The caller keeps their original event unchanged. This matches the functional style of the event pipeline (each step returns a new type).

  5. check_pow validates both id and tag: Following NIP-13 semantics. The id check proves the work; the tag check proves intent. Both are needed for correct validation.

Open Questions

None — scope is well-defined and all design decisions are resolved.