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

8.4 KiB
Raw Blame History

Research: Proof of Work

Topic Summary

Full implementation of NIP-13 proof-of-work: validation/querying and generation. The mining function receives an OwnedEvent with a target difficulty and hashes until the difficulty is achieved, returning a HashedEvent. The design should support running in a separate thread (native) or a Web Worker (wasm).

Philosophy

From ref/building-nostr:

  • PoW is framed as an optional, client-level heuristic — not a protocol requirement. Users can "use web of trust or proof of work heuristics to filter posts."
  • Clients should have user-configurable policies for handling adversarial data. PoW is one tool among several (reputation, payment, WoT).
  • Economic spam prevention (relay charging) is presented as an alternative to computational PoW.
  • NIP-13 is not mentioned by name; the philosophy emphasizes user agency and optional mechanisms over mandated solutions.

Reference Implementation Analysis

applesauce

No PoW implementation found. The applesauce libraries focus on client UI utilities and do not include NIP-13 support.

ndk

No PoW mining or validation in NDK core. NDK does parse min_pow_difficulty from NIP-11 relay info documents (NDKRelayInformation.limitation.min_pow_difficulty), but does not act on it. PoW mining is delegated to external packages.

nostr-gadgets

No PoW implementation found. The library focuses on hints scoring, database utilities, event sets, and outbox logic.

nostrlib

No PoW-specific findings from the Go library search. The library may handle it elsewhere but no dedicated NIP-13 module was found.

nostr-tools

File: nip13.ts

Complete standalone NIP-13 implementation:

  • getPow(hex: string): number — Counts leading zero bits from hex string by iterating 8-bit nibbles.
  • getPowFromBytes(hash: Uint8Array): number — Byte-based version using Math.clz32() for the partial byte.
  • minePow(unsigned: UnsignedEvent, difficulty: number) — Synchronous mining on main thread:
    • Appends ["nonce", count, difficulty] tag
    • Increments nonce counter until SHA-256 hash meets difficulty threshold
    • Uses @noble/hashes for SHA-256
    • Mutates created_at when time rolls over
    • Returns event with computed id field

Design: Simple, synchronous, single-threaded. No worker support. Minimal dependencies.

Test vectors: Validates getPow against known hashes with specific difficulties (073 bits). Tests minePow with difficulty=10.

rust-nostr

Files:

  • crates/nostr/src/nips/nip13.rs — Core PoW utilities
  • crates/nostr/src/event/builder.rs — Mining loop
  • crates/nostr/src/event/id.rs — Validation
  • crates/nostr/src/event/tag/standard.rs — POW tag structure

Data Structures:

TagStandard::POW { nonce: u128, difficulty: u8 }
// Serializes to: ["nonce", "<nonce>", "<difficulty>"]

EventBuilder { pow: Option<u8>, ... }

Core Algorithm — get_leading_zero_bits<T: AsRef<[u8]>>(h: T) -> u8:

  • Iterates bytes: full zero byte → +8 bits; non-zero byte → leading_zeros() CPU intrinsic; return
  • Range 0255

Prefix Generation — get_prefixes_for_difficulty(bits: u8) -> Vec<String>:

  • Converts bit difficulty to valid hex prefixes for filtering/querying
  • Formula: prefix_count = 2^(hex_bits - difficulty_bits)

Single-Threaded Mining (mine_pow_single_thread):

  • Increments nonce from 0
  • For each nonce: push POW tag → compute EventId (SHA-256) → check leading zeros ≥ difficulty → pop tag if fail
  • Returns UnsignedEvent on success

Multi-Threaded Mining (mine_pow_multi_thread, feature pow-multi-thread):

  • Spawns thread::available_parallelism() threads
  • Each thread: starting nonce = thread_id, stride = num_threads
  • Coordination: Arc<AtomicBool> with Ordering::Relaxed
  • First thread to find solution signals others to stop
  • Main thread busy-waits on atomic flag
  • Falls back to single-threaded if 1 core or spawn failure

Validation:

impl EventId {
    pub fn check_pow(&self, difficulty: u8) -> bool {
        nip13::get_leading_zero_bits(self.as_bytes()) >= difficulty
    }
}

Relay enforcement: nostr-relay-builder checks min_pow before accepting events.

