use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use alloy::primitives::{Address, B256, U256};
use alloy::signers::local::PrivateKeySigner;
use decdn_incentive::SignedCapability;
use crate::PoolContext;
pub const CREDENTIAL_SWAP_WAIT: Duration = Duration::from_mins(1);
pub(crate) const NODE_EXPIRY_MARGIN_SECS: u64 = decdn_common::config::capability_expiry_margin_secs(
decdn_common::config::DEFAULT_REDEEM_INTERVAL_SECS,
);
#[derive(Clone, Debug)]
pub struct Credential {
pub signer: Arc<PrivateKeySigner>,
pub capability: SignedCapability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CredentialMismatch {
pub key: Address,
pub capability_signer: Address,
}
impl std::fmt::Display for CredentialMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"voucher key {} is not the signer {} its capability names",
self.key, self.capability_signer
)
}
}
impl std::error::Error for CredentialMismatch {}
impl Credential {
fn checked(
signer: Arc<PrivateKeySigner>,
capability: SignedCapability,
) -> Result<Self, CredentialMismatch> {
let (key, capability_signer) = (signer.address(), capability.capability.signer);
if key != capability_signer {
return Err(CredentialMismatch {
key,
capability_signer,
});
}
Ok(Self { signer, capability })
}
}
#[derive(Clone, Debug)]
struct Current {
generation: u64,
credential: Credential,
}
#[derive(Debug)]
struct SlotInner {
current: tokio::sync::watch::Sender<Current>,
events: tokio::sync::watch::Sender<Option<FundingEvent>>,
swap_wait: std::sync::Mutex<Duration>,
recorded: std::sync::Mutex<Option<(Address, U256)>>,
}
#[derive(Clone, Debug)]
pub struct CredentialSlot {
inner: Arc<SlotInner>,
}
impl CredentialSlot {
pub fn new(
signer: Arc<PrivateKeySigner>,
capability: SignedCapability,
) -> Result<Self, CredentialMismatch> {
let (current, _) = tokio::sync::watch::channel(Current {
generation: 0,
credential: Credential::checked(signer, capability)?,
});
let (events, _) = tokio::sync::watch::channel(None);
Ok(Self {
inner: Arc::new(SlotInner {
current,
events,
swap_wait: std::sync::Mutex::new(CREDENTIAL_SWAP_WAIT),
recorded: std::sync::Mutex::new(None),
}),
})
}
#[must_use]
pub fn with_swap_wait(self, wait: Duration) -> Self {
*self
.inner
.swap_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = wait;
self
}
fn recorded_lock(&self) -> std::sync::MutexGuard<'_, Option<(Address, U256)>> {
self.inner
.recorded
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn record_spend(&self, signer: Address, spent: U256) {
*self.recorded_lock() = Some((signer, spent));
}
pub(crate) fn recorded_spend(&self, signer: Address) -> U256 {
self.recorded_lock()
.filter(|(recorded, _)| *recorded == signer)
.map_or(U256::ZERO, |(_, spent)| spent)
}
pub fn swap(
&self,
signer: Arc<PrivateKeySigner>,
capability: SignedCapability,
) -> Result<(), CredentialMismatch> {
let credential = Credential::checked(signer, capability)?;
self.inner.current.send_modify(|current| {
current.generation = current.generation.saturating_add(1);
current.credential = credential;
});
Ok(())
}
#[must_use]
pub fn current(&self) -> Credential {
self.inner.current.borrow().credential.clone()
}
#[must_use]
pub fn generation(&self) -> u64 {
self.inner.current.borrow().generation
}
#[must_use]
pub fn pool_id(&self) -> B256 {
self.inner
.current
.borrow()
.credential
.capability
.capability
.pool_id
}
#[must_use]
pub fn swap_wait(&self) -> Duration {
*self
.inner
.swap_wait
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[must_use]
pub fn funding_events(&self) -> tokio::sync::watch::Receiver<Option<FundingEvent>> {
self.inner.events.subscribe()
}
pub(crate) fn report(&self, event: Option<FundingEvent>) {
self.inner.events.send_if_modified(|last| {
let changed = *last != event;
if changed {
*last = event;
}
changed
});
}
pub(crate) async fn swapped_since(&self, generation: u64) {
let mut rx = self.inner.current.subscribe();
let _ = rx.wait_for(|current| current.generation > generation).await;
}
pub(crate) fn is_stale(&self, ctx: &std::sync::Mutex<PoolContext>) -> bool {
let current = self.inner.current.borrow().credential.signer.address();
ctx.lock()
.map_or(true, |ctx| ctx.client_signer.address() != current)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FundingEvent {
RunningLow {
headroom: U256,
projected_need: U256,
expires_at: u64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CapabilityCause {
CapSpent,
Expired,
Revoked,
}
impl std::fmt::Display for CapabilityCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::CapSpent => "has spent its cap",
Self::Expired => "has expired",
Self::Revoked => "was revoked",
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FundingNeeded {
NewCapability {
pool: B256,
cause: CapabilityCause,
},
PublisherPool {
pool: B256,
},
}
impl std::fmt::Display for FundingNeeded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NewCapability { pool, cause } => write!(
f,
"funding needed: the capability for pool {pool} {cause}, and no new credential \
came"
),
Self::PublisherPool { pool } => write!(
f,
"funding needed: the capability covers the work, but the nodes refuse the \
funding of pool {pool}; its owner must top it up"
),
}
}
}
impl std::error::Error for FundingNeeded {}
#[derive(Debug, Default)]
#[doc(hidden)]
pub struct RunDemand {
pub(crate) outstanding: AtomicU64,
pub(crate) quotes: QuoteMax,
}
impl RunDemand {
pub fn plan(&self, bytes: u64) {
self.outstanding.store(bytes, Ordering::Relaxed);
}
pub fn done(&self, bytes: u64) {
self.outstanding
.update(Ordering::Relaxed, Ordering::Relaxed, |left| {
left.saturating_sub(bytes)
});
}
pub fn outstanding(&self) -> u64 {
self.outstanding.load(Ordering::Relaxed)
}
}
#[derive(Debug, Default)]
#[doc(hidden)]
pub struct QuoteMax {
rate_per_mb: AtomicU64,
interval_bytes: AtomicU64,
}
impl QuoteMax {
pub(crate) fn observe(&self, rate_per_mb: u64, interval_bytes: u64) {
self.rate_per_mb.fetch_max(rate_per_mb, Ordering::Relaxed);
self.interval_bytes
.fetch_max(interval_bytes, Ordering::Relaxed);
}
pub(crate) fn rate_per_mb(&self) -> u64 {
self.rate_per_mb.load(Ordering::Relaxed)
}
pub(crate) fn interval_bytes(&self) -> u64 {
self.interval_bytes.load(Ordering::Relaxed)
}
}
fn cost(bytes: u64, rate_per_mb: u64) -> U256 {
U256::from(bytes)
.saturating_mul(U256::from(rate_per_mb))
.div_ceil(U256::from(decdn_protocol::MB_BYTES))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct CredentialView {
pub(crate) headroom: U256,
pub(crate) projected_need: U256,
pub(crate) window: U256,
pub(crate) expires_at: u64,
pub(crate) expiring: bool,
}
impl CredentialView {
pub(crate) fn of(
credential: &Credential,
signed: U256,
remaining_bytes: u64,
quotes: &QuoteMax,
now: u64,
) -> Self {
let capability = &credential.capability.capability;
let rate = quotes.rate_per_mb();
Self {
headroom: U256::from(capability.spending_cap).saturating_sub(signed),
projected_need: cost(remaining_bytes, rate),
window: cost(quotes.interval_bytes(), rate),
expires_at: capability.expiry,
expiring: decdn_common::config::inside_capability_expiry_margin(
capability.expiry,
NODE_EXPIRY_MARGIN_SECS,
now,
),
}
}
pub(crate) fn short(&self) -> bool {
self.headroom < self.projected_need.saturating_add(self.window)
}
pub(crate) fn event(&self) -> Option<FundingEvent> {
(self.short() || self.expiring).then_some(FundingEvent::RunningLow {
headroom: self.headroom,
projected_need: self.projected_need,
expires_at: self.expires_at,
})
}
pub(crate) fn cause(&self) -> Option<CapabilityCause> {
if self.expiring {
Some(CapabilityCause::Expired)
} else if self.short() {
Some(CapabilityCause::CapSpent)
} else {
None
}
}
pub(crate) fn funding_cause(&self, capability_refused: bool) -> Option<CapabilityCause> {
self.cause()
.or(self.headroom.is_zero().then_some(CapabilityCause::CapSpent))
.or(capability_refused.then_some(CapabilityCause::Revoked))
}
}
pub(crate) fn signed_past_prior(
ctx: &std::sync::Mutex<PoolContext>,
ledger: &crate::PoolLedger,
) -> U256 {
let prior = ctx.lock().map_or(U256::ZERO, |ctx| ctx.prior_amount);
ledger.committed().amount.saturating_sub(prior)
}
pub(crate) fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;