use alloy::primitives::{Address, U256};
use redb::{Database, Durability, ReadableDatabase, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
use crate::buyer_pool::{
AdvanceOutcome, BuyerLaneProgress, BuyerLoad, BuyerPoolState, BuyerProgressError,
DepositOutcome,
};
use crate::deployment::Deployment;
use crate::lane::{LaneKey, PoolId};
use crate::store::StoreError;
const BUYER_POOL_TABLE: TableDefinition<'static, &'static [u8; 32], &'static [u8]> =
TableDefinition::new("buyer_pool_state_v1");
const BUYER_OWNER_INDEX_TABLE: TableDefinition<'static, &'static [u8; 20], &'static [u8; 32]> =
TableDefinition::new("buyer_pool_owner_index_v1");
const BUYER_SUPPORTED_SCHEMA_VERSION: u32 = 1;
const SANE_TRAILER_MAX_BYTES: usize = 256;
#[derive(Debug, Serialize, Deserialize)]
struct StoredLane {
signer: [u8; 20],
provider: [u8; 20],
last_amount: [u8; 32],
last_bytes: [u8; 32],
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredBuyerPoolState {
schema_version: u32,
pool_id: [u8; 32],
chain_id: [u8; 8],
payment_pool: [u8; 20],
owner: [u8; 20],
token: [u8; 20],
deposit: [u8; 32],
lanes: Vec<StoredLane>,
redeemed_elsewhere: [u8; 32],
}
impl From<&BuyerPoolState> for StoredBuyerPoolState {
fn from(state: &BuyerPoolState) -> Self {
let mut lanes: Vec<StoredLane> = state
.lanes()
.map(|(key, progress)| StoredLane {
signer: key.signer.into(),
provider: key.provider.into(),
last_amount: progress.last_amount.to_be_bytes(),
last_bytes: progress.last_bytes.to_be_bytes(),
})
.collect();
lanes.sort_by_key(|l| (l.signer, l.provider));
Self {
schema_version: BUYER_SUPPORTED_SCHEMA_VERSION,
pool_id: state.pool_id.into(),
chain_id: state.deployment.chain_id.to_be_bytes(),
payment_pool: state.deployment.payment_pool.into(),
owner: state.owner.into(),
token: state.token.into(),
deposit: state.deposit.to_be_bytes(),
lanes,
redeemed_elsewhere: state.redeemed_elsewhere().to_be_bytes(),
}
}
}
impl StoredBuyerPoolState {
fn into_state(self, pool_id: PoolId) -> Result<BuyerPoolState, StoreError> {
if self.schema_version != BUYER_SUPPORTED_SCHEMA_VERSION {
return Err(StoreError::UnsupportedSchema {
found: self.schema_version,
supported: BUYER_SUPPORTED_SCHEMA_VERSION,
});
}
let lanes = self
.lanes
.into_iter()
.map(|l| {
let key = LaneKey {
pool_id,
signer: Address::from(l.signer),
provider: Address::from(l.provider),
};
let progress = BuyerLaneProgress {
last_amount: U256::from_be_bytes(l.last_amount),
last_bytes: U256::from_be_bytes(l.last_bytes),
};
(key, progress)
})
.collect();
Ok(BuyerPoolState::hydrate(
pool_id,
Deployment {
chain_id: u64::from_be_bytes(self.chain_id),
payment_pool: Address::from(self.payment_pool),
},
Address::from(self.owner),
Address::from(self.token),
U256::from_be_bytes(self.deposit),
lanes,
U256::from_be_bytes(self.redeemed_elsewhere),
))
}
}
fn encode_record(state: &BuyerPoolState) -> Result<Vec<u8>, StoreError> {
postcard::to_allocvec(&StoredBuyerPoolState::from(state))
.map_err(|err| StoreError::Codec(format!("buyer record postcard encode: {err}")))
}
fn decode_record(key_bytes: [u8; 32], value_bytes: &[u8]) -> Result<BuyerPoolState, StoreError> {
let pool_id = PoolId::from(key_bytes);
let (stored, remainder): (StoredBuyerPoolState, &[u8]) = postcard::take_from_bytes(value_bytes)
.map_err(|err| StoreError::Corrupt {
pool_id: Some(pool_id),
detail: format!("buyer record postcard decode failed (pool_id {pool_id}): {err}"),
})?;
if stored.pool_id != key_bytes {
return Err(StoreError::Corrupt {
pool_id: Some(pool_id),
detail: format!("buyer record pool_id {pool_id} does not match table key"),
});
}
if remainder.len() > SANE_TRAILER_MAX_BYTES {
tracing::warn!(
%pool_id,
remainder = remainder.len(),
limit = SANE_TRAILER_MAX_BYTES,
event = "buyer_pool_store_excess_trailer",
"buyer pool record has unusually large trailing bytes; possible malicious padding \
or large-additive-field schema skew",
);
}
stored.into_state(pool_id)
}
pub(crate) fn load_all_from<D: ReadableDatabase>(db: &D) -> Result<BuyerLoad, StoreError> {
let read_txn = db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let table = match read_txn.open_table(BUYER_POOL_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(BuyerLoad::default()),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let mut out = Vec::new();
let mut skipped = Vec::new();
let iter = table
.iter()
.map_err(|err| StoreError::Backend(format!("table iter: {err}")))?;
for entry in iter {
let (key_guard, value_guard) =
entry.map_err(|err| StoreError::Backend(format!("iter entry: {err}")))?;
let key_bytes: [u8; 32] = *key_guard.value();
match decode_record(key_bytes, value_guard.value()) {
Ok(state) => out.push(state),
Err(err) => {
let pool_id = PoolId::from(key_bytes);
skipped.push(pool_id);
tracing::error!(
%pool_id,
error = %err,
event = "buyer_pool_store_skip_undecodable_record",
"buyer pool hydration: skipping an undecodable record; its escrowed \
deposit is untracked and will not be auto-reclaimed until the record is \
repaired (other pools remain healthy)",
);
}
}
}
Ok(BuyerLoad {
pools: out,
skipped,
})
}
#[derive(Debug)]
pub struct BuyerPoolTable<'a> {
db: &'a Database,
}
impl<'a> BuyerPoolTable<'a> {
#[must_use]
pub const fn new(db: &'a Database) -> Self {
Self { db }
}
fn begin_durable_write(&self) -> Result<redb::WriteTransaction, StoreError> {
let mut write_txn = self
.db
.begin_write()
.map_err(|err| StoreError::Backend(format!("begin_write: {err}")))?;
write_txn
.set_durability(Durability::Immediate)
.map_err(|err| StoreError::Backend(format!("set_durability: {err}")))?;
Ok(write_txn)
}
pub fn load_all(&self) -> Result<BuyerLoad, StoreError> {
load_all_from(self.db)
}
pub fn record(&self, state: &BuyerPoolState) -> Result<(), StoreError> {
let encoded = encode_record(state)?;
let primary_key: [u8; 32] = state.pool_id.into();
let index_key: [u8; 20] = state.owner.into();
let write_txn = self.begin_durable_write()?;
{
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.insert(&primary_key, encoded.as_slice())
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
}
{
let mut index = write_txn
.open_table(BUYER_OWNER_INDEX_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (index): {err}")))?;
index
.insert(&index_key, &primary_key)
.map_err(|err| StoreError::Backend(format!("insert (index): {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
pub fn forget(&self, owner: Address) -> Result<(), StoreError> {
let index_key: [u8; 20] = owner.into();
let write_txn = self.begin_durable_write()?;
let primary_key = {
let mut index = write_txn
.open_table(BUYER_OWNER_INDEX_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (index): {err}")))?;
let Some(value_guard) = index
.get(&index_key)
.map_err(|err| StoreError::Backend(format!("get (index): {err}")))?
else {
return Ok(());
};
let primary_key: [u8; 32] = *value_guard.value();
drop(value_guard);
index
.remove(&index_key)
.map_err(|err| StoreError::Backend(format!("remove (index): {err}")))?;
primary_key
};
{
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.remove(&primary_key)
.map_err(|err| StoreError::Backend(format!("remove: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
pub fn get_by_pool_id(&self, pool_id: PoolId) -> Result<Option<BuyerPoolState>, StoreError> {
let key: [u8; 32] = pool_id.into();
let read_txn = self
.db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let table = match read_txn.open_table(BUYER_POOL_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let Some(value_guard) = table
.get(&key)
.map_err(|err| StoreError::Backend(format!("get: {err}")))?
else {
return Ok(None);
};
Ok(Some(decode_record(key, value_guard.value())?))
}
pub fn get_by_owner(&self, owner: Address) -> Result<Option<BuyerPoolState>, StoreError> {
let index_key: [u8; 20] = owner.into();
let read_txn = self
.db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let index = match read_txn.open_table(BUYER_OWNER_INDEX_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(err) => return Err(StoreError::Backend(format!("open_table (index): {err}"))),
};
let Some(idx_guard) = index
.get(&index_key)
.map_err(|err| StoreError::Backend(format!("get (index): {err}")))?
else {
return Ok(None);
};
let primary_key: [u8; 32] = *idx_guard.value();
drop(idx_guard);
let table = match read_txn.open_table(BUYER_POOL_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let Some(value_guard) = table
.get(&primary_key)
.map_err(|err| StoreError::Backend(format!("get: {err}")))?
else {
return Ok(None);
};
Ok(Some(decode_record(primary_key, value_guard.value())?))
}
pub fn forget_if_pool(&self, owner: Address, pool_id: PoolId) -> Result<bool, StoreError> {
let index_key: [u8; 20] = owner.into();
let primary_key: [u8; 32] = pool_id.into();
let write_txn = self.begin_durable_write()?;
{
let mut index = write_txn
.open_table(BUYER_OWNER_INDEX_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (index): {err}")))?;
let Some(value_guard) = index
.get(&index_key)
.map_err(|err| StoreError::Backend(format!("get (index): {err}")))?
else {
return Ok(false);
};
let matches = *value_guard.value() == primary_key;
drop(value_guard);
if !matches {
return Ok(false);
}
index
.remove(&index_key)
.map_err(|err| StoreError::Backend(format!("remove (index): {err}")))?;
}
{
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.remove(&primary_key)
.map_err(|err| StoreError::Backend(format!("remove: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(true)
}
pub fn advance_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, StoreError> {
self.write_lane(owner, pool_id, |state| {
state.advance_lane(lane, bytes, amount)
})
}
pub fn rebase_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
anchor: BuyerLaneProgress,
totals: BuyerLaneProgress,
) -> Result<AdvanceOutcome, StoreError> {
self.write_lane(owner, pool_id, |state| {
state.rebase_lane(lane, anchor, totals);
Ok(())
})
}
pub fn seed_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, StoreError> {
self.write_lane(owner, pool_id, |state| state.seed_lane(lane, bytes, amount))
}
fn write_lane(
&self,
owner: Address,
pool_id: PoolId,
apply: impl FnOnce(&mut BuyerPoolState) -> Result<(), BuyerProgressError>,
) -> Result<AdvanceOutcome, StoreError> {
let index_key: [u8; 20] = owner.into();
let primary_key: [u8; 32] = pool_id.into();
let write_txn = self.begin_durable_write()?;
{
let index = write_txn
.open_table(BUYER_OWNER_INDEX_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (index): {err}")))?;
let Some(idx_guard) = index
.get(&index_key)
.map_err(|err| StoreError::Backend(format!("get (index): {err}")))?
else {
return Ok(AdvanceOutcome::UnknownPool);
};
let mapped: [u8; 32] = *idx_guard.value();
drop(idx_guard);
if mapped != primary_key {
return Ok(AdvanceOutcome::PoolMismatch);
}
}
{
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
let Some(value_guard) = table
.get(&primary_key)
.map_err(|err| StoreError::Backend(format!("get: {err}")))?
else {
return Ok(AdvanceOutcome::UnknownPool);
};
let mut state = decode_record(primary_key, value_guard.value())?;
drop(value_guard);
if let Err(err) = apply(&mut state) {
return Ok(AdvanceOutcome::Regressed(err));
}
let encoded = encode_record(&state)?;
table
.insert(&primary_key, encoded.as_slice())
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(AdvanceOutcome::Advanced)
}
pub fn add_deposit(
&self,
owner: Address,
pool_id: PoolId,
additional: U256,
) -> Result<DepositOutcome, StoreError> {
let index_key: [u8; 20] = owner.into();
let primary_key: [u8; 32] = pool_id.into();
let write_txn = self.begin_durable_write()?;
{
let index = write_txn
.open_table(BUYER_OWNER_INDEX_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (index): {err}")))?;
let Some(idx_guard) = index
.get(&index_key)
.map_err(|err| StoreError::Backend(format!("get (index): {err}")))?
else {
return Ok(DepositOutcome::UnknownPool);
};
let mapped: [u8; 32] = *idx_guard.value();
drop(idx_guard);
if mapped != primary_key {
return Ok(DepositOutcome::PoolMismatch);
}
}
let new_deposit = {
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
let Some(value_guard) = table
.get(&primary_key)
.map_err(|err| StoreError::Backend(format!("get: {err}")))?
else {
return Ok(DepositOutcome::UnknownPool);
};
let mut state = decode_record(primary_key, value_guard.value())?;
drop(value_guard);
state.deposit = state.deposit.saturating_add(additional);
let encoded = encode_record(&state)?;
table
.insert(&primary_key, encoded.as_slice())
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
state.deposit
};
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(DepositOutcome::Added(new_deposit))
}
#[cfg(any(test, feature = "test-util"))]
pub fn insert_raw(&self, pool_id: PoolId, bytes: &[u8]) -> Result<(), StoreError> {
let key: [u8; 32] = pool_id.into();
let write_txn = self.begin_durable_write()?;
{
let mut table = write_txn
.open_table(BUYER_POOL_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.insert(&key, bytes)
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests;