rhi

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

commit f0ec6424160c9bf0265ba9bcd24c9bef6f0e0e8c
parent b5cd7fd0f7bbcf9a6dfed8a5330cedfe7a0e0700
Author: triesap <tyson@radroots.org>
Date:   Thu,  2 Jul 2026 21:08:55 +0000

rhi: cover recovered result field drift

- add a reusable recovered result validation fixture
- corrupt every critical recovered result binding field
- assert signed recovered result payload drift fails closed
- validate with the recovered workspace test lane

Diffstat:
Msrc/features/trade_validation_receipt.rs | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 200 insertions(+), 0 deletions(-)

diff --git a/src/features/trade_validation_receipt.rs b/src/features/trade_validation_receipt.rs @@ -3309,6 +3309,76 @@ mod tests { .expect("signed result") } + struct RecoveredResultValidationFixture { + worker: RadrootsNostrKeys, + job: RadrootsNostrEvent, + envelope: radroots_trade::dvm::RadrootsTradeTransitionProofRequestEnvelope, + processed: RhiProcessedJobState, + verified_receipt: RadrootsVerifiedValidationReceipt, + receipt_event_id: String, + result_json: serde_json::Value, + result_tags: Vec<Vec<String>>, + } + + fn recovered_result_validation_fixture() -> RecoveredResultValidationFixture { + let worker = RadrootsNostrKeys::generate(); + let requester = RadrootsNostrKeys::generate(); + let buyer = RadrootsNostrKeys::generate(); + let seller = RadrootsNostrKeys::generate(); + let listing_event = listing_event(&seller); + let (request_event, decision_event) = signed_order_events(&buyer, &seller, &listing_event); + let job = job_request( + &requester, + &worker, + &listing_event, + &request_event, + &decision_event, + RadrootsSp1TradeProofMode::None, + None, + None, + ); + let request_event = radroots_event_from_nostr(&job); + let envelope = parse_transition_proof_request_event(&request_event).expect("envelope"); + let processed = processed_job_for_test(&job); + let verified_receipt = verified_receipt_for_payload( + &envelope.content, + RadrootsValidationReceiptProofSystem::None, + ); + let receipt_event_id = publish_result_id(9); + let result = super::result_payload( + &job, + &processed, + &envelope, + receipt_event_id.as_str(), + verified_receipt.clone(), + &deterministic_policy(), + None, + ) + .expect("result payload"); + let result_json = serde_json::to_value(&result).expect("result json"); + let result_tags = + super::result_tags_from_dvm(&job, &envelope.tags.inputs, receipt_event_id.as_str()) + .expect("result tags"); + RecoveredResultValidationFixture { + worker, + job, + envelope, + processed, + verified_receipt, + receipt_event_id, + result_json, + result_tags, + } + } + + fn set_result_json_field( + value: &mut serde_json::Value, + field: &str, + replacement: impl Into<serde_json::Value>, + ) { + value[field] = replacement.into(); + } + fn verified_receipt_for_payload( request: &RadrootsTradeTransitionProofRequestV1, proof_system: RadrootsValidationReceiptProofSystem, @@ -4723,6 +4793,136 @@ mod tests { )); } + #[test] + fn validate_recovered_result_event_rejects_critical_payload_field_corruption() { + let cases: Vec<(&str, Box<dyn Fn(&mut serde_json::Value)>)> = vec![ + ( + "status", + Box::new(|value| set_result_json_field(value, "status", "failed")), + ), + ( + "worker_role", + Box::new(|value| { + set_result_json_field(value, "worker_role", "authoritative_prover") + }), + ), + ( + "receipt_kind", + Box::new(|value| value["receipt_kind"] = serde_json::json!(1_u32)), + ), + ( + "prover_backend", + Box::new(|value| set_result_json_field(value, "prover_backend", "local_execute")), + ), + ( + "receipt_event_id", + Box::new(|value| { + set_result_json_field(value, "receipt_event_id", publish_result_id(8)) + }), + ), + ( + "request_hash", + Box::new(|value| set_result_json_field(value, "request_hash", hash32('8'))), + ), + ( + "customer_pubkey", + Box::new(|value| set_result_json_field(value, "customer_pubkey", "8".repeat(64))), + ), + ( + "worker_pubkey", + Box::new(|value| set_result_json_field(value, "worker_pubkey", "7".repeat(64))), + ), + ( + "order_id", + Box::new(|value| set_result_json_field(value, "order_id", "order-other")), + ), + ( + "listing_event_id", + Box::new(|value| { + set_result_json_field(value, "listing_event_id", publish_result_id(7)) + }), + ), + ( + "request_event_id", + Box::new(|value| { + set_result_json_field(value, "request_event_id", publish_result_id(6)) + }), + ), + ( + "decision_event_id", + Box::new(|value| { + set_result_json_field(value, "decision_event_id", publish_result_id(5)) + }), + ), + ( + "event_set_root", + Box::new(|value| set_result_json_field(value, "event_set_root", hash32('6'))), + ), + ( + "reducer_output_root", + Box::new(|value| set_result_json_field(value, "reducer_output_root", hash32('7'))), + ), + ( + "public_values_hash", + Box::new(|value| set_result_json_field(value, "public_values_hash", hash32('8'))), + ), + ( + "proof_system", + Box::new(|value| set_result_json_field(value, "proof_system", "sp1_core")), + ), + ( + "proof_mode", + Box::new(|value| set_result_json_field(value, "proof_mode", "core")), + ), + ( + "validation_authority", + Box::new(|value| { + set_result_json_field(value, "validation_authority", "trusted_rhi_service_key") + }), + ), + ( + "confidence", + Box::new(|value| { + set_result_json_field(value, "confidence", "committed_by_trusted_service") + }), + ), + ]; + + for (name, corrupt) in cases { + let fixture = recovered_result_validation_fixture(); + let mut result_json = fixture.result_json; + corrupt(&mut result_json); + let result_event = super::signed_event_from_parts( + &fixture.worker, + KIND_TRADE_TRANSITION_PROOF_RESULT, + serde_json::to_string(&result_json).expect("result json"), + fixture.result_tags.clone(), + Some(fixture.job.created_at.as_secs()), + ) + .expect("result event"); + + let error = super::validate_recovered_result_event( + &result_event, + &fixture.job, + &fixture.processed, + &fixture.envelope, + fixture.receipt_event_id.as_str(), + &fixture.verified_receipt, + &deterministic_policy(), + ) + .expect_err(name); + + assert!( + matches!( + error, + TradeValidationReceiptJobError::RecoveredResultMismatch + | TradeValidationReceiptJobError::Serde(_) + ), + "{name}: {error:?}" + ); + } + } + #[tokio::test] async fn proof_job_records_completed_job_and_skips_duplicate_replay() { let _guard = test_guard();