Add pow
This commit is contained in:
@@ -0,0 +1,322 @@
|
|||||||
|
# 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.
|
||||||
@@ -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.
|
||||||
@@ -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`
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
use coracle_lib::events::{EventContent, OwnedEvent};
|
||||||
|
use coracle_lib::keys::SecretKey;
|
||||||
|
use coracle_lib::pow::{get_leading_zero_bits, get_pow, mine_pow, mine_pow_batch};
|
||||||
|
use coracle_lib::tags::{Tag, Tags};
|
||||||
|
|
||||||
|
fn fixed_secret() -> SecretKey {
|
||||||
|
let bytes: [u8; 32] = [
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
|
||||||
|
25, 26, 27, 28, 29, 30, 31, 32,
|
||||||
|
];
|
||||||
|
SecretKey::from_hex(&hex::encode(bytes)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_event() -> OwnedEvent {
|
||||||
|
EventContent::new("hello nostr", Tags::new())
|
||||||
|
.kind(1)
|
||||||
|
.stamp(1_700_000_000)
|
||||||
|
.own(fixed_secret().public_key())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- get_leading_zero_bits ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_zeros_empty_slice() {
|
||||||
|
assert_eq!(get_leading_zero_bits(&[]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_zeros_all_zeros() {
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0, 0, 0, 0]), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_zeros_first_byte_nonzero() {
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x80, 0x00]), 0);
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x01, 0x00]), 7);
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x0F]), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_zeros_with_zero_prefix() {
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x00, 0x01]), 15);
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x00, 0x00, 0x80]), 16);
|
||||||
|
assert_eq!(get_leading_zero_bits(&[0x00, 0x00, 0x00, 0x01]), 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_zeros_known_hash() {
|
||||||
|
// "000006d8..." → 0x00, 0x00, 0x06 → 16 + 5 = 21
|
||||||
|
let hash = hex::decode("000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(get_leading_zero_bits(&hash), 21);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- get_pow (free function) ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_pow_returns_min_of_hash_and_claim() {
|
||||||
|
let event = test_event();
|
||||||
|
let hashed = mine_pow(&event, 8);
|
||||||
|
// The free function should return at least 8
|
||||||
|
assert!(get_pow(&hashed.id, &hashed.tags) >= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_pow_zero_without_nonce_tag() {
|
||||||
|
let hashed = test_event().hash();
|
||||||
|
assert_eq!(get_pow(&hashed.id, &hashed.tags), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_pow_capped_by_claimed_difficulty() {
|
||||||
|
// Mine at difficulty 4 — even if hash has more leading zeros,
|
||||||
|
// get_pow returns at most 4
|
||||||
|
let event = test_event();
|
||||||
|
let hashed = mine_pow(&event, 4);
|
||||||
|
assert!(get_pow(&hashed.id, &hashed.tags) >= 4);
|
||||||
|
// Actual leading zeros might exceed 4, but get_pow is min(leading, claimed)
|
||||||
|
assert_eq!(get_pow(&hashed.id, &hashed.tags), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HashedEvent::get_pow method ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hashed_event_get_pow_method() {
|
||||||
|
let event = test_event();
|
||||||
|
let hashed = mine_pow(&event, 8);
|
||||||
|
assert!(hashed.get_pow() >= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hashed_event_get_pow_zero_without_nonce() {
|
||||||
|
let hashed = test_event().hash();
|
||||||
|
assert_eq!(hashed.get_pow(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Event::get_pow method ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signed_event_get_pow_method() {
|
||||||
|
let sk = fixed_secret();
|
||||||
|
let event = test_event();
|
||||||
|
let hashed = mine_pow(&event, 8);
|
||||||
|
let sig = sk.sign(&hashed.id);
|
||||||
|
let signed = hashed.sign(sig);
|
||||||
|
assert!(signed.get_pow() >= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- OwnedEvent::mine_pow method ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owned_event_mine_pow_method() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = event.mine_pow(8);
|
||||||
|
assert!(result.get_pow() >= 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- OwnedEvent::mine_pow_batch method ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owned_event_mine_pow_batch_method() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = event.mine_pow_batch(1, 0, 1_000_000);
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert!(result.unwrap().get_pow() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mine_pow (free function) ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_returns_valid_result() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = mine_pow(&event, 8);
|
||||||
|
|
||||||
|
assert!(get_leading_zero_bits(&result.id) >= 8);
|
||||||
|
|
||||||
|
let nonce_tag = result.tags.find("nonce").expect("should have nonce tag");
|
||||||
|
assert_eq!(nonce_tag.get(2), Some("8"));
|
||||||
|
|
||||||
|
let _nonce: u64 = nonce_tag.value().parse().expect("nonce should be a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_preserves_event_fields() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = mine_pow(&event, 4);
|
||||||
|
|
||||||
|
assert_eq!(result.content, event.content);
|
||||||
|
assert_eq!(result.kind, event.kind);
|
||||||
|
assert_eq!(result.created_at, event.created_at);
|
||||||
|
assert_eq!(result.pubkey, event.pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_preserves_existing_tags() {
|
||||||
|
let mut event = test_event();
|
||||||
|
event.tags.0.push(Tag::new("t", ["nostr"]));
|
||||||
|
let result = mine_pow(&event, 4);
|
||||||
|
|
||||||
|
assert!(result.tags.find("t").is_some());
|
||||||
|
assert!(result.tags.find("nonce").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_does_not_mutate_input() {
|
||||||
|
let event = test_event();
|
||||||
|
let tags_before = event.tags.clone();
|
||||||
|
let _result = mine_pow(&event, 4);
|
||||||
|
assert_eq!(event.tags, tags_before);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- mine_pow_batch (free function) ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_batch_returns_none_when_exhausted() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = mine_pow_batch(&event, 64, 0, 10);
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_batch_finds_solution_in_range() {
|
||||||
|
let event = test_event();
|
||||||
|
let result = mine_pow_batch(&event, 1, 0, 1_000_000);
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert!(get_leading_zero_bits(&result.unwrap().id) >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mine_pow_batch_respects_start_offset() {
|
||||||
|
let event = test_event();
|
||||||
|
let a = mine_pow_batch(&event, 4, 0, 1_000_000);
|
||||||
|
let b = mine_pow_batch(&event, 4, 500_000, 1_000_000);
|
||||||
|
|
||||||
|
assert!(a.is_some());
|
||||||
|
assert!(b.is_some());
|
||||||
|
|
||||||
|
let nonce_a = a.unwrap().tags.find("nonce").unwrap().value().to_string();
|
||||||
|
let nonce_b = b.unwrap().tags.find("nonce").unwrap().value().to_string();
|
||||||
|
assert_ne!(nonce_a, nonce_b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Integration: mine then sign then verify ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mined_event_can_be_signed_and_verified() {
|
||||||
|
let sk = fixed_secret();
|
||||||
|
let event = test_event();
|
||||||
|
let hashed = mine_pow(&event, 8);
|
||||||
|
|
||||||
|
let sig = sk.sign(&hashed.id);
|
||||||
|
let signed = hashed.sign(sig);
|
||||||
|
|
||||||
|
assert!(signed.verify_id());
|
||||||
|
signed.verify().expect("mined and signed event should verify");
|
||||||
|
assert!(signed.get_pow() >= 8);
|
||||||
|
}
|
||||||
+29
-30
@@ -8,7 +8,7 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use coracle_lib::encryption as ce;
|
use coracle_lib::encryption as ce;
|
||||||
use coracle_lib::event as cev;
|
use coracle_lib::events as cev;
|
||||||
use coracle_lib::keys as ck;
|
use coracle_lib::keys as ck;
|
||||||
|
|
||||||
fn js_err<E: std::fmt::Display>(e: E) -> JsError {
|
fn js_err<E: std::fmt::Display>(e: E) -> JsError {
|
||||||
@@ -181,16 +181,24 @@ impl Event {
|
|||||||
created_at: u64,
|
created_at: u64,
|
||||||
sk: &SecretKey,
|
sk: &SecretKey,
|
||||||
) -> Result<Event, JsError> {
|
) -> Result<Event, JsError> {
|
||||||
let tags: Vec<Vec<String>> =
|
use coracle_lib::tags::{Tag, Tags};
|
||||||
|
|
||||||
|
let raw_tags: Vec<Vec<String>> =
|
||||||
serde_wasm_bindgen::from_value(tags).map_err(|e| JsError::new(&e.to_string()))?;
|
serde_wasm_bindgen::from_value(tags).map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
// coracle-lib's SecretKey hides the inner secp type behind a
|
let tags = Tags::from(
|
||||||
// crate-private accessor, so round-trip through hex to hand
|
raw_tags
|
||||||
// Event::new the low-level key it wants.
|
.into_iter()
|
||||||
let bytes = hex::decode(sk.0.to_hex()).map_err(js_err)?;
|
.map(|v| Tag::from(v))
|
||||||
let secp_sk = secp256k1::SecretKey::from_slice(&bytes).map_err(js_err)?;
|
.collect::<Vec<_>>(),
|
||||||
Ok(Event(cev::Event::new(
|
);
|
||||||
kind, content, tags, created_at, &secp_sk,
|
|
||||||
)))
|
let hashed = cev::EventContent::new(content, tags)
|
||||||
|
.kind(kind)
|
||||||
|
.stamp(created_at)
|
||||||
|
.own(sk.0.public_key())
|
||||||
|
.hash();
|
||||||
|
let sig = sk.0.sign(&hashed.id);
|
||||||
|
Ok(Event(hashed.sign(sig)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an event from its JSON representation.
|
/// Parse an event from its JSON representation.
|
||||||
@@ -209,12 +217,12 @@ impl Event {
|
|||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn id(&self) -> String {
|
pub fn id(&self) -> String {
|
||||||
self.0.id.clone()
|
hex::encode(self.0.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn pubkey(&self) -> String {
|
pub fn pubkey(&self) -> String {
|
||||||
self.0.pubkey.clone()
|
self.0.pubkey.to_hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter, js_name = createdAt)]
|
#[wasm_bindgen(getter, js_name = createdAt)]
|
||||||
@@ -234,7 +242,7 @@ impl Event {
|
|||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn sig(&self) -> String {
|
pub fn sig(&self) -> String {
|
||||||
self.0.sig.clone()
|
hex::encode(self.0.sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
@@ -242,24 +250,15 @@ impl Event {
|
|||||||
serde_wasm_bindgen::to_value(&self.0.tags).map_err(|e| JsError::new(&e.to_string()))
|
serde_wasm_bindgen::to_value(&self.0.tags).map_err(|e| JsError::new(&e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canonical `[0, pubkey, created_at, kind, tags, content]` serialization.
|
/// Verify both the id and the Schnorr signature.
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn serialize(&self) -> String {
|
|
||||||
self.0.serialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = computeId)]
|
|
||||||
pub fn compute_id(&self) -> String {
|
|
||||||
self.0.compute_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = idIsValid)]
|
|
||||||
pub fn id_is_valid(&self) -> bool {
|
|
||||||
self.0.id_is_valid()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn verify(&self) -> bool {
|
pub fn verify(&self) -> bool {
|
||||||
self.0.verify()
|
self.0.verify().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the recomputed id matches the stored id.
|
||||||
|
#[wasm_bindgen(js_name = verifyId)]
|
||||||
|
pub fn verify_id(&self) -> bool {
|
||||||
|
self.0.verify_id()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user