lib

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

commit 471b96eba04905ba893a44ab0387986c4658f8f9
parent ccfd9b520733d9cd682f0a02727dfdff6a59234a
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 22:34:52 +0000

sp1-host-trade: cover host proof helpers

- Exclude test scaffolding from coverage counters for host proof tests
- Add inline SP1 proof-envelope fixture coverage for digest and binding validation
- Cover verifier metadata, proof-material, reference, and receipt-binding edge cases
- Verify radroots_sp1_host_trade coverage at 99.787234/100.000000/99.635036/100.000000

Diffstat:
Mcrates/sp1_host_trade/Cargo.toml | 5+++++
Mcrates/sp1_host_trade/src/lib.rs | 552++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 555 insertions(+), 2 deletions(-)

diff --git a/crates/sp1_host_trade/Cargo.toml b/crates/sp1_host_trade/Cargo.toml @@ -38,3 +38,8 @@ sp1-build = { workspace = true, optional = true } [dev-dependencies] radroots_events = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["rt-multi-thread"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + 'cfg(coverage_nightly)', +] } diff --git a/crates/sp1_host_trade/src/lib.rs b/crates/sp1_host_trade/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![forbid(unsafe_code)] use base64::Engine; @@ -1739,13 +1740,13 @@ struct ProofEnvelopeDigestMaterial<'a> { } #[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::{ RadrootsSp1TradeHostError, RadrootsSp1TradeProofMode, generate_order_acceptance_proof, validation_receipt_for_order_acceptance_proof, verify_order_acceptance_proof_artifact_structure, }; - #[cfg(all(feature = "sp1_proving", radroots_sp1_guest_elf))] use base64::Engine; use radroots_events::{RadrootsNostrEvent, kinds::KIND_TRADE_VALIDATION_RECEIPT}; use radroots_sp1_guest_trade::{ @@ -1763,7 +1764,8 @@ mod tests { use radroots_trade::validation_receipt::{ RadrootsValidationReceiptExpectedBinding, RadrootsValidationReceiptProof, RadrootsValidationReceiptProofSystem, RadrootsValidationReceiptResult, - validation_receipt_event_build, verify_validation_receipt_event, + RadrootsValidationReceiptType, validation_receipt_event_build, + verify_validation_receipt_event, }; #[cfg(feature = "sp1_verify")] use serde::Deserialize; @@ -1905,6 +1907,552 @@ mod tests { } } + fn inline_sp1_artifact_fixture() -> ( + super::RadrootsSp1TradeProofBundle, + super::RadrootsSp1TradeProofEnvelope, + ) { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let execution = &bundle.execution; + let proof_bytes = b"deterministic proof bytes"; + let verifying_key_bytes = b"deterministic verifying key bytes"; + let public_values_hash = execution.public_values_hash.clone(); + let canonical_public_values = execution.canonical_public_values.clone(); + let result = execution.public_values.result; + let program_hash = execution + .public_values + .sp1_program_hash + .clone() + .expect("program hash"); + let verifying_key_hash = execution + .public_values + .sp1_verifying_key_hash + .clone() + .expect("verifying key hash"); + let listing_event_id = execution + .public_values + .listing_event_id + .clone() + .expect("listing event id"); + let root_event_id = execution + .public_values + .root_event_id + .clone() + .expect("root event id"); + let target_event_id = execution + .public_values + .target_event_id + .clone() + .expect("target event id"); + let event_set_root = execution.public_values.event_set_root.clone(); + let previous_state_root = execution.public_values.previous_state_root.clone(); + let new_state_root = execution.public_values.new_state_root.clone(); + let changed_records_root = execution.public_values.changed_records_root.clone(); + let error_bitmap = execution.public_values.error_bitmap.clone(); + let mut envelope = super::RadrootsSp1TradeProofEnvelope { + schema_version: super::RADROOTS_SP1_TRADE_PROOF_ARTIFACT_SCHEMA_VERSION, + sp1_version_line: super::RADROOTS_SP1_TRADE_SP1_VERSION_LINE.to_string(), + proof_system: RadrootsValidationReceiptProofSystem::Sp1Core + .as_str() + .to_string(), + proof_mode: "core".to_string(), + proof_codec: super::RADROOTS_SP1_TRADE_PROOF_CODEC.to_string(), + proof_content_hash: super::hash_bytes("radroots:sp1-proof-content:v1", proof_bytes), + proof_digest: String::new(), + public_values_hash: public_values_hash.clone(), + canonical_public_values_hash: super::hash_bytes( + "radroots:sp1-canonical-public-values:v1", + &canonical_public_values, + ), + sp1_program_hash: program_hash.clone(), + sp1_verifying_key_hash: verifying_key_hash.clone(), + sp1_verifying_key_codec: super::RADROOTS_SP1_TRADE_VERIFYING_KEY_CODEC.to_string(), + sp1_verifying_key_base64: base64::engine::general_purpose::STANDARD + .encode(verifying_key_bytes), + receipt_type: RadrootsValidationReceiptType::TradeTransition + .as_str() + .to_string(), + receipt_result: super::validation_receipt_result_label( + super::validation_receipt_result_from_public_values(result), + ) + .to_string(), + listing_event_id, + root_event_id, + target_event_id, + event_set_root, + previous_state_root, + new_state_root, + changed_records_root, + error_bitmap, + proof_content_base64: base64::engine::general_purpose::STANDARD.encode(proof_bytes), + }; + envelope.proof_digest = super::proof_digest_for_envelope(&envelope).expect("digest"); + let envelope_json = serde_json::to_vec(&envelope).expect("envelope json"); + let envelope_base64 = base64::engine::general_purpose::STANDARD.encode(envelope_json); + let mut bundle = bundle; + bundle.proof = super::RadrootsSp1TradeProofArtifact { + inline_proof_base64: Some(envelope_base64), + mode: Some("core".to_string()), + program_hash: Some(program_hash), + proof_digest: envelope.proof_digest.clone(), + proof_reference: None, + public_values_hash, + system: RadrootsValidationReceiptProofSystem::Sp1Core, + verifying_key_hash: Some(verifying_key_hash), + }; + (bundle, envelope) + } + + fn refresh_proof_envelope_digest(envelope: &mut super::RadrootsSp1TradeProofEnvelope) { + envelope.proof_digest = super::proof_digest_for_envelope(envelope).expect("digest"); + } + + fn assert_missing_none_proof_material( + execution: &RadrootsSp1TradePublicValuesExecution, + mut artifact: super::RadrootsSp1TradeProofArtifact, + ) { + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("none material"), + RadrootsSp1TradeHostError::MissingProofMaterial + ); + } + + #[test] + fn enum_labels_and_display_surfaces_are_complete() { + let modes = [ + ( + RadrootsSp1TradeProofMode::None, + None, + RadrootsValidationReceiptProofSystem::None, + ), + ( + RadrootsSp1TradeProofMode::Core, + Some("core"), + RadrootsValidationReceiptProofSystem::Sp1Core, + ), + ( + RadrootsSp1TradeProofMode::Compressed, + Some("compressed"), + RadrootsValidationReceiptProofSystem::Sp1Compressed, + ), + ( + RadrootsSp1TradeProofMode::Groth16, + Some("groth16"), + RadrootsValidationReceiptProofSystem::Sp1Groth16, + ), + ( + RadrootsSp1TradeProofMode::Plonk, + Some("plonk"), + RadrootsValidationReceiptProofSystem::Sp1Plonk, + ), + ]; + for (mode, label, system) in modes { + assert_eq!(mode.proof_system(), system); + assert_eq!(mode.mode_label(), label); + if let Some(label) = label { + assert_eq!(RadrootsSp1TradeProofMode::from_label(label), Some(mode)); + } + } + assert_eq!(RadrootsSp1TradeProofMode::from_label("legacy"), None); + + for backend in [ + super::RadrootsSp1TradeProverBackend::Disabled, + super::RadrootsSp1TradeProverBackend::DeterministicNone, + super::RadrootsSp1TradeProverBackend::LocalExecute, + super::RadrootsSp1TradeProverBackend::LocalCpuProve, + super::RadrootsSp1TradeProverBackend::LocalCudaProve, + super::RadrootsSp1TradeProverBackend::RemoteHttpProve, + ] { + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label(backend.as_str()), + Some(backend) + ); + assert_eq!(backend.to_string(), backend.as_str()); + } + assert_eq!( + super::RadrootsSp1TradeProverBackend::from_label("legacy"), + None + ); + + for engine in [ + super::RadrootsSp1TradeProofEngine::Cpu, + super::RadrootsSp1TradeProofEngine::Cuda, + ] { + assert_eq!( + super::RadrootsSp1TradeProofEngine::from_label(engine.as_str()), + Some(engine) + ); + assert_eq!(engine.to_string(), engine.as_str()); + } + assert_eq!(super::RadrootsSp1TradeProofEngine::from_label("tpu"), None); + assert_eq!( + super::RadrootsSp1TradeWorkerResultStatus::Succeeded.to_string(), + "succeeded" + ); + assert_eq!( + super::RadrootsSp1TradeWorkerRole::NonAuthoritativeProver.to_string(), + "non_authoritative_prover" + ); + let artifact = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle") + .proof; + assert_eq!( + super::RadrootsSp1TradeResolvedProofArtifact::inline(artifact.clone()).artifact, + artifact + ); + assert_eq!( + RadrootsSp1TradeHostError::from( + radroots_sp1_guest_trade::RadrootsSp1TradeGuestError::UnsupportedProofTarget + ), + RadrootsSp1TradeHostError::Guest + ); + } + + #[test] + fn inline_proof_envelope_helpers_validate_digest_and_references() { + let (bundle, envelope) = inline_sp1_artifact_fixture(); + verify_order_acceptance_proof_artifact_structure(&bundle.execution, &bundle.proof) + .expect("inline artifact structure"); + let envelope_base64 = bundle + .proof + .inline_proof_base64 + .as_deref() + .expect("inline envelope"); + let decoded = super::decode_proof_envelope_base64(envelope_base64).expect("decode"); + assert_eq!(decoded, envelope); + assert_eq!( + super::proof_content_bytes_from_envelope(&decoded).expect("content"), + b"deterministic proof bytes" + ); + let reference = + super::proof_reference_for_proof_envelope_base64(envelope_base64).expect("reference"); + assert_eq!( + super::proof_reference_digest(reference.as_str()) + .expect("digest") + .len(), + 64 + ); + assert!(matches!( + super::proof_reference_for_proof_envelope_base64("not-base64"), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + assert!(matches!( + super::decode_proof_envelope_base64( + base64::engine::general_purpose::STANDARD + .encode(b"not json") + .as_str() + ), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + assert_eq!( + super::proof_reference_digest("legacy-reference").expect_err("prefix"), + RadrootsSp1TradeHostError::InvalidSp1ProofReference + ); + } + + #[test] + fn proof_envelope_metadata_mismatches_are_reported_by_field_family() { + let (bundle, envelope) = inline_sp1_artifact_fixture(); + let execution = &bundle.execution; + let artifact = &bundle.proof; + + let mut bad = envelope.clone(); + bad.schema_version += 1; + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let metadata_cases: [fn(&mut super::RadrootsSp1TradeProofEnvelope); 9] = [ + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_version_line = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_codec = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_system = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.proof_mode = "compressed".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.public_values_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_program_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_hash = + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_codec = "legacy".to_string(); + }, + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.sp1_verifying_key_base64.clear(); + }, + ]; + for apply in metadata_cases { + let mut bad = envelope.clone(); + apply(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + } + + let mut bad = envelope.clone(); + bad.proof_digest = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad).expect_err("digest"), + RadrootsSp1TradeHostError::ProofDigestMismatch + ); + + let mut bad = envelope.clone(); + bad.proof_content_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let mut bad = envelope.clone(); + bad.proof_content_base64 = "not-base64".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert!(matches!( + super::verify_proof_envelope(execution, artifact, &bad), + Err(RadrootsSp1TradeHostError::Sp1ProofMaterialDecode(_)) + )); + + let mut bad = envelope.clone(); + bad.canonical_public_values_hash = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + refresh_proof_envelope_digest(&mut bad); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad).expect_err("public values"), + RadrootsSp1TradeHostError::PublicValuesHashMismatch + ); + + let envelope_cases: [(&str, fn(&mut super::RadrootsSp1TradeProofEnvelope)); 10] = [ + ( + "receipt_type", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.receipt_type = "legacy".to_string(); + }, + ), + ( + "result", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.receipt_result = "invalid".to_string(); + }, + ), + ( + "listing_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.listing_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "root_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.root_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "target_event_id", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.target_event_id = + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.event_set_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.previous_state_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.new_state_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.changed_records_root = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ( + "state_roots", + |envelope: &mut super::RadrootsSp1TradeProofEnvelope| { + envelope.error_bitmap = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + .to_string(); + }, + ), + ]; + for (field, apply) in envelope_cases { + let mut bad = envelope.clone(); + apply(&mut bad); + refresh_proof_envelope_digest(&mut bad); + assert_eq!( + super::verify_proof_envelope(execution, artifact, &bad) + .expect_err("binding mismatch"), + RadrootsSp1TradeHostError::ValidationReceiptBindingMismatch(field) + ); + } + } + + #[test] + fn proof_artifact_structure_rejects_material_edge_cases() { + let (bundle, _) = inline_sp1_artifact_fixture(); + let execution = &bundle.execution; + let inline_proof_base64 = bundle + .proof + .inline_proof_base64 + .clone() + .expect("inline envelope"); + + let base_none_artifact = + generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle") + .proof; + + let mut artifact = base_none_artifact.clone(); + artifact.proof_digest = + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("proof digest"), + RadrootsSp1TradeHostError::ProofDigestMismatch + ); + + let mut artifact = base_none_artifact.clone(); + artifact.inline_proof_base64 = Some(inline_proof_base64); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.mode = Some("core".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.program_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact.clone(); + artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = base_none_artifact; + artifact.verifying_key_hash = + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string()); + assert_missing_none_proof_material(execution, artifact); + + let mut artifact = bundle.proof.clone(); + artifact.inline_proof_base64 = None; + artifact.proof_reference = None; + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("missing sp1 material"), + RadrootsSp1TradeHostError::MissingProofMaterial + ); + + let mut artifact = bundle.proof.clone(); + artifact.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64))); + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("conflict"), + RadrootsSp1TradeHostError::ProofMaterialConflict + ); + + let mut artifact = bundle.proof.clone(); + artifact.verifying_key_hash = + Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".to_string()); + artifact.proof_digest = + super::proof_digest_for_execution(execution, &artifact).expect("digest"); + assert_eq!( + verify_order_acceptance_proof_artifact_structure(execution, &artifact) + .expect_err("verifying key mismatch"), + RadrootsSp1TradeHostError::Sp1VerifyingKeyHashMismatch + ); + + let invalid_reference = format!("radroots-proof://sha256/{}", "A".repeat(64)); + assert_eq!( + super::proof_reference_digest(invalid_reference.as_str()) + .expect_err("uppercase digest"), + RadrootsSp1TradeHostError::InvalidSp1ProofReference + ); + let err = super::referenced_order_acceptance_proof_artifact_for_execution( + execution, + RadrootsSp1TradeProofMode::None, + format!("radroots-proof://sha256/{}", "1".repeat(64)), + ) + .expect_err("none reference"); + assert_eq!(err, RadrootsSp1TradeHostError::Sp1ProofModeRequired); + } + + #[test] + fn validation_receipt_requires_event_bindings() { + let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None) + .expect("proof bundle"); + let cases: [(&str, fn(&mut RadrootsSp1TradePublicValuesExecution)); 3] = [ + ("listing_event_id", |execution| { + execution.public_values.listing_event_id = None; + }), + ("root_event_id", |execution| { + execution.public_values.root_event_id = None; + }), + ("target_event_id", |execution| { + execution.public_values.target_event_id = None; + }), + ]; + for (field, clear) in cases { + let mut bundle = bundle.clone(); + clear(&mut bundle.execution); + assert_eq!( + validation_receipt_for_order_acceptance_proof(&bundle) + .expect_err("missing binding"), + RadrootsSp1TradeHostError::MissingReceiptBinding(field) + ); + } + } + #[test] fn execute_public_values_and_bind_validation_receipt() { let bundle = generate_order_acceptance_proof(&witness(), RadrootsSp1TradeProofMode::None)