signing_evidence.rs (4172B)
1 //! Exact signature facts bound to an already committed signing attempt. 2 3 use radroots_event::SignedEvent; 4 use radroots_event_codec::verify::{self, Nip01SignatureVerifier, RawEvent}; 5 6 use super::{ 7 AuthoredAtomicCommand, AuthoredAtomicOutcome, AuthoredAtomicReceipt, ClaimAuthoredTarget, 8 ClaimAuthoredWork, 9 }; 10 use crate::{ 11 Error, 12 authored::{AuthoredArtifact, AuthoredArtifactId, WorkClaim}, 13 journal::OperationInstanceId, 14 }; 15 16 /// Records an existing authored signature without granting scheduling authority. 17 /// 18 /// The backend must retrieve its own immutable receipt for [`Self::claim_command`] 19 /// inside the recording transaction. A caller-supplied receipt is not durable 20 /// attempt authority. The original claim may have expired or been superseded; 21 /// new signing, admission and delivery still require their ordinary fences. 22 #[derive(Clone, Debug, Eq, PartialEq)] 23 pub struct RecordSignedArtifact { 24 operation_id: OperationInstanceId, 25 artifact_id: AuthoredArtifactId, 26 claim: WorkClaim, 27 event: SignedEvent, 28 observed_at_unix_ms: u64, 29 } 30 31 impl RecordSignedArtifact { 32 pub fn new( 33 operation_id: OperationInstanceId, 34 artifact_id: AuthoredArtifactId, 35 claim: WorkClaim, 36 event: SignedEvent, 37 observed_at_unix_ms: u64, 38 ) -> Result<Self, Error> { 39 claim.validate()?; 40 if observed_at_unix_ms < claim.acquired_at_unix_ms() { 41 return Err(Error::AtomicWorkflowMismatch); 42 } 43 let id_verified = verify::id(RawEvent::new(event.envelope().clone())) 44 .map_err(|_| Error::InvalidAuthoredArtifact)?; 45 verify::signature(id_verified, &Nip01SignatureVerifier) 46 .map_err(|_| Error::InvalidAuthoredArtifact)?; 47 Ok(Self { 48 operation_id, 49 artifact_id, 50 claim, 51 event, 52 observed_at_unix_ms, 53 }) 54 } 55 56 pub const fn operation_id(&self) -> OperationInstanceId { 57 self.operation_id 58 } 59 pub const fn artifact_id(&self) -> AuthoredArtifactId { 60 self.artifact_id 61 } 62 pub const fn claim(&self) -> &WorkClaim { 63 &self.claim 64 } 65 pub const fn event(&self) -> &SignedEvent { 66 &self.event 67 } 68 pub const fn observed_at_unix_ms(&self) -> u64 { 69 self.observed_at_unix_ms 70 } 71 72 pub fn claim_command(&self) -> AuthoredAtomicCommand { 73 AuthoredAtomicCommand::Claim(ClaimAuthoredWork::new( 74 ClaimAuthoredTarget::ArtifactSigning(self.artifact_id), 75 self.claim.clone(), 76 )) 77 } 78 79 /// Applies facts only after exact backend-owned attempt provenance is checked. 80 /// 81 /// The first signed bytes are immutable. An identical result changes nothing; 82 /// a different result conflicts. Cancellation and terminal failure survive. 83 pub fn apply_to( 84 &self, 85 artifact: &mut AuthoredArtifact, 86 original_claim: &AuthoredAtomicReceipt, 87 ) -> Result<(), Error> { 88 let AuthoredAtomicOutcome::Artifact(original) = original_claim.outcome() else { 89 return Err(Error::AtomicWorkflowMismatch); 90 }; 91 original.validate()?; 92 artifact.validate()?; 93 if !original_claim.matches_command(&self.claim_command()) 94 || original_claim.committed_at_unix_ms() != self.claim.acquired_at_unix_ms() 95 || original.signing_claim() != Some(&self.claim) 96 || original.operation_id() != self.operation_id 97 || artifact.operation_id() != self.operation_id 98 || original.artifact_id() != self.artifact_id 99 || artifact.artifact_id() != self.artifact_id 100 || original.plan() != artifact.plan() 101 || original.origin() != artifact.origin() 102 || original.ordinal() != artifact.ordinal() 103 || original.created_at_unix_ms() != artifact.created_at_unix_ms() 104 || original.revision() > artifact.revision() 105 || original.updated_at_unix_ms() > artifact.updated_at_unix_ms() 106 { 107 return Err(Error::AtomicWorkflowMismatch); 108 } 109 artifact.record_signed_fact(self.event.clone(), self.observed_at_unix_ms) 110 } 111 }