use std::path::Path;
use std::sync::Arc;
use auths_core::witness::{
AsyncWitnessProvider, CollectionError, ReceiptCollector, StoredReceipt, VerifiedReceipt,
};
use auths_infra_http::HttpAsyncWitnessClient;
use super::types::{Prefix, Said};
use crate::keri::event::EventReceipts;
use crate::storage::receipts::{GitReceiptStorage, ReceiptStorage};
use crate::witness_config::{WitnessConfig, WitnessPolicy};
#[allow(clippy::expect_used)]
fn shared_runtime() -> &'static tokio::runtime::Runtime {
static RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("INVARIANT: tokio runtime builder with standard settings cannot fail")
})
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WitnessIntegrationError {
#[error("Receipt collection failed: {0}")]
Collection(#[from] CollectionError),
#[error("witness quorum not met: {valid} valid receipt(s), need {required}")]
QuorumNotMet {
required: usize,
valid: usize,
},
#[error("Receipt storage failed: {0}")]
Storage(#[from] crate::error::StorageError),
#[error("Tokio runtime error: {0}")]
Runtime(#[from] std::io::Error),
}
impl auths_core::error::AuthsErrorInfo for WitnessIntegrationError {
fn error_code(&self) -> &'static str {
match self {
Self::Collection(_) => "AUTHS-E4971",
Self::Storage(_) => "AUTHS-E4972",
Self::Runtime(_) => "AUTHS-E4973",
Self::QuorumNotMet { .. } => "AUTHS-E4974",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::Collection(_) => {
Some("Check witness server connectivity and threshold configuration")
}
Self::QuorumNotMet { .. } => {
Some("Too few witnesses returned a valid, verifiable receipt for this event")
}
Self::Storage(_) => Some("Check storage backend permissions"),
Self::Runtime(_) => None,
}
}
}
pub fn collect_and_store_receipts(
repo_path: &Path,
prefix: &Prefix,
event_said: &Said,
event_json: &[u8],
config: &WitnessConfig,
now: chrono::DateTime<chrono::Utc>,
) -> Result<Vec<StoredReceipt>, WitnessIntegrationError> {
if config.policy == WitnessPolicy::Skip || !config.is_enabled() {
return Ok(vec![]);
}
let witnesses: Vec<(Prefix, Arc<dyn AsyncWitnessProvider>)> = config
.witnesses
.iter()
.map(|w| {
let client = HttpAsyncWitnessClient::new(w.url.to_string(), config.threshold)
.with_timeout(std::time::Duration::from_millis(config.timeout_ms));
(
w.aid.clone(),
Arc::new(client) as Arc<dyn AsyncWitnessProvider>,
)
})
.collect();
let collector = ReceiptCollector::new(witnesses, config.threshold, config.timeout_ms);
let result = {
let prefix_newtype = Prefix::new_unchecked(prefix.as_str().to_string());
let event_json = event_json.to_vec();
let rt = shared_runtime();
rt.block_on(async {
collector
.collect(&prefix_newtype, event_said, &event_json)
.await
})
};
match result {
Ok(collected) => {
let valid: Vec<StoredReceipt> = collected
.into_iter()
.map(VerifiedReceipt::into_stored)
.collect();
if valid.len() < config.threshold {
match config.policy {
WitnessPolicy::Enforce => {
return Err(WitnessIntegrationError::QuorumNotMet {
required: config.threshold,
valid: valid.len(),
});
}
WitnessPolicy::Warn => {
log::warn!(
"Witness quorum not met after verification (policy=Warn): {} valid, need {}",
valid.len(),
config.threshold,
);
}
WitnessPolicy::Skip => {} }
}
let storage = GitReceiptStorage::new(repo_path);
let event_receipts = EventReceipts::new(event_said.as_str(), valid);
storage
.store_receipts(prefix, &event_receipts, now)
.map_err(WitnessIntegrationError::Storage)?;
log::info!(
"Stored {} verified witness receipt(s) for event {} (prefix {})",
event_receipts.count(),
event_said.as_str(),
prefix.as_str(),
);
Ok(event_receipts.receipts)
}
Err(e) => match config.policy {
WitnessPolicy::Warn => {
log::warn!(
"Witness receipt collection failed (policy=Warn, continuing): {}",
e
);
Ok(vec![])
}
WitnessPolicy::Enforce => Err(WitnessIntegrationError::Collection(e)),
WitnessPolicy::Skip => Ok(vec![]), },
}
}