use alloc::vec::Vec;
use nectar_postage::{Batch, BatchId};
use nectar_postage_issuer::{CounterError, CounterMode, CounterTable};
use crate::{MAX_BUCKET_DEPTH, MAX_COUNTER_BITS, Result, UsageError};
pub(crate) const fn map_counter_error(err: CounterError) -> UsageError {
match err {
CounterError::InvalidBucket { bucket } => UsageError::InvalidBucket { bucket },
CounterError::BucketFull { bucket, capacity } => {
UsageError::BucketFull { bucket, capacity }
}
CounterError::RingExhausted { bucket } => UsageError::RingExhausted { bucket },
CounterError::CounterLength { expected, got } => {
UsageError::CounterLength { expected, got }
}
CounterError::CounterOverflow {
bucket,
count,
capacity,
} => UsageError::CounterOverflow {
bucket,
count,
capacity,
},
_ => UsageError::Malformed("unexpected counter error"),
}
}
pub(crate) const fn validate_geometry(depth: u8, bucket_depth: u8) -> Result<()> {
if bucket_depth > MAX_BUCKET_DEPTH
|| depth < bucket_depth
|| depth - bucket_depth > MAX_COUNTER_BITS
{
return Err(UsageError::InvalidGeometry {
depth,
bucket_depth,
});
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mutability {
#[default]
Immutable,
Mutable,
}
impl Mutability {
pub const fn from_batch(batch: &Batch) -> Self {
if batch.immutable() {
Self::Immutable
} else {
Self::Mutable
}
}
pub const fn is_mutable(self) -> bool {
matches!(self, Self::Mutable)
}
const fn mode(self) -> CounterMode {
match self {
Self::Immutable => CounterMode::Fill,
Self::Mutable => CounterMode::Ring,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageTable {
pub(crate) batch_id: BatchId,
pub(crate) counters: CounterTable,
}
impl UsageTable {
pub fn new(
batch_id: BatchId,
depth: u8,
bucket_depth: u8,
mutability: Mutability,
) -> Result<Self> {
validate_geometry(depth, bucket_depth)?;
Ok(Self {
batch_id,
counters: CounterTable::new(depth, bucket_depth, mutability.mode()),
})
}
pub fn from_batch(batch: &Batch) -> Result<Self> {
Self::new(
batch.id(),
batch.depth(),
batch.bucket_depth(),
Mutability::from_batch(batch),
)
}
pub fn from_counts(
batch_id: BatchId,
depth: u8,
bucket_depth: u8,
counts: Vec<u32>,
mutability: Mutability,
) -> Result<Self> {
validate_geometry(depth, bucket_depth)?;
let counters = CounterTable::from_counts(depth, bucket_depth, mutability.mode(), counts)
.map_err(map_counter_error)?;
Ok(Self { batch_id, counters })
}
pub const fn batch_id(&self) -> BatchId {
self.batch_id
}
pub const fn is_mutable(&self) -> bool {
self.counters.is_ring()
}
pub const fn depth(&self) -> u8 {
self.counters.depth()
}
pub const fn bucket_depth(&self) -> u8 {
self.counters.bucket_depth()
}
pub const fn bucket_count(&self) -> u32 {
self.counters.bucket_count()
}
pub const fn bucket_capacity(&self) -> u32 {
self.counters.bucket_capacity()
}
pub const fn total_capacity(&self) -> u64 {
self.counters.total_capacity()
}
pub const fn total_issued(&self) -> u64 {
self.counters.total_issued()
}
pub fn counts(&self) -> &[u32] {
self.counters.counts()
}
pub fn count(&self, bucket: u32) -> Result<u32> {
self.counters.count(bucket).map_err(map_counter_error)
}
pub fn max_count(&self) -> u32 {
self.counters.max_count()
}
pub fn min_count(&self) -> u32 {
self.counters.min_count()
}
pub fn has_capacity(&self, bucket: u32) -> Result<bool> {
self.counters
.has_capacity(bucket)
.map_err(map_counter_error)
}
pub(crate) const fn counters(&self) -> &CounterTable {
&self.counters
}
pub(crate) const fn counters_mut(&mut self) -> &mut CounterTable {
&mut self.counters
}
pub(crate) fn dilute(&mut self, new_depth: u8) -> Result<()> {
let current = self.counters.depth();
if new_depth < current {
return Err(UsageError::DepthDecrease {
current,
requested: new_depth,
});
}
validate_geometry(new_depth, self.counters.bucket_depth())?;
self.counters.set_depth(new_depth);
Ok(())
}
pub(crate) fn merge_max(&mut self, other: &Self) -> Result<()> {
if self.is_mutable() || other.is_mutable() {
return Err(UsageError::MutableMerge);
}
if self.batch_id != other.batch_id || self.bucket_depth() != other.bucket_depth() {
return Err(UsageError::BatchMismatch);
}
let depth = self.depth().max(other.depth());
validate_geometry(depth, self.bucket_depth())?;
self.counters.merge_counts_max(other.counters(), depth);
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct TableView<'a> {
table: &'a UsageTable,
}
impl<'a> TableView<'a> {
pub(crate) const fn new(table: &'a UsageTable) -> Self {
Self { table }
}
pub const fn batch_id(&self) -> BatchId {
self.table.batch_id
}
pub const fn is_mutable(&self) -> bool {
self.table.is_mutable()
}
pub const fn depth(&self) -> u8 {
self.table.depth()
}
pub const fn bucket_depth(&self) -> u8 {
self.table.bucket_depth()
}
pub const fn bucket_count(&self) -> u32 {
self.table.bucket_count()
}
pub const fn bucket_capacity(&self) -> u32 {
self.table.bucket_capacity()
}
pub const fn total_capacity(&self) -> u64 {
self.table.total_capacity()
}
pub const fn total_issued(&self) -> u64 {
self.table.total_issued()
}
pub fn counts(&self) -> &[u32] {
self.table.counts()
}
pub fn count(&self, bucket: u32) -> Result<u32> {
self.table.count(bucket)
}
pub fn max_count(&self) -> u32 {
self.table.max_count()
}
pub fn min_count(&self) -> u32 {
self.table.min_count()
}
pub fn has_capacity(&self, bucket: u32) -> Result<bool> {
self.table.has_capacity(bucket)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{Address, b256};
use nectar_postage::Batch;
use super::*;
fn batch_id() -> BatchId {
b256!("0x1122334455667788112233445566778811223344556677881122334455667788")
}
fn immutable_counts() -> Vec<u32> {
vec![0u32; 1usize << 16]
}
fn batch_with(depth: u8, bucket_depth: u8, immutable: bool) -> Batch {
Batch::new(
batch_id(),
1_000,
0,
Address::repeat_byte(0x11),
depth,
bucket_depth,
immutable,
)
}
#[test]
fn geometry_bounds() {
let imm = Mutability::Immutable;
assert!(UsageTable::new(batch_id(), 20, 16, imm).is_ok());
assert!(UsageTable::new(batch_id(), 16, 16, imm).is_ok());
assert!(UsageTable::new(batch_id(), 47, 16, imm).is_ok());
assert!(UsageTable::new(batch_id(), 48, 16, imm).is_err());
assert!(UsageTable::new(batch_id(), 15, 16, imm).is_err());
assert!(UsageTable::new(batch_id(), 20, 17, imm).is_err());
}
#[test]
fn mutability_enum_matches_old_constructors() {
let immutable = UsageTable::new(batch_id(), 20, 16, Mutability::Immutable).unwrap();
assert!(!immutable.is_mutable());
let mutable = UsageTable::new(batch_id(), 20, 16, Mutability::Mutable).unwrap();
assert!(mutable.is_mutable());
let from_counts_imm = UsageTable::from_counts(
batch_id(),
17,
16,
immutable_counts(),
Mutability::Immutable,
)
.unwrap();
assert!(!from_counts_imm.is_mutable());
let from_counts_mut =
UsageTable::from_counts(batch_id(), 17, 16, immutable_counts(), Mutability::Mutable)
.unwrap();
assert!(from_counts_mut.is_mutable());
}
#[test]
fn from_batch_picks_table_from_polarity() {
let immutable_batch = batch_with(20, 16, true);
let immutable_table = UsageTable::from_batch(&immutable_batch).unwrap();
assert!(!immutable_table.is_mutable());
assert_eq!(immutable_table.batch_id(), batch_id());
assert_eq!(immutable_table.depth(), 20);
assert_eq!(immutable_table.bucket_depth(), 16);
let mutable_batch = batch_with(20, 16, false);
let mutable_table = UsageTable::from_batch(&mutable_batch).unwrap();
assert!(mutable_table.is_mutable());
assert_eq!(mutable_table.depth(), 20);
}
#[test]
fn from_counts_sums_issued_and_rejects_overflow() {
let mut counts = vec![0u32; 1usize << 16];
counts[7] = 2;
let table =
UsageTable::from_counts(batch_id(), 17, 16, counts, Mutability::Immutable).unwrap();
assert_eq!(table.bucket_capacity(), 2);
assert_eq!(table.total_issued(), 2);
assert_eq!(table.max_count(), 2);
assert_eq!(table.min_count(), 0);
let mut over = vec![0u32; 1usize << 16];
over[7] = 3; assert_eq!(
UsageTable::from_counts(batch_id(), 17, 16, over, Mutability::Immutable),
Err(UsageError::CounterOverflow {
bucket: 7,
count: 3,
capacity: 2
})
);
}
#[test]
fn dilute_grows_capacity_only() {
let mut counts = vec![0u32; 1usize << 16];
counts[0] = 1;
let mut table =
UsageTable::from_counts(batch_id(), 17, 16, counts, Mutability::Immutable).unwrap();
table.dilute(18).unwrap();
assert_eq!(table.bucket_capacity(), 4);
assert_eq!(table.count(0).unwrap(), 1);
assert_eq!(
table.dilute(17),
Err(UsageError::DepthDecrease {
current: 18,
requested: 17
})
);
}
#[test]
fn merge_takes_elementwise_max() {
let mut counts_a = vec![0u32; 1usize << 16];
counts_a[0] = 2;
let mut counts_b = vec![0u32; 1usize << 16];
counts_b[0] = 1;
counts_b[1] = 1;
let mut a =
UsageTable::from_counts(batch_id(), 18, 16, counts_a, Mutability::Immutable).unwrap();
let b =
UsageTable::from_counts(batch_id(), 19, 16, counts_b, Mutability::Immutable).unwrap();
a.merge_max(&b).unwrap();
assert_eq!(a.depth(), 19);
assert_eq!(a.count(0).unwrap(), 2);
assert_eq!(a.count(1).unwrap(), 1);
assert_eq!(a.total_issued(), 3);
}
#[test]
fn merge_max_rejects_mutable() {
let zero = || vec![0u32; 1usize << 16];
let mut a =
UsageTable::from_counts(batch_id(), 18, 16, zero(), Mutability::Mutable).unwrap();
let b = UsageTable::from_counts(batch_id(), 18, 16, zero(), Mutability::Immutable).unwrap();
assert_eq!(a.merge_max(&b), Err(UsageError::MutableMerge));
let mut c =
UsageTable::from_counts(batch_id(), 18, 16, zero(), Mutability::Immutable).unwrap();
let d = UsageTable::from_counts(batch_id(), 18, 16, zero(), Mutability::Mutable).unwrap();
assert_eq!(c.merge_max(&d), Err(UsageError::MutableMerge));
}
}