tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 30af8957ec13b7eef5534d82f9092c211bdfd393
parent b69b5d6b4bccde2fe03f2ff7241c0e30d2600162
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 20:41:32 -0700

crypto: add bounded verification service

Diffstat:
MCargo.lock | 28++++++++++++++++++++++++++++
Mcrates/tangle_crypto/Cargo.toml | 1+
Mcrates/tangle_crypto/src/lib.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 154 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -212,6 +212,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -373,6 +379,7 @@ dependencies = [ "k256", "sha2", "tangle_protocol", + "tokio", ] [[package]] @@ -383,6 +390,27 @@ dependencies = [ ] [[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/tangle_crypto/Cargo.toml b/crates/tangle_crypto/Cargo.toml @@ -11,6 +11,7 @@ description = "Nostr event hashing and signature verification for tangle" k256 = { version = "0.13", features = ["schnorr"] } sha2 = "0.10" tangle_protocol = { path = "../tangle_protocol" } +tokio = { version = "1", features = ["macros", "rt", "sync", "time"] } [lints] workspace = true diff --git a/crates/tangle_crypto/src/lib.rs b/crates/tangle_crypto/src/lib.rs @@ -1,9 +1,12 @@ #![forbid(unsafe_code)] +use std::sync::Arc; + use k256::schnorr::signature::Verifier; use k256::schnorr::{Signature, VerifyingKey}; use sha2::{Digest, Sha256}; use tangle_protocol::{Event, EventId, UnsignedEvent, canonical_event_json}; +use tokio::sync::Semaphore; pub fn compute_event_id(event: &UnsignedEvent) -> EventId { let event_id = compute_event_id_hex(event); @@ -54,6 +57,61 @@ pub fn verify_event_signature(event: &Event) -> Result<(), String> { .map_err(|_| "event signature verification failed".to_owned()) } +#[derive(Clone, Debug)] +pub struct VerificationService { + semaphore: Arc<Semaphore>, + max_concurrent: usize, +} + +impl VerificationService { + pub fn new(max_concurrent: usize) -> Result<Self, String> { + if max_concurrent == 0 { + return Err("verification concurrency limit must be greater than zero".to_owned()); + } + Ok(Self { + semaphore: Arc::new(Semaphore::new(max_concurrent)), + max_concurrent, + }) + } + + pub fn max_concurrent(&self) -> usize { + self.max_concurrent + } + + pub fn available_permits(&self) -> usize { + self.semaphore.available_permits() + } + + pub fn close(&self) { + self.semaphore.close(); + } + + pub async fn verify_event(&self, event: &Event) -> Result<VerificationOutcome, String> { + let permit = self + .semaphore + .clone() + .acquire_owned() + .await + .map_err(|_| "verification service is closed".to_owned())?; + let result = verify_event_signature(event); + drop(permit); + result.map(|_| VerificationOutcome { + event_id: event.id().clone(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerificationOutcome { + event_id: EventId, +} + +impl VerificationOutcome { + pub fn event_id(&self) -> &EventId { + &self.event_id + } +} + fn lower_hex(bytes: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut output = String::with_capacity(bytes.len() * 2); @@ -95,14 +153,16 @@ fn hex_value(value: u8, scalar: &str) -> Result<u8, String> { #[cfg(test)] mod tests { use super::{ - compute_event_id, compute_event_id_hex, event_id_matches, fixed_hex_bytes, lower_hex, - verify_event_id, verify_event_signature, + VerificationService, compute_event_id, compute_event_id_hex, event_id_matches, + fixed_hex_bytes, lower_hex, verify_event_id, verify_event_signature, }; use k256::schnorr::signature::Signer; use k256::schnorr::{Signature, SigningKey}; + use std::time::Duration; use tangle_protocol::{ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, }; + use tokio::time::timeout; #[test] fn event_id_hashes_canonical_event_bytes() { @@ -229,6 +289,69 @@ mod tests { ); } + #[tokio::test] + async fn verification_service_accepts_valid_events_and_rejects_invalid_events() { + let event = signed_event(); + let invalid = Event::new( + EventId::new(&"f".repeat(EventId::HEX_LENGTH)).expect("id"), + event.unsigned().clone(), + event.sig().clone(), + ); + let service = VerificationService::new(2).expect("service"); + + let outcome = service.verify_event(&event).await.expect("verified"); + + assert_eq!(service.max_concurrent(), 2); + assert_eq!(service.available_permits(), 2); + assert_eq!(outcome.event_id(), event.id()); + assert!( + service + .verify_event(&invalid) + .await + .expect_err("invalid") + .starts_with("event id mismatch") + ); + } + + #[tokio::test] + async fn verification_service_enforces_limit_and_reports_closed_state() { + let event = signed_event(); + let service = VerificationService::new(1).expect("service"); + let permit = service + .semaphore + .clone() + .acquire_owned() + .await + .expect("permit"); + + assert_eq!(service.available_permits(), 0); + assert!( + timeout(Duration::from_millis(20), service.verify_event(&event)) + .await + .is_err() + ); + drop(permit); + assert!( + timeout(Duration::from_secs(1), service.verify_event(&event)) + .await + .expect("timeout") + .is_ok() + ); + service.close(); + assert_eq!( + service.verify_event(&event).await.expect_err("closed"), + "verification service is closed" + ); + } + + #[test] + fn verification_service_rejects_zero_limit() { + assert_eq!( + VerificationService::new(0).expect_err("zero"), + "verification concurrency limit must be greater than zero" + ); + } + fn unsigned_event(tags: Vec<Tag>, content: &str) -> UnsignedEvent { UnsignedEvent::new( PublicKeyHex::new(&"1".repeat(PublicKeyHex::HEX_LENGTH)).expect("pubkey"),