commit a4efd068975fe63435e2ba566a5d75e0bf881cbc
parent ff7485ed723cc45a5824fc0b061374cedf1dfade
Author: triesap <tyson@radroots.org>
Date: Fri, 5 Jun 2026 20:37:10 -0700
crypto: add nostr event id hashing
Diffstat:
4 files changed, 212 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3,12 +3,72 @@
version = 4
[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -75,6 +135,17 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -90,6 +161,14 @@ name = "tangle"
version = "0.1.0"
[[package]]
+name = "tangle_crypto"
+version = "0.1.0"
+dependencies = [
+ "sha2",
+ "tangle_protocol",
+]
+
+[[package]]
name = "tangle_protocol"
version = "0.1.0"
dependencies = [
@@ -97,12 +176,24 @@ dependencies = [
]
[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-members = ["crates/tangle", "crates/tangle_protocol"]
+members = ["crates/tangle", "crates/tangle_crypto", "crates/tangle_protocol"]
resolver = "2"
[workspace.package]
diff --git a/crates/tangle_crypto/Cargo.toml b/crates/tangle_crypto/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "tangle_crypto"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+rust-version.workspace = true
+license.workspace = true
+description = "Nostr event hashing and signature verification for tangle"
+
+[dependencies]
+sha2 = "0.10"
+tangle_protocol = { path = "../tangle_protocol" }
+
+[lints]
+workspace = true
diff --git a/crates/tangle_crypto/src/lib.rs b/crates/tangle_crypto/src/lib.rs
@@ -0,0 +1,105 @@
+#![forbid(unsafe_code)]
+
+use sha2::{Digest, Sha256};
+use tangle_protocol::{Event, EventId, UnsignedEvent, canonical_event_json};
+
+pub fn compute_event_id(event: &UnsignedEvent) -> EventId {
+ let event_id = compute_event_id_hex(event);
+ EventId::new(&event_id).expect("sha256 emits 32-byte lowercase hex")
+}
+
+pub fn compute_event_id_hex(event: &UnsignedEvent) -> String {
+ let canonical = canonical_event_json(event);
+ let digest = Sha256::digest(canonical.as_bytes());
+ lower_hex(&digest)
+}
+
+pub fn event_id_matches(event: &Event) -> bool {
+ compute_event_id(event.unsigned()) == *event.id()
+}
+
+pub fn verify_event_id(event: &Event) -> Result<(), String> {
+ let expected = compute_event_id(event.unsigned());
+ if event.id() == &expected {
+ Ok(())
+ } else {
+ Err(format!(
+ "event id mismatch: expected {}, got {}",
+ expected,
+ event.id()
+ ))
+ }
+}
+
+fn lower_hex(bytes: &[u8]) -> String {
+ const HEX: &[u8; 16] = b"0123456789abcdef";
+ let mut output = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ output.push(char::from(HEX[usize::from(byte >> 4)]));
+ output.push(char::from(HEX[usize::from(byte & 0x0f)]));
+ }
+ output
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{compute_event_id, compute_event_id_hex, event_id_matches, verify_event_id};
+ use tangle_protocol::{
+ Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent,
+ };
+
+ #[test]
+ fn event_id_hashes_canonical_event_bytes() {
+ let event = unsigned_event(Vec::new(), "");
+
+ assert_eq!(
+ compute_event_id_hex(&event),
+ "da90287b43a114ad00f2a87854947df1251b9a0f148b1707b9241c73f11569ae"
+ );
+ assert_eq!(
+ compute_event_id(&event).as_str(),
+ "da90287b43a114ad00f2a87854947df1251b9a0f148b1707b9241c73f11569ae"
+ );
+ }
+
+ #[test]
+ fn event_id_verification_reports_match_and_mismatch() {
+ let unsigned = unsigned_event(
+ vec![Tag::from_parts("t", &["radroots"]).expect("tag")],
+ "radroots cafe",
+ );
+ let event_id = compute_event_id(&unsigned);
+ let event = Event::new(
+ event_id,
+ unsigned.clone(),
+ SignatureHex::new(&"b".repeat(SignatureHex::HEX_LENGTH)).expect("sig"),
+ );
+ let wrong_event = Event::new(
+ EventId::new(&"f".repeat(EventId::HEX_LENGTH)).expect("id"),
+ unsigned,
+ SignatureHex::new(&"b".repeat(SignatureHex::HEX_LENGTH)).expect("sig"),
+ );
+
+ assert!(event_id_matches(&event));
+ assert_eq!(verify_event_id(&event), Ok(()));
+ assert!(!event_id_matches(&wrong_event));
+ assert_eq!(
+ verify_event_id(&wrong_event).expect_err("mismatch"),
+ format!(
+ "event id mismatch: expected {}, got {}",
+ compute_event_id(wrong_event.unsigned()),
+ wrong_event.id()
+ )
+ );
+ }
+
+ fn unsigned_event(tags: Vec<Tag>, content: &str) -> UnsignedEvent {
+ UnsignedEvent::new(
+ PublicKeyHex::new(&"1".repeat(PublicKeyHex::HEX_LENGTH)).expect("pubkey"),
+ UnixTimestamp::new(1_714_124_433),
+ Kind::new(1).expect("kind"),
+ tags,
+ content,
+ )
+ }
+}