323 lines
11 KiB
Markdown
323 lines
11 KiB
Markdown
# Proof of Work
|
|
|
|
Every event id is a SHA-256 hash. Most of the time that hash is
|
|
whatever it happens to be — nobody cares about the specific bit
|
|
pattern. Proof of work changes that. NIP-13 defines a convention where
|
|
a client burns CPU time searching for a nonce that makes the event id
|
|
start with a target number of leading zero bits. The result is an event
|
|
that carries a verifiable proof of computational effort baked into its
|
|
identity.
|
|
|
|
The purpose is spam resistance. A relay can require that incoming events
|
|
have, say, 20 leading zero bits. Generating one costs the publisher
|
|
roughly 2^20 hash attempts — a second or two on modern hardware — but
|
|
spamming a thousand events costs a thousand times that. It is not a
|
|
substitute for moderation, but it raises the floor.
|
|
|
|
The protocol mechanism is a single tag: `["nonce", "<counter>",
|
|
"<target>"]`. The first value is the nonce that was found; the second
|
|
is the difficulty the miner was aiming for. Both are needed: the nonce
|
|
changes the event id (because tags are part of the hash input), and the
|
|
target records intent so that a verifier can distinguish a miner who
|
|
committed to 20 bits of work from one who committed to 4 and got lucky.
|
|
|
|
This chapter builds three things: a function to count leading zero bits,
|
|
a function to read the validated proof-of-work difficulty from an event,
|
|
and a pair of mining functions that search for a valid nonce and return
|
|
a `HashedEvent` with the proof embedded. The mining API is batch-based:
|
|
callers control the loop, which keeps the library free of
|
|
platform-specific threading or async machinery.
|
|
|
|
## The module
|
|
|
|
```rust {file=coracle-lib/src/lib.rs}
|
|
pub mod pow;
|
|
```
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
//! NIP-13 proof of work: difficulty validation and mining.
|
|
//!
|
|
//! [`mine_pow`] blocks until a valid nonce is found. [`mine_pow_batch`]
|
|
//! tries a bounded range of nonces and returns `None` if the batch is
|
|
//! exhausted, giving the caller control over cancellation and
|
|
//! parallelization.
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
use crate::events::{Event, HashedEvent, OwnedEvent};
|
|
use crate::tags::{Tag, Tags};
|
|
```
|
|
|
|
## Counting leading zero bits
|
|
|
|
Everything else in this module builds on one primitive: counting how many
|
|
leading bits of a byte slice are zero. A hash with 20 leading zero bits
|
|
starts with at least five zero hex characters (20 / 4 = 5). Difficulty
|
|
is measured in bits, not hex characters, because bits give finer
|
|
granularity — you can require 21 bits of work, not just 20 or 24.
|
|
|
|
The algorithm walks through bytes. Each fully-zero byte contributes
|
|
eight bits. The first non-zero byte contributes its own leading zeros
|
|
(via the `leading_zeros` intrinsic, which compiles to a single CPU
|
|
instruction on most architectures), and then we stop — no byte after
|
|
that can contribute leading zeros.
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
/// Count the number of leading zero bits in a byte slice.
|
|
///
|
|
/// Returns 0 for an empty slice. For a 32-byte SHA-256 hash, the
|
|
/// maximum return value is 256.
|
|
pub fn get_leading_zero_bits(hash: &[u8]) -> u8 {
|
|
let mut count: u8 = 0;
|
|
for &byte in hash {
|
|
if byte == 0 {
|
|
count += 8;
|
|
} else {
|
|
count += byte.leading_zeros() as u8;
|
|
break;
|
|
}
|
|
}
|
|
count
|
|
}
|
|
```
|
|
|
|
## Validation
|
|
|
|
Validating proof of work on an existing event means answering the
|
|
question: how much work did this event *commit to*? That's the minimum
|
|
of two values — the actual leading zero bits in the hash and the
|
|
difficulty claimed in the nonce tag. The hash proves work was done; the
|
|
tag proves intent. A miner who targeted difficulty 4 and happened to
|
|
land on 20 zero bits only committed to 4 bits of work.
|
|
|
|
The core logic takes just an id and tags so it can be shared across
|
|
event types.
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
/// Return the validated proof-of-work difficulty for an event id and tags.
|
|
///
|
|
/// The result is the minimum of the actual leading zero bits in `id`
|
|
/// and the difficulty target claimed in the `nonce` tag. Returns 0 if
|
|
/// no nonce tag is present.
|
|
pub fn get_pow(id: &[u8; 32], tags: &Tags) -> u8 {
|
|
let leading = get_leading_zero_bits(id);
|
|
|
|
let claimed: u8 = tags
|
|
.find("nonce")
|
|
.and_then(|t| t.get(2))
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(0);
|
|
|
|
leading.min(claimed)
|
|
}
|
|
```
|
|
|
|
The nonce tag's third entry (index 2) holds the difficulty target as a
|
|
decimal string. If the tag is absent or the value doesn't parse, the
|
|
claimed target is zero, so `get_pow` returns zero — no verifiable
|
|
commitment was made.
|
|
|
|
### Methods on event types
|
|
|
|
Both `HashedEvent` and `Event` carry an id and tags, so both get a
|
|
`get_pow` method that delegates to the free function. This lets callers
|
|
write `event.get_pow() >= 20` regardless of whether the event has been
|
|
signed yet.
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
impl HashedEvent {
|
|
/// Return the validated proof-of-work difficulty of this event.
|
|
///
|
|
/// The result is the minimum of the actual leading zero bits in the
|
|
/// event id and the difficulty claimed in the nonce tag.
|
|
pub fn get_pow(&self) -> u8 {
|
|
get_pow(&self.id, &self.tags)
|
|
}
|
|
}
|
|
|
|
impl Event {
|
|
/// Return the validated proof-of-work difficulty of this event.
|
|
///
|
|
/// The result is the minimum of the actual leading zero bits in the
|
|
/// event id and the difficulty claimed in the nonce tag.
|
|
pub fn get_pow(&self) -> u8 {
|
|
get_pow(&self.id, &self.tags)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Mining
|
|
|
|
Mining is a brute-force search. For each candidate nonce, the miner
|
|
builds a nonce tag, constructs the canonical serialization that the
|
|
events chapter defined, hashes it, and checks the leading zero bits. If
|
|
the hash meets the target, the search is over; if not, it tries the
|
|
next nonce.
|
|
|
|
The canonical form is the same JSON array from the events chapter:
|
|
`[0, pubkey, created_at, kind, tags, content]`. The nonce tag is
|
|
appended to the event's existing tags before serialization, so the
|
|
nonce value feeds into the hash. Changing the nonce changes the hash —
|
|
that's the whole mechanism.
|
|
|
|
Two free functions expose this search at different levels of control,
|
|
and `OwnedEvent` gets methods that delegate to them.
|
|
|
|
### `mine_pow` — the simple interface
|
|
|
|
For callers who just want a result and are willing to block until it
|
|
arrives:
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
/// Mine proof of work for an event, blocking until a valid nonce is found.
|
|
///
|
|
/// Appends a `["nonce", "<counter>", "<difficulty>"]` tag to the event's
|
|
/// tags and searches for a nonce that produces an event id with at least
|
|
/// `difficulty` leading zero bits. Returns the resulting `HashedEvent`.
|
|
///
|
|
/// This function does not return until a solution is found. For
|
|
/// cancellation or parallelization, use [`mine_pow_batch`] instead.
|
|
pub fn mine_pow(event: &OwnedEvent, difficulty: u8) -> HashedEvent {
|
|
let mut start: u64 = 0;
|
|
loop {
|
|
let batch_size: u64 = 1_000_000;
|
|
if let Some(result) = mine_pow_batch(event, difficulty, start, batch_size) {
|
|
return result;
|
|
}
|
|
start += batch_size;
|
|
}
|
|
}
|
|
```
|
|
|
|
### `mine_pow_batch` — the batch interface
|
|
|
|
For callers who need control over when to stop or how to distribute
|
|
work across threads or web workers:
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
/// Try to mine proof of work over a bounded range of nonces.
|
|
///
|
|
/// Searches nonces from `start` to `start + count - 1`. Returns
|
|
/// `Some(HashedEvent)` if a nonce producing at least `difficulty`
|
|
/// leading zero bits is found, or `None` if the entire range is
|
|
/// exhausted without a match.
|
|
///
|
|
/// # Parallelization
|
|
///
|
|
/// To distribute work across N workers, give each worker a
|
|
/// non-overlapping range: worker *i* calls
|
|
/// `mine_pow_batch(event, difficulty, i * chunk, chunk)`.
|
|
/// The first worker to return `Some` wins; the rest can be cancelled
|
|
/// by simply not issuing further batches.
|
|
pub fn mine_pow_batch(
|
|
event: &OwnedEvent,
|
|
difficulty: u8,
|
|
start: u64,
|
|
count: u64,
|
|
) -> Option<HashedEvent> {
|
|
let difficulty_str = difficulty.to_string();
|
|
let mut tags = event.tags.clone();
|
|
tags.0.push(Tag::new("nonce", ["0", &difficulty_str]));
|
|
let nonce_idx = tags.0.len() - 1;
|
|
|
|
let end = start.saturating_add(count);
|
|
for nonce in start..end {
|
|
tags.0[nonce_idx].0[1] = nonce.to_string();
|
|
|
|
let canonical = serde_json::json!([
|
|
0,
|
|
event.pubkey.to_hex(),
|
|
event.created_at,
|
|
event.kind,
|
|
&tags,
|
|
&event.content,
|
|
])
|
|
.to_string();
|
|
|
|
let id: [u8; 32] = Sha256::digest(canonical.as_bytes()).into();
|
|
|
|
if get_leading_zero_bits(&id) >= difficulty {
|
|
return Some(HashedEvent {
|
|
content: event.content.clone(),
|
|
kind: event.kind,
|
|
tags,
|
|
created_at: event.created_at,
|
|
pubkey: event.pubkey,
|
|
id,
|
|
});
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
```
|
|
|
|
The batch function clones the event's tags once, appends a placeholder
|
|
nonce tag, then mutates the nonce value in place on each iteration.
|
|
The canonical JSON is rebuilt every iteration — the nonce string changes
|
|
each time, so the serialization must too — but the tag vector itself is
|
|
reused rather than reallocated.
|
|
|
|
The returned `HashedEvent` includes the winning nonce tag in its tags.
|
|
From there it slots into the normal event pipeline: call `.sign()` to
|
|
produce a full `Event` ready for the wire.
|
|
|
|
### Methods on `OwnedEvent`
|
|
|
|
For callers who prefer the method style, `OwnedEvent` delegates to the
|
|
free functions:
|
|
|
|
```rust {file=coracle-lib/src/pow.rs}
|
|
impl OwnedEvent {
|
|
/// Mine proof of work, blocking until a valid nonce is found.
|
|
///
|
|
/// See [`mine_pow`] for details.
|
|
pub fn mine_pow(&self, difficulty: u8) -> HashedEvent {
|
|
mine_pow(self, difficulty)
|
|
}
|
|
|
|
/// Try to mine proof of work over a bounded range of nonces.
|
|
///
|
|
/// See [`mine_pow_batch`] for details.
|
|
pub fn mine_pow_batch(
|
|
&self,
|
|
difficulty: u8,
|
|
start: u64,
|
|
count: u64,
|
|
) -> Option<HashedEvent> {
|
|
mine_pow_batch(self, difficulty, start, count)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Usage patterns
|
|
|
|
The split between `mine_pow` and `mine_pow_batch` keeps the library
|
|
code simple while supporting a range of caller strategies:
|
|
|
|
**Blocking single-threaded** — the common case. `event.mine_pow(20)`
|
|
blocks until it finds a valid nonce and returns the `HashedEvent`.
|
|
|
|
**Batched with cancellation** — a UI that wants to let the user abort.
|
|
The caller loops over `event.mine_pow_batch(20, cursor, 100_000)`,
|
|
incrementing `cursor` by 100,000 each round. Between rounds it checks
|
|
whether the user pressed cancel.
|
|
|
|
**Parallel native threads** — each of N threads gets a non-overlapping
|
|
chunk of the nonce space. The first thread to return `Some` wins; the
|
|
rest are abandoned.
|
|
|
|
**WASM web workers** — the main thread posts a batch range to a worker
|
|
via `postMessage`. The worker calls `mine_pow_batch`, posts the result
|
|
back. To cancel, the main thread simply stops posting new batches. No
|
|
shared memory, no atomics, no platform-specific API in the library.
|
|
|
|
**Validation** — a relay checking incoming events writes
|
|
`event.get_pow() >= 20`. The method works on both `HashedEvent` and
|
|
`Event`.
|
|
|
|
## What's next
|
|
|
|
The next chapter introduces filters — the query language clients use to
|
|
ask relays for events matching a set of criteria.
|