tangle


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

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:
MCargo.lock | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 2+-
Acrates/tangle_crypto/Cargo.toml | 15+++++++++++++++
Acrates/tangle_crypto/src/lib.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, + ) + } +}