Add pow
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
# 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 (0–73 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:**
|
||||
```rust
|
||||
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 0–255
|
||||
|
||||
**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:**
|
||||
```rust
|
||||
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:
|
||||
```rust
|
||||
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`
|
||||
Reference in New Issue
Block a user