use thiserror::Error;
use crate::event::{Event, EventBuilder, EventBuilderError, EventId, Tag, TagKind, Tags};
use crate::key::{Keys, PublicKey};
use crate::types::{Timestamp, TimestampError};
pub const NONCE_TAG: &str = "nonce";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum PowError {
#[error("event id has {actual} leading zero bits, need {expected}")]
InsufficientWork {
actual: u8,
expected: u8,
},
#[error("committed difficulty {actual} < {expected}")]
InsufficientCommitment {
actual: u8,
expected: u8,
},
#[error("nonce tag commitment is not a valid u8 integer")]
InvalidCommitment,
#[error("strict PoW verification requires a committed difficulty")]
MissingCommitment,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum MineError {
#[error(transparent)]
Clock(#[from] TimestampError),
#[error(transparent)]
Builder(#[from] EventBuilderError),
#[error("nonce search exhausted u64 space; refresh created_at and retry")]
NonceExhausted,
}
#[must_use]
pub fn count_leading_zero_bits(bytes: &[u8]) -> u8 {
let mut total: u8 = 0;
for &b in bytes {
if b == 0 {
total = total.saturating_add(8);
} else {
let z = u8::try_from(b.leading_zeros()).unwrap_or(0);
return total.saturating_add(z);
}
}
total
}
#[must_use]
pub fn event_id_difficulty(id: &EventId) -> u8 {
count_leading_zero_bits(&id.to_byte_array())
}
pub fn committed_difficulty(event: &Event) -> Result<Option<u8>, PowError> {
let kind = TagKind::from_wire(NONCE_TAG);
let Some(tag) = event.tags.find_first(&kind) else {
return Ok(None);
};
let Some(commitment) = tag.values().get(2) else {
return Ok(None);
};
commitment
.parse::<u8>()
.map(Some)
.map_err(|_| PowError::InvalidCommitment)
}
pub fn verify_pow(event: &Event, min_difficulty: u8) -> Result<(), PowError> {
let actual = event_id_difficulty(&event.id);
if actual < min_difficulty {
return Err(PowError::InsufficientWork {
actual,
expected: min_difficulty,
});
}
if let Some(commitment) = committed_difficulty(event)?
&& commitment < min_difficulty
{
return Err(PowError::InsufficientCommitment {
actual: commitment,
expected: min_difficulty,
});
}
Ok(())
}
pub fn verify_pow_strict(event: &Event, min_difficulty: u8) -> Result<(), PowError> {
verify_pow(event, min_difficulty)?;
if min_difficulty > 0 && committed_difficulty(event)?.is_none() {
return Err(PowError::MissingCommitment);
}
Ok(())
}
pub fn mine(
builder: &EventBuilder,
pubkey: PublicKey,
difficulty: u8,
) -> Result<PowAttempt, MineError> {
PowAttempt::mine(builder, pubkey, difficulty)
}
pub fn mine_and_sign(
builder: &EventBuilder,
keys: &Keys,
difficulty: u8,
) -> Result<Event, MineError> {
let attempt = PowAttempt::mine(builder, *keys.public_key(), difficulty)?;
Ok(attempt.into_signed_with_keys(keys)?)
}
#[derive(Debug, Clone)]
pub struct PowAttempt {
pub unsigned: crate::event::UnsignedEvent,
pub iterations: u64,
pub difficulty: u8,
}
impl PowAttempt {
pub(crate) fn mine(
builder: &EventBuilder,
pubkey: PublicKey,
difficulty: u8,
) -> Result<Self, MineError> {
let created_at = match builder.current_created_at() {
Some(ts) => ts,
None => Timestamp::now()?,
};
let kind = builder.current_kind();
let content = builder.current_content().to_owned();
let nonce_kind = TagKind::from_wire(NONCE_TAG);
let prefix: Vec<Tag> = builder
.current_tags()
.iter()
.filter(|t| t.kind() != nonce_kind)
.cloned()
.collect();
let mut iterations: u64 = 0;
loop {
iterations = iterations.checked_add(1).ok_or(MineError::NonceExhausted)?;
let mut tags = prefix.clone();
tags.push(make_nonce_tag(iterations, difficulty));
let unsigned = crate::event::UnsignedEvent::new(
pubkey,
created_at,
kind,
Tags::from_vec(tags),
content.clone(),
);
if event_id_difficulty(&unsigned.id) >= difficulty {
return Ok(Self {
unsigned,
iterations,
difficulty,
});
}
}
}
pub fn into_signed_with_keys(self, keys: &Keys) -> Result<Event, EventBuilderError> {
Ok(self.unsigned.sign_with_keys(keys)?)
}
}
fn make_nonce_tag(nonce: u64, difficulty: u8) -> Tag {
Tag::with(
&TagKind::from_wire(NONCE_TAG),
[nonce.to_string(), difficulty.to_string()],
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Kind;
fn keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn count_zero_bits_examples() {
assert_eq!(count_leading_zero_bits(&[]), 0);
assert_eq!(count_leading_zero_bits(&[0xff]), 0);
assert_eq!(count_leading_zero_bits(&[0x80]), 0);
assert_eq!(count_leading_zero_bits(&[0x40]), 1);
assert_eq!(count_leading_zero_bits(&[0x01]), 7);
assert_eq!(count_leading_zero_bits(&[0x00, 0xff]), 8);
assert_eq!(count_leading_zero_bits(&[0x00, 0x80]), 8);
assert_eq!(count_leading_zero_bits(&[0x00, 0x00, 0x10]), 19);
assert_eq!(count_leading_zero_bits(&[0x00; 4]), 32);
}
#[test]
fn mine_low_difficulty() {
let builder = EventBuilder::text_note("hello").created_at(Timestamp::from_secs(1));
let event = mine_and_sign(&builder, &keys(), 8).unwrap();
assert_eq!(event.kind, Kind::TEXT_NOTE);
verify_pow(&event, 8).unwrap();
event.verify().unwrap();
}
#[test]
fn mine_writes_nonce_commitment() {
let builder = EventBuilder::text_note("commit").created_at(Timestamp::from_secs(2));
let event = mine_and_sign(&builder, &keys(), 6).unwrap();
let commitment = committed_difficulty(&event).unwrap();
assert_eq!(commitment, Some(6));
}
#[test]
fn mine_replaces_existing_nonce_tag() {
let builder = EventBuilder::text_note("ignore-me")
.created_at(Timestamp::from_secs(3))
.tag(Tag::new(["nonce", "0", "0"]).unwrap());
let event = mine_and_sign(&builder, &keys(), 4).unwrap();
let count = event
.tags
.iter()
.filter(|t| t.kind() == TagKind::from_wire(NONCE_TAG))
.count();
assert_eq!(count, 1);
}
#[test]
fn verify_pow_rejects_low_id_difficulty() {
let event = EventBuilder::text_note("no-pow")
.created_at(Timestamp::from_secs(4))
.sign_with_keys(&keys())
.unwrap();
let err = verify_pow(&event, 32).unwrap_err();
assert!(matches!(err, PowError::InsufficientWork { .. }));
}
#[test]
fn verify_pow_rejects_low_commitment() {
let builder = EventBuilder::text_note("commit-fail").created_at(Timestamp::from_secs(5));
let event = mine_and_sign(&builder, &keys(), 8).unwrap();
let err = verify_pow(&event, 16).unwrap_err();
assert!(matches!(
err,
PowError::InsufficientWork { .. } | PowError::InsufficientCommitment { actual: 8, .. }
));
}
#[test]
fn verify_pow_zero_difficulty_accepts_anything() {
let event = EventBuilder::text_note("anything")
.created_at(Timestamp::from_secs(6))
.sign_with_keys(&keys())
.unwrap();
verify_pow(&event, 0).unwrap();
}
#[test]
fn invalid_commitment_is_reported() {
let event = EventBuilder::text_note("bad-commit")
.created_at(Timestamp::from_secs(7))
.tag(Tag::new(["nonce", "1", "abc"]).unwrap())
.sign_with_keys(&keys())
.unwrap();
let err = committed_difficulty(&event).unwrap_err();
assert!(matches!(err, PowError::InvalidCommitment));
}
#[test]
fn verify_pow_strict_rejects_missing_commitment() {
let event = EventBuilder::text_note("no-nonce")
.created_at(Timestamp::from_secs(8))
.sign_with_keys(&keys())
.unwrap();
let err = verify_pow_strict(&event, 1).unwrap_err();
assert!(matches!(err, PowError::MissingCommitment));
verify_pow_strict(&event, 0).unwrap();
}
#[test]
fn verify_pow_strict_accepts_when_commitment_meets_floor() {
let builder = EventBuilder::text_note("strict-ok").created_at(Timestamp::from_secs(9));
let event = mine_and_sign(&builder, &keys(), 6).unwrap();
verify_pow_strict(&event, 6).unwrap();
let err = verify_pow_strict(&event, 7).unwrap_err();
assert!(matches!(
err,
PowError::InsufficientWork { .. } | PowError::InsufficientCommitment { actual: 6, .. }
));
}
#[test]
fn nip13_spec_example_difficulty_and_commitment() {
let id_hex = "000006d8c378af1779d2feebc7603a125d99eca0ccf1085959b307f64e5dd358";
let id = id_hex.parse::<EventId>().unwrap();
assert_eq!(event_id_difficulty(&id), 21);
let pubkey =
PublicKey::parse("a48380f4cfcc1ad5378294fcac36439770f9c878dd880ffa94bb74ea54a6f243")
.unwrap();
let event = Event::from_parts(
id,
pubkey,
Timestamp::from_secs(1_651_794_653),
Kind::TEXT_NOTE,
Tags::from_vec(vec![Tag::new(["nonce", "776797", "20"]).unwrap()]),
"It's just me mining my own business".to_owned(),
keys().sign_schnorr(&[0u8; 32]),
);
assert_eq!(committed_difficulty(&event).unwrap(), Some(20));
verify_pow(&event, 0).unwrap();
verify_pow(&event, 20).unwrap();
let commitment_short = verify_pow(&event, 21).unwrap_err();
assert!(matches!(
commitment_short,
PowError::InsufficientCommitment {
actual: 20,
expected: 21,
}
));
let id_short = verify_pow(&event, 22).unwrap_err();
assert!(matches!(
id_short,
PowError::InsufficientWork {
actual: 21,
expected: 22,
}
));
}
}