rhi

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

services_hardening_presence_desired_state.rs (11973B)


      1 #![forbid(unsafe_code)]
      2 #![cfg(any(target_os = "linux", target_os = "macos"))]
      3 
      4 use std::{fs, os::unix::fs::PermissionsExt, path::Path};
      5 
      6 use radroots_service_sqlite::{MigrationAppliedAtUnixSeconds, MigrationBuildIdentity};
      7 use radroots_storage::event::SourceGeneration;
      8 use rhi::{
      9     RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform, RhiConfigDocumentV1,
     10     RhiConfigProfile, RhiPresenceDesiredAuthority, RhiPresenceDesiredErrorKind,
     11     RhiPresenceDesiredMode, RhiStateMetadata, apply_rhi_configuration, initialize_rhi_state,
     12     open_rhi_state_inspection, open_rhi_state_read_write_from_config, parse_rhi_cli_v1_from,
     13     parse_rhi_config_v1, resolve_rhi_runtime_context, validate_rhi_presence_desired_authority,
     14 };
     15 use sqlx::{ConnectOptions, Connection, Row, SqliteConnection, sqlite::SqliteConnectOptions};
     16 
     17 const EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     18 const CONTRACT: &str =
     19     include_str!("../contracts/services_hardening/presence_desired_state.v1.json");
     20 const SOURCE: &str = include_str!("../src/presence_desired.rs");
     21 
     22 fn runtime(root: &Path) -> rhi::RhiRuntimeContext {
     23     let invocation = parse_rhi_cli_v1_from([
     24         "rhi",
     25         "--profile",
     26         "repo-local",
     27         "--instance",
     28         "primary",
     29         "--repo-local-root",
     30         root.to_str().expect("UTF-8 root"),
     31         "run",
     32     ])
     33     .expect("invocation");
     34     resolve_rhi_runtime_context(
     35         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
     36         &invocation,
     37     )
     38     .expect("runtime")
     39 }
     40 
     41 fn config(source: &str) -> RhiConfigDocumentV1 {
     42     parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("configuration")
     43 }
     44 
     45 fn evidence(at: u64) -> (MigrationAppliedAtUnixSeconds, MigrationBuildIdentity) {
     46     let applied_at = MigrationAppliedAtUnixSeconds::new(at).expect("migration time");
     47     let build = MigrationBuildIdentity::new(
     48         env!("CARGO_PKG_VERSION"),
     49         "1111111111111111111111111111111111111111",
     50         "053d0c750bf9cd683c6ea37cefe7e79617ba629f",
     51         "rustc-test",
     52         "test-target",
     53         "service-host",
     54         1,
     55         rhi::RHI_STATE_SCHEMA_VERSION,
     56         1,
     57         1,
     58         1,
     59     )
     60     .expect("build identity");
     61     (applied_at, build)
     62 }
     63 
     64 async fn offline_connection(runtime: &rhi::RhiRuntimeContext) -> SqliteConnection {
     65     let options = SqliteConnectOptions::new()
     66         .filename(runtime.artifacts().state_database())
     67         .create_if_missing(false)
     68         .foreign_keys(false)
     69         .disable_statement_logging();
     70     SqliteConnection::connect_with(&options)
     71         .await
     72         .expect("offline connection")
     73 }
     74 
     75 #[tokio::test]
     76 async fn desired_state_is_durable_exact_replay_and_semantic_change_only() {
     77     let directory = tempfile::tempdir().expect("root");
     78     let runtime = runtime(directory.path());
     79     fs::create_dir_all(runtime.context().paths().state()).expect("state directory");
     80     fs::set_permissions(
     81         runtime.context().paths().state(),
     82         fs::Permissions::from_mode(0o700),
     83     )
     84     .expect("state mode");
     85     let original = config(EXAMPLE);
     86     let metadata = RhiStateMetadata::new(
     87         &runtime,
     88         &original,
     89         SourceGeneration::new([0x5a; 32]).expect("source generation"),
     90         1_725_000_000_000,
     91     )
     92     .expect("metadata");
     93     let (first_at, first_build) = evidence(1_725_000_000);
     94     initialize_rhi_state(&runtime, &metadata, first_at, &first_build)
     95         .await
     96         .expect("initialize");
     97 
     98     let original_authority =
     99         RhiPresenceDesiredAuthority::from_config(&original).expect("authority");
    100     let host = open_rhi_state_read_write_from_config(&runtime, &original, first_at, &first_build)
    101         .await
    102         .expect("writer");
    103     let repository = host.repositories().desired_presence();
    104     assert_eq!(repository.current().await.expect("initial read"), None);
    105     let first = repository
    106         .commit(&original_authority)
    107         .await
    108         .expect("first commit");
    109     assert!(first.changed());
    110     assert_eq!(first.state().generation(), 1);
    111     assert_eq!(first.state().mode(), RhiPresenceDesiredMode::Enabled);
    112     assert!(first.state().profile());
    113     assert!(first.state().application_handler());
    114     assert_eq!(first.state().target_count(), 2);
    115     assert_eq!(first.state().required_target_count(), 1);
    116     assert_eq!(first.state().queue_capacity(), 64);
    117     let replay = repository
    118         .commit(&original_authority)
    119         .await
    120         .expect("exact replay");
    121     assert!(!replay.changed());
    122     assert_eq!(replay.state(), first.state());
    123     assert_eq!(
    124         repository.current().await.expect("current"),
    125         Some(first.state())
    126     );
    127     host.close().await.expect("writer close");
    128 
    129     let inspection = open_rhi_state_inspection(&runtime, &metadata)
    130         .await
    131         .expect("inspection");
    132     let inspected = inspection
    133         .repositories()
    134         .desired_presence()
    135         .current()
    136         .await
    137         .expect("inspection read");
    138     assert_eq!(inspected, Some(first.state()));
    139     let rejected = inspection
    140         .repositories()
    141         .desired_presence()
    142         .commit(&original_authority)
    143         .await
    144         .expect_err("inspection mutation");
    145     assert_eq!(rejected.kind(), RhiPresenceDesiredErrorKind::InvalidMode);
    146     inspection.close().await.expect("inspection close");
    147 
    148     let unrelated_source = EXAMPLE.replacen("level = \"info\"", "level = \"debug\"", 1);
    149     let unrelated = config(&unrelated_source);
    150     let unrelated_authority =
    151         RhiPresenceDesiredAuthority::from_config(&unrelated).expect("unrelated authority");
    152     assert_eq!(
    153         unrelated_authority.desired_sha256(),
    154         original_authority.desired_sha256()
    155     );
    156     assert_eq!(
    157         validate_rhi_presence_desired_authority(&unrelated, &original_authority)
    158             .expect_err("full configuration binding")
    159             .kind(),
    160         RhiPresenceDesiredErrorKind::Binding
    161     );
    162     let (second_at, second_build) = evidence(1_725_000_001);
    163     apply_rhi_configuration(&runtime, &original, &unrelated, second_at, &second_build)
    164         .await
    165         .expect("apply unrelated configuration");
    166     let host =
    167         open_rhi_state_read_write_from_config(&runtime, &unrelated, second_at, &second_build)
    168             .await
    169             .expect("writer after unrelated config");
    170     let replay = host
    171         .repositories()
    172         .desired_presence()
    173         .commit(&unrelated_authority)
    174         .await
    175         .expect("semantic replay");
    176     assert!(!replay.changed());
    177     assert_eq!(replay.state().generation(), 1);
    178     host.close().await.expect("writer close");
    179 
    180     let presence_source = unrelated_source.replace("profile = true", "profile = false");
    181     let presence_changed = config(&presence_source);
    182     let changed_authority =
    183         RhiPresenceDesiredAuthority::from_config(&presence_changed).expect("changed authority");
    184     assert_ne!(
    185         changed_authority.desired_sha256(),
    186         original_authority.desired_sha256()
    187     );
    188     let (third_at, third_build) = evidence(1_725_000_002);
    189     apply_rhi_configuration(
    190         &runtime,
    191         &unrelated,
    192         &presence_changed,
    193         third_at,
    194         &third_build,
    195     )
    196     .await
    197     .expect("apply presence configuration");
    198     let host =
    199         open_rhi_state_read_write_from_config(&runtime, &presence_changed, third_at, &third_build)
    200             .await
    201             .expect("writer after presence change");
    202     let stale = host
    203         .repositories()
    204         .desired_presence()
    205         .commit(&original_authority)
    206         .await
    207         .expect_err("stale config authority");
    208     assert_eq!(stale.kind(), RhiPresenceDesiredErrorKind::Binding);
    209     let changed = host
    210         .repositories()
    211         .desired_presence()
    212         .commit(&changed_authority)
    213         .await
    214         .expect("changed desired state");
    215     assert!(changed.changed());
    216     assert_eq!(changed.state().generation(), 2);
    217     assert!(!changed.state().profile());
    218     assert!(changed.state().application_handler());
    219     assert_eq!(changed.state().target_count(), 2);
    220     assert_eq!(
    221         changed.state().desired_sha256(),
    222         changed_authority.desired_sha256()
    223     );
    224     host.close().await.expect("final writer close");
    225 
    226     let mut connection = offline_connection(&runtime).await;
    227     let row = sqlx::query(
    228         "SELECT COUNT(*) AS row_count, generation, length(desired_sha256) AS digest_bytes \
    229          FROM presence_desired_state",
    230     )
    231     .fetch_one(&mut connection)
    232     .await
    233     .expect("durable desired state");
    234     assert_eq!(row.try_get::<i64, _>("row_count").unwrap(), 1);
    235     assert_eq!(row.try_get::<i64, _>("generation").unwrap(), 2);
    236     assert_eq!(row.try_get::<i64, _>("digest_bytes").unwrap(), 32);
    237     assert!(
    238         sqlx::query("UPDATE presence_desired_state SET generation = generation")
    239             .execute(&mut connection)
    240             .await
    241             .is_err()
    242     );
    243     assert!(
    244         sqlx::query("DELETE FROM presence_desired_state")
    245             .execute(&mut connection)
    246             .await
    247             .is_err()
    248     );
    249     connection.close().await.expect("offline close");
    250 
    251     let database = fs::read(runtime.artifacts().state_database()).expect("database bytes");
    252     for forbidden in [
    253         b"wss://relay-primary.example".as_slice(),
    254         b"relay-primary".as_slice(),
    255         directory.path().to_string_lossy().as_bytes(),
    256     ] {
    257         assert!(
    258             !database
    259                 .windows(forbidden.len())
    260                 .any(|window| window == forbidden)
    261         );
    262     }
    263 }
    264 
    265 #[test]
    266 fn machine_contract_freezes_desired_state_without_publication_effects() {
    267     let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("contract");
    268     assert_eq!(contract["schema"], "radroots.rhi.presence-desired-state");
    269     assert_eq!(contract["contract_version"], 1);
    270     assert_eq!(contract["authority"]["maximum_targets"], 32);
    271     assert_eq!(
    272         contract["reference_vector"]["target_set_sha256"],
    273         "959f04012841ae6e9bf3e109468b4f66cfa9d966aac1df36df09e45c1e1c48f9"
    274     );
    275     assert_eq!(
    276         contract["reference_vector"]["desired_state_sha256"],
    277         "7235f1e386e839427625dc364df7b51ee74d39d5f170e12b25cf2c42fd7731f0"
    278     );
    279     assert_eq!(contract["effects"]["clock"], false);
    280     assert_eq!(contract["effects"]["entropy"], false);
    281     assert_eq!(contract["effects"]["network"], false);
    282     assert_eq!(contract["effects"]["relay_io"], false);
    283     assert_eq!(
    284         contract["separate_publication_authority"]
    285             .as_array()
    286             .unwrap()
    287             .len(),
    288         7
    289     );
    290     for required in [
    291         "DESIRED_STATE_DOMAIN",
    292         "TARGET_SET_DOMAIN",
    293         "pub fn validate_rhi_presence_desired_authority(",
    294         "pub async fn commit(",
    295         "pub async fn current(",
    296         "require_current_config(transaction, authority).await?",
    297         "LIMIT 2",
    298         "ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown",
    299     ] {
    300         assert!(
    301             SOURCE.contains(required),
    302             "missing Step204 boundary {required}"
    303         );
    304     }
    305     for forbidden in [
    306         "SystemTime",
    307         "OsRng",
    308         "thread_rng",
    309         "tokio::spawn",
    310         "std::net",
    311         "NostrEventAdapter",
    312         "sign_nostr_event",
    313     ] {
    314         assert!(
    315             !SOURCE.contains(forbidden),
    316             "unexpected desired-state effect {forbidden}"
    317         );
    318     }
    319     for kind in [
    320         RhiPresenceDesiredErrorKind::InvalidConfiguration,
    321         RhiPresenceDesiredErrorKind::TargetInventory,
    322         RhiPresenceDesiredErrorKind::InvalidMode,
    323         RhiPresenceDesiredErrorKind::Binding,
    324         RhiPresenceDesiredErrorKind::ResourceExhausted,
    325         RhiPresenceDesiredErrorKind::Storage,
    326         RhiPresenceDesiredErrorKind::CommitOutcomeUnknown,
    327     ] {
    328         let rendered = format!("{kind:?}");
    329         assert!(!rendered.contains("relay-primary"));
    330     }
    331 }