rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

services_hardening_publication_contract.rs (6855B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::error::Error;
      4 
      5 use rhi::{
      6     RHI_PUBLICATION_CONTRACT_VERSION, RHI_PUBLICATION_MAX_ATTEMPTS, RHI_PUBLICATION_MAX_TARGETS,
      7     RhiConfigProfile, RhiPublicationAuthority, RhiPublicationMode, parse_rhi_config_v1,
      8 };
      9 use serde_json::json;
     10 
     11 const CONTRACT: &str = include_str!("../contracts/services_hardening/publication_outbox.v1.json");
     12 const EXAMPLE: &[u8] = include_bytes!("../contracts/services_hardening/config.v1.example.toml");
     13 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     14 const PUBLICATION_SOURCE: &str = include_str!("../src/publication.rs");
     15 const CATALOG_SOURCE: &str = include_str!("../src/state_catalog.rs");
     16 const README: &str = include_str!("../README");
     17 
     18 #[test]
     19 fn machine_contract_freezes_step_198_authority_and_schema() {
     20     let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
     21     assert_eq!(contract["schema"], "radroots.rhi.publication-outbox");
     22     assert_eq!(contract["schema_version"], 1);
     23     assert_eq!(
     24         contract["contract_version"],
     25         RHI_PUBLICATION_CONTRACT_VERSION
     26     );
     27     // This Step198 contract records the schema version that introduced the
     28     // outbox. Later forward-only migrations are governed by their own
     29     // contracts and must not silently rewrite this historical evidence.
     30     assert_eq!(contract["state_schema_version"], 7);
     31     assert_eq!(
     32         contract["publication_modes"],
     33         json!(["required", "disabled"])
     34     );
     35     assert_eq!(
     36         contract["authority"]["required"]["target_count"]["maximum"],
     37         RHI_PUBLICATION_MAX_TARGETS
     38     );
     39     assert_eq!(
     40         contract["schema_objects"]["immutable"],
     41         json!([
     42             "evidence_manifests",
     43             "trade_projections",
     44             "attestation_reports",
     45             "signed_attestation_events",
     46             "publication_attempts"
     47         ])
     48     );
     49     assert_eq!(
     50         contract["schema_objects"]["compare_and_swap"],
     51         json!(["publication_outbox", "publication_targets"])
     52     );
     53     assert_eq!(
     54         contract["schema_objects"]["target_states"],
     55         json!([
     56             "pending",
     57             "submitted",
     58             "accepted",
     59             "rejected",
     60             "rate_limited",
     61             "auth_required",
     62             "failed",
     63             "unknown"
     64         ])
     65     );
     66     assert_eq!(contract["payload"]["signed_bytes_maximum"], 32_768);
     67     assert_eq!(contract["effects"]["sqlite_query_or_mutation"], false);
     68     assert_eq!(contract["effects"]["relay_or_network"], false);
     69     for forbidden in [
     70         "implicit_publication_mode",
     71         "disabled_mode_target_or_retry_state",
     72         "caller_forged_target",
     73         "non_write_relay_target",
     74         "mutable_signed_payload",
     75         "mutable_target_identity",
     76         "raw_upstream_error_text",
     77         "raw_sqlite_handle",
     78         "relay_io",
     79         "event_rebuild",
     80         "event_reserialize",
     81         "event_resign",
     82         "unbounded_queue_or_target_inventory",
     83     ] {
     84         assert!(
     85             contract["forbidden"]
     86                 .as_array()
     87                 .expect("forbidden")
     88                 .iter()
     89                 .any(|value| value == forbidden),
     90             "missing forbidden boundary {forbidden}"
     91         );
     92     }
     93 }
     94 
     95 #[test]
     96 fn canonical_example_has_literal_publication_identities() {
     97     let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
     98     let authority = RhiPublicationAuthority::from_config(&config).expect("authority");
     99     assert_eq!(authority.mode(), RhiPublicationMode::Required);
    100     assert_eq!(authority.targets().len(), 2);
    101     let retry = authority.retry_policy().expect("retry");
    102     assert_eq!(retry.maximum_attempts(), 10);
    103     assert!(retry.maximum_attempts() <= RHI_PUBLICATION_MAX_ATTEMPTS);
    104     assert_eq!(
    105         lower_hex(authority.target_set_sha256()),
    106         "fc044570890935bc41f4763b92d0e079ea0288e0a77a03fc1e299a4c830bbd76"
    107     );
    108     assert_eq!(
    109         lower_hex(authority.authority_sha256()),
    110         "6df5c3bf1bbb5c7b57d81bd1ee600677501a05d6e3c3aaf7577d64ffcbd9566b"
    111     );
    112 }
    113 
    114 #[test]
    115 fn module_and_side_effect_authority_remain_private_and_deferred() {
    116     assert!(LIB_SOURCE.contains("mod publication;"));
    117     assert!(!LIB_SOURCE.contains("pub mod publication;"));
    118     assert!(LIB_SOURCE.contains("RhiPublicationAuthority"));
    119     assert!(README.contains("## Explicit publication authority and durable schema"));
    120     assert!(README.contains(
    121         "[`publication_outbox.v1.json`](contracts/services_hardening/publication_outbox.v1.json)"
    122     ));
    123     for table in [
    124         "evidence_manifests",
    125         "trade_projections",
    126         "attestation_reports",
    127         "signed_attestation_events",
    128         "publication_outbox",
    129         "publication_targets",
    130         "publication_attempts",
    131     ] {
    132         assert!(CATALOG_SOURCE.contains(&format!("CREATE TABLE {table}")));
    133     }
    134     assert!(CATALOG_SOURCE.contains("OR OLD.state = 'accepted'"));
    135     assert!(CATALOG_SOURCE.contains("AND next_attempt_unix_ms IS NOT NULL"));
    136     assert!(CATALOG_SOURCE.contains("AND lease_owner IS NOT NULL"));
    137     assert!(CATALOG_SOURCE.contains("AND lease_expires_unix_ms IS NOT NULL"));
    138     for forbidden in [
    139         "sqlx::",
    140         "radroots_transport",
    141         "PublicationSink",
    142         "SystemTime",
    143         "thread_rng",
    144         "OsRng",
    145         "std::fs",
    146         "std::net",
    147         "tokio::spawn",
    148         "spawn_blocking",
    149         "pub fn sqlite",
    150         "pub fn transaction",
    151         "pub fn connection",
    152         "pub fn into_inner",
    153     ] {
    154         assert!(
    155             !PUBLICATION_SOURCE.contains(forbidden),
    156             "premature publication authority {forbidden}"
    157         );
    158     }
    159 }
    160 
    161 #[test]
    162 fn public_authority_debug_and_errors_reveal_no_targets_or_digests() {
    163     let config = parse_rhi_config_v1(EXAMPLE, RhiConfigProfile::Production).expect("config");
    164     let authority = RhiPublicationAuthority::from_config(&config).expect("authority");
    165     let rendered = format!("{authority:?}");
    166     assert!(!rendered.contains("relay-primary"));
    167     assert!(!rendered.contains(&lower_hex(authority.authority_sha256())));
    168 
    169     let invalid = core::str::from_utf8(EXAMPLE)
    170         .expect("utf8")
    171         .replace("mode = \"required\"", "mode = \"invalid-secret\"");
    172     let error = parse_rhi_config_v1(invalid.as_bytes(), RhiConfigProfile::Production)
    173         .expect_err("invalid config");
    174     let rendered = format!("{error} {error:?}");
    175     assert!(!rendered.contains("invalid-secret"));
    176     assert!(Error::source(&error).is_none());
    177 }
    178 
    179 fn lower_hex(bytes: &[u8]) -> String {
    180     const DIGITS: &[u8; 16] = b"0123456789abcdef";
    181     let mut output = String::with_capacity(bytes.len() * 2);
    182     for byte in bytes {
    183         output.push(char::from(DIGITS[usize::from(byte >> 4)]));
    184         output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
    185     }
    186     output
    187 }