diff --git a/Cargo.lock b/Cargo.lock index b4c03b4..5900d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,12 +52,27 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -152,6 +167,18 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -174,8 +201,8 @@ dependencies = [ [[package]] name = "cultcache-rs" -version = "0.2.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=3bf1c0cea53e0092187fa2e59a75ba1571947ffe#3bf1c0cea53e0092187fa2e59a75ba1571947ffe" +version = "0.3.1" +source = "git+https://github.com/GameCult/CultLib.git?rev=7665df33ae1b4335c3343914e2b85fde01d48311#7665df33ae1b4335c3343914e2b85fde01d48311" dependencies = [ "anyhow", "chrono", @@ -186,14 +213,16 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "serde_stacker", + "stacker", "uuid", "windows-sys 0.59.0", ] [[package]] name = "cultcache-rs-derive" -version = "0.1.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=3bf1c0cea53e0092187fa2e59a75ba1571947ffe#3bf1c0cea53e0092187fa2e59a75ba1571947ffe" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=7665df33ae1b4335c3343914e2b85fde01d48311#7665df33ae1b4335c3343914e2b85fde01d48311" dependencies = [ "proc-macro2", "quote", @@ -202,19 +231,21 @@ dependencies = [ [[package]] name = "cultmesh-rs" -version = "0.1.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=3bf1c0cea53e0092187fa2e59a75ba1571947ffe#3bf1c0cea53e0092187fa2e59a75ba1571947ffe" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=7665df33ae1b4335c3343914e2b85fde01d48311#7665df33ae1b4335c3343914e2b85fde01d48311" dependencies = [ "anyhow", + "base64", "cultcache-rs", "cultnet-rs", + "p256", "serde", ] [[package]] name = "cultnet-rs" -version = "0.1.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=3bf1c0cea53e0092187fa2e59a75ba1571947ffe#3bf1c0cea53e0092187fa2e59a75ba1571947ffe" +version = "0.3.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=7665df33ae1b4335c3343914e2b85fde01d48311#7665df33ae1b4335c3343914e2b85fde01d48311" dependencies = [ "aes-gcm", "anyhow", @@ -222,7 +253,9 @@ dependencies = [ "chrono", "cultcache-rs", "ed25519-dalek", + "fs2", "hmac", + "libc", "rand", "rmp-serde", "rmpv", @@ -286,10 +319,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -314,6 +362,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -336,6 +403,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -396,6 +473,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -444,6 +522,17 @@ dependencies = [ "polyval", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -582,6 +671,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + [[package]] name = "odin-core" version = "0.1.0" @@ -624,6 +722,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -681,6 +791,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -690,6 +809,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.45" @@ -758,6 +887,16 @@ dependencies = [ "libc", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rmp" version = "0.8.15" @@ -816,6 +955,20 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.28" @@ -875,6 +1028,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_stacker" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" +dependencies = [ + "serde", + "serde_core", + "stacker", +] + [[package]] name = "sha2" version = "0.10.9" @@ -918,6 +1082,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] @@ -947,6 +1112,19 @@ dependencies = [ "der", ] +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/crates/odin-core/Cargo.toml b/crates/odin-core/Cargo.toml index 76d2955..508bfbb 100644 --- a/crates/odin-core/Cargo.toml +++ b/crates/odin-core/Cargo.toml @@ -11,9 +11,9 @@ path = "src/lib.rs" [dependencies] anyhow = "1" -cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultcache-rs" } -cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultmesh-rs" } -cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultnet-rs" } +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultcache-rs" } +cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultmesh-rs" } +cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultnet-rs" } serde_json = "1" serde = { version = "1", features = ["derive"] } diff --git a/crates/odin-daemon/Cargo.toml b/crates/odin-daemon/Cargo.toml index a5707d5..268e841 100644 --- a/crates/odin-daemon/Cargo.toml +++ b/crates/odin-daemon/Cargo.toml @@ -13,9 +13,9 @@ path = "src/lib.rs" signal-hook = "0.3" anyhow = "1" chrono = "0.4" -cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultcache-rs" } -cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultmesh-rs" } -cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "3bf1c0cea53e0092187fa2e59a75ba1571947ffe", package = "cultnet-rs" } +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultcache-rs" } +cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultmesh-rs" } +cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "7665df33ae1b4335c3343914e2b85fde01d48311", package = "cultnet-rs" } fs2 = "0.4" rmp-serde = "1.3" serde = "1" diff --git a/crates/odin-daemon/src/main.rs b/crates/odin-daemon/src/main.rs index d7d5c39..2e59cb4 100644 --- a/crates/odin-daemon/src/main.rs +++ b/crates/odin-daemon/src/main.rs @@ -1930,6 +1930,122 @@ mod tests { Ok(()) } + /// A receipt-capable publisher: says hello with both receipt frames, puts + /// each document under its own id, and returns the schema frames Odin + /// answers with (its hello first, then one answer per put). + fn receipt_peer( + target: SocketAddr, + connection_id: u32, + puts: Vec<(&'static str, &'static str, &'static str)>, + ) -> Result> { + let socket = UdpSocket::bind("127.0.0.1:0")?; + socket.set_read_timeout(Some(Duration::from_millis(100)))?; + let mut transport = CultNetRudpSocketTransportConnection::new( + CultNetRudpSocketTransportOptions::client("receipt-peer", socket, target, connection_id), + )?; + transport.connect(Vec::new())?; + let deadline = Instant::now() + Duration::from_secs(5); + while !transport.connected() { + let _ = transport.receive_once()?; + transport.poll_resends()?; + ensure!(Instant::now() < deadline, "timed out connecting"); + } + let mut send = |message: CultNetMessage| -> Result<()> { + transport.send_reliable( + "schema", + encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?, + )?; + Ok(()) + }; + send(CultNetMessage::Hello { + runtime_id: "receipt-peer".into(), + runtime_kind: "test".into(), + agent_id: None, + role: None, + display_name: None, + supported_document_types: None, + supported_mutation_contracts: None, + supported_message_versions: Some(vec![ + "cultnet.write_admitted.v0".into(), + "cultnet.error.v1".into(), + ]), + transport_profiles: None, + supports_schema_catalog: None, + })?; + for (id, schema, key) in &puts { + send(CultNetMessage::DocumentPutRaw { + message_id: (*id).into(), + document: CultNetRawDocumentRecord { + schema_id: (*schema).into(), + record_key: (*key).into(), + stored_at: rfc3339_millis(unix_millis()?)?, + payload_encoding: CultNetRawPayloadEncoding::Messagepack, + payload: vec![1], + source_runtime_id: None, + source_agent_id: None, + source_role: None, + tags: None, + }, + })?; + } + let mut frames = Vec::new(); + let deadline = Instant::now() + Duration::from_secs(5); + while frames.len() < puts.len() + 1 && Instant::now() < deadline { + if let Some(frame) = transport.receive_once()? { + if frame.channel_id == "schema" { + frames.push(cultnet_rs::decode_cultnet_message_from_slice( + &frame.payload, + CultNetWireContract::CultNetSchemaV0, + )?); + } + } + transport.poll_resends()?; + } + Ok(frames) + } + + /// The CultNet write receipt through Odin's own sink and server: an + /// admitted put is answered `write_admitted` carrying its id, and a put the + /// sink refuses is answered `error.v1` carrying its id, never dropped. + #[test] + fn a_receipt_peer_is_told_admitted_or_refused_for_each_put_by_id() -> Result<()> { + let mut odin = activated_odin()?; + let target = odin.server.local_addr()?; + let frames = odin.serve_while(move || { + receipt_peer( + target, + PEER_CONNECTION_BASE + 2, + vec![ + ("ok-1", "ghostlight.doc.v1", "doc-1"), + ("refused-1", "odin.topology_publisher_watermark.v1", TARGET), + ], + ) + })??; + assert_eq!(frames.len(), 3, "hello and one answer per put: {frames:?}"); + assert!(matches!(&frames[0], CultNetMessage::Hello { .. })); + assert_eq!( + frames[1], + CultNetMessage::WriteAdmitted { + message_id: "ok-1".into(), + operation: "put".into(), + schema_id: "ghostlight.doc.v1".into(), + record_key: "doc-1".into(), + } + ); + let CultNetMessage::ErrorV1 { message_id, .. } = &frames[2] else { + panic!("a refused put must be answered error.v1: {:?}", frames[2]); + }; + assert_eq!(message_id.as_deref(), Some("refused-1")); + assert_eq!(odin.server.session_count(), 1, "the refusal kept the session"); + let stored: BTreeSet = SingleFileMessagePackBackingStore::new(&odin.store) + .pull_all_read_only_snapshot()? + .into_iter() + .map(|entry| entry.key) + .collect(); + assert!(stored.contains("doc-1"), "the admitted put is stored"); + Ok(()) + } + /// Soul's 70-lingering-session probe: with the server's session table /// full of publishers that never Disconnect, Odin's own heartbeat still /// lands. The table is Odin's own production server bound to the candidate