rhi

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

services_hardening_trade_ingest.rs (15970B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::error::Error;
      4 
      5 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
      6 use rhi::{
      7     RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT, RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES,
      8     RHI_TRADE_EVENT_ID_MAX_BYTES, RHI_TRADE_EVENT_PUBLIC_KEY_MAX_BYTES,
      9     RHI_TRADE_EVENT_SIGNATURE_MAX_BYTES, RHI_TRADE_INGEST_CONTRACT_VERSION, RhiConfigProfile,
     10     RhiTradeMutationAdmissionErrorKind, RhiTradeMutationAdmissionLimits,
     11     RhiTradeMutationAuthoredTimePolicy, RhiTradeMutationObservedAtUnixSeconds,
     12     admit_rhi_trade_mutation_event, parse_rhi_config_v1,
     13 };
     14 use serde_json::{Map, Value, json};
     15 use sha2::{Digest, Sha256};
     16 
     17 const CONFIG: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
     18 const CONTRACT: &str = include_str!("../contracts/services_hardening/trade_ingest.v1.json");
     19 const VECTOR: &str = include_str!("../contracts/conformance/vectors/trade_ingest_proposal.v1.json");
     20 
     21 fn configuration(overrides: &[(&str, usize)]) -> rhi::RhiConfigDocumentV1 {
     22     let mut source = CONFIG.to_owned();
     23     for (field, value) in overrides {
     24         let prefix = format!("{field} = ");
     25         let original = source
     26             .lines()
     27             .find(|line| line.starts_with(&prefix))
     28             .expect("configured event limit")
     29             .to_owned();
     30         source = source.replacen(&original, &format!("{field} = {value}"), 1);
     31     }
     32     parse_rhi_config_v1(source.as_bytes(), RhiConfigProfile::RepoLocal).expect("test configuration")
     33 }
     34 
     35 fn limits(overrides: &[(&str, usize)]) -> RhiTradeMutationAdmissionLimits {
     36     RhiTradeMutationAdmissionLimits::from_config(&configuration(overrides)).expect("event limits")
     37 }
     38 
     39 fn vector() -> Value {
     40     serde_json::from_str(VECTOR).expect("trade-ingest vector")
     41 }
     42 
     43 fn valid_wire() -> Vec<u8> {
     44     vector()["raw_json"]
     45         .as_str()
     46         .expect("raw event")
     47         .as_bytes()
     48         .to_vec()
     49 }
     50 
     51 fn observed(value: u64) -> RhiTradeMutationObservedAtUnixSeconds {
     52     RhiTradeMutationObservedAtUnixSeconds::new(value).expect("observation")
     53 }
     54 
     55 fn policy(value: u64) -> RhiTradeMutationAuthoredTimePolicy {
     56     RhiTradeMutationAuthoredTimePolicy::new(value).expect("time policy")
     57 }
     58 
     59 fn error(bytes: &[u8]) -> RhiTradeMutationAdmissionErrorKind {
     60     admit_rhi_trade_mutation_event(limits(&[]), bytes, observed(1_784_347_200), policy(0))
     61         .expect_err("event must fail")
     62         .kind()
     63 }
     64 
     65 fn keys(seed: u8) -> Keys {
     66     Keys::parse(&format!("{seed:02x}{}", "00".repeat(31))).expect("test keys")
     67 }
     68 
     69 fn fixture_keys() -> Keys {
     70     Keys::parse("10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5")
     71         .expect("approved fixture keys")
     72 }
     73 
     74 fn signed_variant(
     75     kind: u16,
     76     created_at: u64,
     77     content: String,
     78     tags: Vec<Tag>,
     79     keys: &Keys,
     80 ) -> Vec<u8> {
     81     let event = EventBuilder::new(Kind::Custom(kind), content)
     82         .tags(tags)
     83         .custom_created_at(Timestamp::from_secs(created_at))
     84         .sign_with_keys(keys)
     85         .expect("signed event");
     86     serde_json::to_vec(&event).expect("event JSON")
     87 }
     88 
     89 fn vector_parts() -> (String, Vec<Tag>, u64) {
     90     let event: Value = serde_json::from_slice(&valid_wire()).expect("event JSON");
     91     let content = event["content"].as_str().expect("content").to_owned();
     92     let tags = event["tags"]
     93         .as_array()
     94         .expect("tags")
     95         .iter()
     96         .map(|tag| {
     97             let values = tag
     98                 .as_array()
     99                 .expect("tag")
    100                 .iter()
    101                 .map(|value| value.as_str().expect("tag element").to_owned())
    102                 .collect::<Vec<_>>();
    103             Tag::parse(values).expect("typed tag")
    104         })
    105         .collect::<Vec<_>>();
    106     let created_at = event["created_at"].as_u64().expect("created_at");
    107     (content, tags, created_at)
    108 }
    109 
    110 #[test]
    111 fn machine_contract_vector_and_configuration_projection_are_exact() {
    112     let contract: Value = serde_json::from_str(CONTRACT).expect("trade-ingest contract");
    113     assert_eq!(contract["schema"], "radroots.rhi.trade-ingest.v1");
    114     assert_eq!(contract["contract_version"], 1);
    115     assert_eq!(RHI_TRADE_INGEST_CONTRACT_VERSION, 1);
    116     assert_eq!(contract["wire"]["original_wire_cap_before_parse"], true);
    117     assert_eq!(
    118         contract["verification"]["registered_kinds"],
    119         json!([3470, 3471, 3472, 3473, 3474])
    120     );
    121     assert_eq!(contract["authored_time"]["default"], "none");
    122     assert_eq!(contract["effects"]["sqlite"], false);
    123 
    124     let digest = format!("{:x}", Sha256::digest(VECTOR.as_bytes()));
    125     assert_eq!(contract["conformance_vector"]["sha256"], digest);
    126 
    127     let limits = limits(&[]);
    128     assert_eq!(limits.wire_bytes(), 262_144);
    129     assert_eq!(limits.content_bytes(), 131_072);
    130     assert_eq!(limits.tag_count(), 1_024);
    131     assert_eq!(limits.tag_total_elements(), 4_096);
    132     assert_eq!(limits.tag_element_bytes(), 4_096);
    133     assert_eq!(limits.tag_total_bytes(), 131_072);
    134     assert_eq!(RHI_TRADE_EVENT_ID_MAX_BYTES, 64);
    135     assert_eq!(RHI_TRADE_EVENT_PUBLIC_KEY_MAX_BYTES, 64);
    136     assert_eq!(RHI_TRADE_EVENT_SIGNATURE_MAX_BYTES, 128);
    137     assert_eq!(RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT, 64);
    138     assert_eq!(RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES, 65_536);
    139 }
    140 
    141 #[test]
    142 fn promoted_lib_proposal_vector_is_verified_and_retained_exactly() {
    143     let bytes = valid_wire();
    144     let admitted =
    145         admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_200), policy(0))
    146             .expect("canonical signed mutation");
    147     assert_eq!(admitted.original_bytes(), bytes);
    148     assert_eq!(admitted.event_id().to_hex(), vector()["event_id"]);
    149     assert_eq!(admitted.event_kind(), 3470);
    150     assert_eq!(admitted.authored_at_unix_seconds(), 1_784_347_200);
    151     assert_eq!(
    152         admitted.mutation().mutation_id.as_ref(),
    153         Some(admitted.mutation_id())
    154     );
    155 
    156     let rendered = format!("{admitted:?}");
    157     assert!(!rendered.contains(vector()["event_id"].as_str().expect("event id")));
    158     assert!(!rendered.contains(&admitted.mutation_id().to_hex()));
    159     assert!(!rendered.contains("farm-1"));
    160 }
    161 
    162 #[test]
    163 fn every_configured_wire_limit_is_exact_and_precedes_verification() {
    164     let bytes = valid_wire();
    165     let value: Value = serde_json::from_slice(&bytes).expect("event");
    166     let tags = value["tags"].as_array().expect("tags");
    167     let content_bytes = value["content"].as_str().expect("content").len();
    168     let tag_count = tags.len();
    169     let tag_elements = tags
    170         .iter()
    171         .map(|tag| tag.as_array().expect("tag").len())
    172         .sum::<usize>();
    173     let tag_bytes = tags
    174         .iter()
    175         .flat_map(|tag| tag.as_array().expect("tag"))
    176         .map(|value| value.as_str().expect("element").len())
    177         .sum::<usize>();
    178     let tag_element_bytes = tags
    179         .iter()
    180         .flat_map(|tag| tag.as_array().expect("tag"))
    181         .map(|value| value.as_str().expect("element").len())
    182         .max()
    183         .expect("element");
    184 
    185     for (field, exact, rejected) in [
    186         (
    187             "wire_bytes",
    188             bytes.len(),
    189             RhiTradeMutationAdmissionErrorKind::EventTooLarge,
    190         ),
    191         (
    192             "content_bytes",
    193             content_bytes,
    194             RhiTradeMutationAdmissionErrorKind::EventContentTooLarge,
    195         ),
    196         (
    197             "tag_count",
    198             tag_count,
    199             RhiTradeMutationAdmissionErrorKind::TooManyTags,
    200         ),
    201         (
    202             "tag_total_elements",
    203             tag_elements,
    204             RhiTradeMutationAdmissionErrorKind::TooManyTagElements,
    205         ),
    206         (
    207             "tag_element_bytes",
    208             tag_element_bytes,
    209             RhiTradeMutationAdmissionErrorKind::TagElementTooLarge,
    210         ),
    211         (
    212             "tag_total_bytes",
    213             tag_bytes,
    214             RhiTradeMutationAdmissionErrorKind::TagsTooLarge,
    215         ),
    216     ] {
    217         admit_rhi_trade_mutation_event(
    218             limits(&[(field, exact)]),
    219             &bytes,
    220             observed(1_784_347_200),
    221             policy(0),
    222         )
    223         .unwrap_or_else(|failure| panic!("{field} exact boundary failed: {failure}"));
    224         let failure = admit_rhi_trade_mutation_event(
    225             limits(&[(field, exact - 1)]),
    226             &bytes,
    227             observed(1_784_347_200),
    228             policy(0),
    229         )
    230         .expect_err("just below required capacity");
    231         assert_eq!(failure.kind(), rejected, "{field}");
    232     }
    233 }
    234 
    235 #[test]
    236 fn original_bytes_identifiers_duplicates_utf8_and_required_shape_fail_closed() {
    237     assert_eq!(error(&[]), RhiTradeMutationAdmissionErrorKind::EmptyEvent);
    238     assert_eq!(
    239         error(&[0xff]),
    240         RhiTradeMutationAdmissionErrorKind::InvalidEventUtf8
    241     );
    242 
    243     let oversized = vec![b' '; limits(&[]).wire_bytes() + 1];
    244     assert_eq!(
    245         error(&oversized),
    246         RhiTradeMutationAdmissionErrorKind::EventTooLarge
    247     );
    248 
    249     let valid = String::from_utf8(valid_wire()).expect("UTF-8 event");
    250     let duplicate = valid.replacen("\"id\":", "\"id\":\"11\",\"id\":", 1);
    251     assert_eq!(
    252         error(duplicate.as_bytes()),
    253         RhiTradeMutationAdmissionErrorKind::DuplicateEventField
    254     );
    255 
    256     let mut value: Value = serde_json::from_str(&valid).expect("event");
    257     value["id"] = Value::String("1".repeat(65));
    258     assert_eq!(
    259         error(&serde_json::to_vec(&value).expect("event")),
    260         RhiTradeMutationAdmissionErrorKind::EventIdentifierTooLarge
    261     );
    262     value["id"] = Value::Null;
    263     assert_eq!(
    264         error(&serde_json::to_vec(&value).expect("event")),
    265         RhiTradeMutationAdmissionErrorKind::MalformedEvent
    266     );
    267 }
    268 
    269 #[test]
    270 fn outer_extensions_are_bounded_but_never_gain_semantic_authority() {
    271     let mut exact: Value = serde_json::from_slice(&valid_wire()).expect("event");
    272     let object = exact.as_object_mut().expect("object");
    273     for index in 0..RHI_TRADE_EVENT_EXTRA_FIELD_MAX_COUNT {
    274         object.insert(format!("extension_{index:02}"), json!(index));
    275     }
    276     let exact_bytes = serde_json::to_vec(&exact).expect("event");
    277     admit_rhi_trade_mutation_event(
    278         limits(&[]),
    279         &exact_bytes,
    280         observed(1_784_347_200),
    281         policy(0),
    282     )
    283     .expect("exact extra count");
    284     exact
    285         .as_object_mut()
    286         .expect("object")
    287         .insert("extension_over".to_owned(), json!(true));
    288     assert_eq!(
    289         error(&serde_json::to_vec(&exact).expect("event")),
    290         RhiTradeMutationAdmissionErrorKind::TooManyExtraFields
    291     );
    292 
    293     let mut exact_bytes_value: Value = serde_json::from_slice(&valid_wire()).expect("event");
    294     exact_bytes_value
    295         .as_object_mut()
    296         .expect("object")
    297         .insert("extra".to_owned(), Value::String("x".repeat(65_526)));
    298     admit_rhi_trade_mutation_event(
    299         limits(&[]),
    300         &serde_json::to_vec(&exact_bytes_value).expect("event"),
    301         observed(1_784_347_200),
    302         policy(0),
    303     )
    304     .expect("exact extra byte budget");
    305     exact_bytes_value["extra"] = Value::String("x".repeat(65_527));
    306     assert_eq!(
    307         error(&serde_json::to_vec(&exact_bytes_value).expect("event")),
    308         RhiTradeMutationAdmissionErrorKind::ExtraFieldsTooLarge
    309     );
    310 }
    311 
    312 #[test]
    313 fn event_id_signature_kind_author_content_and_tags_are_independent_checks() {
    314     let mut value: Value = serde_json::from_slice(&valid_wire()).expect("event");
    315     value["id"] = Value::String("0".repeat(64));
    316     assert_eq!(
    317         error(&serde_json::to_vec(&value).expect("event")),
    318         RhiTradeMutationAdmissionErrorKind::InvalidEventId
    319     );
    320 
    321     value = serde_json::from_slice(&valid_wire()).expect("event");
    322     value["sig"] = Value::String("0".repeat(128));
    323     assert_eq!(
    324         error(&serde_json::to_vec(&value).expect("event")),
    325         RhiTradeMutationAdmissionErrorKind::InvalidSignature
    326     );
    327 
    328     let (content, tags, created_at) = vector_parts();
    329     let unsupported = signed_variant(9_999, created_at, content.clone(), tags.clone(), &keys(9));
    330     assert_eq!(
    331         error(&unsupported),
    332         RhiTradeMutationAdmissionErrorKind::UnsupportedKind
    333     );
    334     let future_unsupported = signed_variant(
    335         9_999,
    336         created_at + 10,
    337         content.clone(),
    338         tags.clone(),
    339         &keys(9),
    340     );
    341     assert_eq!(
    342         error(&future_unsupported),
    343         RhiTradeMutationAdmissionErrorKind::UnsupportedKind
    344     );
    345 
    346     let wrong_author = signed_variant(3_470, created_at, content.clone(), tags.clone(), &keys(9));
    347     assert_eq!(
    348         error(&wrong_author),
    349         RhiTradeMutationAdmissionErrorKind::InvalidAuthor
    350     );
    351 
    352     assert_eq!(fixture_keys().public_key().to_hex(), vector()["pubkey"]);
    353     let mut noncanonical_content = content.clone();
    354     noncanonical_content.insert(1, ' ');
    355     let noncanonical = signed_variant(
    356         3_470,
    357         created_at,
    358         noncanonical_content.clone(),
    359         tags.clone(),
    360         &fixture_keys(),
    361     );
    362     assert_eq!(
    363         error(&noncanonical),
    364         RhiTradeMutationAdmissionErrorKind::InvalidMutation
    365     );
    366     let future_noncanonical = signed_variant(
    367         3_470,
    368         created_at + 10,
    369         noncanonical_content,
    370         tags.clone(),
    371         &fixture_keys(),
    372     );
    373     assert_eq!(
    374         error(&future_noncanonical),
    375         RhiTradeMutationAdmissionErrorKind::InvalidMutation
    376     );
    377 
    378     let mut duplicate_tags = tags;
    379     duplicate_tags.push(Tag::parse(["d", "99999999999999999999999999999999"]).expect("tag"));
    380     let duplicate = signed_variant(3_470, created_at, content, duplicate_tags, &fixture_keys());
    381     assert_eq!(
    382         error(&duplicate),
    383         RhiTradeMutationAdmissionErrorKind::InvalidMutation
    384     );
    385 }
    386 
    387 #[test]
    388 fn authored_time_policy_is_explicit_inclusive_old_safe_and_overflow_bounded() {
    389     assert!(RhiTradeMutationObservedAtUnixSeconds::new(0).is_err());
    390     assert!(RhiTradeMutationObservedAtUnixSeconds::new(i64::MAX as u64).is_ok());
    391     assert!(RhiTradeMutationObservedAtUnixSeconds::new(i64::MAX as u64 + 1).is_err());
    392     assert!(RhiTradeMutationAuthoredTimePolicy::new(i64::MAX as u64).is_ok());
    393     assert!(RhiTradeMutationAuthoredTimePolicy::new(i64::MAX as u64 + 1).is_err());
    394 
    395     let bytes = valid_wire();
    396     admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_202), policy(0))
    397         .expect("old lineage event");
    398     admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_198), policy(2))
    399         .expect("inclusive future boundary");
    400     assert_eq!(
    401         admit_rhi_trade_mutation_event(limits(&[]), &bytes, observed(1_784_347_197), policy(2),)
    402             .expect_err("excessive future")
    403             .kind(),
    404         RhiTradeMutationAdmissionErrorKind::AuthoredTimeRejected
    405     );
    406 
    407     let (content, tags, _) = vector_parts();
    408     let unrepresentable =
    409         signed_variant(3_470, i64::MAX as u64 + 1, content, tags, &fixture_keys());
    410     assert_eq!(
    411         admit_rhi_trade_mutation_event(
    412             limits(&[]),
    413             &unrepresentable,
    414             observed(i64::MAX as u64),
    415             policy(0),
    416         )
    417         .expect_err("unrepresentable authored time")
    418         .kind(),
    419         RhiTradeMutationAdmissionErrorKind::InvalidAuthoredTime
    420     );
    421 }
    422 
    423 #[test]
    424 fn errors_and_accepted_debug_are_source_free_and_redacted() {
    425     let secret = "trade-secret-evidence-marker";
    426     let malformed = format!("{{\"content\":\"{secret}\"}}");
    427     let failure =
    428         admit_rhi_trade_mutation_event(limits(&[]), malformed.as_bytes(), observed(1), policy(0))
    429             .expect_err("malformed event");
    430     assert!(failure.source().is_none());
    431     for rendered in [failure.to_string(), format!("{failure:?}")] {
    432         assert!(!rendered.contains(secret));
    433         assert!(!rendered.contains("content"));
    434         assert!(!rendered.contains("serde"));
    435     }
    436 }
    437 
    438 #[test]
    439 fn extra_byte_measurement_matches_the_frozen_member_formula() {
    440     let mut object = Map::new();
    441     object.insert("extra".to_owned(), Value::String("x".repeat(65_526)));
    442     let encoded = serde_json::to_vec(&Value::Object(object)).expect("JSON");
    443     assert_eq!(encoded.len() - 2, RHI_TRADE_EVENT_EXTRA_JSON_MAX_BYTES);
    444 }