use std::collections::HashSet;
use std::path::{Path, PathBuf};
use crossbeam_queue::SegQueue;
use dashmap::DashMap;
use dashmap::mapref::entry::Entry;
use alloy::primitives::{Address, B256, U256};
use decdn_common::identity;
pub use decdn_incentive::Deployment;
use decdn_incentive::buyer_pool_table::BuyerPoolTable;
use decdn_incentive::store::{
CheckpointKey, KeyedCheckpointStore, PendingSettle, PendingSettleStore, PoolStateStore,
StoreError,
};
use decdn_incentive::{
AdvanceOutcome, BuyerLaneProgress, BuyerLoad, BuyerPoolState, BuyerPoolStore, DepositOutcome,
LaneChain, LaneKey, LaneState, PoolId,
};
use redb::{Database, Durability, ReadableDatabase, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
const LANES_DB_FILE: &str = decdn_common::data_dir::NODE_LANES_DB_FILE;
const SETTLE_DB_FILE: &str = decdn_common::data_dir::NODE_SETTLE_DB_FILE;
const CHECKPOINT_DB_FILE: &str = decdn_common::data_dir::NODE_CHECKPOINT_DB_FILE;
const BUYER_DB_FILE: &str = decdn_common::data_dir::NODE_BUYER_DB_FILE;
const LANE_KEY_LEN: usize = 72;
#[cfg(unix)]
const DB_FILE_MODE: u32 = 0o600;
const SUPPORTED_SCHEMA_VERSION: u32 = 1;
const SANE_TRAILER_MAX_BYTES: usize = 256;
const LANE_TABLE: TableDefinition<'_, &[u8; LANE_KEY_LEN], &[u8]> =
TableDefinition::new("lane_state_v1");
const PENDING_SETTLE_TABLE: TableDefinition<'_, &[u8; 32], u64> =
TableDefinition::new("pending_settle_v1");
const BUYER_PENDING_SETTLE_TABLE: TableDefinition<'_, &[u8; 32], u64> =
TableDefinition::new("buyer_pending_settle_v1");
const WATCHER_CHECKPOINT_TABLE: TableDefinition<'_, &str, u64> =
TableDefinition::new("watcher_checkpoint_v1");
const DEPLOYMENT_TABLE: TableDefinition<'_, &str, &[u8]> = TableDefinition::new("deployment_v1");
const DEPLOYMENT_KEY: &str = "payment_pool";
fn lane_key_bytes(key: &LaneKey) -> [u8; LANE_KEY_LEN] {
let mut out = [0u8; LANE_KEY_LEN];
out[..32].copy_from_slice(key.pool_id.as_slice());
out[32..52].copy_from_slice(key.signer.as_slice());
out[52..].copy_from_slice(key.provider.as_slice());
out
}
fn lane_key_parts(bytes: &[u8; LANE_KEY_LEN]) -> (B256, Address, Address) {
let mut pool = [0u8; 32];
pool.copy_from_slice(&bytes[..32]);
let mut signer = [0u8; 20];
signer.copy_from_slice(&bytes[32..52]);
let mut provider = [0u8; 20];
provider.copy_from_slice(&bytes[52..]);
(
B256::from(pool),
Address::from(signer),
Address::from(provider),
)
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredLaneState {
schema_version: u32,
last_amount: [u8; 32],
last_bytes_delivered: [u8; 32],
signature: Vec<u8>,
cap: [u8; 32],
expiry: u64,
registered_until: u64,
chain_root: [u8; 32],
chunk_price: [u8; 32],
verified_index: u8,
tip: [u8; 32],
owner_sig: Vec<u8>,
paid_cumulative: [u8; 32],
}
impl From<&LaneState> for StoredLaneState {
fn from(state: &LaneState) -> Self {
Self {
schema_version: SUPPORTED_SCHEMA_VERSION,
last_amount: state.last_amount().to_be_bytes(),
last_bytes_delivered: state.last_bytes_delivered().to_be_bytes(),
signature: state.last_signature().map_or_else(Vec::new, |s| s.to_vec()),
cap: state.cap.to_be_bytes(),
expiry: state.expiry,
registered_until: state.registered_until,
owner_sig: state.owner_sig.map_or_else(Vec::new, |s| s.to_vec()),
paid_cumulative: state.paid_cumulative.to_be_bytes(),
chain_root: state.chain().chain_root.into(),
chunk_price: state.chain().chunk_price.to_be_bytes(),
verified_index: state.chain().verified_index,
tip: state.chain().tip.into(),
}
}
}
fn decode_signature(
bytes: &[u8],
pool_id: B256,
what: &str,
) -> Result<Option<[u8; 65]>, StoreError> {
if bytes.is_empty() {
return Ok(None);
}
let len = bytes.len();
Ok(Some(<[u8; 65]>::try_from(bytes).map_err(|_| {
StoreError::Corrupt {
pool_id: Some(pool_id),
detail: format!("stored {what} signature is {len} bytes, expected 0 or 65"),
}
})?))
}
impl StoredLaneState {
fn into_state(
self,
pool_id: B256,
signer: Address,
provider: Address,
) -> Result<LaneState, StoreError> {
if self.schema_version > SUPPORTED_SCHEMA_VERSION {
return Err(StoreError::UnsupportedSchema {
found: self.schema_version,
supported: SUPPORTED_SCHEMA_VERSION,
});
}
let last_signature = decode_signature(&self.signature, pool_id, "voucher")?;
let owner_sig = decode_signature(&self.owner_sig, pool_id, "owner capability")?;
let mut state = LaneState::hydrate(
pool_id,
signer,
provider,
U256::from_be_bytes(self.cap),
self.expiry,
U256::from_be_bytes(self.last_amount),
U256::from_be_bytes(self.last_bytes_delivered),
last_signature,
LaneChain {
chain_root: B256::from(self.chain_root),
chunk_price: U256::from_be_bytes(self.chunk_price),
verified_index: self.verified_index,
tip: B256::from(self.tip),
},
);
state.registered_until = self.registered_until;
state.owner_sig = owner_sig;
state.paid_cumulative = U256::from_be_bytes(self.paid_cumulative);
Ok(state)
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum LaneSlot {
Live(LaneState),
Tombstoned,
}
#[derive(Debug)]
pub struct PersistentPoolStateStore {
lanes_db: Database,
settle_db: Database,
checkpoint_db: Database,
buyer_db: Database,
path: PathBuf,
lanes: DashMap<LaneKey, LaneSlot>,
dirty: SegQueue<LaneKey>,
}
impl PersistentPoolStateStore {
pub fn open(data_dir: &Path, deployment: Deployment) -> Result<Self, StoreError> {
Self::open_with(data_dir, deployment, Self::tighten_permissions)
}
pub(crate) fn open_with<F>(
data_dir: &Path,
deployment: Deployment,
chmod_fn: F,
) -> Result<Self, StoreError>
where
F: Fn(&Path) -> Result<(), StoreError>,
{
identity::ensure_data_dir(data_dir).map_err(|err| {
StoreError::Backend(format!(
"data_dir {} failed security check: {err:#}",
data_dir.display()
))
})?;
let path = data_dir.join(LANES_DB_FILE);
let lanes_db = Self::open_hardened_db(&path, &chmod_fn)?;
let settle_db = Self::open_hardened_db(&data_dir.join(SETTLE_DB_FILE), &chmod_fn)?;
let checkpoint_db = Self::open_hardened_db(&data_dir.join(CHECKPOINT_DB_FILE), &chmod_fn)?;
let buyer_db = Self::open_hardened_db(&data_dir.join(BUYER_DB_FILE), &chmod_fn)?;
Self::bind_deployment(&lanes_db, &settle_db, &checkpoint_db, deployment)?;
let lanes = Self::hydrate_lanes(&lanes_db)?;
Ok(Self {
lanes_db,
settle_db,
checkpoint_db,
buyer_db,
path,
lanes,
dirty: SegQueue::new(),
})
}
fn open_hardened_db<F>(path: &Path, chmod_fn: &F) -> Result<Database, StoreError>
where
F: Fn(&Path) -> Result<(), StoreError>,
{
let file_existed_before_open = match std::fs::metadata(path) {
Ok(meta) if meta.len() == 0 => {
return Err(StoreError::Corrupt {
pool_id: None,
detail: format!(
"redb store file at {} is empty (length 0). \
This is either a manual truncation or a filesystem rollback, \
either of which silently discards durable payment state \
(for lanes.redb this re-opens the issue #527 voucher-replay window). \
Restore from backup, or delete the file deliberately to start fresh \
(forfeiting prior history).",
path.display()
),
});
}
Ok(_) => true,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(file) => {
drop(file);
false
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => true,
Err(err) => return Err(StoreError::Io(err)),
}
}
Err(err) => return Err(StoreError::Io(err)),
};
let db = Database::create(path).map_err(|err| {
StoreError::Backend(format!(
"failed to open redb store file at {}: {err}. \
Removing the file forfeits its durability guard \
— restore from backup or investigate the corruption.",
path.display()
))
})?;
if let Err(chmod_err) = chmod_fn(path) {
drop(db);
Self::handle_chmod_failure(path, &chmod_err, file_existed_before_open);
return Err(chmod_err);
}
Ok(db)
}
fn bind_deployment(
lanes_db: &Database,
settle_db: &Database,
checkpoint_db: &Database,
deployment: Deployment,
) -> Result<(), StoreError> {
let read_txn = lanes_db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read (deployment): {err}")))?;
let stamp = match read_txn.open_table(DEPLOYMENT_TABLE) {
Ok(table) => table
.get(DEPLOYMENT_KEY)
.map_err(|err| StoreError::Backend(format!("get (deployment): {err}")))?
.map(|guard| Deployment::from_bytes(guard.value()))
.transpose()?,
Err(redb::TableError::TableDoesNotExist(_)) => None,
Err(err) => {
return Err(StoreError::Backend(format!(
"open_table (deployment): {err}"
)));
}
};
let foreign = match stamp {
Some(stamp) if stamp == deployment => return Ok(()),
None => None,
Some(foreign) => Some(foreign),
};
if let Some(foreign) = foreign {
Self::log_forfeited_lanes(&read_txn, foreign, deployment);
}
drop(read_txn);
if foreign.is_some() {
Self::delete_tables(settle_db, "settle", |txn| {
txn.delete_table(PENDING_SETTLE_TABLE)?;
txn.delete_table(BUYER_PENDING_SETTLE_TABLE)?;
Ok(())
})?;
Self::delete_tables(checkpoint_db, "checkpoint", |txn| {
txn.delete_table(WATCHER_CHECKPOINT_TABLE)?;
Ok(())
})?;
}
let mut txn = lanes_db
.begin_write()
.map_err(|err| StoreError::Backend(format!("begin_write (deployment): {err}")))?;
txn.set_durability(Durability::Immediate)
.map_err(|err| StoreError::Backend(format!("set_durability (deployment): {err}")))?;
if foreign.is_some() {
txn.delete_table(LANE_TABLE)
.map_err(|err| StoreError::Backend(format!("delete_table (lanes): {err}")))?;
}
{
let mut table = txn
.open_table(DEPLOYMENT_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table (deployment): {err}")))?;
table
.insert(DEPLOYMENT_KEY, deployment.to_bytes().as_slice())
.map_err(|err| StoreError::Backend(format!("insert (deployment): {err}")))?;
}
txn.commit()
.map_err(|err| StoreError::Backend(format!("commit (deployment): {err}")))
}
fn log_forfeited_lanes(
read_txn: &redb::ReadTransaction,
foreign: Deployment,
configured: Deployment,
) {
let mut dropped_lanes: u64 = 0;
let mut undecodable_lanes: u64 = 0;
let mut forfeited = U256::ZERO;
let mut audit_err: Option<String> = None;
match read_txn.open_table(LANE_TABLE) {
Ok(table) => match table.iter() {
Ok(iter) => {
for entry in iter {
let (key_guard, value_guard) = match entry {
Ok(pair) => pair,
Err(err) => {
audit_err = Some(format!("lanes iter entry: {err}"));
break;
}
};
let key_bytes: [u8; LANE_KEY_LEN] = *key_guard.value();
dropped_lanes = dropped_lanes.saturating_add(1);
match Self::log_one_forfeited_lane(&key_bytes, value_guard.value(), foreign)
{
Some(unredeemed) => {
forfeited = forfeited.saturating_add(unredeemed);
}
None => {
undecodable_lanes = undecodable_lanes.saturating_add(1);
}
}
}
}
Err(err) => audit_err = Some(format!("lanes iter: {err}")),
},
Err(redb::TableError::TableDoesNotExist(_)) => {}
Err(err) => audit_err = Some(format!("open_table (lanes): {err}")),
}
if let Some(error) = audit_err {
tracing::warn!(
error,
"could not enumerate the seller lanes for the forfeit audit; proceeding \
with the deployment rebind's drop, forfeited value unknown"
);
}
tracing::warn!(
foreign_chain_id = foreign.chain_id,
foreign_payment_pool = %foreign.payment_pool,
configured_chain_id = configured.chain_id,
configured_payment_pool = %configured.payment_pool,
dropped_lanes,
undecodable_lanes,
forfeited_micro_usdc = %forfeited,
"dropping seller lane state, pending settles and watcher checkpoints written \
against another PaymentPool deployment; pool ids repeat across deployments, \
and the dropped lanes' unredeemed vouchers are forfeit"
);
}
fn log_one_forfeited_lane(
key_bytes: &[u8; LANE_KEY_LEN],
value_bytes: &[u8],
foreign: Deployment,
) -> Option<U256> {
match decode_record(key_bytes, value_bytes) {
Ok(state) => {
let unredeemed = forfeited_value(&state);
tracing::warn!(
pool_id = %state.pool_id,
signer = %state.signer,
provider = %state.provider,
owed = %state.owed(),
last_amount = %state.last_amount(),
paid_cumulative = %state.paid_cumulative,
unredeemed_micro_usdc = %unredeemed,
foreign_chain_id = foreign.chain_id,
foreign_payment_pool = %foreign.payment_pool,
"dropping this seller lane with the deployment rebind; \
this line is the last record of its unredeemed claim \
on the old deployment"
);
Some(unredeemed)
}
Err(err) => {
let (pool_id, signer, provider) = lane_key_parts(key_bytes);
tracing::warn!(
%pool_id,
%signer,
%provider,
error = %err,
foreign_chain_id = foreign.chain_id,
foreign_payment_pool = %foreign.payment_pool,
"dropping an undecodable seller lane with the \
deployment rebind; its value cannot be reported"
);
None
}
}
}
fn delete_tables(
db: &Database,
what: &str,
delete: impl FnOnce(&redb::WriteTransaction) -> Result<(), redb::TableError>,
) -> Result<(), StoreError> {
let mut txn = db
.begin_write()
.map_err(|err| StoreError::Backend(format!("begin_write ({what} clear): {err}")))?;
txn.set_durability(Durability::Immediate)
.map_err(|err| StoreError::Backend(format!("set_durability ({what} clear): {err}")))?;
delete(&txn)
.map_err(|err| StoreError::Backend(format!("delete_table ({what} clear): {err}")))?;
txn.commit()
.map_err(|err| StoreError::Backend(format!("commit ({what} clear): {err}")))
}
fn hydrate_lanes(db: &Database) -> Result<DashMap<LaneKey, LaneSlot>, StoreError> {
let read_txn = db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let table = match read_txn.open_table(LANE_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(DashMap::new()),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let out = DashMap::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; LANE_KEY_LEN] = *key_guard.value();
let state = decode_record(&key_bytes, value_guard.value())?;
out.insert(state.key(), LaneSlot::Live(state));
}
Ok(out)
}
fn handle_chmod_failure(path: &Path, chmod_err: &StoreError, file_existed_before_open: bool) {
let observed_mode = Self::observed_mode_string(path);
if file_existed_before_open {
tracing::error!(
path = %path.display(),
observed_mode = %observed_mode,
event = "lane_store_chmod_fail_preserve",
%chmod_err,
"chmod failed on pre-existing lane state store; refusing to delete (would reopen issue #527 replay window). \
Investigate the permission error and chmod the file to 0o600 manually before retry.",
);
return;
}
if let Err(remove_err) = std::fs::remove_file(path) {
tracing::error!(
%remove_err,
path = %path.display(),
observed_mode = %observed_mode,
event = "lane_store_chmod_fail_remove_failed",
%chmod_err,
"chmod failed on freshly-created lane state store, and removal also failed; \
file persists with current mode. chmod 0o600 manually before next start.",
);
}
}
fn observed_mode_string(path: &Path) -> String {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
match std::fs::metadata(path) {
Ok(meta) => format!("{:o}", meta.permissions().mode() & 0o777),
Err(_) => "unknown".into(),
}
}
#[cfg(not(unix))]
{
let _ = path;
"n/a (non-unix)".into()
}
}
#[cfg(unix)]
fn tighten_permissions(path: &Path) -> Result<(), StoreError> {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(path).map_err(|source| StoreError::PermissionTighten {
path: path.to_path_buf(),
source,
})?;
if meta.permissions().mode() & 0o777 == DB_FILE_MODE {
return Ok(());
}
let perms = std::fs::Permissions::from_mode(DB_FILE_MODE);
std::fs::set_permissions(path, perms).map_err(|source| StoreError::PermissionTighten {
path: path.to_path_buf(),
source,
})?;
Ok(())
}
#[cfg(not(unix))]
fn tighten_permissions(_path: &Path) -> Result<(), StoreError> {
tracing::debug!(
"lane state store: file-mode tightening skipped on non-unix; relying on data_dir ACL",
);
Ok(())
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
fn forfeited_value(state: &LaneState) -> U256 {
state.owed().saturating_sub(state.paid_cumulative)
}
fn decode_record(
key_bytes: &[u8; LANE_KEY_LEN],
value_bytes: &[u8],
) -> Result<LaneState, StoreError> {
let (pool_id, signer, provider) = lane_key_parts(key_bytes);
let (stored, remainder): (StoredLaneState, &[u8]) = postcard::take_from_bytes(value_bytes)
.map_err(|err| StoreError::Corrupt {
pool_id: Some(pool_id),
detail: format!("postcard decode failed: {err}"),
})?;
if remainder.len() > SANE_TRAILER_MAX_BYTES {
tracing::warn!(
%pool_id,
remainder = remainder.len(),
limit = SANE_TRAILER_MAX_BYTES,
event = "lane_store_excess_trailer",
"lane state record has unusually large trailing bytes; possible malicious padding or large-additive-field schema skew",
);
}
stored.into_state(pool_id, signer, provider)
}
impl PoolStateStore for PersistentPoolStateStore {
fn load_all(&self) -> Result<Vec<LaneState>, StoreError> {
Ok(self
.lanes
.iter()
.filter_map(|entry| match entry.value() {
LaneSlot::Live(state) => Some(state.clone()),
LaneSlot::Tombstoned => None,
})
.collect())
}
fn get(&self, key: LaneKey) -> Result<Option<LaneState>, StoreError> {
Ok(self.lanes.get(&key).and_then(|entry| match entry.value() {
LaneSlot::Live(state) => Some(state.clone()),
LaneSlot::Tombstoned => None,
}))
}
fn record(&self, state: &LaneState) -> Result<(), StoreError> {
let key = state.key();
let mut next = state.clone();
match self.lanes.entry(key) {
Entry::Occupied(mut occ) => {
if let LaneSlot::Live(existing) = occ.get() {
next.registered_until = next.registered_until.max(existing.registered_until);
if next.owner_sig.is_none() {
next.owner_sig = existing.owner_sig;
}
next.paid_cumulative = next.paid_cumulative.max(existing.paid_cumulative);
}
occ.insert(LaneSlot::Live(next));
}
Entry::Vacant(vac) => {
vac.insert(LaneSlot::Live(next));
}
}
self.dirty.push(key);
Ok(())
}
fn set_registered_until(&self, key: LaneKey, registered_until: u64) -> Result<(), StoreError> {
let Some(mut slot) = self.lanes.get_mut(&key) else {
return Ok(());
};
let LaneSlot::Live(state) = slot.value_mut() else {
return Ok(());
};
if registered_until > state.registered_until {
state.registered_until = registered_until;
self.dirty.push(key);
}
Ok(())
}
fn set_paid_cumulative(&self, key: LaneKey, paid_cumulative: U256) -> Result<(), StoreError> {
let Some(mut slot) = self.lanes.get_mut(&key) else {
return Ok(());
};
let LaneSlot::Live(state) = slot.value_mut() else {
return Ok(());
};
if paid_cumulative > state.paid_cumulative {
state.paid_cumulative = paid_cumulative;
self.dirty.push(key);
}
Ok(())
}
fn forget(&self, key: LaneKey) -> Result<(), StoreError> {
self.lanes.insert(key, LaneSlot::Tombstoned);
self.dirty.push(key);
Ok(())
}
fn flush(&self) -> Result<(), StoreError> {
let mut drained: HashSet<LaneKey> = HashSet::new();
while let Some(key) = self.dirty.pop() {
drained.insert(key);
}
if drained.is_empty() {
return Ok(());
}
let mut writes: Vec<(LaneKey, Vec<u8>)> = Vec::new();
let mut tombstones: Vec<LaneKey> = Vec::new();
for key in &drained {
let slot = self.lanes.get(key).map(|entry| entry.value().clone());
match slot {
Some(LaneSlot::Live(state)) => {
match postcard::to_allocvec(&StoredLaneState::from(&state)) {
Ok(encoded) => writes.push((*key, encoded)),
Err(err) => {
for key in &drained {
self.dirty.push(*key);
}
return Err(StoreError::Codec(format!(
"postcard encode failed: {err}"
)));
}
}
}
Some(LaneSlot::Tombstoned) => tombstones.push(*key),
None => {}
}
}
let mut snapshot = FlushSnapshot { writes, tombstones };
snapshot.sort_by_table_key();
if let Err(err) = self.commit_snapshot(&snapshot) {
for (key, _) in &snapshot.writes {
self.dirty.push(*key);
}
for key in &snapshot.tombstones {
self.dirty.push(*key);
}
tracing::warn!(
dirty_lanes = snapshot.writes.len() + snapshot.tombstones.len(),
error = %err,
"lane store commit failed; every drained key is re-queued for the next flush"
);
return Err(err);
}
for key in &snapshot.tombstones {
self.lanes
.remove_if(key, |_, slot| matches!(slot, LaneSlot::Tombstoned));
}
Ok(())
}
}
struct FlushSnapshot {
writes: Vec<(LaneKey, Vec<u8>)>,
tombstones: Vec<LaneKey>,
}
impl FlushSnapshot {
fn sort_by_table_key(&mut self) {
self.writes
.sort_by_cached_key(|(lane, _)| lane_key_bytes(lane));
self.tombstones.sort_by_cached_key(lane_key_bytes);
}
}
impl PersistentPoolStateStore {
fn commit_snapshot(&self, snapshot: &FlushSnapshot) -> Result<(), StoreError> {
let mut write_txn = self
.lanes_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}")))?;
{
let mut table = write_txn
.open_table(LANE_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
for (key, encoded) in &snapshot.writes {
let key_bytes = lane_key_bytes(key);
table
.insert(&key_bytes, encoded.as_slice())
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
}
for key in &snapshot.tombstones {
let key_bytes = lane_key_bytes(key);
table
.remove(&key_bytes)
.map_err(|err| StoreError::Backend(format!("remove: {err}")))?;
}
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
}
#[cfg(any(test, feature = "anvil-e2e"))]
impl PersistentPoolStateStore {
pub fn insert_raw_buyer_record(&self, pool_id: PoolId, bytes: &[u8]) -> Result<(), StoreError> {
BuyerPoolTable::new(&self.buyer_db).insert_raw(pool_id, bytes)
}
}
#[derive(Debug, Clone)]
pub struct BuyerPoolStoreHandle {
inner: std::sync::Arc<PersistentPoolStateStore>,
}
impl BuyerPoolStoreHandle {
#[must_use]
pub const fn new(inner: std::sync::Arc<PersistentPoolStateStore>) -> Self {
Self { inner }
}
fn table(&self) -> BuyerPoolTable<'_> {
BuyerPoolTable::new(&self.inner.buyer_db)
}
}
impl BuyerPoolStore for BuyerPoolStoreHandle {
fn load_all(&self) -> Result<BuyerLoad, StoreError> {
self.table().load_all()
}
fn record(&self, state: &BuyerPoolState) -> Result<(), StoreError> {
self.table().record(state)
}
fn forget(&self, owner: Address) -> Result<(), StoreError> {
self.table().forget(owner)
}
fn forget_if_pool(&self, owner: Address, pool_id: PoolId) -> Result<bool, StoreError> {
self.table().forget_if_pool(owner, pool_id)
}
fn get_by_pool_id(&self, pool_id: PoolId) -> Result<Option<BuyerPoolState>, StoreError> {
self.table().get_by_pool_id(pool_id)
}
fn get_by_owner(&self, owner: Address) -> Result<Option<BuyerPoolState>, StoreError> {
self.table().get_by_owner(owner)
}
fn advance_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, StoreError> {
self.table()
.advance_progress(owner, pool_id, lane, bytes, amount)
}
fn rebase_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
anchor: BuyerLaneProgress,
totals: BuyerLaneProgress,
) -> Result<AdvanceOutcome, StoreError> {
self.table()
.rebase_progress(owner, pool_id, lane, anchor, totals)
}
fn seed_progress(
&self,
owner: Address,
pool_id: PoolId,
lane: LaneKey,
bytes: U256,
amount: U256,
) -> Result<AdvanceOutcome, StoreError> {
self.table()
.seed_progress(owner, pool_id, lane, bytes, amount)
}
fn add_deposit(
&self,
owner: Address,
pool_id: PoolId,
additional: U256,
) -> Result<DepositOutcome, StoreError> {
self.table().add_deposit(owner, pool_id, additional)
}
fn delegate_lanes(
&self,
deployment: Deployment,
pool_id: PoolId,
signer: Address,
) -> Result<Vec<(Address, BuyerLaneProgress)>, StoreError> {
self.table().delegate_lanes(deployment, pool_id, signer)
}
fn advance_delegate_progress(
&self,
deployment: Deployment,
lane: LaneKey,
totals: BuyerLaneProgress,
) -> Result<AdvanceOutcome, StoreError> {
self.table()
.advance_delegate_progress(deployment, lane, totals)
}
fn rebase_delegate_progress(
&self,
deployment: Deployment,
lane: LaneKey,
anchor: BuyerLaneProgress,
totals: BuyerLaneProgress,
) -> Result<AdvanceOutcome, StoreError> {
self.table()
.rebase_delegate_progress(deployment, lane, anchor, totals)
}
}
impl PersistentPoolStateStore {
fn pending_record_in(
&self,
table_def: TableDefinition<'_, &[u8; 32], u64>,
entry: &PendingSettle,
) -> Result<(), StoreError> {
let key: [u8; 32] = entry.pool_id.into();
let mut write_txn = self
.settle_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}")))?;
{
let mut table = write_txn
.open_table(table_def)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.insert(&key, entry.settle_after)
.map_err(|err| StoreError::Backend(format!("insert: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
fn pending_load_from(
&self,
table_def: TableDefinition<'_, &[u8; 32], u64>,
) -> Result<Vec<PendingSettle>, StoreError> {
let read_txn = self
.settle_db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let table = match read_txn.open_table(table_def) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let mut out = 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}")))?;
out.push(PendingSettle {
pool_id: B256::from(*key_guard.value()),
settle_after: value_guard.value(),
});
}
Ok(out)
}
fn pending_forget_in(
&self,
table_def: TableDefinition<'_, &[u8; 32], u64>,
pool_id: B256,
) -> Result<(), StoreError> {
let key: [u8; 32] = pool_id.into();
{
let read_txn = self
.settle_db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
match read_txn.open_table(table_def) {
Ok(_) => {}
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(()),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
}
}
let mut write_txn = self
.settle_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}")))?;
{
let mut table = write_txn
.open_table(table_def)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.remove(&key)
.map_err(|err| StoreError::Backend(format!("remove: {err}")))?;
}
write_txn
.commit()
.map_err(|err| StoreError::Backend(format!("commit (fsync): {err}")))?;
Ok(())
}
}
impl PendingSettleStore for PersistentPoolStateStore {
fn record_pending(&self, entry: &PendingSettle) -> Result<(), StoreError> {
self.pending_record_in(PENDING_SETTLE_TABLE, entry)
}
fn load_pending(&self) -> Result<Vec<PendingSettle>, StoreError> {
self.pending_load_from(PENDING_SETTLE_TABLE)
}
fn forget_pending(&self, pool_id: B256) -> Result<(), StoreError> {
self.pending_forget_in(PENDING_SETTLE_TABLE, pool_id)
}
}
#[derive(Debug, Clone)]
pub struct BuyerPendingSettleStoreHandle {
inner: std::sync::Arc<PersistentPoolStateStore>,
}
impl BuyerPendingSettleStoreHandle {
#[must_use]
pub const fn new(inner: std::sync::Arc<PersistentPoolStateStore>) -> Self {
Self { inner }
}
}
impl PendingSettleStore for BuyerPendingSettleStoreHandle {
fn record_pending(&self, entry: &PendingSettle) -> Result<(), StoreError> {
self.inner
.pending_record_in(BUYER_PENDING_SETTLE_TABLE, entry)
}
fn load_pending(&self) -> Result<Vec<PendingSettle>, StoreError> {
self.inner.pending_load_from(BUYER_PENDING_SETTLE_TABLE)
}
fn forget_pending(&self, pool_id: B256) -> Result<(), StoreError> {
self.inner
.pending_forget_in(BUYER_PENDING_SETTLE_TABLE, pool_id)
}
}
impl KeyedCheckpointStore for PersistentPoolStateStore {
fn load_checkpoint(&self, key: CheckpointKey) -> Result<Option<u64>, StoreError> {
let read_txn = self
.checkpoint_db
.begin_read()
.map_err(|err| StoreError::Backend(format!("begin_read: {err}")))?;
let table = match read_txn.open_table(WATCHER_CHECKPOINT_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(err) => return Err(StoreError::Backend(format!("open_table: {err}"))),
};
let value = table
.get(key.as_str())
.map_err(|err| StoreError::Backend(format!("get: {err}")))?;
Ok(value.map(|g| g.value()))
}
fn record_checkpoint(&self, key: CheckpointKey, block: u64) -> Result<(), StoreError> {
let mut write_txn = self
.checkpoint_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}")))?;
{
let mut table = write_txn
.open_table(WATCHER_CHECKPOINT_TABLE)
.map_err(|err| StoreError::Backend(format!("open_table: {err}")))?;
table
.insert(key.as_str(), block)
.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;