use std::future::Future;
use std::time::Duration;
use alloy::primitives::{B256, U256};
use decdn_incentive::chain::{CHUNK_BYTES, MAX_CHAIN_LENGTH};
use decdn_protocol::MB_BYTES;
use decdn_protocol::client::WatermarkBundle;
use tokio::sync::Mutex;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Cumulative {
pub bytes: U256,
pub amount: U256,
}
impl Cumulative {
#[must_use]
const fn plus(self, other: Self) -> Self {
Self {
bytes: self.bytes.saturating_add(other.bytes),
amount: self.amount.saturating_add(other.amount),
}
}
#[must_use]
const fn minus(self, other: Self) -> Self {
Self {
bytes: self.bytes.saturating_sub(other.bytes),
amount: self.amount.saturating_sub(other.amount),
}
}
}
impl From<&WatermarkBundle> for Cumulative {
fn from(bundle: &WatermarkBundle) -> Self {
let proved = U256::from(bundle.verified_index);
Self {
bytes: U256::from(bundle.bytes_delivered)
.saturating_add(proved.saturating_mul(U256::from(CHUNK_BYTES))),
amount: U256::from(bundle.amount)
.saturating_add(proved.saturating_mul(U256::from(bundle.chunk_price))),
}
}
}
#[must_use]
fn next_voucher(cur: &Cumulative, delta_bytes: u64, rate_per_mb: u64) -> Cumulative {
let amount_delta = U256::from(delta_bytes)
.saturating_mul(U256::from(rate_per_mb))
.div_ceil(U256::from(MB_BYTES));
Cumulative {
bytes: cur.bytes.saturating_add(U256::from(delta_bytes)),
amount: cur.amount.saturating_add(amount_delta),
}
}
#[derive(Debug)]
struct ChainEpoch {
preimages: Box<[B256; 256]>,
chunk_price: U256,
released: u8,
}
impl ChainEpoch {
fn open(chunk_price: U256) -> Self {
let seed = decdn_incentive::chain::random_seed();
let mut preimages = Box::new([B256::ZERO; 256]);
let mut acc = seed;
for index in (0..=usize::from(MAX_CHAIN_LENGTH)).rev() {
if let Some(slot) = preimages.get_mut(index) {
*slot = acc;
}
acc = alloy::primitives::keccak256(acc);
}
Self {
preimages,
chunk_price,
released: 0,
}
}
fn root(&self) -> B256 {
self.preimages.first().copied().unwrap_or(B256::ZERO)
}
fn next_index(&self) -> Option<u8> {
(self.released < MAX_CHAIN_LENGTH).then(|| self.released + 1)
}
}
#[derive(Debug)]
struct Pipeline {
accrued: Cumulative,
prev_accrued: Cumulative,
last_proof: Option<LastProof>,
committed: Cumulative,
prev: Option<Cumulative>,
armed: Option<Armed>,
generation: u64,
unsaved_rebase: Option<Cumulative>,
healed_from: Option<Cumulative>,
}
impl Pipeline {
fn overwrite(&mut self, cum: Cumulative) {
self.committed = cum;
self.accrued = Cumulative::default();
self.prev = None;
self.prev_accrued = Cumulative::default();
self.armed = None;
self.last_proof = None;
}
fn settlement(&self) -> Cumulative {
let owed = self.committed.plus(self.accrued);
match self.armed {
Some(armed) if armed.voucher.amount > owed.amount => armed.voucher,
_ => owed,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Armed {
voucher: Cumulative,
folded: Cumulative,
}
#[derive(Debug)]
enum Displaced {
Nothing,
Replaced(Option<ChainEpoch>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LastProof {
Voucher { amount: U256 },
Reveal {
chain_root: B256,
index: u8,
chunk_price: U256,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StreamProof {
Voucher {
amount: U256,
generation: u64,
},
Reveal {
chain_root: B256,
index: u8,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Rebase {
Rebased {
from: Cumulative,
},
Stale,
Refused,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EpochAction {
Keep,
Open,
Roll,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ChainCommit {
pub chain_root: B256,
pub chunk_price: U256,
}
impl ChainCommit {
pub const SEALED: Self = Self {
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Released {
pub preimage: B256,
pub index: u8,
pub chain_root: B256,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Metered {
Released(Released),
Exhausted,
Moved,
}
#[derive(Debug)]
pub struct PoolLedger {
issuance: Mutex<()>,
pipeline: std::sync::Mutex<Pipeline>,
epoch: std::sync::Mutex<Option<ChainEpoch>>,
retired: std::sync::Mutex<Displaced>,
send_deadline: Duration,
unpaid: std::sync::Mutex<Vec<([u8; 32], u64, u64)>>,
}
pub(crate) const VOUCHER_SEND_DEADLINE: Duration = Duration::from_secs(20);
pub(crate) fn merge_spans(mut spans: Vec<(u64, u64)>) -> Vec<(u64, u64)> {
spans.retain(|&(start, end)| start < end);
spans.sort_unstable();
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(spans.len());
for (start, end) in spans {
match merged.last_mut() {
Some(last) if start <= last.1 => last.1 = last.1.max(end),
_ => merged.push((start, end)),
}
}
merged
.into_iter()
.map(|(start, end)| (start, end - start))
.collect()
}
impl PoolLedger {
#[must_use]
pub fn new(seed: Cumulative) -> Self {
Self {
issuance: Mutex::new(()),
pipeline: std::sync::Mutex::new(Pipeline {
committed: seed,
prev: None,
armed: None,
accrued: Cumulative::default(),
prev_accrued: Cumulative::default(),
last_proof: None,
generation: 0,
unsaved_rebase: None,
healed_from: None,
}),
epoch: std::sync::Mutex::new(None),
retired: std::sync::Mutex::new(Displaced::Nothing),
send_deadline: VOUCHER_SEND_DEADLINE,
unpaid: std::sync::Mutex::new(Vec::new()),
}
}
pub(crate) fn note_unpaid(&self, hash: [u8; 32], start: u64, end: u64) {
if start >= end {
return;
}
self.unpaid
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((hash, start, end));
}
pub(crate) fn take_unpaid(&self, hash: [u8; 32]) -> Vec<(u64, u64)> {
let mut spans: Vec<(u64, u64)> = Vec::new();
self.unpaid
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|&(h, start, end)| {
if h == hash {
spans.push((start, end));
false
} else {
true
}
});
merge_spans(spans)
}
#[cfg(test)]
#[must_use]
pub(crate) const fn with_send_deadline(mut self, send_deadline: Duration) -> Self {
self.send_deadline = send_deadline;
self
}
async fn under_deadline<Fut>(&self, send: Fut) -> anyhow::Result<()>
where
Fut: Future<Output = anyhow::Result<()>>,
{
match tokio::time::timeout(self.send_deadline, send).await {
Ok(result) => result,
Err(_) => Err(anyhow::anyhow!(
"voucher send exceeded {:?}; failing the lane to trigger failover",
self.send_deadline
)),
}
}
fn pipeline(&self) -> std::sync::MutexGuard<'_, Pipeline> {
self.pipeline
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[must_use]
pub fn committed(&self) -> Cumulative {
let pipeline = self.pipeline();
pipeline.committed.plus(pipeline.accrued)
}
#[must_use]
pub fn settlement(&self) -> Cumulative {
self.pipeline().settlement()
}
#[doc(hidden)]
pub async fn issue<F, Fut>(
&self,
delta_bytes: u64,
rate_per_mb: u64,
epoch: EpochAction,
exchange: F,
) -> anyhow::Result<Cumulative>
where
F: FnOnce(Cumulative, ChainCommit) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
self.issue_stamped(delta_bytes, rate_per_mb, epoch, exchange)
.await
.map(|(next, _generation)| next)
}
pub(crate) async fn issue_stamped<F, Fut>(
&self,
delta_bytes: u64,
rate_per_mb: u64,
epoch: EpochAction,
exchange: F,
) -> anyhow::Result<(Cumulative, u64)>
where
F: FnOnce(Cumulative, ChainCommit) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
let _issuing = self.issuance.lock().await;
let (frontier, accrued) = {
let pipeline = self.pipeline();
(pipeline.settlement(), pipeline.accrued)
};
let epoch = if accrued.amount.is_zero() && accrued.bytes.is_zero() {
epoch
} else {
EpochAction::Roll
};
let next = next_voucher(&frontier, delta_bytes, rate_per_mb);
let commit = self.commit_epoch(epoch, rate_per_mb);
self.pipeline().armed = Some(Armed {
voucher: next,
folded: accrued,
});
self.under_deadline(exchange(next, commit)).await?;
let generation = {
let mut pipeline = self.pipeline();
pipeline.prev = Some(pipeline.committed);
pipeline.prev_accrued = pipeline.accrued;
pipeline.committed = next;
pipeline.accrued = Cumulative::default();
pipeline.armed = None;
pipeline.last_proof = Some(LastProof::Voucher {
amount: next.amount,
});
pipeline.generation
};
Ok((next, generation))
}
#[cfg(test)]
pub(crate) async fn confirm_armed(&self, amount: U256) -> Option<Cumulative> {
self.confirm_armed_stamped(amount)
.await
.map(|(confirmed, _generation)| confirmed)
}
pub(crate) async fn confirm_armed_stamped(&self, amount: U256) -> Option<(Cumulative, u64)> {
let _issuing = self.issuance.lock().await;
let mut pipeline = self.pipeline();
let armed = pipeline
.armed
.filter(|armed| armed.voucher.amount == amount)?;
pipeline.armed = None;
pipeline.prev = Some(pipeline.committed);
pipeline.prev_accrued = armed.folded;
pipeline.committed = armed.voucher;
pipeline.accrued = pipeline.accrued.minus(armed.folded);
pipeline.last_proof = Some(LastProof::Voucher { amount });
Some((armed.voucher, pipeline.generation))
}
pub(crate) async fn reanchor<F, Fut>(&self, exchange: F) -> anyhow::Result<Option<B256>>
where
F: FnOnce(Cumulative, ChainCommit) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
let _issuing = self.issuance.lock().await;
let Some(commit) = self.epoch().as_ref().map(|live| ChainCommit {
chain_root: live.root(),
chunk_price: live.chunk_price,
}) else {
return Ok(None);
};
let anchor = self.pipeline().committed;
self.under_deadline(exchange(anchor, commit)).await?;
Ok(Some(commit.chain_root))
}
fn commit_epoch(&self, action: EpochAction, rate_per_mb: u64) -> ChainCommit {
let mut slot = self.epoch();
let replacement = match action {
EpochAction::Keep => None,
EpochAction::Open if slot.is_some() => None,
EpochAction::Open | EpochAction::Roll => Some(Some(open_epoch(rate_per_mb))),
};
*self.retired() = match replacement {
Some(next) => Displaced::Replaced(std::mem::replace(&mut *slot, next)),
None => Displaced::Nothing,
};
slot.as_ref()
.map_or(ChainCommit::SEALED, |live| ChainCommit {
chain_root: live.root(),
chunk_price: live.chunk_price,
})
}
fn epoch(&self) -> std::sync::MutexGuard<'_, Option<ChainEpoch>> {
self.epoch
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn retired(&self) -> std::sync::MutexGuard<'_, Displaced> {
self.retired
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[must_use]
#[doc(hidden)]
pub fn chain_root(&self) -> Option<B256> {
self.epoch().as_ref().map(ChainEpoch::root)
}
#[doc(hidden)]
pub async fn meter<F, Fut>(
&self,
anchored: Option<B256>,
exchange: F,
) -> anyhow::Result<Metered>
where
F: FnOnce(Released) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
let _issuing = self.issuance.lock().await;
let (released, price) = {
let slot = self.epoch();
let Some(live) = slot.as_ref() else {
return Ok(Metered::Exhausted);
};
if anchored != Some(live.root()) {
return Ok(Metered::Moved);
}
let Some(index) = live.next_index() else {
return Ok(Metered::Exhausted);
};
let Some(preimage) = live.preimages.get(usize::from(index)).copied() else {
return Ok(Metered::Exhausted);
};
(
Released {
preimage,
index,
chain_root: live.root(),
},
live.chunk_price,
)
};
self.under_deadline(exchange(released)).await?;
{
let mut slot = self.epoch();
if let Some(live) = slot.as_mut() {
live.released = released.index;
}
}
{
let mut pipeline = self.pipeline();
pipeline.accrued.amount = pipeline.accrued.amount.saturating_add(price);
pipeline.accrued.bytes = pipeline
.accrued
.bytes
.saturating_add(U256::from(CHUNK_BYTES));
pipeline.last_proof = Some(LastProof::Reveal {
chain_root: released.chain_root,
index: released.index,
chunk_price: price,
});
}
Ok(Metered::Released(released))
}
#[must_use]
#[doc(hidden)]
pub fn reseed(&self, cum: Cumulative) -> bool {
{
let mut pipeline = self.pipeline();
if cum.amount <= pipeline.committed.plus(pipeline.accrued).amount {
return false;
}
pipeline.overwrite(cum);
pipeline.healed_from = Some(cum);
}
self.retire_chain();
true
}
#[doc(hidden)]
pub async fn rebase(&self, cum: Cumulative, proof_generation: Option<u64>) -> Rebase {
self.rebase_if(cum, proof_generation, |from| {
cum.amount <= from.amount && cum != from
})
.await
}
pub(crate) async fn rebase_ahead_on_bytes(
&self,
cum: Cumulative,
proof_generation: Option<u64>,
) -> Rebase {
self.rebase_if(cum, proof_generation, |from| {
cum.amount <= from.amount && cum.bytes > from.bytes
})
.await
}
async fn rebase_if(
&self,
cum: Cumulative,
proof_generation: Option<u64>,
admits: impl FnOnce(Cumulative) -> bool,
) -> Rebase {
let _issuing = self.issuance.lock().await;
let from = {
let mut pipeline = self.pipeline();
if proof_generation.is_some_and(|g| g < pipeline.generation) {
return Rebase::Stale;
}
let from = pipeline.committed.plus(pipeline.accrued);
if !admits(from) {
return Rebase::Refused;
}
pipeline.overwrite(cum);
pipeline.generation = pipeline.generation.saturating_add(1);
pipeline.unsaved_rebase = Some(cum);
pipeline.healed_from = Some(cum);
from
};
self.retire_chain();
Rebase::Rebased { from }
}
#[must_use]
#[doc(hidden)]
pub fn generation(&self) -> u64 {
self.pipeline().generation
}
#[must_use]
pub fn take_unsaved_rebase(&self) -> Option<Cumulative> {
self.pipeline().unsaved_rebase.take()
}
pub(crate) async fn retire_unadopted_chain(&self, bundle: Cumulative) -> Option<B256> {
let _issuing = self.issuance.lock().await;
let root = self.chain_root()?;
{
let mut pipeline = self.pipeline();
if pipeline
.healed_from
.is_some_and(|h| h.amount >= bundle.amount && h.bytes >= bundle.bytes)
{
return None;
}
let settlement = pipeline.settlement();
pipeline.overwrite(settlement);
pipeline.healed_from = Some(bundle);
}
self.retire_chain();
Some(root)
}
fn retire_chain(&self) {
*self.epoch() = None;
*self.retired() = Displaced::Nothing;
}
pub(crate) fn resolve_reject(&self, rejected: StreamProof) -> bool {
let mut pipeline = self.pipeline();
match (rejected, pipeline.last_proof) {
(
StreamProof::Reveal { chain_root, index },
Some(LastProof::Reveal {
chain_root: last_root,
index: last_index,
chunk_price,
}),
) if chain_root == last_root && index == last_index => {
pipeline.last_proof = None;
pipeline.accrued.amount = pipeline.accrued.amount.saturating_sub(chunk_price);
pipeline.accrued.bytes = pipeline
.accrued
.bytes
.saturating_sub(U256::from(CHUNK_BYTES));
drop(pipeline);
if let Some(live) = self.epoch().as_mut()
&& live.root() == chain_root
{
live.released = live.released.saturating_sub(1);
}
true
}
(
StreamProof::Voucher { amount, .. },
Some(LastProof::Voucher {
amount: last_amount,
}),
) if amount == last_amount => {
pipeline.last_proof = None;
let Some(prev) = pipeline.prev.take() else {
return false;
};
pipeline.committed = prev;
pipeline.accrued = pipeline
.accrued
.plus(std::mem::take(&mut pipeline.prev_accrued));
drop(pipeline);
let mut epoch = self.epoch();
if let Displaced::Replaced(chain) =
std::mem::replace(&mut *self.retired(), Displaced::Nothing)
{
*epoch = chain;
}
true
}
(StreamProof::Voucher { amount, .. }, _)
if pipeline
.armed
.is_some_and(|armed| armed.voucher.amount == amount) =>
{
pipeline.armed = None;
true
}
_ => false,
}
}
}
fn open_epoch(rate_per_mb: u64) -> ChainEpoch {
ChainEpoch::open(U256::from(rate_per_mb))
}
#[cfg(test)]
mod tests;