Skip to main content

aura_amp/
evidence.rs

1//! AMP evidence storage and consensus evidence effects.
2//!
3//! This module provides the `AmpEvidenceEffects` trait adapter that bridges
4//! Layer 4 AMP operations to non-canonical evidence storage. Evidence is not
5//! required to reconstruct AMP channel state, so we keep it in StorageEffects
6//! behind an explicit trait to avoid conflating it with journal facts.
7
8use aura_consensus::ConsensusId;
9use aura_core::effects::StorageEffects;
10use aura_core::types::identifiers::{AuthorityId, ContextId};
11use aura_core::{AuraError, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeMap;
14
15// ============================================================================
16// Evidence Types
17// ============================================================================
18
19/// Minimal evidence CRDT for AMP consensus.
20#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct EvidenceRecord {
22    /// Map of consensus id -> collected evidence bytes (opaque to AMP)
23    pub entries: BTreeMap<String, Vec<u8>>,
24}
25
26impl EvidenceRecord {
27    /// Merge a delta into this record (last-write-wins per key).
28    pub fn merge(&mut self, delta: EvidenceDelta) {
29        for (cid, bytes) in delta.entries {
30            self.entries.insert(cid, bytes);
31        }
32    }
33}
34
35/// Delta type for evidence propagation.
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct EvidenceDelta {
38    pub entries: BTreeMap<String, Vec<u8>>,
39}
40
41// ============================================================================
42// Evidence Storage Keys
43// ============================================================================
44
45/// Storage key namespace for AMP evidence records.
46/// All AMP evidence keys are prefixed with this to avoid collisions with
47/// journal facts (which use `relational:` prefix) or other storage users.
48pub const AMP_EVIDENCE_KEY_PREFIX: &str = "amp/evidence/";
49
50/// Generate a storage key for a consensus id's evidence.
51pub(crate) fn evidence_key(cid: ConsensusId) -> String {
52    format!("{}{}", AMP_EVIDENCE_KEY_PREFIX, hex::encode(cid.0 .0))
53}
54
55// ============================================================================
56// AmpEvidenceEffects Trait
57// ============================================================================
58
59/// Evidence storage for AMP consensus (non-canonical cache).
60///
61/// Evidence is not required to reconstruct AMP channel state, so we keep it in
62/// StorageEffects behind an explicit trait to avoid conflating it with journal facts.
63#[async_trait::async_trait]
64pub trait AmpEvidenceEffects: StorageEffects + Sized {
65    /// Carry evidence deltas keyed by consensus id.
66    async fn merge_evidence_delta(&self, cid: ConsensusId, delta: EvidenceDelta) -> Result<()>;
67
68    /// Retrieve accumulated evidence for a consensus id.
69    async fn evidence_for(&self, cid: ConsensusId) -> Result<Option<EvidenceRecord>>;
70
71    /// Insert evidence delta tracking witness participation in consensus.
72    async fn insert_evidence_delta(
73        &self,
74        witness: AuthorityId,
75        consensus_id: ConsensusId,
76        context: ContextId,
77    ) -> Result<()>;
78
79    /// Scoped evidence store wrapper to keep evidence handling separate.
80    fn evidence_store(&self) -> AmpEvidenceStore<'_, Self>
81    where
82        Self: Sized,
83    {
84        AmpEvidenceStore { effects: self }
85    }
86}
87
88/// Blanket implementation of `AmpEvidenceEffects` for any type implementing `StorageEffects`.
89#[async_trait::async_trait]
90impl<E: StorageEffects> AmpEvidenceEffects for E {
91    async fn merge_evidence_delta(&self, cid: ConsensusId, delta: EvidenceDelta) -> Result<()> {
92        self.evidence_store().merge_delta(cid, delta).await
93    }
94
95    async fn evidence_for(&self, cid: ConsensusId) -> Result<Option<EvidenceRecord>> {
96        self.evidence_store().current(cid).await
97    }
98
99    async fn insert_evidence_delta(
100        &self,
101        witness: AuthorityId,
102        consensus_id: ConsensusId,
103        context: ContextId,
104    ) -> Result<()> {
105        // Create evidence delta recording witness participation
106        let evidence_entry = format!("witness:{witness}:context:{context}");
107        let mut delta = EvidenceDelta::default();
108        delta
109            .entries
110            .insert(hex::encode(consensus_id.0 .0), evidence_entry.into_bytes());
111
112        self.merge_evidence_delta(consensus_id, delta).await
113    }
114}
115
116// ============================================================================
117// AmpEvidenceStore
118// ============================================================================
119
120/// Evidence store separated from context journal to avoid accidental coupling.
121///
122/// This provides a scoped view into storage for evidence records, keeping
123/// evidence handling separate from journal fact operations.
124pub struct AmpEvidenceStore<'a, E: ?Sized + StorageEffects> {
125    effects: &'a E,
126}
127
128impl<'a, E: ?Sized + StorageEffects> AmpEvidenceStore<'a, E> {
129    /// Merge an evidence delta into the record for a consensus id.
130    pub async fn merge_delta(&self, cid: ConsensusId, delta: EvidenceDelta) -> Result<()> {
131        let mut record = self.current(cid).await?.unwrap_or_default();
132        record.merge(delta);
133        let bytes =
134            serde_json::to_vec(&record).map_err(|e| AuraError::serialization(e.to_string()))?;
135        self.effects
136            .store(&evidence_key(cid), bytes)
137            .await
138            .map_err(|e| AuraError::storage(e.to_string()))
139    }
140
141    /// Retrieve the current evidence record for a consensus id.
142    pub async fn current(&self, cid: ConsensusId) -> Result<Option<EvidenceRecord>> {
143        match self.effects.retrieve(&evidence_key(cid)).await {
144            Ok(Some(bytes)) => {
145                let record: EvidenceRecord = serde_json::from_slice(&bytes)
146                    .map_err(|e| AuraError::serialization(e.to_string()))?;
147                Ok(Some(record))
148            }
149            Ok(None) => Ok(None),
150            Err(e) => Err(AuraError::storage(e.to_string())),
151        }
152    }
153}