//! Execution receipts in a repository (spec §14.6): kept beside the state //! they were made on, with the runner's identity and signature. use super::*; use crate::receipt::SignedReceipt; /// A stored receipt and what verifying it found. #[derive(Clone, Debug)] pub struct ReceiptInfo { /// The receipt attestation's ObjectRef, which names it. pub attest: ObjectRef, pub signed: SignedReceipt, /// Why it does not verify, if it does not. pub problem: Option, /// The runner's key was revoked (spec §14.2). pub revoked: bool, } impl Repo { /// Verify `sr` and keep it: the receipt, the attestation with its /// signatures, and the runner's identity. Returns the attestation's /// ObjectRef. Receipts for another repository are refused. pub fn add_receipt(&self, sr: &SignedReceipt) -> Result { sr.verify() .map_err(|e| RepoError::Msg(format!("receipt does not verify: {e}")))?; if sr.attest.repo != self.id()? { return fail("that receipt is for another repository"); } self.store.put_identity(&sr.runner)?; self.store.put_receipt(&sr.receipt)?; let r = self.store.put_attest(&sr.attest)?; for sig in &sr.sigs { self.store.put_signature(&r, sig)?; } self.store.index_receipt(&sr.state(), &r)?; Ok(r) } /// The receipt named by attestation `r`, verified again from the store. pub fn receipt(&self, r: &ObjectRef) -> Result { let attest = self.store.get_attest(r)?; let AttestBody::Receipt { receipt, .. } = &attest.body else { return fail(format!("#{} is not a receipt", r.short(12))); }; let receipt = self.store.get_receipt(receipt)?; let sigs = self.store.signatures(r)?; let Some(key) = sigs.first().map(|s| s.key) else { return fail(format!("receipt #{} has no signature", r.short(12))); }; let runner = self.store.get_identity(&key)?; let signed = SignedReceipt { receipt, attest, sigs, runner, }; let problem = signed.verify().err().map(|e| e.to_string()); Ok(ReceiptInfo { attest: *r, revoked: self.is_revoked(&key)?, signed, problem, }) } /// Every receipt kept for the state of `heads`, oldest first. pub fn receipts(&self, heads: &[ChangeHash]) -> Result> { let state = hash_object( ObjectType::State, &StateObject::new(heads.to_vec()).encode(), ); let mut out = self .store .receipts_for(&state)? .iter() .map(|r| self.receipt(r)) .collect::>>()?; out.sort_by_key(|i| i.signed.attest.hlc); Ok(out) } /// Receipts to keep through garbage collection: those whose state's /// heads `keep` holds, with their receipts, runners and outputs. /// Keep a signed approval (oxygenhub#235): verified, for this repository, /// with the signer's identity and indexed by the state it approves, so /// garbage collection keeps it while that state lives. Who may approve is /// the caller's rule. pub fn add_approval(&self, sa: &crate::approval::SignedApproval) -> Result { sa.verify() .map_err(|e| RepoError::Msg(format!("approval does not verify: {e}")))?; if sa.attest.repo != self.id()? { return fail("that approval is for another repository"); } let state = sa .statement() .map_err(|e| RepoError::Msg(e.to_string()))? .state; self.store.put_identity(&sa.signer)?; let r = self.store.put_attest(&sa.attest)?; for sig in &sa.sigs { self.store.put_signature(&r, sig)?; } self.store.index_approval(&state, &r)?; Ok(r) } /// Approvals of states garbage collection keeps are kept, with their /// signatures and signers. pub(crate) fn reachable_approvals(&self, keep: &mut HashSet) -> Result<()> { for state in self.store.approval_states()? { if !keep.contains(&state) { let heads = match self.store.get_state(&state) { Ok(s) => s.heads, Err(StoreError::NotFound(_)) => vec![], Err(e) => return Err(e.into()), }; if !heads.iter().all(|h| keep.contains(h)) { continue; } keep.insert(state); } for a in self.store.approvals_for(&state)? { keep.insert(a); for sig in self.store.signatures(&a)? { keep.insert(sig.key); } } } Ok(()) } pub(crate) fn reachable_receipts(&self, keep: &mut HashSet) -> Result<()> { for state in self.store.receipt_states()? { let heads = match self.store.get_state(&state) { Ok(s) => s.heads, // A receipt may arrive before its state object; its heads // are unknown, so keep it. Err(StoreError::NotFound(_)) => vec![], Err(e) => return Err(e.into()), }; if !heads.iter().all(|h| keep.contains(h)) { continue; } keep.insert(state); for a in self.store.receipts_for(&state)? { keep.insert(a); for sig in self.store.signatures(&a)? { keep.insert(sig.key); } let Ok(attest) = self.store.get_attest(&a) else { continue; }; let AttestBody::Receipt { receipt, .. } = attest.body else { continue; }; keep.insert(receipt); let Ok(r) = self.store.get_receipt(&receipt) else { continue; }; for b in r.outputs.iter().chain(&r.inputs) { if keep.insert(*b) { if let Ok(bytes) = self.store.get_typed(b, ObjectType::Blob) { keep.extend(Blob::decode(&bytes)?.chunks); } } } } } Ok(()) } }