lib

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

rhi_attestation.rs (14425B)


      1 #![cfg(feature = "json")]
      2 
      3 use std::error::Error as _;
      4 
      5 use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
      6 use radroots_event::{
      7     admission::RawEvent,
      8     envelope::{EventEnvelope, EventEnvelopeParts},
      9     id::EventId,
     10     trade::canonical_jcs_value,
     11 };
     12 use radroots_event_codec::{
     13     decode::rhi::{
     14         RadrootsRhiEvidenceAttestationError, RadrootsRhiEvidenceAttestationV1,
     15         rhi_evidence_attestation_from_event, rhi_evidence_attestation_from_verified_event,
     16         validate_rhi_evidence_attestation_supersession, validate_rhi_evidence_attestation_tags,
     17     },
     18     encode::rhi::{
     19         RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES, rhi_evidence_attestation_event_build,
     20         rhi_evidence_attestation_event_build_with_extra_tags,
     21     },
     22     verify::Nip01SignatureVerifier,
     23 };
     24 use serde_json::Value;
     25 use sha2::{Digest as _, Sha256};
     26 
     27 const VECTORS: &str = include_str!(
     28     "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json"
     29 );
     30 const DIGEST_DOMAIN: &[u8] = b"radroots:rhi-evidence-attestation-statement:v1\0";
     31 
     32 fn vectors() -> Value {
     33     serde_json::from_str(VECTORS).expect("RHI conformance vectors")
     34 }
     35 
     36 fn vector<'a>(vectors: &'a Value, id: &str) -> &'a Value {
     37     vectors["vectors"]
     38         .as_array()
     39         .expect("vector list")
     40         .iter()
     41         .find(|vector| vector["id"] == id)
     42         .unwrap_or_else(|| panic!("missing vector {id}"))
     43 }
     44 
     45 fn positive_content(vector: &Value) -> &str {
     46     vector["expected"]["canonical_event_content_utf8"]
     47         .as_str()
     48         .expect("canonical event content")
     49 }
     50 
     51 fn tags(vector: &Value) -> Vec<Vec<String>> {
     52     vector["expected"]["tags"]
     53         .as_array()
     54         .expect("tag list")
     55         .iter()
     56         .map(|tag| {
     57             tag.as_array()
     58                 .expect("tag")
     59                 .iter()
     60                 .map(|value| value.as_str().expect("tag value").to_owned())
     61                 .collect()
     62         })
     63         .collect()
     64 }
     65 
     66 fn structural_event(
     67     kind: u32,
     68     author: &str,
     69     tags: Vec<Vec<String>>,
     70     content: String,
     71 ) -> EventEnvelope {
     72     EventEnvelope::new(EventEnvelopeParts {
     73         id: "0".repeat(64),
     74         author: author.to_owned(),
     75         created_at: 1_800_000_000,
     76         kind,
     77         tags,
     78         content,
     79         sig: "1".repeat(128),
     80     })
     81     .expect("structurally valid event")
     82 }
     83 
     84 fn canonical_report_from_statement(statement: Value) -> String {
     85     let payload = canonical_jcs_value(&statement).expect("canonical statement");
     86     let mut hasher = Sha256::new();
     87     hasher.update(DIGEST_DOMAIN);
     88     hasher.update(payload.as_bytes());
     89     let digest = hex::encode(hasher.finalize());
     90     let mut report = statement;
     91     let object = report.as_object_mut().expect("statement object");
     92     object.insert("report_id".to_owned(), Value::String(digest.clone()));
     93     object.insert("statement_digest".to_owned(), Value::String(digest));
     94     canonical_jcs_value(&report).expect("canonical report")
     95 }
     96 
     97 #[test]
     98 fn all_frozen_rhi_vectors_execute_their_governed_boundaries() {
     99     let vectors = vectors();
    100     let current_vector = vector(&vectors, "rhi_evidence_attestation_current_001");
    101     let superseding_vector = vector(&vectors, "rhi_evidence_attestation_superseding_002");
    102     let current =
    103         RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content(current_vector))
    104             .expect("current report");
    105     let superseding = RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content(
    106         superseding_vector,
    107     ))
    108     .expect("superseding report");
    109 
    110     for (fixture, attestation) in [
    111         (current_vector, &current),
    112         (superseding_vector, &superseding),
    113     ] {
    114         let built = rhi_evidence_attestation_event_build(attestation);
    115         assert_eq!(built.kind, 3441);
    116         assert_eq!(built.tags, tags(fixture));
    117         assert_eq!(built.content, positive_content(fixture));
    118         validate_rhi_evidence_attestation_tags(attestation, &built.tags)
    119             .expect("independent tag validator");
    120         let parsed = rhi_evidence_attestation_from_event(&structural_event(
    121             built.kind,
    122             &attestation.issuer().to_hex(),
    123             built.tags,
    124             built.content,
    125         ))
    126         .expect("structural parser");
    127         assert_eq!(&parsed, attestation);
    128     }
    129 
    130     let negative_ids = vectors["vectors"]
    131         .as_array()
    132         .expect("vectors")
    133         .iter()
    134         .filter(|vector| vector["kind"] == "rhi.evidence_attestation.invalid")
    135         .map(|vector| vector["id"].as_str().expect("vector id"))
    136         .collect::<Vec<_>>();
    137     assert_eq!(negative_ids.len(), 11);
    138 
    139     let current_parts = rhi_evidence_attestation_event_build(&current);
    140     let error = rhi_evidence_attestation_from_event(&structural_event(
    141         3440,
    142         &current.issuer().to_hex(),
    143         current_parts.tags.clone(),
    144         current_parts.content.clone(),
    145     ))
    146     .expect_err("wrong kind");
    147     assert_vector_error(&vectors, "rhi_evidence_attestation_wrong_kind_003", error);
    148 
    149     let error = rhi_evidence_attestation_from_event(&structural_event(
    150         current_parts.kind,
    151         "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af",
    152         current_parts.tags.clone(),
    153         current_parts.content.clone(),
    154     ))
    155     .expect_err("wrong author");
    156     assert_vector_error(&vectors, "rhi_evidence_attestation_wrong_author_004", error);
    157 
    158     let error = rhi_evidence_attestation_from_event(&structural_event(
    159         current_parts.kind,
    160         &current.issuer().to_hex(),
    161         current_parts.tags.clone(),
    162         format!("{} ", current_parts.content),
    163     ))
    164     .expect_err("noncanonical content");
    165     assert_vector_error(
    166         &vectors,
    167         "rhi_evidence_attestation_noncanonical_content_005",
    168         error,
    169     );
    170 
    171     let mut digest_mismatch: Value = serde_json::from_str(&current_parts.content).unwrap();
    172     digest_mismatch["statement_digest"] = Value::String("0".repeat(64));
    173     let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    174         canonical_jcs_value(&digest_mismatch).unwrap(),
    175     )
    176     .expect_err("digest mismatch");
    177     assert_vector_error(
    178         &vectors,
    179         "rhi_evidence_attestation_digest_mismatch_006",
    180         error,
    181     );
    182 
    183     let mut unknown_outcome: Value = serde_json::from_str(&current_parts.content).unwrap();
    184     unknown_outcome["outcome"] = Value::String("complete".to_owned());
    185     let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    186         canonical_jcs_value(&unknown_outcome).unwrap(),
    187     )
    188     .expect_err("unknown outcome");
    189     assert_vector_error(
    190         &vectors,
    191         "rhi_evidence_attestation_unknown_outcome_007",
    192         error,
    193     );
    194 
    195     let mut missing_claim = current_parts.tags.clone();
    196     missing_claim.retain(|tag| tag.get(2).map(String::as_str) != Some("claim"));
    197     assert_vector_error(
    198         &vectors,
    199         "rhi_evidence_attestation_missing_claim_tag_008",
    200         validate_rhi_evidence_attestation_tags(&current, &missing_claim).unwrap_err(),
    201     );
    202 
    203     let mut duplicate_trade = current_parts.tags.clone();
    204     duplicate_trade.push(vec!["d".to_owned(), "9".repeat(32)]);
    205     assert_vector_error(
    206         &vectors,
    207         "rhi_evidence_attestation_duplicate_trade_tag_009",
    208         validate_rhi_evidence_attestation_tags(&current, &duplicate_trade).unwrap_err(),
    209     );
    210 
    211     let mut duplicate_statement = current_parts.tags.clone();
    212     duplicate_statement.push(vec!["x".to_owned(), "9".repeat(64), "statement".to_owned()]);
    213     assert_vector_error(
    214         &vectors,
    215         "rhi_evidence_attestation_duplicate_statement_tag_010",
    216         validate_rhi_evidence_attestation_tags(&current, &duplicate_statement).unwrap_err(),
    217     );
    218 
    219     let mut incomplete: Value = serde_json::from_str(&current_parts.content).unwrap();
    220     incomplete["supersedes_report_id"] = Value::String("7".repeat(64));
    221     let error = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    222         canonical_jcs_value(&incomplete).unwrap(),
    223     )
    224     .expect_err("incomplete supersession");
    225     assert_vector_error(
    226         &vectors,
    227         "rhi_evidence_attestation_incomplete_supersession_011",
    228         error,
    229     );
    230 
    231     let mut current_statement = current_vector["input"]["statement_payload"].clone();
    232     current_statement["trade_generation"] = Value::from(8);
    233     current_statement["observed_at_unix_s"] = Value::from(1_800_000_100_u64);
    234     let ordered_current = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    235         canonical_report_from_statement(current_statement.clone()),
    236     )
    237     .unwrap();
    238     let current_event_id = EventId::parse("8".repeat(64)).unwrap();
    239     current_statement["trade_generation"] = Value::from(7);
    240     current_statement["observed_at_unix_s"] = Value::from(1_800_000_200_u64);
    241     current_statement["supersedes_report_id"] =
    242         Value::String(hex::encode(ordered_current.statement_digest()));
    243     current_statement["supersedes_event_id"] = Value::String(current_event_id.to_hex());
    244     let stale = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    245         canonical_report_from_statement(current_statement),
    246     )
    247     .unwrap();
    248     assert_vector_error(
    249         &vectors,
    250         "rhi_evidence_attestation_stale_supersession_012",
    251         validate_rhi_evidence_attestation_supersession(&ordered_current, &current_event_id, &stale)
    252             .unwrap_err(),
    253     );
    254 
    255     assert_vector_error(
    256         &vectors,
    257         "rhi_evidence_attestation_caller_structural_tag_013",
    258         rhi_evidence_attestation_event_build_with_extra_tags(
    259             &current,
    260             &[vec!["d".to_owned(), current.trade_id().to_hex()]],
    261         )
    262         .unwrap_err(),
    263     );
    264 }
    265 
    266 fn assert_vector_error(vectors: &Value, id: &str, error: RadrootsRhiEvidenceAttestationError) {
    267     assert_eq!(
    268         error.code(),
    269         vector(vectors, id)["expected"]["error_code"]
    270             .as_str()
    271             .expect("error code"),
    272         "{id}"
    273     );
    274 }
    275 
    276 #[test]
    277 fn signed_attestation_requires_and_preserves_the_verified_typestate() {
    278     let vectors = vectors();
    279     let base = vector(&vectors, "rhi_evidence_attestation_current_001");
    280     let keys = Keys::parse("0101010101010101010101010101010101010101010101010101010101010101")
    281         .expect("fixture keys");
    282     let mut statement = base["input"]["statement_payload"].clone();
    283     statement["issuer_pubkey"] = Value::String(keys.public_key().to_hex());
    284     let report = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    285         canonical_report_from_statement(statement),
    286     )
    287     .expect("fixture report");
    288     let parts = rhi_evidence_attestation_event_build(&report);
    289     let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
    290         .tags(
    291             parts
    292                 .tags
    293                 .into_iter()
    294                 .map(Tag::parse)
    295                 .collect::<Result<Vec<_>, _>>()
    296                 .expect("tags"),
    297         )
    298         .custom_created_at(Timestamp::from_secs(1_800_000_000))
    299         .sign_with_keys(&keys)
    300         .expect("signed event");
    301     let envelope = EventEnvelope::new(EventEnvelopeParts {
    302         id: event.id.to_hex(),
    303         author: event.pubkey.to_hex(),
    304         created_at: event.created_at.as_secs(),
    305         kind: u32::from(event.kind.as_u16()),
    306         tags: event
    307             .tags
    308             .iter()
    309             .map(|tag| tag.as_slice().to_vec())
    310             .collect(),
    311         content: event.content,
    312         sig: event.sig.to_string(),
    313     })
    314     .expect("event envelope");
    315     let verified = RawEvent::new(envelope)
    316         .verify_id()
    317         .expect("verified id")
    318         .verify_signature(&Nip01SignatureVerifier)
    319         .expect("verified signature");
    320     assert_eq!(
    321         rhi_evidence_attestation_from_verified_event(&verified).unwrap(),
    322         report
    323     );
    324 }
    325 
    326 #[test]
    327 fn malformed_shapes_bounds_and_diagnostics_fail_closed() {
    328     let vectors = vectors();
    329     let fixture = vector(&vectors, "rhi_evidence_attestation_current_001");
    330     let report =
    331         RadrootsRhiEvidenceAttestationV1::from_canonical_content(positive_content(fixture))
    332             .expect("report");
    333     let parts = rhi_evidence_attestation_event_build(&report);
    334 
    335     let mut reordered = parts.tags.clone();
    336     reordered.swap(0, 1);
    337     assert_eq!(
    338         validate_rhi_evidence_attestation_tags(&report, &reordered).unwrap_err(),
    339         RadrootsRhiEvidenceAttestationError::InvalidTagShape
    340     );
    341     let mut malformed_marker = parts.tags.clone();
    342     malformed_marker[2][2] = "unknown".to_owned();
    343     assert_eq!(
    344         validate_rhi_evidence_attestation_tags(&report, &malformed_marker).unwrap_err(),
    345         RadrootsRhiEvidenceAttestationError::MissingClaimTag
    346     );
    347     let mut unknown = parts.tags.clone();
    348     unknown.push(vec!["a".to_owned(), "value".to_owned()]);
    349     assert_eq!(
    350         validate_rhi_evidence_attestation_tags(&report, &unknown).unwrap_err(),
    351         RadrootsRhiEvidenceAttestationError::UnexpectedTag
    352     );
    353     assert_eq!(
    354         RadrootsRhiEvidenceAttestationV1::from_canonical_content(vec![
    355             b'x';
    356             RADROOTS_RHI_EVIDENCE_ATTESTATION_MAXIMUM_BYTES
    357                 + 1
    358         ])
    359         .unwrap_err(),
    360         RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent
    361     );
    362     let mut missing_supersession_fields: Value =
    363         serde_json::from_str(parts.content.as_str()).expect("report JSON");
    364     let object = missing_supersession_fields
    365         .as_object_mut()
    366         .expect("report object");
    367     object.remove("supersedes_event_id");
    368     object.remove("supersedes_report_id");
    369     assert_eq!(
    370         RadrootsRhiEvidenceAttestationV1::from_canonical_content(
    371             canonical_jcs_value(&missing_supersession_fields).expect("canonical malformed report")
    372         )
    373         .unwrap_err(),
    374         RadrootsRhiEvidenceAttestationError::NoncanonicalReportContent
    375     );
    376 
    377     let diagnostic = format!(
    378         "{0:?} {0}",
    379         RadrootsRhiEvidenceAttestationError::StatementDigestMismatch
    380     );
    381     for secret in [
    382         report.issuer().to_hex(),
    383         report.trade_id().to_hex(),
    384         hex::encode(report.statement_digest()),
    385     ] {
    386         assert!(!diagnostic.contains(&secret));
    387     }
    388     assert!(
    389         RadrootsRhiEvidenceAttestationError::StatementDigestMismatch
    390             .source()
    391             .is_none()
    392     );
    393     assert!(!format!("{report:?}").contains(&report.issuer().to_hex()));
    394 }