use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use tokio::sync::{OnceCell, Semaphore};
use super::client::{CheckoutWire, LicenceClient, LicenceClientError};
use super::{
CheckoutGrant, CheckoutGrantStore, LicenceError, Outcome, SignedGrant, sha256_hex, verify_grant,
};
use crate::chain::ChainManager;
use crate::gate::{GateBackend, GateDecision};
use crate::mesh_admit::PeerClass;
use crate::mesh_artifact::ArtifactExchange;
use crate::mesh_artifact_types::ArtifactKey;
use crate::mesh_delivery::PeerCtx;
use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
pub const GATE_ACTION: &str = "cog.checkout";
pub const EVENT_KIND_CHECKOUT_GRANTED: &str = "cog.checkout.granted";
pub const EVENT_KIND_CHECKOUT_REFUSED: &str = "cog.checkout.refused";
#[derive(Debug, Clone, Copy)]
pub enum CheckoutCaller<'a> {
Kernel,
Local(&'a str),
Peer(&'a PeerCtx),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CheckoutRefusal {
#[error("caller is not an admitted node")]
NotAdmitted,
#[error("governance gate refused: {0}")]
GateDenied(String),
#[error("bad request: {0}")]
BadRequest(String),
#[error("licence refused: {0}")]
Licence(String),
#[error("licence link failed: {0}")]
LicenceLink(String),
#[error("returned grant did not verify: {0}")]
BadGrant(String),
#[error("the Seed's clock is ahead of this node's")]
SeedClockSkew,
#[error("artifact does not match the grant: {0}")]
ArtifactMismatch(String),
#[error("too many checkout requests from this member")]
RateLimited,
#[error("the steward is busy")]
Busy,
#[error("could not store the artifact: {0}")]
Storage(String),
}
impl CheckoutRefusal {
pub fn code(&self) -> String {
match self {
Self::NotAdmitted => "not_admitted".into(),
Self::GateDenied(_) => "gate_denied".into(),
Self::BadRequest(_) => "bad_request".into(),
Self::Licence(c) => c.clone(),
Self::LicenceLink(_) => "licence_unreachable".into(),
Self::BadGrant(_) => "bad_grant".into(),
Self::SeedClockSkew => "seed_clock_skew".into(),
Self::ArtifactMismatch(_) => "artifact_mismatch".into(),
Self::Storage(_) => "storage_failed".into(),
Self::RateLimited => "rate_limited".into(),
Self::Busy => "busy".into(),
}
}
}
impl From<LicenceClientError> for CheckoutRefusal {
fn from(e: LicenceClientError) -> Self {
match e {
LicenceClientError::Refused { code, .. } => Self::Licence(code),
LicenceClientError::Transport(w) => Self::LicenceLink(w),
LicenceClientError::BadResponse(w) => Self::BadGrant(w),
}
}
}
#[async_trait]
pub trait GrantFlood: Send + Sync + 'static {
async fn flood(&self, grant: &SignedGrant);
}
#[derive(Debug, Default)]
pub struct NoFlood;
#[async_trait]
impl GrantFlood for NoFlood {
async fn flood(&self, _: &SignedGrant) {}
}
pub fn install_grant(
store: &CheckoutGrantStore,
exchange: &ArtifactExchange,
signed: &SignedGrant,
) -> Result<Outcome, LicenceError> {
let out = store.accept_grant(signed)?;
let g: CheckoutGrant = serde_json::from_str(&signed.payload)
.map_err(|e| LicenceError::Malformed(e.to_string()))?;
for v in store.verified_grants() {
if v.grant().cog_id == g.cog_id && v.grant().version == g.version {
exchange.grant_checkout(&v);
}
}
Ok(out)
}
#[derive(Debug, Clone)]
pub struct RelayLimits {
pub per_peer: u32,
pub window: Duration,
pub concurrent: usize,
pub max_peers: usize,
pub per_local: u32,
pub local_total: u32,
}
impl Default for RelayLimits {
fn default() -> Self {
Self {
per_peer: 5,
window: Duration::from_secs(60),
concurrent: 8,
max_peers: 1024,
per_local: 5,
local_total: 20,
}
}
}
type Key = (String, String, String);
type Shared = Arc<OnceCell<Result<SignedGrant, CheckoutRefusal>>>;
pub struct CheckoutRelay {
store: Arc<CheckoutGrantStore>,
exchange: Arc<ArtifactExchange>,
client: Arc<dyn LicenceClient>,
gate: Arc<dyn GateBackend>,
flood: Arc<dyn GrantFlood>,
chain: Option<Arc<ChainManager>>,
inflight: Mutex<HashMap<Key, Shared>>,
limits: RelayLimits,
buckets: Mutex<HashMap<String, (Instant, u32)>>,
local_total: Mutex<(Instant, u32)>,
slots: Semaphore,
}
impl CheckoutRelay {
pub fn new(
store: Arc<CheckoutGrantStore>,
exchange: Arc<ArtifactExchange>,
client: Arc<dyn LicenceClient>,
gate: Arc<dyn GateBackend>,
flood: Arc<dyn GrantFlood>,
chain: Option<Arc<ChainManager>>,
) -> Self {
let limits = RelayLimits::default();
Self {
store,
exchange,
client,
gate,
flood,
chain,
inflight: Mutex::default(),
slots: Semaphore::new(limits.concurrent),
buckets: Mutex::default(),
local_total: Mutex::new((Instant::now(), 0)),
limits,
}
}
pub fn with_limits(mut self, limits: RelayLimits) -> Self {
self.slots = Semaphore::new(limits.concurrent);
self.limits = limits;
self
}
fn allow_local_total(&self) -> bool {
let now = Instant::now();
let mut t = self.local_total.lock().unwrap_or_else(|p| p.into_inner());
if now.duration_since(t.0) >= self.limits.window {
*t = (now, 0);
}
if t.1 >= self.limits.local_total {
return false;
}
t.1 += 1;
true
}
fn allow_peer(&self, peer: &str) -> bool {
self.allow_key(peer, self.limits.per_peer)
}
fn allow_key(&self, peer: &str, budget: u32) -> bool {
let now = Instant::now();
let mut m = self.buckets.lock().unwrap_or_else(|p| p.into_inner());
if m.len() >= self.limits.max_peers && !m.contains_key(peer) {
let w = self.limits.window;
m.retain(|_, (start, _)| now.duration_since(*start) < w);
if m.len() >= self.limits.max_peers {
return false;
}
}
let e = m.entry(peer.to_owned()).or_insert((now, 0));
if now.duration_since(e.0) >= self.limits.window {
*e = (now, 0);
}
if e.1 >= budget {
return false;
}
e.1 += 1;
true
}
pub async fn handle(
&self,
caller: CheckoutCaller<'_>,
req: &CheckoutWire,
) -> Result<SignedGrant, CheckoutRefusal> {
let who = match caller {
CheckoutCaller::Kernel => "kernel".to_owned(),
CheckoutCaller::Local(p) => format!("local:{p}"),
CheckoutCaller::Peer(p) if p.node_verified && p.class == PeerClass::Node => {
p.peer_id.clone()
}
CheckoutCaller::Peer(_) => {
let e = CheckoutRefusal::NotAdmitted;
self.chain_refused(req, "unadmitted", &e);
return Err(e);
}
};
let allowed = match caller {
CheckoutCaller::Kernel => true,
CheckoutCaller::Peer(_) => self.allow_peer(&who),
CheckoutCaller::Local(_) => self.allow_key(&who, self.limits.per_local) && self.allow_local_total(),
};
if !allowed {
return Err(CheckoutRefusal::RateLimited);
}
let Ok(_slot) = self.slots.try_acquire() else {
return Err(CheckoutRefusal::Busy);
};
let r = self.checked(&who, req).await;
if let Err(e) = &r {
self.chain_refused(req, &who, e);
}
r
}
async fn checked(&self, who: &str, req: &CheckoutWire) -> Result<SignedGrant, CheckoutRefusal> {
req.validate().map_err(CheckoutRefusal::BadRequest)?;
let ctx = serde_json::json!({
"cog_id": req.cog_id, "version": req.version, "arch": req.arch,
});
match self.gate.check(who, GATE_ACTION, &ctx) {
GateDecision::Permit { .. } => {}
GateDecision::Defer { reason } | GateDecision::Deny { reason, .. } => {
return Err(CheckoutRefusal::GateDenied(reason));
}
}
let key = (req.cog_id.clone(), req.version.clone(), req.arch.clone());
let cell: Shared = {
let mut m = self.inflight.lock().unwrap_or_else(|p| p.into_inner());
m.entry(key.clone()).or_default().clone()
};
let ran = AtomicBool::new(false);
let out = cell
.get_or_init(|| async {
ran.store(true, Ordering::Relaxed);
let r = self.relay(req).await;
let mut m = self.inflight.lock().unwrap_or_else(|p| p.into_inner());
if m.get(&key).is_some_and(|c| Arc::ptr_eq(c, &cell)) {
m.remove(&key);
}
r
})
.await;
if let Ok(signed) = out
&& let Ok(g) = serde_json::from_str::<CheckoutGrant>(&signed.payload)
{
let b3 = g.artifact(&req.arch).map(|a| a.blake3.clone()).unwrap_or_default();
self.chain_granted(who, req, &g, &b3, !ran.load(Ordering::Relaxed));
}
out.clone()
}
async fn relay(&self, req: &CheckoutWire) -> Result<SignedGrant, CheckoutRefusal> {
let signed = self.client.checkout(req).await?;
let binding = self
.store
.active_binding()
.ok_or_else(|| CheckoutRefusal::BadGrant("no binding".into()))?;
let pk = hex_decode_exact::<32>(&binding.grant_pubkey)
.ok_or_else(|| CheckoutRefusal::BadGrant("bound grant key".into()))?;
let mesh = self
.store
.local_mesh_id()
.get()
.ok_or_else(|| CheckoutRefusal::BadGrant("no local mesh id".into()))?;
let g = verify_grant(&signed, &pk, &mesh).map_err(|e| CheckoutRefusal::BadGrant(e.to_string()))?;
if g.cog_id != req.cog_id || (req.version != "latest" && g.version != req.version) {
return Err(CheckoutRefusal::BadGrant("grant is for another cog or version".into()));
}
let art = g
.artifact(&req.arch)
.ok_or_else(|| CheckoutRefusal::BadGrant(format!("grant has no {} artifact", req.arch)))?
.clone();
let hash = hex_decode_exact::<32>(&art.blake3)
.ok_or_else(|| CheckoutRefusal::BadGrant("artifact hash".into()))?;
match self.exchange.resolve(&ArtifactKey::Content(hash)) {
Some(d) => self.check_held_sha256(&d, &art.sha256)?,
None => self.fetch_and_seed(&art.blake3, &art.sha256, art.size).await?,
}
install_grant(&self.store, &self.exchange, &signed).map_err(|e| match e {
LicenceError::NotYetValid => CheckoutRefusal::SeedClockSkew,
e => CheckoutRefusal::BadGrant(e.to_string()),
})?;
self.flood.flood(&signed).await;
Ok(signed)
}
fn check_held_sha256(
&self,
d: &crate::mesh_artifact_types::ArtifactDescriptor,
sha256: &str,
) -> Result<(), CheckoutRefusal> {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
self.exchange
.read_to(&d.id(), &mut |b| {
h.update(b);
Ok(())
})
.map_err(|e| CheckoutRefusal::Storage(e.to_string()))?;
if hex_encode(&h.finalize()) != sha256 {
return Err(CheckoutRefusal::ArtifactMismatch("sha256 of the held bytes".into()));
}
Ok(())
}
async fn fetch_and_seed(&self, blake3: &str, sha256: &str, size: u64) -> Result<(), CheckoutRefusal> {
let limit = self.exchange.config().max_artifact_bytes;
if size > limit {
return Err(CheckoutRefusal::ArtifactMismatch(format!("{size} bytes is over the limit")));
}
let bytes = self.client.artifact(blake3, size).await?;
if bytes.len() as u64 != size {
return Err(CheckoutRefusal::ArtifactMismatch("size".into()));
}
if sha256_hex(&bytes) != sha256 {
return Err(CheckoutRefusal::ArtifactMismatch("sha256".into()));
}
if hex_encode(blake3::hash(&bytes).as_bytes()) != blake3 {
return Err(CheckoutRefusal::ArtifactMismatch("blake3".into()));
}
self.exchange.seed_bytes(&bytes).map_err(|e| CheckoutRefusal::Storage(e.to_string()))?;
Ok(())
}
fn chain_granted(&self, who: &str, req: &CheckoutWire, g: &CheckoutGrant, blake3: &str, merged: bool) {
if let Some(cm) = &self.chain {
cm.append(
"licence",
EVENT_KIND_CHECKOUT_GRANTED,
Some(serde_json::json!({
"cog_id": g.cog_id, "version": g.version, "arch": req.arch,
"grant_id": g.grant_id, "seq": g.seq, "blake3": blake3, "requester": who,
"merged": merged,
})),
);
}
}
fn chain_refused(&self, req: &CheckoutWire, who: &str, e: &CheckoutRefusal) {
if let Some(cm) = &self.chain {
cm.append(
"licence",
EVENT_KIND_CHECKOUT_REFUSED,
Some(serde_json::json!({
"cog_id": req.cog_id, "version": req.version, "arch": req.arch,
"code": e.code(), "requester": who,
})),
);
}
}
}