commit 30af8957ec13b7eef5534d82f9092c211bdfd393
parent b69b5d6b4bccde2fe03f2ff7241c0e30d2600162
Author: triesap <tyson@radroots.org>
Date: Fri, 5 Jun 2026 20:41:32 -0700
crypto: add bounded verification service
Diffstat:
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"),