use nectar_postage::{Batch, BatchId, StampDigest, StampError, StampIndex, calculate_bucket};
use nectar_primitives::SwarmAddress;
pub trait StampIssuer {
fn prepare_stamp(
&mut self,
address: &SwarmAddress,
timestamp: u64,
) -> Result<StampDigest, StampError>;
fn batch_id(&self) -> BatchId;
fn batch_depth(&self) -> u8;
fn bucket_depth(&self) -> u8;
fn max_bucket_utilization(&self) -> u32;
fn bucket_utilization(&self, bucket: u32) -> u32;
fn bucket_has_capacity(&self, bucket: u32) -> bool;
fn stamps_issued(&self) -> u64;
fn total_capacity(&self) -> u64 {
1u64 << self.batch_depth()
}
fn bucket_capacity(&self) -> u32 {
1u32 << (self.batch_depth() - self.bucket_depth())
}
fn bucket_count(&self) -> u32 {
1u32 << self.bucket_depth()
}
fn is_near_capacity(&self, threshold: f64) -> bool {
let max_util = self.max_bucket_utilization() as f64;
let capacity = self.bucket_capacity() as f64;
max_util / capacity >= threshold
}
}
#[derive(Debug, Clone)]
pub struct MemoryIssuer {
batch_id: BatchId,
depth: u8,
bucket_depth: u8,
bucket_indices: alloc::vec::Vec<u32>,
max_utilization: u32,
stamps_issued: u64,
}
extern crate alloc;
impl MemoryIssuer {
pub fn new(batch_id: BatchId, depth: u8, bucket_depth: u8) -> Self {
let bucket_count = 1usize << bucket_depth;
Self {
batch_id,
depth,
bucket_depth,
bucket_indices: alloc::vec![0u32; bucket_count],
max_utilization: 0,
stamps_issued: 0,
}
}
pub fn from_batch(batch: &Batch) -> Self {
Self::new(batch.id(), batch.depth(), batch.bucket_depth())
}
}
impl StampIssuer for MemoryIssuer {
fn prepare_stamp(
&mut self,
address: &SwarmAddress,
timestamp: u64,
) -> Result<StampDigest, StampError> {
let bucket = calculate_bucket(address, self.bucket_depth);
let bucket_idx = bucket as usize;
let current_index = self.bucket_indices[bucket_idx];
let bucket_capacity = 1u32 << (self.depth - self.bucket_depth);
if current_index >= bucket_capacity {
return Err(StampError::BucketFull {
bucket,
capacity: bucket_capacity,
});
}
self.bucket_indices[bucket_idx] = current_index + 1;
self.stamps_issued += 1;
if current_index + 1 > self.max_utilization {
self.max_utilization = current_index + 1;
}
let index = StampIndex::new(bucket, current_index);
Ok(StampDigest::new(*address, self.batch_id, index, timestamp))
}
fn batch_id(&self) -> BatchId {
self.batch_id
}
fn batch_depth(&self) -> u8 {
self.depth
}
fn bucket_depth(&self) -> u8 {
self.bucket_depth
}
fn max_bucket_utilization(&self) -> u32 {
self.max_utilization
}
fn bucket_utilization(&self, bucket: u32) -> u32 {
self.bucket_indices
.get(bucket as usize)
.copied()
.unwrap_or(0)
}
fn bucket_has_capacity(&self, bucket: u32) -> bool {
let bucket_idx = bucket as usize;
if bucket_idx >= self.bucket_indices.len() {
return false;
}
let bucket_capacity = 1u32 << (self.depth - self.bucket_depth);
self.bucket_indices[bucket_idx] < bucket_capacity
}
fn stamps_issued(&self) -> u64 {
self.stamps_issued
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::B256;
fn test_address(leading: u16) -> SwarmAddress {
let mut bytes = [0u8; 32];
bytes[0] = (leading >> 8) as u8;
bytes[1] = leading as u8;
SwarmAddress::new(bytes)
}
#[test]
fn test_memory_issuer_basic() {
let batch_id = B256::ZERO;
let issuer = MemoryIssuer::new(batch_id, 20, 16);
assert_eq!(issuer.batch_id(), batch_id);
assert_eq!(issuer.batch_depth(), 20);
assert_eq!(issuer.bucket_depth(), 16);
assert_eq!(issuer.max_bucket_utilization(), 0);
assert_eq!(issuer.stamps_issued(), 0);
assert_eq!(issuer.bucket_count(), 65536);
assert_eq!(issuer.bucket_capacity(), 16);
}
#[test]
fn test_memory_issuer_prepare_stamp() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 20, 16);
let address = test_address(0xCBE5);
let digest = issuer.prepare_stamp(&address, 12345).unwrap();
assert_eq!(digest.batch_id, B256::ZERO);
assert_eq!(digest.index.bucket(), 0xCBE5);
assert_eq!(digest.index.index(), 0);
assert_eq!(digest.timestamp, 12345);
assert_eq!(issuer.stamps_issued(), 1);
assert_eq!(issuer.max_bucket_utilization(), 1);
}
#[test]
fn test_memory_issuer_increments_index() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 20, 16);
let address = test_address(0xCBE5);
let d1 = issuer.prepare_stamp(&address, 1).unwrap();
let d2 = issuer.prepare_stamp(&address, 2).unwrap();
let d3 = issuer.prepare_stamp(&address, 3).unwrap();
assert_eq!(d1.index.index(), 0);
assert_eq!(d2.index.index(), 1);
assert_eq!(d3.index.index(), 2);
assert_eq!(issuer.stamps_issued(), 3);
}
#[test]
fn test_memory_issuer_bucket_full() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 17, 16);
let address = test_address(0xABCD);
assert!(issuer.prepare_stamp(&address, 1).is_ok());
assert!(issuer.prepare_stamp(&address, 2).is_ok());
let result = issuer.prepare_stamp(&address, 3);
assert!(matches!(
result,
Err(StampError::BucketFull {
bucket: 0xABCD,
capacity: 2
})
));
}
#[test]
fn test_memory_issuer_bucket_utilization() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 20, 16);
let addr1 = test_address(0x1234);
let addr2 = test_address(0x5678);
issuer.prepare_stamp(&addr1, 1).unwrap();
issuer.prepare_stamp(&addr1, 2).unwrap();
issuer.prepare_stamp(&addr2, 3).unwrap();
assert_eq!(issuer.bucket_utilization(0x1234), 2);
assert_eq!(issuer.bucket_utilization(0x5678), 1);
assert_eq!(issuer.bucket_utilization(0x9999), 0);
}
#[test]
fn test_memory_issuer_capacity_check() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 17, 16);
let address = test_address(0x0001);
assert!(issuer.bucket_has_capacity(0x0001));
issuer.prepare_stamp(&address, 1).unwrap();
assert!(issuer.bucket_has_capacity(0x0001));
issuer.prepare_stamp(&address, 2).unwrap();
assert!(!issuer.bucket_has_capacity(0x0001));
}
#[test]
fn test_memory_issuer_near_capacity() {
let mut issuer = MemoryIssuer::new(B256::ZERO, 18, 16);
let address = test_address(0x0001);
assert!(!issuer.is_near_capacity(0.5));
issuer.prepare_stamp(&address, 1).unwrap();
issuer.prepare_stamp(&address, 2).unwrap();
assert!(issuer.is_near_capacity(0.5));
assert!(!issuer.is_near_capacity(0.75));
issuer.prepare_stamp(&address, 3).unwrap();
assert!(issuer.is_near_capacity(0.75));
}
}