Add pow
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user