This commit is contained in:
Jon Staab
2026-04-21 06:18:49 -07:00
parent b96ad44d3a
commit c8f6bc1652
5 changed files with 888 additions and 30 deletions
+125
View File
@@ -0,0 +1,125 @@
# 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 bits**`get_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. **Validation**`check_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. **Mining**`mine_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
```rust
/// 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.