lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 426c0ab6287f5a8bbcb36eced40eaafa147a2215
parent 086041a2ad73228468954103bf2d307f7dd9c945
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 05:54:22 +0000

transport: require verified signed events

- Verify signer results with BIP340 before returning trusted typestate.
- Require verified events across outbox reload, runtime, and Nostr dispatch.
- Reject corrupted signatures before event-store or adapter mutation.
- Refresh release, feature-matrix, and authenticated outbox manifests.

Diffstat:
MCHANGELOG.md | 6++++++
Mcontracts/outbox_feature_matrix.toml | 1-
Mcontracts/releases/1.0.0-alpha.1.toml | 11+++++++++++
Mcrates/authority/Cargo.toml | 8+++++++-
Mcrates/authority/src/authorization.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mcrates/authority/src/error.rs | 21+++++++++++++++++++++
Mcrates/outbox/Cargo.toml | 4++--
Mcrates/outbox/contracts/migration_authority_v1.manifest.json | 25++++++++++++-------------
Mcrates/outbox/contracts/migration_authority_v1.manifest.sha256 | 2+-
Mcrates/outbox/contracts/phase1_publication_v1.manifest.json | 12++++++------
Mcrates/outbox/contracts/phase1_publication_v1.manifest.sha256 | 2+-
Mcrates/outbox/src/error.rs | 3+++
Mcrates/outbox/src/model.rs | 14+++++++-------
Mcrates/outbox/src/store.rs | 221+++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/runtime/Cargo.toml | 3++-
Mcrates/runtime/src/transport.rs | 84+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/transport/src/error.rs | 2++
Mcrates/transport_nostr/Cargo.toml | 10++++++++--
Mcrates/transport_nostr/src/outbox.rs | 24++++++++++++++----------
Mcrates/transport_nostr/src/publish.rs | 33+++++++++++++++++++++------------
Mcrates/transport_nostr/tests/transport.rs | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
21 files changed, 453 insertions(+), 227 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -39,6 +39,12 @@ publish policy both pass for the same source revision. signed-event bytes are immutable, while dispatch intent, uncertain results, receipts, and accepted-observation repair identities remain durable without performing network publication. +<!-- release-change: verified-signed-publication-boundaries --> +- Authority signing now verifies BIP340 signatures before returning trusted + results. Outbox inputs, persisted reloads, runtime dispatch and inbound + observations, and direct Nostr publication require the verified signed-event + typestate; invalid or corrupted signatures fail before durable, event-store, + or relay-adapter mutation. - Event-store schema initialization now uses a transactional, checksummed migration authority with exact legacy-baseline adoption, shared-database catalog scoping, tamper-evident fail-closed managed history, exact catalog diff --git a/contracts/outbox_feature_matrix.toml b/contracts/outbox_feature_matrix.toml @@ -8,7 +8,6 @@ sqlite = [ "dep:radroots_event_codec", "dep:sqlx", "dep:url", - "radroots_event/signature", "sqlx/sqlite-bundled", ] runtime-tokio = ["sqlite", "sqlx/runtime-tokio", "dep:tokio"] diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -573,3 +573,14 @@ semver_impacts = [ "change_exported_algorithm_behavior", ] summary = "Add authenticated schema version 2 and a sealed Phase 1 publication state machine that persists exact artifact, readiness, signed-event, target-policy, dispatch-intent, receipt, and observation-repair authority under bounded canonical targets, stable raw fixed-width identities, revision compare-and-swap, and opaque expiring claims without performing network publication." + +[[changes]] +id = "verified-signed-publication-boundaries" +classification = "breaking" +semver_impacts = [ + "change_exported_field_type", + "change_exported_function_signature", + "add_enum_variant", + "change_exported_algorithm_behavior", +] +summary = "Require BIP340-verified signed-event typestate at authority, outbox, runtime, inbound-observation, and direct Nostr publication boundaries; reject invalid signatures before durable, event-store, or adapter mutation, including corrupted stored signatures on reload." diff --git a/crates/authority/Cargo.toml b/crates/authority/Cargo.toml @@ -21,10 +21,16 @@ local_signer = [ ] [dependencies] -radroots_event = { workspace = true, default-features = false } +radroots_event = { workspace = true, default-features = false, features = [ + "signature", +] } radroots_nostr = { workspace = true, optional = true, default-features = false } [dev-dependencies] +radroots_nostr = { workspace = true, default-features = false, features = [ + "events", + "std", +] } serde_json = { workspace = true, default-features = false, features = [ "alloc", ] } diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs @@ -3,7 +3,7 @@ use crate::{RadrootsActorContext, RadrootsAuthorityError, RadrootsEventSigner}; use radroots_event::contract::{RadrootsEventContract, event_contract}; use radroots_event::draft::{ - RadrootsDraftError, RadrootsEventDraft, RadrootsSignedEvent, + RadrootsDraftError, RadrootsEventDraft, RadrootsSignedEvent, RadrootsVerifiedSignedEvent, validate_signed_nostr_event_matches_draft, }; #[cfg(test)] @@ -75,7 +75,7 @@ pub fn sign_authorized_draft<S>( actor: &RadrootsActorContext, signer: &S, draft: &RadrootsEventDraft, -) -> Result<RadrootsSignedEvent, RadrootsAuthorityError> +) -> Result<RadrootsVerifiedSignedEvent, RadrootsAuthorityError> where S: RadrootsEventSigner + ?Sized, { @@ -87,7 +87,7 @@ fn sign_authorized_draft_with_validator<S, V>( signer: &S, draft: &RadrootsEventDraft, validate_draft: V, -) -> Result<RadrootsSignedEvent, RadrootsAuthorityError> +) -> Result<RadrootsVerifiedSignedEvent, RadrootsAuthorityError> where S: RadrootsEventSigner + ?Sized, V: FnOnce(&RadrootsEventDraft) -> Result<(), RadrootsDraftError>, @@ -97,7 +97,9 @@ where validate_draft(draft).map_err(RadrootsAuthorityError::DraftValidation)?; let signed_event = signer.sign_frozen_draft(draft)?; validate_signed_event_matches_draft(&signed_event, draft)?; - Ok(signed_event) + signed_event + .verify_signature() + .map_err(RadrootsAuthorityError::SignedEventSignatureVerification) } pub fn validate_signed_event_matches_draft( @@ -175,6 +177,14 @@ mod tests { }; use radroots_event::ids::RadrootsPublicKey; use radroots_event::kinds::{KIND_CLASSIFIED_LISTING, KIND_POST, KIND_TRADE_PROPOSAL}; + use radroots_nostr::prelude::{ + RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft, + }; + + const FIXTURE_ALICE_SECRET_KEY_HEX: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; + const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; fn hex_64(character: char) -> String { std::iter::repeat_n(character, 64).collect() @@ -345,6 +355,40 @@ mod tests { } } + struct ValidSigner { + keys: RadrootsNostrKeys, + pubkey: RadrootsPublicKey, + } + + impl ValidSigner { + fn fixture() -> Self { + let secret_key = RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX) + .expect("fixture secret key"); + Self { + keys: RadrootsNostrKeys::new(secret_key), + pubkey: RadrootsPublicKey::parse(FIXTURE_ALICE_PUBLIC_KEY_HEX) + .expect("fixture public key"), + } + } + } + + impl RadrootsEventSigner for ValidSigner { + fn pubkey(&self) -> &RadrootsPublicKey { + &self.pubkey + } + + fn sign_frozen_draft( + &self, + draft: &RadrootsEventDraft, + ) -> Result<RadrootsSignedEvent, RadrootsSignerError> { + radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| { + RadrootsSignerError::SigningFailed { + message: error.to_string(), + } + }) + } + } + fn signed_event_from_draft(draft: &RadrootsEventDraft) -> RadrootsSignedEvent { signed_event_from_parts( draft.expected_pubkey_str().to_owned(), @@ -632,17 +676,35 @@ mod tests { } #[test] - fn authorized_actor_and_signer_return_signed_event() { + fn authorized_actor_and_signer_return_verified_signed_event() { + let draft = operational_listing_event_draft(FIXTURE_ALICE_PUBLIC_KEY_HEX); + let actor = seller_actor(FIXTURE_ALICE_PUBLIC_KEY_HEX); + let signer = ValidSigner::fixture(); + + let signed = sign_authorized_draft(&actor, &signer, &draft).expect("signed"); + + assert_eq!( + signed.signed_event().id_str(), + draft.expected_event_id_str() + ); + assert_eq!( + signed.signed_event().pubkey_str(), + draft.expected_pubkey_str() + ); + assert_eq!(signed.signed_event().kind(), KIND_CLASSIFIED_LISTING); + } + + #[test] + fn matching_id_with_invalid_signature_is_rejected() { let pubkey = hex_64('a'); let draft = operational_listing_event_draft(pubkey.as_str()); let actor = seller_actor(pubkey.as_str()); let signer = StaticSigner::new(pubkey.as_str()); - let signed = sign_authorized_draft(&actor, &signer, &draft).expect("signed"); - - assert_eq!(signed.id_str(), draft.expected_event_id_str()); - assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str()); - assert_eq!(signed.kind(), KIND_CLASSIFIED_LISTING); + assert!(matches!( + sign_authorized_draft(&actor, &signer, &draft), + Err(RadrootsAuthorityError::SignedEventSignatureVerification(_)) + )); } #[test] diff --git a/crates/authority/src/error.rs b/crates/authority/src/error.rs @@ -89,6 +89,8 @@ pub enum RadrootsAuthorityError { computed_event_id: String, }, + SignedEventSignatureVerification(radroots_event::draft::RadrootsSignatureVerificationError), + Signer(RadrootsSignerError), } @@ -201,6 +203,9 @@ impl fmt::Display for RadrootsAuthorityError { f, "signed event computed id mismatch: expected {expected_event_id}, computed {computed_event_id}" ), + Self::SignedEventSignatureVerification(error) => { + write!(f, "signed event signature verification failed: {error}") + } Self::Signer(error) => write!(f, "signer error: {error}"), } } @@ -211,6 +216,7 @@ impl std::error::Error for RadrootsAuthorityError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::DraftValidation(error) => Some(error), + Self::SignedEventSignatureVerification(error) => Some(error), Self::Signer(error) => Some(error), _ => None, } @@ -404,6 +410,21 @@ mod tests { authority_error.source().expect("signer source").to_string(), "signing failed: deterministic failure" ); + + let signature_error = RadrootsAuthorityError::SignedEventSignatureVerification( + radroots_event::draft::RadrootsSignatureVerificationError::VerificationFailed, + ); + assert_eq!( + signature_error.to_string(), + "signed event signature verification failed: signed event signature verification failed" + ); + assert_eq!( + signature_error + .source() + .expect("signature verification source") + .to_string(), + "signed event signature verification failed" + ); } #[test] diff --git a/crates/outbox/Cargo.toml b/crates/outbox/Cargo.toml @@ -18,7 +18,6 @@ sqlite = [ "dep:radroots_event_codec", "dep:sqlx", "dep:url", - "radroots_event/signature", "sqlx/sqlite-bundled", ] runtime-tokio = ["sqlite", "sqlx/runtime-tokio", "dep:tokio"] @@ -31,8 +30,9 @@ event-store-adapter = [ [dependencies] radroots_event = { workspace = true, default-features = false, features = [ - "std", "serde", + "signature", + "std", ] } radroots_event_store = { workspace = true, default-features = false, optional = true } radroots_event_codec = { workspace = true, default-features = false, features = [ diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json @@ -32,7 +32,6 @@ "dep:radroots_event_codec", "dep:sqlx", "dep:url", - "radroots_event/signature", "sqlx/sqlite-bundled" ], "feature": "sqlite" @@ -83,10 +82,10 @@ } ], "source": { - "byte_length": 1001, + "byte_length": 971, "hash_algorithm": "sha256_bytes_v1", "path": "contracts/outbox_feature_matrix.toml", - "sha256": "bc6578853c07f8901019e007e1cd9d7e87c41e0ef92425ad17781e528bcd15a1" + "sha256": "a668160e547ac00b6ff9bb8d64277190fc62eb45a63ec343b6b1771440c398cb" } }, "generated_runtime": { @@ -216,10 +215,10 @@ "source_files": [ { "file": { - "byte_length": 1873, + "byte_length": 1858, "hash_algorithm": "sha256_bytes_v1", "path": "crates/outbox/Cargo.toml", - "sha256": "95364a40f818d5d7aa4589a42774e650b590e1e1e7d4c3ed4dc8220cc8cadcbf" + "sha256": "0c1df46ac2450865c4fca94d6426395a8e4505a028d4507f9addd0400852d78e" }, "role": "outbox_package_manifest" }, @@ -234,10 +233,10 @@ }, { "file": { - "byte_length": 11111, + "byte_length": 11269, "hash_algorithm": "sha256_bytes_v1", "path": "crates/outbox/src/error.rs", - "sha256": "6f7a918670700696c6e853e45c2b722a1f68551334ca709e77c148f4617809a9" + "sha256": "496eca49b17431890251224b9d1818370a8174056f2dab57002f224dbfdb917d" }, "role": "outbox_error_surface" }, @@ -279,10 +278,10 @@ }, { "file": { - "byte_length": 318698, + "byte_length": 320981, "hash_algorithm": "sha256_bytes_v1", "path": "crates/outbox/src/store.rs", - "sha256": "7f324a44658eb670ab82d30eaa80fe290a93f1cc3203e94ec62517ab015182c9" + "sha256": "6c983dc2854dbb62de093cf748e0b761965f37c32bcf5d2f9bcaee56eb2f2d99" }, "role": "store_integration" }, @@ -351,19 +350,19 @@ }, { "file": { - "byte_length": 25965, + "byte_length": 26484, "hash_algorithm": "sha256_bytes_v1", "path": "contracts/releases/1.0.0-alpha.1.toml", - "sha256": "a23dae6f24e7d4202826970b2f8c6a7d942d79e068c3ab1f17e5fc96e0ca1603" + "sha256": "331a32e5ca7ba615a04e5837a1a06173f9d1d5ba663103255206d9dd39097dd8" }, "role": "release_record" }, { "file": { - "byte_length": 36955, + "byte_length": 37356, "hash_algorithm": "sha256_bytes_v1", "path": "CHANGELOG.md", - "sha256": "68e74a02a28a5c24643befca7038d143c5f5e6f941e314e98b1d6dfc1094e28d" + "sha256": "bb26037e84cfb120ba372320dc15b5e1e865f49a8835b8635847df3bf9ab25f5" }, "role": "release_notes" } diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256 @@ -1 +1 @@ -94a67ed7f4726eb45fc8481cf231a8129a776d4a73918a2bb371811763fa2589 +2494868f0acb947a36e1adb0d3829203de7b54c731e401d705ac7dec41566964 diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.json b/crates/outbox/contracts/phase1_publication_v1.manifest.json @@ -48,9 +48,9 @@ "source_files": [ { "file": { - "byte_length": 1873, + "byte_length": 1858, "path": "crates/outbox/Cargo.toml", - "sha256": "95364a40f818d5d7aa4589a42774e650b590e1e1e7d4c3ed4dc8220cc8cadcbf" + "sha256": "0c1df46ac2450865c4fca94d6426395a8e4505a028d4507f9addd0400852d78e" }, "role": "outbox_package_manifest" }, @@ -128,17 +128,17 @@ }, { "file": { - "byte_length": 25965, + "byte_length": 26484, "path": "contracts/releases/1.0.0-alpha.1.toml", - "sha256": "a23dae6f24e7d4202826970b2f8c6a7d942d79e068c3ab1f17e5fc96e0ca1603" + "sha256": "331a32e5ca7ba615a04e5837a1a06173f9d1d5ba663103255206d9dd39097dd8" }, "role": "release_record" }, { "file": { - "byte_length": 36955, + "byte_length": 37356, "path": "CHANGELOG.md", - "sha256": "68e74a02a28a5c24643befca7038d143c5f5e6f941e314e98b1d6dfc1094e28d" + "sha256": "bb26037e84cfb120ba372320dc15b5e1e865f49a8835b8635847df3bf9ab25f5" }, "role": "release_notes" } diff --git a/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 b/crates/outbox/contracts/phase1_publication_v1.manifest.sha256 @@ -1 +1 @@ -99a1a2466aa97d3adb02def1c2cd20d7cc95fd364221c4e694ac6aa6e3c272d3 +b7be8bc95902ad61bdbd7ffcf9022fd28036b94e4a2abeff8119dfb21a1efc74 diff --git a/crates/outbox/src/error.rs b/crates/outbox/src/error.rs @@ -30,6 +30,9 @@ pub enum RadrootsOutboxError { #[error("Signed event error: {0}")] SignedEvent(#[from] radroots_event::draft::RadrootsSignedEventError), + #[error("Signed event signature verification failed: {0}")] + SignedEventSignature(#[from] radroots_event::draft::RadrootsSignatureVerificationError), + #[error("delivery targets cannot be empty")] EmptyDeliveryTargets, diff --git a/crates/outbox/src/model.rs b/crates/outbox/src/model.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] use crate::RadrootsOutboxError; -use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent}; +use radroots_event::draft::{RadrootsEventDraft, RadrootsVerifiedSignedEvent}; use radroots_event::ids::{RadrootsTradeId, RadrootsTradeMutationId}; use radroots_transport::{ RadrootsTransportKind, RadrootsTransportMeshScopeId, RadrootsTransportOutcomeKind, @@ -309,7 +309,7 @@ impl RadrootsOutboxOperationInput { pub struct RadrootsOutboxSignedOperationInput { pub operation_kind: String, pub draft: RadrootsEventDraft, - pub signed_event: RadrootsSignedEvent, + pub signed_event: RadrootsVerifiedSignedEvent, pub delivery_plan: RadrootsOutboxDeliveryPlanInput, pub idempotency_key: Option<String>, pub event_store_inserted: bool, @@ -321,7 +321,7 @@ impl RadrootsOutboxSignedOperationInput { pub fn new( operation_kind: impl Into<String>, draft: RadrootsEventDraft, - signed_event: RadrootsSignedEvent, + signed_event: RadrootsVerifiedSignedEvent, delivery_plan: RadrootsOutboxDeliveryPlanInput, event_store_inserted: bool, event_store_ingested_at_ms: i64, @@ -392,7 +392,7 @@ pub struct RadrootsOutboxSignedTradeMutationInput { pub mutation_id: RadrootsTradeMutationId, pub canonical_payload_sha256: String, pub draft: RadrootsEventDraft, - pub signed_event: RadrootsSignedEvent, + pub signed_event: RadrootsVerifiedSignedEvent, pub delivery_plan: RadrootsOutboxDeliveryPlanInput, pub idempotency_key: Option<String>, pub event_store_inserted: bool, @@ -408,7 +408,7 @@ impl RadrootsOutboxSignedTradeMutationInput { mutation_id: RadrootsTradeMutationId, canonical_payload_sha256: impl Into<String>, draft: RadrootsEventDraft, - signed_event: RadrootsSignedEvent, + signed_event: RadrootsVerifiedSignedEvent, delivery_plan: RadrootsOutboxDeliveryPlanInput, event_store_inserted: bool, event_store_ingested_at_ms: i64, @@ -481,7 +481,7 @@ pub struct RadrootsOutboxEventRecord { pub event_id: String, pub expected_pubkey: String, pub draft: RadrootsEventDraft, - pub signed_event: Option<RadrootsSignedEvent>, + pub signed_event: Option<RadrootsVerifiedSignedEvent>, pub raw_event_json: Option<String>, pub state: RadrootsOutboxEventState, pub attempt_count: i64, @@ -558,7 +558,7 @@ pub struct RadrootsOutboxClaimedEvent { pub claim_token: String, pub active_delivery_plan_id: Option<i64>, pub draft: RadrootsEventDraft, - pub signed_event: Option<RadrootsSignedEvent>, + pub signed_event: Option<RadrootsVerifiedSignedEvent>, pub delivery_targets: Vec<RadrootsOutboxDeliveryTargetRecord>, } diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -21,7 +21,8 @@ use crate::sqlite_lifecycle::{ }; use radroots_event::RadrootsEventKindClass; use radroots_event::draft::{ - RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft, + RadrootsEventDraft, RadrootsSignedEvent, RadrootsVerifiedSignedEvent, + validate_signed_nostr_event_matches_draft, }; use radroots_event::ids::{RadrootsTradeId, RadrootsTradeMutationId}; use radroots_event::kinds::TRADE_MUTATION_EVENT_KINDS; @@ -164,7 +165,7 @@ impl RadrootsOutbox { input: &RadrootsOutboxSignedOperationInput, ) -> Result<RadrootsOutboxIdempotencyPreflight, RadrootsOutboxError> { ensure_generic_outbox_draft_allowed(&input.draft)?; - validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; + validate_signed_nostr_event_matches_draft(input.signed_event.signed_event(), &input.draft)?; let prepared = prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( @@ -202,7 +203,7 @@ impl RadrootsOutbox { &self, input: &RadrootsOutboxSignedTradeMutationInput, ) -> Result<RadrootsOutboxIdempotencyPreflight, RadrootsOutboxError> { - validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; + validate_signed_nostr_event_matches_draft(input.signed_event.signed_event(), &input.draft)?; let semantic = validate_trade_mutation_input( input.trade_id.as_str(), input.mutation_id.as_str(), @@ -363,7 +364,7 @@ impl RadrootsOutbox { input: RadrootsOutboxSignedOperationInput, ) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> { ensure_generic_outbox_draft_allowed(&input.draft)?; - validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; + validate_signed_nostr_event_matches_draft(input.signed_event.signed_event(), &input.draft)?; let prepared = prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( @@ -443,7 +444,7 @@ impl RadrootsOutbox { .bind(input.draft.expected_pubkey_str()) .bind(draft_json.as_str()) .bind(signed_event_json.as_str()) - .bind(input.signed_event.raw_json()) + .bind(input.signed_event.signed_event().raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(input.created_at_ms) .bind(bool_i64(input.event_store_inserted)) @@ -619,7 +620,7 @@ impl RadrootsOutbox { &self, input: RadrootsOutboxSignedTradeMutationInput, ) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> { - validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; + validate_signed_nostr_event_matches_draft(input.signed_event.signed_event(), &input.draft)?; let semantic = validate_trade_mutation_input( input.trade_id.as_str(), input.mutation_id.as_str(), @@ -754,7 +755,7 @@ impl RadrootsOutbox { .bind(input.draft.expected_pubkey_str()) .bind(draft_json.as_str()) .bind(signed_event_json.as_str()) - .bind(input.signed_event.raw_json()) + .bind(input.signed_event.signed_event().raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(input.created_at_ms) .bind(bool_i64(input.event_store_inserted)) @@ -786,7 +787,7 @@ impl RadrootsOutbox { input: RadrootsOutboxSignedOperationInput, ) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> { ensure_generic_outbox_draft_allowed(&input.draft)?; - validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?; + validate_signed_nostr_event_matches_draft(input.signed_event.signed_event(), &input.draft)?; let prepared = prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?; let operation_digest = operation_idempotency_digest( @@ -863,7 +864,7 @@ impl RadrootsOutbox { .bind(input.draft.expected_pubkey_str()) .bind(draft_json.as_str()) .bind(signed_event_json.as_str()) - .bind(input.signed_event.raw_json()) + .bind(input.signed_event.signed_event().raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(input.created_at_ms) .bind(bool_i64(input.event_store_inserted)) @@ -1125,19 +1126,19 @@ impl RadrootsOutbox { &self, outbox_event_id: i64, claim_token: &str, - signed_event: RadrootsSignedEvent, + signed_event: RadrootsVerifiedSignedEvent, now_ms: i64, - ) -> Result<RadrootsSignedEvent, RadrootsOutboxError> { + ) -> Result<RadrootsVerifiedSignedEvent, RadrootsOutboxError> { let mut tx = self.pool.begin().await?; let record = event_by_id_tx(&mut tx, outbox_event_id).await?; let stored = record.claim_token.as_deref(); if stored != Some(claim_token) { return Err(RadrootsOutboxError::ClaimTokenMismatch { outbox_event_id }); } - if signed_event.id_str() != record.event_id { + if signed_event.signed_event().id_str() != record.event_id { return Err(RadrootsOutboxError::SignedEventIdMismatch { expected_event_id: record.event_id, - actual_event_id: signed_event.id_str().to_owned(), + actual_event_id: signed_event.signed_event().id_str().to_owned(), }); } let signed_event_json = signed_event_wire_json(&signed_event)?; @@ -1145,7 +1146,7 @@ impl RadrootsOutbox { "UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = ?, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, last_error = NULL, updated_at_ms = ? WHERE outbox_event_id = ? AND claim_token = ?", ) .bind(signed_event_json.as_str()) - .bind(signed_event.raw_json()) + .bind(signed_event.signed_event().raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(now_ms) .bind(outbox_event_id) @@ -1243,8 +1244,11 @@ impl RadrootsOutbox { RadrootsTransportObservationType::LocalImport, observed_at_ms, )?; - let ingest = RadrootsEventIngest::from_signed_event(signed_event.clone(), observed_at_ms)? - .with_observation(observation); + let ingest = RadrootsEventIngest::from_signed_event( + signed_event.signed_event().clone(), + observed_at_ms, + )? + .with_observation(observation); let receipt = event_store.ingest_event(ingest).await?; let event_store_inserted = receipt.persistence.is_inserted(); let changed = sqlx::query( @@ -2217,7 +2221,7 @@ async fn signed_event_lifecycle_for_plans( async fn ensure_event_signed( tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, outbox_event_id: i64, - signed_event: &RadrootsSignedEvent, + signed_event: &RadrootsVerifiedSignedEvent, event_store_inserted: bool, event_store_ingested_at_ms: i64, ) -> Result<(), RadrootsOutboxError> { @@ -2226,7 +2230,7 @@ async fn ensure_event_signed( "UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = CASE WHEN state IN ('draft_queued', 'sign_retryable', 'signing') THEN ? ELSE state END, event_store_ingested = 1, event_store_inserted = ?, event_store_ingested_at_ms = ? WHERE outbox_event_id = ? AND signed_event_json IS NULL", ) .bind(signed_event_json.as_str()) - .bind(signed_event.raw_json()) + .bind(signed_event.signed_event().raw_json()) .bind(RadrootsOutboxEventState::Signed.as_str()) .bind(bool_i64(event_store_inserted)) .bind(event_store_ingested_at_ms) @@ -2658,11 +2662,11 @@ fn event_from_row( row.try_get("raw_event_json")?, )?; if let Some(signed_event) = signed_event.as_ref() - && signed_event.id_str() != event_id + && signed_event.signed_event().id_str() != event_id { return Err(RadrootsOutboxError::SignedEventIdMismatch { expected_event_id: event_id, - actual_event_id: signed_event.id_str().to_owned(), + actual_event_id: signed_event.signed_event().id_str().to_owned(), }); } let state = RadrootsOutboxEventState::parse(row.try_get::<String, _>("state")?.as_str())?; @@ -2710,7 +2714,7 @@ fn signed_event_from_storage( outbox_event_id: i64, signed_event_json: Option<String>, raw_event_json: Option<String>, -) -> Result<Option<RadrootsSignedEvent>, RadrootsOutboxError> { +) -> Result<Option<RadrootsVerifiedSignedEvent>, RadrootsOutboxError> { match (signed_event_json, raw_event_json) { (None, None) => Ok(None), (Some(_), None) => Err(RadrootsOutboxError::StoredSignedEventMissingRawJson( @@ -2722,6 +2726,8 @@ fn signed_event_from_storage( (Some(signed_json), Some(raw_json)) => { let wire = RadrootsNip01EventWire::parse_json(signed_json.as_str())?; RadrootsSignedEvent::from_wire_verified_id(wire, raw_json) + .map_err(RadrootsOutboxError::from)? + .verify_signature() .map(Some) .map_err(Into::into) } @@ -2729,9 +2735,9 @@ fn signed_event_from_storage( } fn signed_event_wire_json( - signed_event: &RadrootsSignedEvent, + signed_event: &RadrootsVerifiedSignedEvent, ) -> Result<String, RadrootsOutboxError> { - serde_json::to_string(signed_event.wire()).map_err(Into::into) + serde_json::to_string(signed_event.signed_event().wire()).map_err(Into::into) } fn delivery_plan_from_row( @@ -3945,7 +3951,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), delivery_plan(vec![ nostr_target(NOSTR_PRIMARY_WSS), nostr_target(NOSTR_SECONDARY_WSS), @@ -3969,7 +3975,7 @@ mod tests { canonical.mutation_id.clone(), sha256_hex(canonical.content.as_bytes()), draft, - signed_event, + verified_signed_event(signed_event), delivery_plan(targets), true, created_at_ms + 7, @@ -4000,6 +4006,12 @@ mod tests { RadrootsNostrKeys::new(secret_key) } + fn verified_signed_event(signed_event: RadrootsSignedEvent) -> RadrootsVerifiedSignedEvent { + signed_event + .verify_signature() + .expect("fixture signature must verify") + } + async fn table_count(outbox: &RadrootsOutbox, table_name: &str) -> i64 { let sql = format!("SELECT COUNT(*) FROM {table_name}"); sqlx::query_scalar(sqlx::AssertSqlSafe(sql)) @@ -4500,7 +4512,10 @@ mod tests { sqlx::query( "UPDATE outbox_event SET signed_event_json = ?, event_id = ? WHERE outbox_event_id = ?", ) - .bind(signed_event_wire_json(&signed_event).expect("signed wire JSON")) + .bind( + signed_event_wire_json(&verified_signed_event(signed_event.clone())) + .expect("signed wire JSON"), + ) .bind(hex_64('0')) .bind(signed_receipt.outbox_event_id) .execute(outbox.pool()) @@ -4713,7 +4728,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft.clone(), - signed_event.clone(), + verified_signed_event(signed_event.clone()), delivery_plan(vec![nostr_target("wss://plan-one.example")]), true, 1_007, @@ -4728,7 +4743,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), delivery_plan(vec![nostr_target("wss://plan-two.example")]), true, 1_107, @@ -4893,7 +4908,7 @@ mod tests { .complete_signing( receipt.outbox_event_id, "wrong-claim", - signed.clone(), + verified_signed_event(signed.clone()), 1_010, ) .await, @@ -4905,7 +4920,12 @@ mod tests { .expect("sign different event"); assert!(matches!( outbox - .complete_signing(receipt.outbox_event_id, "sign-claim", other_signed, 1_020,) + .complete_signing( + receipt.outbox_event_id, + "sign-claim", + verified_signed_event(other_signed), + 1_020, + ) .await, Err(RadrootsOutboxError::SignedEventIdMismatch { .. }) )); @@ -4918,7 +4938,12 @@ mod tests { .expect("complete signing trigger"); assert!(matches!( outbox - .complete_signing(receipt.outbox_event_id, "sign-claim", signed.clone(), 1_030,) + .complete_signing( + receipt.outbox_event_id, + "sign-claim", + verified_signed_event(signed.clone()), + 1_030, + ) .await, Err(RadrootsOutboxError::ClaimTokenMismatch { .. }) )); @@ -4944,7 +4969,12 @@ mod tests { .expect("reclaimed unsigned"); assert_eq!(reclaimed.state, RadrootsOutboxEventState::Signing); outbox - .complete_signing(receipt.outbox_event_id, "sign-claim-two", signed, 1_050) + .complete_signing( + receipt.outbox_event_id, + "sign-claim-two", + verified_signed_event(signed), + 1_050, + ) .await .expect("complete signing"); @@ -5292,7 +5322,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_trade_mutation", draft, - signed_event, + verified_signed_event(signed_event), delivery_plan(vec![nostr_target(NOSTR_PRIMARY_WSS)]), true, 1_007, @@ -5425,14 +5455,20 @@ mod tests { .expect("event") .expect("event"); assert_eq!(event.state, RadrootsOutboxEventState::Signed); - assert_eq!(event.signed_event, Some(signed_event)); + assert_eq!( + event.signed_event, + Some(verified_signed_event(signed_event)) + ); let second = outbox .enqueue_signed_trade_mutation_operation( signed_trade_mutation_input( &canonical, draft, - event.signed_event.expect("stored signed event"), + event + .signed_event + .expect("stored signed event") + .into_signed_event(), vec![nostr_target("wss://relay-3.example.com")], 1_100, ) @@ -5624,7 +5660,7 @@ mod tests { canonical.mutation_id, "0".repeat(64), draft, - signed_event, + verified_signed_event(signed_event), delivery_plan(vec![nostr_target(NOSTR_PRIMARY_WSS)]), true, 1_007, @@ -5674,7 +5710,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.no_wait.local", 11, @@ -5946,7 +5982,10 @@ mod tests { .expect("event") .expect("event"); assert_eq!(event.state, RadrootsOutboxEventState::Signed); - assert_eq!(event.signed_event, Some(signed_event)); + assert_eq!( + event.signed_event, + Some(verified_signed_event(signed_event)) + ); let claimed = outbox .claim_next_ready_signed_event("publisher", "claim-a", 2_000, 1_000) @@ -6044,7 +6083,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -6155,7 +6194,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -6286,7 +6325,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -6393,7 +6432,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft.clone(), - signed_event.clone(), + verified_signed_event(signed_event.clone()), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.primary-plan", 1, @@ -6413,7 +6452,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.secondary-plan", 1, @@ -6536,7 +6575,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft.clone(), - signed_event.clone(), + verified_signed_event(signed_event.clone()), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.invalid_plan", 1, @@ -6556,7 +6595,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.sibling", 1, @@ -6679,7 +6718,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft.clone(), - signed_event.clone(), + verified_signed_event(signed_event.clone()), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.cancel", 1, @@ -6699,7 +6738,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.cancel.sibling", 1, @@ -6762,7 +6801,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 7, @@ -6872,7 +6911,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -6942,7 +6981,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7010,7 +7049,7 @@ mod tests { .preflight_signed_operation_idempotency(&RadrootsOutboxSignedOperationInput::new( "publish_post", default_behavior_draft, - default_behavior_signed_event, + verified_signed_event(default_behavior_signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7040,7 +7079,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft.clone(), - signed_event.clone(), + verified_signed_event(signed_event.clone()), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7060,7 +7099,7 @@ mod tests { RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7108,7 +7147,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", reject_draft, - reject_signed_event, + verified_signed_event(reject_signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7130,7 +7169,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", deferred_draft, - deferred_signed_event, + verified_signed_event(deferred_signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.reticulum.default", 1, @@ -7260,7 +7299,12 @@ mod tests { radroots_nostr_sign_frozen_draft(&fixture_keys(), &claimed.draft).expect("signed"); outbox - .complete_signing(receipt.outbox_event_id, "claim-a", signed, 1_100) + .complete_signing( + receipt.outbox_event_id, + "claim-a", + verified_signed_event(signed), + 1_100, + ) .await .expect("complete signing"); @@ -7299,7 +7343,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.explicit.multi_target", 1, @@ -7401,7 +7445,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.explicit.multi_target", 1, @@ -7438,7 +7482,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.explicit.multi_target", 1, @@ -7495,7 +7539,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -7603,7 +7647,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -7747,7 +7791,7 @@ mod tests { .enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new( "publish_post", draft, - signed_event, + verified_signed_event(signed_event), RadrootsOutboxDeliveryPlanInput::new( "transport.nostr.local", 1, @@ -7882,7 +7926,7 @@ mod tests { .complete_signing( receipt.outbox_event_id, claimed.claim_token.as_str(), - signed.clone(), + verified_signed_event(signed.clone()), 1_100, ) .await @@ -7954,7 +7998,7 @@ mod tests { .complete_signing( receipt.outbox_event_id, claimed.claim_token.as_str(), - signed, + verified_signed_event(signed), 1_100, ) .await @@ -7995,7 +8039,7 @@ mod tests { #[cfg(feature = "event-store-adapter")] #[tokio::test] - async fn local_ingest_rejects_an_invalid_signature_without_marking_the_outbox_or_store() { + async fn corrupt_stored_signature_is_rejected_before_local_ingest() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); let event_store = RadrootsEventStore::open_memory() .await @@ -8012,38 +8056,45 @@ mod tests { .expect("claimed"); let signed = radroots_nostr_sign_frozen_draft(&fixture_keys(), &claimed.draft).expect("signed"); - let mut wire = signed.wire().clone(); - wire.sig = "0".repeat(128); - let raw_json = serde_json::to_string(&wire).expect("invalid-signature wire JSON"); - let invalid_signed = RadrootsSignedEvent::from_wire_verified_id(wire, raw_json) - .expect("event id remains valid when only the signature changes"); outbox .complete_signing( receipt.outbox_event_id, claimed.claim_token.as_str(), - invalid_signed, + verified_signed_event(signed.clone()), 1_100, ) .await .expect("complete signing"); - outbox - .claim_next_ready_event("publisher", "claim-b", 3_000, 1_100) - .await - .expect("claim") - .expect("publish claim"); + let mut wire = signed.wire().clone(); + wire.sig = "0".repeat(128); + let invalid_wire_json = serde_json::to_string(&wire).expect("invalid-signature wire JSON"); + sqlx::query( + "UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ? WHERE outbox_event_id = ?", + ) + .bind(invalid_wire_json.as_str()) + .bind(invalid_wire_json.as_str()) + .bind(receipt.outbox_event_id) + .execute(outbox.pool()) + .await + .expect("corrupt stored signature"); let error = outbox - .ingest_signed_event_local(&event_store, receipt.outbox_event_id, "claim-b", 2_200) + .claim_next_ready_event("publisher", "claim-b", 3_000, 1_100) .await - .expect_err("invalid signature must fail before local storage"); - assert!(matches!(error, RadrootsOutboxError::EventStore(_))); + .expect_err("invalid signature must fail while loading publish authority"); + assert!(matches!( + error, + RadrootsOutboxError::SignedEventSignature(_) + )); - let stored_outbox = outbox - .get_event(receipt.outbox_event_id) - .await - .expect("outbox lookup") - .expect("outbox event"); - assert!(!stored_outbox.event_store_ingested); + let event_store_ingested: i64 = sqlx::query_scalar( + "SELECT event_store_ingested FROM outbox_event WHERE outbox_event_id = ?", + ) + .bind(receipt.outbox_event_id) + .fetch_one(outbox.pool()) + .await + .expect("stored event-store state"); + assert_eq!(event_store_ingested, 0); assert_eq!( event_store .status_summary() diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml @@ -28,8 +28,9 @@ config = { workspace = true } getrandom = { workspace = true } radroots_log = { workspace = true, features = ["std"] } radroots_event = { workspace = true, optional = true, default-features = false, features = [ - "std", "serde", + "signature", + "std", ] } radroots_protected_store = { workspace = true, features = ["std"] } radroots_runtime_paths = { workspace = true } diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -3,7 +3,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire}; +use radroots_event::{draft::RadrootsVerifiedSignedEvent, wire::RadrootsNip01EventWire}; #[cfg(feature = "transport-workers")] use radroots_transport::RadrootsTransportTargetReceipt; use radroots_transport::{ @@ -56,20 +56,24 @@ impl From<RadrootsTransportError> for RadrootsRuntimeTransportError { #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsRuntimeTransportPayload { - SignedEvent(Box<RadrootsSignedEvent>), + SignedEvent(Box<RadrootsVerifiedSignedEvent>), OpaqueBytes { label: String, bytes: Vec<u8> }, } impl RadrootsRuntimeTransportPayload { - pub fn signed_event(event: RadrootsSignedEvent) -> Self { + pub fn signed_event(event: RadrootsVerifiedSignedEvent) -> Self { Self::SignedEvent(Box::new(event)) } pub fn verified_signed_event_json( - event: &RadrootsSignedEvent, + event: &RadrootsVerifiedSignedEvent, ) -> Result<RadrootsTransportPayload, RadrootsTransportError> { verify_signed_event_raw_json_matches_event(event)?; - RadrootsTransportPayload::unchecked_signed_event_json(event.id_str(), event.raw_json()) + let signed_event = event.signed_event(); + RadrootsTransportPayload::unchecked_signed_event_json( + signed_event.id_str(), + signed_event.raw_json(), + ) } pub fn transport_payload(&self) -> Result<RadrootsTransportPayload, RadrootsTransportError> { @@ -83,14 +87,15 @@ impl RadrootsRuntimeTransportPayload { } fn verify_signed_event_raw_json_matches_event( - event: &RadrootsSignedEvent, + event: &RadrootsVerifiedSignedEvent, ) -> Result<(), RadrootsTransportError> { - let wire = RadrootsNip01EventWire::parse_json(event.raw_json()) + let signed_event = event.signed_event(); + let wire = RadrootsNip01EventWire::parse_json(signed_event.raw_json()) .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?; - if wire.id.as_str() != event.id_str() { + if wire.id.as_str() != signed_event.id_str() { return Err(RadrootsTransportError::InvalidPayloadId); } - (wire == *event.wire()) + (wire == *signed_event.wire()) .then_some(()) .ok_or(RadrootsTransportError::InvalidPayloadBytes) } @@ -594,13 +599,13 @@ pub struct RadrootsRuntimeInboundObservation { #[cfg(feature = "transport-workers")] impl RadrootsRuntimeInboundObservation { pub fn verified_signed_event( - event: &RadrootsSignedEvent, + event: &RadrootsVerifiedSignedEvent, transport_kind: RadrootsTransportKind, endpoint_uri: impl Into<String>, observed_at_ms: i64, ) -> Self { Self { - event_id: event.id_str().to_owned(), + event_id: event.signed_event().id_str().to_owned(), verified: true, transport_kind, endpoint_uri: endpoint_uri.into(), @@ -610,7 +615,7 @@ impl RadrootsRuntimeInboundObservation { pub fn require_verified_for_signed_event( &self, - event: &RadrootsSignedEvent, + event: &RadrootsVerifiedSignedEvent, ) -> Result<(), RadrootsRuntimeTransportError> { if !self.verified { return Err( @@ -619,11 +624,11 @@ impl RadrootsRuntimeInboundObservation { }, ); } - if self.event_id != event.id_str() { + if self.event_id != event.signed_event().id_str() { return Err( RadrootsRuntimeTransportError::InboundObservationEventMismatch { observation_event_id: self.event_id.clone(), - signed_event_id: event.id_str().to_owned(), + signed_event_id: event.signed_event().id_str().to_owned(), }, ); } @@ -635,7 +640,7 @@ impl RadrootsRuntimeInboundObservation { pub trait RadrootsRuntimeInboundObservationSink: Send + Sync { fn record_verified_observation<'a>( &'a self, - event: RadrootsSignedEvent, + event: RadrootsVerifiedSignedEvent, observation: RadrootsRuntimeInboundObservation, ) -> RadrootsRuntimeTransportFuture<'a, ()>; } @@ -643,7 +648,7 @@ pub trait RadrootsRuntimeInboundObservationSink: Send + Sync { #[cfg(feature = "transport-workers")] pub fn record_verified_inbound_observation<'a, S>( sink: &'a S, - event: RadrootsSignedEvent, + event: RadrootsVerifiedSignedEvent, observation: RadrootsRuntimeInboundObservation, ) -> RadrootsRuntimeTransportFuture<'a, ()> where @@ -671,7 +676,10 @@ mod tests { RadrootsRuntimeLeaseRecord, record_verified_inbound_observation, recover_expired_leases, }; #[cfg(feature = "transport-workers")] - use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire}; + use radroots_event::{ + draft::{RadrootsSignedEvent, RadrootsVerifiedSignedEvent}, + wire::RadrootsNip01EventWire, + }; use radroots_transport::{ RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, @@ -885,28 +893,14 @@ mod tests { } #[cfg(feature = "transport-workers")] - fn signed_event() -> RadrootsSignedEvent { - let pubkey = "e".repeat(64); - let sig = "f".repeat(128); - let mut wire = RadrootsNip01EventWire { - id: String::new(), - pubkey: pubkey.clone(), - created_at: 10, - kind: 1, - tags: Vec::new(), - content: "hello".to_owned(), - sig: sig.clone(), - extra: Default::default(), - }; - wire.id = wire - .computed_event_id() - .expect("computed event id") - .into_string(); - let raw_json = format!( - "{{\"id\":\"{id}\",\"pubkey\":\"{pubkey}\",\"created_at\":10,\"kind\":1,\"tags\":[],\"content\":\"hello\",\"sig\":\"{sig}\"}}", - id = wire.id.as_str() - ); - RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") + fn signed_event() -> RadrootsVerifiedSignedEvent { + let raw_json = r#"{"id":"fb3f42caf9db337a7f1c0d49cd8ba5191f08dc1c419ed0640f7ea48a924e3bf3","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1781632860,"kind":1,"tags":[],"content":"The first strawberries are ready.","sig":"dba0a86fee54304c2b419742f186e74d7edca5fc7234c8aa294651de9bc2f16bf829d46f36ec759a767c4ccd1841a73243eae89afd5f6c89b2243491bfbb5f50"}"#.to_owned(); + let wire = + RadrootsNip01EventWire::parse_json(raw_json.as_str()).expect("signed event wire"); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json) + .expect("signed event identity") + .verify_signature() + .expect("signed event signature") } #[cfg(feature = "transport-workers")] @@ -926,8 +920,8 @@ mod tests { }; assert_eq!(payload, via_variant); - assert_eq!(event_id, event.id_str()); - assert_eq!(raw_json, event.raw_json()); + assert_eq!(event_id, event.signed_event().id_str()); + assert_eq!(raw_json, event.signed_event().raw_json()); } #[cfg(feature = "transport-workers")] @@ -939,11 +933,11 @@ mod tests { impl RadrootsRuntimeInboundObservationSink for RecordingInboundSink { fn record_verified_observation<'a>( &'a self, - event: RadrootsSignedEvent, + event: RadrootsVerifiedSignedEvent, observation: RadrootsRuntimeInboundObservation, ) -> RadrootsRuntimeTransportFuture<'a, ()> { Box::pin(async move { - assert_eq!(event.id_str(), self.expected_event_id); + assert_eq!(event.signed_event().id_str(), self.expected_event_id); assert_eq!(observation.event_id, self.expected_event_id); assert!(observation.verified); Ok(()) @@ -1564,7 +1558,7 @@ mod tests { async fn inbound_observation_sink_requires_verified_signed_events() { let event = signed_event(); let sink = RecordingInboundSink { - expected_event_id: event.id_str().to_owned(), + expected_event_id: event.signed_event().id_str().to_owned(), }; let observation = RadrootsRuntimeInboundObservation::verified_signed_event( @@ -1578,7 +1572,7 @@ mod tests { .expect("record observation"); let unverified = RadrootsRuntimeInboundObservation { - event_id: event.id_str().to_owned(), + event_id: event.signed_event().id_str().to_owned(), verified: false, transport_kind: RadrootsTransportKind::Nostr, endpoint_uri: "wss://relay.example".to_owned(), diff --git a/crates/transport/src/error.rs b/crates/transport/src/error.rs @@ -35,6 +35,7 @@ pub enum RadrootsTransportError { InvalidPayloadLabel, EmptyPayloadBytes, InvalidPayloadBytes, + InvalidPayloadSignature, InvalidPayloadDigest, PayloadDigestMismatch, } @@ -105,6 +106,7 @@ impl fmt::Display for RadrootsTransportError { Self::InvalidPayloadLabel => f.write_str("transport payload label is invalid"), Self::EmptyPayloadBytes => f.write_str("transport payload bytes are empty"), Self::InvalidPayloadBytes => f.write_str("transport payload bytes are invalid"), + Self::InvalidPayloadSignature => f.write_str("transport payload signature is invalid"), Self::InvalidPayloadDigest => f.write_str("transport payload digest is invalid"), Self::PayloadDigestMismatch => { f.write_str("transport payload digest does not match payload bytes") diff --git a/crates/transport_nostr/Cargo.toml b/crates/transport_nostr/Cargo.toml @@ -20,7 +20,12 @@ client = [ "radroots_nostr/client", "radroots_nostr/events", ] -storage = ["dep:radroots_event_store", "dep:radroots_outbox", "client"] +storage = [ + "dep:radroots_event_store", + "dep:radroots_outbox", + "radroots_outbox/event-store-adapter", + "client", +] runtime-tokio = [ "dep:tokio", "storage", @@ -30,8 +35,9 @@ runtime-tokio = [ [dependencies] radroots_event = { workspace = true, default-features = false, features = [ - "std", "serde", + "signature", + "std", ] } radroots_event_store = { workspace = true, optional = true, default-features = false, features = [ "sqlite", diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs @@ -7,7 +7,7 @@ use crate::{ RadrootsRelayTransportError, RadrootsRelayUrlPolicy, publish_signed_event, verified_signed_event_payload, }; -use radroots_event::draft::RadrootsSignedEvent; +use radroots_event::draft::RadrootsVerifiedSignedEvent; use radroots_event_store::{ RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation, RadrootsTransportObservationType, @@ -115,7 +115,7 @@ where .await?; return Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id_str().to_owned(), + event_id: signed_event.signed_event().id_str().to_owned(), attempted_count: 0, accepted_count: publishable.accepted_count, retryable_count: 0, @@ -137,13 +137,13 @@ where .try_with_idempotency_key(outbox_publish_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, - signed_event.id_str(), + signed_event.signed_event().id_str(), active_delivery_plan_id, ))?; let publish = match publish_signed_event(adapter, request).await { Ok(receipt) => receipt, Err(RadrootsRelayTransportError::Transport(message)) => adapter_transport_failure_receipt( - signed_event.id_str().to_owned(), + signed_event.signed_event().id_str().to_owned(), target_strings, 0, message, @@ -260,7 +260,7 @@ where .await?; return Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id_str().to_owned(), + event_id: signed_event.signed_event().id_str().to_owned(), attempted_count: 0, accepted_count: publishable.accepted_count, retryable_count: 0, @@ -281,7 +281,7 @@ where let request_id = outbox_publish_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, - signed_event.id_str(), + signed_event.signed_event().id_str(), publishable.active_delivery_plan_id, ); let payload = @@ -338,7 +338,7 @@ where Ok(RadrootsOutboxPublishReceipt { local_ingest, - event_id: signed_event.id_str().to_owned(), + event_id: signed_event.signed_event().id_str().to_owned(), attempted_count: target_receipts .iter() .filter(|receipt| receipt.attempted) @@ -781,6 +781,7 @@ fn transport_error_to_relay_error(error: RadrootsTransportError) -> RadrootsRela | RadrootsTransportError::RequiredTargetNotRequested | RadrootsTransportError::EmptyDeliveryRequestId | RadrootsTransportError::InvalidDeliveryRequestId + | RadrootsTransportError::InvalidPayloadSignature | RadrootsTransportError::InvalidDeliveryTimestamp => { RadrootsRelayTransportError::Transport(error.to_string()) } @@ -1031,7 +1032,7 @@ fn satisfaction_policy_for_remaining_count( async fn ingest_publish_observation( event_store: &RadrootsEventStore, - signed_event: &RadrootsSignedEvent, + signed_event: &RadrootsVerifiedSignedEvent, relay_url: &str, observed_at_ms: i64, ) -> Result<(), RadrootsRelayTransportError> { @@ -1041,8 +1042,11 @@ async fn ingest_publish_observation( RadrootsTransportObservationType::PublishAck, observed_at_ms, )?; - let ingest = RadrootsEventIngest::from_signed_event(signed_event.clone(), observed_at_ms)? - .with_observation(observation); + let ingest = RadrootsEventIngest::from_signed_event( + signed_event.signed_event().clone(), + observed_at_ms, + )? + .with_observation(observation); event_store.ingest_event(ingest).await?; Ok(()) } diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs @@ -5,7 +5,10 @@ use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransport #[cfg(feature = "client")] use core::time::Duration; use futures::future::BoxFuture; -use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire}; +use radroots_event::{ + draft::{RadrootsSignedEvent, RadrootsVerifiedSignedEvent}, + wire::RadrootsNip01EventWire, +}; use radroots_transport::{ RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportFetchReceipt, @@ -30,7 +33,7 @@ pub const RADROOTS_RELAY_PUBLISH_IDEMPOTENCY_KEY_MAX_BYTES: usize = 256; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRelayPublishRequest { - signed_event: RadrootsSignedEvent, + signed_event: RadrootsVerifiedSignedEvent, targets: RadrootsRelayTargetSet, satisfaction_policy: RadrootsTransportSatisfactionPolicy, idempotency_key: Option<String>, @@ -39,7 +42,7 @@ pub struct RadrootsRelayPublishRequest { impl RadrootsRelayPublishRequest { pub fn new( - signed_event: RadrootsSignedEvent, + signed_event: RadrootsVerifiedSignedEvent, targets: RadrootsRelayTargetSet, now_ms: i64, ) -> Result<Self, RadrootsRelayTransportError> { @@ -71,7 +74,7 @@ impl RadrootsRelayPublishRequest { Ok(self) } - pub fn signed_event(&self) -> &RadrootsSignedEvent { + pub fn signed_event(&self) -> &RadrootsVerifiedSignedEvent { &self.signed_event } @@ -188,8 +191,9 @@ pub trait RadrootsRelayPublishAdapter: Send + Sync { } pub fn verified_signed_event_payload( - signed_event: &RadrootsSignedEvent, + signed_event: &RadrootsVerifiedSignedEvent, ) -> Result<RadrootsTransportPayload, RadrootsTransportError> { + let signed_event = signed_event.signed_event(); RadrootsTransportPayload::unchecked_signed_event_json( signed_event.id_str(), signed_event.raw_json(), @@ -565,7 +569,7 @@ mod contract_tests { fn signed_event_from_transport_payload( payload: &RadrootsTransportPayload, -) -> Result<RadrootsSignedEvent, RadrootsTransportError> { +) -> Result<RadrootsVerifiedSignedEvent, RadrootsTransportError> { let RadrootsTransportPayload::SignedEventJson { event_id, raw_json, .. } = payload @@ -578,7 +582,9 @@ fn signed_event_from_transport_payload( return Err(RadrootsTransportError::InvalidPayloadId); } RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.clone()) - .map_err(|_| RadrootsTransportError::InvalidPayloadBytes) + .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)? + .verify_signature() + .map_err(|_| RadrootsTransportError::InvalidPayloadSignature) } fn relay_targets_from_transport_targets( @@ -658,7 +664,7 @@ where A: RadrootsRelayPublishAdapter, { request.validate()?; - let event_id = request.signed_event.id_str().to_owned(); + let event_id = request.signed_event.signed_event().id_str().to_owned(); let satisfaction_policy = request.satisfaction_policy.clone(); let requested_relays = request.targets.relay_strings(); let target_count = request.targets.len(); @@ -841,7 +847,7 @@ impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter { self.captured_raw_events .lock() .map_err(captured_raw_event_lock_error)? - .push(request.signed_event.raw_json().to_owned()); + .push(request.signed_event.signed_event().raw_json().to_owned()); Ok(request .targets .relays() @@ -887,9 +893,12 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter { ) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>> { Box::pin(async move { - let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json()) - .map_err(|error| RadrootsRelayTransportError::NostrEventJson(error.to_string()))?; - ensure_raw_event_matches_signed_event(&event, &request.signed_event)?; + let event = + RadrootsNostrEvent::from_json(request.signed_event.signed_event().raw_json()) + .map_err(|error| { + RadrootsRelayTransportError::NostrEventJson(error.to_string()) + })?; + ensure_raw_event_matches_signed_event(&event, request.signed_event.signed_event())?; let target_strings = request.targets.relay_strings(); for relay_url in &target_strings { self.client diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs @@ -1,6 +1,6 @@ use futures::future::BoxFuture; use nostr::{EventBuilder, JsonUtil}; -use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent}; +use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsVerifiedSignedEvent}; use radroots_event::kinds::{ KIND_DELETION_REQUEST, KIND_FOLLOW, KIND_GEOCHAT, KIND_POST, KIND_PROFILE, }; @@ -466,6 +466,12 @@ fn signed_event_with_kind_and_hashtag( RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") } +fn verified_signed_event(signed_event: RadrootsSignedEvent) -> RadrootsVerifiedSignedEvent { + signed_event + .verify_signature() + .expect("fixture signature must verify") +} + fn signed_raw_event_with_kind_and_hashtag(content: &str, kind: u32, hashtag: &str) -> nostr::Event { test_event_builder( kind, @@ -483,7 +489,7 @@ async fn complete_claimed_signing( now_ms: i64, ) -> RadrootsSignedEvent { if let Some(signed_event) = claimed.signed_event.clone() { - return signed_event; + return signed_event.into_signed_event(); } let signed_event = radroots_nostr_sign_frozen_draft(&fixture_keys(), &claimed.draft).expect("signed event"); @@ -491,11 +497,12 @@ async fn complete_claimed_signing( .complete_signing( claimed.outbox_event_id, claimed.claim_token.as_str(), - signed_event, + verified_signed_event(signed_event), now_ms, ) .await .expect("complete signing") + .into_signed_event() } fn nostr_target(relay_url: &str) -> RadrootsTransportTarget { @@ -1111,9 +1118,13 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() { let receipt = publish_signed_event( &adapter, - radroots_transport_nostr::RadrootsRelayPublishRequest::new(signed.clone(), targets, 1_000) - .expect("publish request") - .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::quorum_accepted(2)), + radroots_transport_nostr::RadrootsRelayPublishRequest::new( + verified_signed_event(signed.clone()), + targets, + 1_000, + ) + .expect("publish request") + .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::quorum_accepted(2)), ) .await .expect("publish"); @@ -1175,10 +1186,38 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() { ); } +#[tokio::test] +async fn nostr_transport_facade_rejects_invalid_signature_before_adapter_publish() { + let signed = signed_post("invalid facade signature"); + let mut wire: serde_json::Value = + serde_json::from_str(signed.raw_json()).expect("signed event JSON"); + wire["sig"] = serde_json::Value::String("0".repeat(128)); + let invalid_raw_json = serde_json::to_string(&wire).expect("invalid signature JSON"); + let adapter = RadrootsMockRelayPublishAdapter::new(); + let transport = RadrootsNostrTransport::new(&adapter); + let request = RadrootsTransportDeliveryRequest::new( + "facade-invalid-signature", + RadrootsTransportPayload::unchecked_signed_event_json(signed.id_str(), invalid_raw_json) + .expect("transport payload"), + RadrootsTransportTargetSet::new(vec![nostr_target(RELAY_PRIMARY_WSS)]).expect("targets"), + RadrootsTransportSatisfactionPolicy::all_accepted(), + ) + .expect("delivery request"); + + let error = transport + .deliver(request) + .await + .expect_err("invalid signature must fail before publish"); + + assert_eq!(error, RadrootsTransportError::InvalidPayloadSignature); + assert!(adapter.captured_raw_events().is_empty()); +} + #[test] fn verified_signed_event_payload_preserves_transport_payload_identity() { let signed = signed_post("verified payload"); - let payload = verified_signed_event_payload(&signed).expect("verified payload"); + let payload = verified_signed_event_payload(&verified_signed_event(signed.clone())) + .expect("verified payload"); let RadrootsTransportPayload::SignedEventJson { event_id, raw_json, @@ -1457,7 +1496,7 @@ async fn nostr_transport_facade_matches_canonical_equivalent_relay_receipts() { let relay_receipt = publish_signed_event( &SlashSpelledRelayReceiptPublishAdapter, RadrootsRelayPublishRequest::new( - signed, + verified_signed_event(signed), RadrootsRelayTargetSet::new(vec![RELAY_PRIMARY_WSS], RadrootsRelayUrlPolicy::Public) .expect("targets"), 1_070, @@ -1515,7 +1554,7 @@ async fn publish_receipts_track_terminal_skipped_and_adapter_errors() { let receipt = publish_signed_event( &adapter, - RadrootsRelayPublishRequest::new(signed.clone(), targets, 1_050) + RadrootsRelayPublishRequest::new(verified_signed_event(signed.clone()), targets, 1_050) .expect("publish request") .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::all_accepted()), ) @@ -1541,7 +1580,7 @@ async fn publish_receipts_track_terminal_skipped_and_adapter_errors() { let error = publish_signed_event( &TransportFailurePublishAdapter, RadrootsRelayPublishRequest::new( - signed, + verified_signed_event(signed), RadrootsRelayTargetSet::new(vec![RELAY_PRIMARY_WSS], RadrootsRelayUrlPolicy::Public) .expect("targets"), 1_060, @@ -1572,7 +1611,7 @@ async fn publish_required_target_policy_uses_relay_fingerprints() { let receipt = publish_signed_event( &adapter, - RadrootsRelayPublishRequest::new(signed, targets, 1_070) + RadrootsRelayPublishRequest::new(verified_signed_event(signed), targets, 1_070) .expect("publish request") .with_satisfaction_policy( RadrootsTransportSatisfactionPolicy::required_targets( @@ -1601,9 +1640,13 @@ async fn publish_all_policy_uses_requested_target_count() { let receipt = publish_signed_event( &PartialPublishAdapter, - RadrootsRelayPublishRequest::new(signed.clone(), targets.clone(), 1_080) - .expect("publish request") - .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::all_accepted()), + RadrootsRelayPublishRequest::new( + verified_signed_event(signed.clone()), + targets.clone(), + 1_080, + ) + .expect("publish request") + .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::all_accepted()), ) .await .expect("publish"); @@ -1622,7 +1665,7 @@ async fn publish_all_policy_uses_requested_target_count() { let no_wait = publish_signed_event( &PartialPublishAdapter, - RadrootsRelayPublishRequest::new(signed, targets, 1_081) + RadrootsRelayPublishRequest::new(verified_signed_event(signed), targets, 1_081) .expect("publish request") .with_satisfaction_policy(RadrootsTransportSatisfactionPolicy::NoWait), ) @@ -1636,8 +1679,12 @@ async fn publish_all_policy_uses_requested_target_count() { async fn publish_rejects_untrusted_adapter_receipt_provenance() { let signed = signed_post("adapter provenance"); let request = || { - RadrootsRelayPublishRequest::new(signed.clone(), primary_relay_target(), 1_090) - .expect("publish request") + RadrootsRelayPublishRequest::new( + verified_signed_event(signed.clone()), + primary_relay_target(), + 1_090, + ) + .expect("publish request") }; assert!(matches!( @@ -1672,7 +1719,7 @@ fn relay_publish_request_rejects_negative_time() { let signed = signed_post("negative publish time"); assert!(matches!( - RadrootsRelayPublishRequest::new(signed, primary_relay_target(), -1), + RadrootsRelayPublishRequest::new(verified_signed_event(signed), primary_relay_target(), -1,), Err(RadrootsRelayTransportError::InvalidTimestamp { field: "now_ms", value: -1, @@ -1683,9 +1730,13 @@ fn relay_publish_request_rejects_negative_time() { #[test] fn relay_publish_request_seals_fields_and_validates_idempotency_keys() { let signed = signed_post("sealed publish request"); - let request = RadrootsRelayPublishRequest::new(signed.clone(), primary_relay_target(), 7) - .expect("publish request"); - assert_eq!(request.signed_event(), &signed); + let request = RadrootsRelayPublishRequest::new( + verified_signed_event(signed.clone()), + primary_relay_target(), + 7, + ) + .expect("publish request"); + assert_eq!(request.signed_event().signed_event(), &signed); assert_eq!(request.targets().len(), 1); assert_eq!( request.satisfaction_policy(), @@ -1719,15 +1770,16 @@ async fn relay_publish_request_rejects_unrequested_required_target_before_adapte .expect("required target") .fingerprint() .clone(); - let request = RadrootsRelayPublishRequest::new(signed, primary_relay_target(), 8) - .expect("publish request") - .with_satisfaction_policy( - RadrootsTransportSatisfactionPolicy::required_targets( - RadrootsTransportSatisfactionClass::Accepted, - vec![required.clone()], - ) - .expect("required policy"), - ); + let request = + RadrootsRelayPublishRequest::new(verified_signed_event(signed), primary_relay_target(), 8) + .expect("publish request") + .with_satisfaction_policy( + RadrootsTransportSatisfactionPolicy::required_targets( + RadrootsTransportSatisfactionClass::Accepted, + vec![required.clone()], + ) + .expect("required policy"), + ); let adapter = RadrootsMockRelayPublishAdapter::new(); assert!(matches!(