Design decisions:

  • nonce: u128 — virtually inexhaustible nonce space
  • difficulty: u8 — matches SHA-256 bit width
  • Feature-gated multi-threading keeps no_std compatibility
  • Busy-wait polling (not ideal but simple)
  • Timestamp captured once before mining, shared across threads

welshman

File: packages/util/src/Pow.ts

Worker-based asynchronous mining:

  • makePow(event: OwnedEvent, difficulty: number): ProofOfWork

    • Returns { worker: Worker, result: Promise<HashedEvent> }
    • Creates Web Worker from inline Blob URL
    • Worker receives event + difficulty via postMessage
    • Worker runs mining loop using crypto.subtle.digest("SHA-256", ...)
    • Nonce tag mutated in-place for performance: tag[1] = count.toString()
    • Supports start and step parameters for distributing across multiple workers
  • getPow(event: HashedEvent): number — Validates difficulty from event hash. Counts leading zero bytes, then uses Math.clz32() on final byte.

  • estimateWork(difficulty: number) — Cost estimation using benchmark: benchmark_ms * 2^(difficulty - benchmarkDifficulty). benchmarkDifficulty = 15.

Integration in thunk.ts:

  • PoW applied before signing (necessary because nonce changes the event ID)
  • For gift-wrapped events (NIP-59): PoW on the wrapper
  • Uses AbortSignal.timeout(30_000) for timeout protection
  • Logs warning if event already signed (PoW would change ID, invalidating signature)

Design: Non-blocking via Workers. Single worker by default but architecture supports multi-worker distribution via start/step. Uses Web Crypto API (available in workers).

Common Patterns

  1. Difficulty metric: All implementations use leading zero bits in SHA-256 hash (NIP-13 standard). Not byte-aligned — allows fine-grained difficulty.

  2. Tag format: Universal ["nonce", "<counter>", "<target_difficulty>"] tag.

  3. Mining loop: Increment nonce, recompute hash, check leading zeros. Simple brute force — no shortcuts possible.

  4. PoW before signing: All implementations compute PoW before the event is signed, since the nonce tag changes the event ID which is what gets signed.

  5. Leading zero counting: Two approaches:

    • Byte iteration + CPU intrinsic (leading_zeros() / Math.clz32())
    • Hex string nibble iteration (nostr-tools)
  6. Parallelism strategies diverge:

    • rust-nostr: OS threads with atomic bool coordination
    • welshman: Web Workers with message passing
    • nostr-tools: no parallelism (synchronous)

Considerations for Our Implementation

API Design

The chapter should take an OwnedEvent (has pubkey but no hash yet) and return a HashedEvent (has computed ID meeting difficulty). This fits naturally into the event lifecycle from chapter 05.

The mining function should be pure and blocking — it takes inputs and returns a result. Threading/worker dispatch is the caller's responsibility. This keeps the core function platform-agnostic:

  • Native: caller wraps in std::thread::spawn or tokio::spawn_blocking
  • WASM: caller runs in a Web Worker

Validation

Two levels:

  1. get_leading_zero_bits(hash: &[u8]) -> u8 — pure utility
  2. check_pow(event: &HashedEvent, difficulty: u8) -> bool — checks event's nonce tag difficulty matches actual hash difficulty

Querying

get_prefixes_for_difficulty(difficulty: u8) -> Vec<String> enables relay-side filtering by event ID prefix. Useful for REQ filters.

Dependencies

  • SHA-256 already available from the events chapter
  • No additional crates needed for single-threaded PoW
  • u128 nonce type is standard Rust

Threading Considerations

Rather than building threading into the mining function:

  • Keep mine_pow(event: OwnedEvent, difficulty: u8) -> HashedEvent as a blocking function
  • Document how callers can parallelize (thread per core, strided nonces via start/step parameters)
  • For WASM: the function runs inside a worker; the host page dispatches to it

This matches welshman's approach and avoids platform-specific code in the core library.

Nonce Strategy

Support start and step parameters to enable callers to distribute work:

mine_pow(event: OwnedEvent, difficulty: u8, start: u64, step: u64) -> HashedEvent
  • Single thread: start=0, step=1
  • N threads: thread i uses start=i, step=N