use alloy::dyn_abi::Eip712Domain;
use alloy::primitives::{Address, B256, U256};
use crate::store::{PoolStateStore, StoreError};
use crate::voucher::{SignedVoucher, VoucherError};
pub type PoolId = B256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LaneKey {
pub pool_id: B256,
pub signer: Address,
pub provider: Address,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaneState {
pub pool_id: B256,
pub signer: Address,
pub provider: Address,
pub cap: U256,
pub expiry: u64,
pub registered_until: u64,
pub owner_sig: Option<[u8; 65]>,
pub paid_cumulative: U256,
last_amount: U256,
last_bytes_delivered: U256,
last_signature: Option<[u8; 65]>,
chain: LaneChain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LaneChain {
pub chain_root: B256,
pub chunk_price: U256,
pub verified_index: u8,
pub tip: B256,
}
impl LaneChain {
pub const NONE: Self = Self {
chain_root: B256::ZERO,
chunk_price: U256::ZERO,
verified_index: 0,
tip: B256::ZERO,
};
#[must_use]
pub const fn opened(chain_root: B256, chunk_price: U256) -> Self {
Self {
chain_root,
chunk_price,
verified_index: 0,
tip: chain_root,
}
}
#[must_use]
pub fn accrued(&self) -> U256 {
self.chunk_price * U256::from(self.verified_index)
}
#[must_use]
pub fn accrued_bytes(&self) -> U256 {
U256::from(crate::chain::CHUNK_BYTES) * U256::from(self.verified_index)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RedeemClaim {
pub amount: U256,
pub bytes_delivered: U256,
pub signature: [u8; 65],
pub chain: LaneChain,
}
impl RedeemClaim {
#[must_use]
pub fn value(&self) -> U256 {
self.amount + self.chain.accrued()
}
#[must_use]
pub fn bytes_value(&self) -> U256 {
self.bytes_delivered + self.chain.accrued_bytes()
}
pub fn chain_meter(&self) -> Result<U256, crate::chain::ChainMeterError> {
crate::chain::pack_chain_meter(self.chain.chunk_price, self.chain.verified_index)
}
}
impl LaneState {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn hydrate(
pool_id: B256,
signer: Address,
provider: Address,
cap: U256,
expiry: u64,
last_amount: U256,
last_bytes_delivered: U256,
last_signature: Option<[u8; 65]>,
chain: LaneChain,
) -> Self {
Self {
pool_id,
signer,
provider,
cap,
expiry,
registered_until: 0,
owner_sig: None,
paid_cumulative: U256::ZERO,
last_amount,
last_bytes_delivered,
last_signature,
chain,
}
}
pub fn clamp_to_registration(&mut self, cap: u64, expiry: u64) -> bool {
let before = (self.cap, self.expiry, self.registered_until);
self.cap = self.cap.min(U256::from(cap));
let held_expiry = expiry.max(1);
self.expiry = if self.expiry == 0 {
held_expiry
} else {
self.expiry.min(held_expiry)
};
self.registered_until = self.registered_until.max(expiry);
before != (self.cap, self.expiry, self.registered_until)
}
#[must_use]
pub const fn key(&self) -> LaneKey {
LaneKey {
pool_id: self.pool_id,
signer: self.signer,
provider: self.provider,
}
}
#[must_use]
pub const fn last_amount(&self) -> U256 {
self.last_amount
}
#[must_use]
pub const fn last_bytes_delivered(&self) -> U256 {
self.last_bytes_delivered
}
#[must_use]
pub const fn last_signature(&self) -> Option<&[u8; 65]> {
self.last_signature.as_ref()
}
#[must_use]
pub const fn chain(&self) -> LaneChain {
self.chain
}
#[must_use]
pub fn live_claim(&self) -> Option<RedeemClaim> {
self.last_signature.map(|signature| RedeemClaim {
amount: self.last_amount,
bytes_delivered: self.last_bytes_delivered,
signature,
chain: self.chain,
})
}
#[must_use]
pub fn owed_bytes(&self) -> U256 {
self.live_claim().map_or(U256::ZERO, |c| c.bytes_value())
}
#[must_use]
pub fn owed(&self) -> U256 {
self.live_claim().map_or(U256::ZERO, |c| c.value())
}
#[must_use]
pub fn adopt_chain(&self, signed: &SignedVoucher) -> Option<Self> {
let chain_root = signed.voucher.chain_root;
if chain_root.is_zero() {
return None;
}
let mut next = self.clone();
if self.chain.chain_root == chain_root {
if self.last_signature.is_some() {
return None;
}
next.chain.chunk_price = signed.voucher.chunk_price;
next.last_amount = signed.voucher.amount;
next.last_bytes_delivered = signed.voucher.bytes_delivered;
next.last_signature = Some(signed.signature.as_bytes());
return Some(next);
}
if self.chain.verified_index > 0 {
return None;
}
if signed.voucher.amount != self.last_amount
|| signed.voucher.bytes_delivered != self.last_bytes_delivered
{
return None;
}
next.chain = LaneChain::opened(chain_root, signed.voucher.chunk_price);
next.last_signature = Some(signed.signature.as_bytes());
Some(next)
}
pub fn advance_preimage(
&self,
root: B256,
index: u8,
preimage: B256,
) -> Result<(Self, PreimageApplied), PoolError> {
let walked = self.preimage_frontier(root, index);
let walked_ok = match walked {
Some((verified_index, tip)) => {
crate::chain::verify_forward(preimage, index - verified_index, tip)
}
None => false,
};
self.advance_preimage_verified(root, index, preimage, walked, walked_ok)
}
#[must_use]
pub fn preimage_frontier(&self, root: B256, index: u8) -> Option<(u8, B256)> {
let target = self.chain_slot(root)?;
(index > target.verified_index).then_some((target.verified_index, target.tip))
}
pub fn advance_preimage_verified(
&self,
root: B256,
index: u8,
preimage: B256,
walked: Option<(u8, B256)>,
walked_ok: bool,
) -> Result<(Self, PreimageApplied), PoolError> {
let Some(target) = self.chain_slot(root) else {
return Ok((self.clone(), PreimageApplied::ZERO));
};
if index <= target.verified_index {
return Ok((self.clone(), PreimageApplied::ZERO));
}
let verified = match walked {
Some((wv, wtip)) if wv == target.verified_index && wtip == target.tip => walked_ok,
_ => crate::chain::verify_forward(preimage, index - target.verified_index, target.tip),
};
if !verified {
return Err(PoolError::BadPreimage {
index,
verified: target.verified_index,
});
}
let steps = U256::from(index - target.verified_index);
let applied = PreimageApplied {
amount_delta: target.chunk_price * steps,
bytes_delta: U256::from(crate::chain::CHUNK_BYTES) * steps,
};
let mut next = self.clone();
next.chain.verified_index = index;
next.chain.tip = preimage;
let claimed = self.last_amount.saturating_add(next.chain.accrued());
if claimed > self.cap {
return Err(PoolError::CapExceeded {
cap: self.cap,
got: claimed,
});
}
Ok((next, applied))
}
fn chain_slot(&self, root: B256) -> Option<LaneChain> {
(!root.is_zero() && root == self.chain.chain_root).then_some(self.chain)
}
pub fn apply_voucher(
&mut self,
signed: &SignedVoucher,
domain: &Eip712Domain,
store: &dyn PoolStateStore,
) -> Result<VoucherApplied, PoolError> {
let (next, applied) = self.stage_voucher(signed, domain)?;
store.record(&next)?;
*self = next;
Ok(applied)
}
pub fn stage_voucher(
&self,
signed: &SignedVoucher,
domain: &Eip712Domain,
) -> Result<(Self, VoucherApplied), PoolError> {
if signed.voucher.pool_id != self.pool_id {
return Err(PoolError::WrongPool {
expected: self.pool_id,
got: signed.voucher.pool_id,
});
}
if signed.voucher.provider != self.provider {
return Err(PoolError::WrongProvider {
expected: self.provider,
got: signed.voucher.provider,
});
}
signed
.verify_signer(self.signer, domain)
.map_err(PoolError::Signature)?;
self.advance_presigned(signed)
}
pub fn advance_presigned(
&self,
signed: &SignedVoucher,
) -> Result<(Self, VoucherApplied), PoolError> {
debug_assert_eq!(
signed.voucher.pool_id, self.pool_id,
"advance_presigned: voucher pool_id must match the lane's"
);
debug_assert_eq!(
signed.voucher.provider, self.provider,
"advance_presigned: voucher provider must match the lane's"
);
if signed.voucher.amount <= self.last_amount {
if signed.voucher.amount == self.last_amount
&& !signed.voucher.chain_root.is_zero()
&& signed.voucher.chain_root != self.chain.chain_root
&& let Some(live) = self.live_claim()
&& live.value() > signed.voucher.amount
{
return Err(PoolError::UnderFold {
axis: FoldAxis::Amount,
owed: live.value(),
got: signed.voucher.amount,
});
}
return Err(PoolError::AmountRegression {
last: self.last_amount,
got: signed.voucher.amount,
});
}
if signed.voucher.bytes_delivered < self.last_bytes_delivered {
return Err(PoolError::BytesRegression {
last: self.last_bytes_delivered,
got: signed.voucher.bytes_delivered,
});
}
if signed.voucher.amount > self.cap {
return Err(PoolError::CapExceeded {
cap: self.cap,
got: signed.voucher.amount,
});
}
let mut next = self.clone();
let amount_delta = signed.voucher.amount - self.last_amount;
let bytes_delta = signed.voucher.bytes_delivered - self.last_bytes_delivered;
if signed.voucher.chain_root == self.chain.chain_root {
next.chain.chunk_price = signed.voucher.chunk_price;
} else {
if let Some(live) = self.live_claim() {
if live.value() > signed.voucher.amount {
return Err(PoolError::UnderFold {
axis: FoldAxis::Amount,
owed: live.value(),
got: signed.voucher.amount,
});
}
if live.bytes_value() > signed.voucher.bytes_delivered {
return Err(PoolError::UnderFold {
axis: FoldAxis::Bytes,
owed: live.bytes_value(),
got: signed.voucher.bytes_delivered,
});
}
}
next.chain = LaneChain::opened(signed.voucher.chain_root, signed.voucher.chunk_price);
}
next.last_amount = signed.voucher.amount;
next.last_bytes_delivered = signed.voucher.bytes_delivered;
next.last_signature = Some(signed.signature.as_bytes());
Ok((
next,
VoucherApplied {
amount_delta,
bytes_delta,
},
))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VoucherApplied {
amount_delta: U256,
bytes_delta: U256,
}
impl VoucherApplied {
#[must_use]
pub const fn amount_delta(&self) -> U256 {
self.amount_delta
}
#[must_use]
pub const fn bytes_delta(&self) -> U256 {
self.bytes_delta
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PreimageApplied {
amount_delta: U256,
bytes_delta: U256,
}
impl PreimageApplied {
pub const ZERO: Self = Self {
amount_delta: U256::ZERO,
bytes_delta: U256::ZERO,
};
#[must_use]
pub const fn amount_delta(&self) -> U256 {
self.amount_delta
}
#[must_use]
pub const fn bytes_delta(&self) -> U256 {
self.bytes_delta
}
#[must_use]
pub fn advanced(&self) -> bool {
!self.amount_delta.is_zero() || !self.bytes_delta.is_zero()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FoldAxis {
Amount,
Bytes,
}
impl std::fmt::Display for FoldAxis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Amount => "amount",
Self::Bytes => "bytes_delivered",
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum PoolError {
#[error("voucher pool_id {got} does not match expected {expected}")]
WrongPool {
expected: B256,
got: B256,
},
#[error("voucher provider {got} does not match expected {expected}")]
WrongProvider {
expected: Address,
got: Address,
},
#[error("voucher amount {got} not greater than last accepted {last}")]
AmountRegression {
last: U256,
got: U256,
},
#[error("voucher {axis} {got} does not fold the live claim {owed}")]
UnderFold {
axis: FoldAxis,
owed: U256,
got: U256,
},
#[error("voucher bytes_delivered {got} less than last accepted {last}")]
BytesRegression {
last: U256,
got: U256,
},
#[error("voucher amount {got} exceeds capability cap {cap}")]
CapExceeded {
cap: U256,
got: U256,
},
#[error("released preimage at index {index} does not reach the tip verified at {verified}")]
BadPreimage {
index: u8,
verified: u8,
},
#[error(transparent)]
Signature(#[from] VoucherError),
#[error("pool state store failed: {0}")]
Store(#[from] StoreError),
}
#[cfg(test)]
#[allow(clippy::similar_names)] mod tests;