pub mod states;
use std::marker::PhantomData;
use namada_sdk::parameters;
use namada_sdk::state::{self, WlState};
use crate::shell::block_alloc::states::WithNormalTxs;
#[allow(unused_imports)]
use crate::tendermint_proto::abci::RequestPrepareProposal;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum AllocFailure {
Rejected { bin_resource_left: u64 },
OverflowsBin { bin_resource: u64 },
}
pub struct BlockResources<'tx> {
tx: &'tx [u8],
gas: u64,
}
impl<'tx> BlockResources<'tx> {
pub fn new(tx: &'tx [u8], gas: u64) -> Self {
Self { tx, gas }
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct BlockSpace;
#[derive(Debug, Default, Clone, Copy)]
pub struct BlockGas;
pub trait Resource {
type Input<'r>;
fn usage_of(input: Self::Input<'_>) -> u64;
}
impl Resource for BlockSpace {
type Input<'r> = &'r [u8];
fn usage_of(input: Self::Input<'_>) -> u64 {
input.len() as u64
}
}
impl Resource for BlockGas {
type Input<'r> = u64;
fn usage_of(input: Self::Input<'_>) -> u64 {
input
}
}
#[derive(Debug, Default)]
pub struct BlockAllocator<State> {
_state: PhantomData<*const State>,
block: TxBin<BlockSpace>,
protocol_txs: TxBin<BlockSpace>,
normal_txs: NormalTxsBins,
}
impl<D, H> From<&WlState<D, H>>
for BlockAllocator<states::BuildingProtocolTxBatch<WithNormalTxs>>
where
D: 'static + state::DB + for<'iter> state::DBIter<'iter>,
H: 'static + state::StorageHasher,
{
#[inline]
fn from(storage: &WlState<D, H>) -> Self {
Self::init(
parameters::read_max_proposal_bytes(storage)
.expect("Must be able to read ProposalBytes from storage")
.get(),
parameters::get_max_block_gas(storage).unwrap(),
)
}
}
impl BlockAllocator<states::BuildingProtocolTxBatch<WithNormalTxs>> {
#[inline]
pub fn init(
cometbft_max_block_space_in_bytes: u64,
max_block_gas: u64,
) -> Self {
let max = cometbft_max_block_space_in_bytes;
Self {
_state: PhantomData,
block: TxBin::init(max),
protocol_txs: {
let allotted_space_in_bytes = threshold::ONE_HALF.over(max);
TxBin::init(allotted_space_in_bytes)
},
normal_txs: NormalTxsBins::new(max_block_gas),
}
}
}
impl BlockAllocator<states::BuildingNormalTxBatch> {
#[inline]
pub fn init(
tendermint_max_block_space_in_bytes: u64,
max_block_gas: u64,
) -> Self {
let max = tendermint_max_block_space_in_bytes;
Self {
_state: PhantomData,
block: TxBin::init(max),
protocol_txs: TxBin::default(),
normal_txs: NormalTxsBins {
space: TxBin::init(tendermint_max_block_space_in_bytes),
gas: TxBin::init(max_block_gas),
},
}
}
}
impl<State> BlockAllocator<State> {
#[inline]
fn unoccupied_space_in_bytes(&self) -> u64 {
let total_bin_space = self
.protocol_txs
.occupied
.checked_add(self.normal_txs.space.occupied)
.expect("Shouldn't overflow");
self.block
.allotted
.checked_sub(total_bin_space)
.expect("Shouldn't underflow")
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct TxBin<R: Resource> {
occupied: u64,
allotted: u64,
_resource: PhantomData<R>,
}
impl<R: Resource> TxBin<R> {
#[inline]
pub fn resource_left(&self) -> u64 {
self.allotted
.checked_sub(self.occupied)
.expect("Shouldn't underflow")
}
#[inline]
pub fn init(max_capacity: u64) -> Self {
Self {
allotted: max_capacity,
occupied: 0,
_resource: PhantomData,
}
}
#[inline]
pub fn shrink_to_fit(&mut self) {
self.allotted = self.occupied;
}
pub fn try_dump(
&mut self,
resource: R::Input<'_>,
) -> Result<(), AllocFailure> {
let resource = R::usage_of(resource);
if resource > self.allotted {
let bin_size = self.allotted;
return Err(AllocFailure::OverflowsBin {
bin_resource: bin_size,
});
}
let occupied = self
.occupied
.checked_add(resource)
.expect("Shouldn't overflow");
if occupied <= self.allotted {
self.occupied = occupied;
Ok(())
} else {
let bin_resource_left = self.resource_left();
Err(AllocFailure::Rejected { bin_resource_left })
}
}
}
#[derive(Debug, Default)]
pub struct NormalTxsBins {
space: TxBin<BlockSpace>,
gas: TxBin<BlockGas>,
}
impl NormalTxsBins {
pub fn new(max_gas: u64) -> Self {
Self {
space: TxBin::default(),
gas: TxBin::init(max_gas),
}
}
pub fn try_dump(&mut self, tx: &[u8], gas: u64) -> Result<(), String> {
self.space.try_dump(tx).map_err(|e| match e {
AllocFailure::Rejected { .. } => {
"No more space left in the block for normal txs".to_string()
}
AllocFailure::OverflowsBin { .. } => "The given wrapper tx is \
larger than the remaining \
available block space"
.to_string(),
})?;
self.gas.try_dump(gas).map_err(|e| match e {
AllocFailure::Rejected { .. } => {
"No more gas left in the block for wrapper txs".to_string()
}
AllocFailure::OverflowsBin { .. } => {
"The given wrapper tx requires more gas than available to the \
entire block"
.to_string()
}
})
}
}
pub mod threshold {
use num_rational::Ratio;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Threshold(Ratio<u64>);
impl Threshold {
const fn new(numer: u64, denom: u64) -> Self {
let numer = if numer > denom { denom } else { numer };
Self(Ratio::new_raw(numer, denom))
}
pub fn over(self, free_space_in_bytes: u64) -> u64 {
use num_traits::ops::checked::CheckedMul;
(self
.0
.checked_mul(&free_space_in_bytes.into())
.expect("Must not overflow"))
.to_integer()
}
}
pub const ONE_HALF: Threshold = Threshold::new(1, 2);
}
#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)]
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use proptest::prelude::*;
use super::states::{
BuildingNormalTxBatch, BuildingProtocolTxBatch, NextState, TryAlloc,
};
use super::*;
use crate::shims::abcipp_shim_types::shim::TxBytes;
type BsaInitialProtocolTxs =
BlockAllocator<BuildingProtocolTxBatch<WithNormalTxs>>;
type BsaNormalTxs = BlockAllocator<BuildingNormalTxBatch>;
#[derive(Debug)]
struct PropTx {
tendermint_max_block_space_in_bytes: u64,
max_block_gas: u64,
protocol_txs: Vec<TxBytes>,
normal_txs: Vec<TxBytes>,
}
#[test]
fn test_filling_up_with_protocol() {
const BLOCK_SIZE: u64 = 60;
const BLOCK_GAS: u64 = 1_000;
let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS);
assert!(alloc.try_alloc(&[0; 29]).is_ok());
let mut alloc = alloc.next_state();
assert_eq!(alloc.protocol_txs.allotted, 29);
assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 29);
assert!(alloc.try_alloc(BlockResources::new(&[0; 17], 0)).is_ok());
let mut alloc = alloc.next_state();
assert_eq!(alloc.protocol_txs.allotted, BLOCK_SIZE - (29 + 17));
assert!(alloc.try_alloc(&[0; 14]).is_ok());
assert_matches!(
alloc.try_alloc(&[0; 1]),
Err(AllocFailure::Rejected { .. })
);
}
#[test]
fn test_less_than_half_protocol() {
const BLOCK_SIZE: u64 = 60;
const BLOCK_GAS: u64 = 1_000;
let mut alloc = BsaInitialProtocolTxs::init(BLOCK_SIZE, BLOCK_GAS);
assert!(alloc.try_alloc(&[0; 18]).is_ok());
let mut alloc = alloc.next_state();
assert_eq!(alloc.protocol_txs.allotted, 18);
assert_eq!(alloc.normal_txs.space.allotted, BLOCK_SIZE - 18);
assert!(alloc.try_alloc(BlockResources::new(&[0; 42], 0)).is_ok());
assert_matches!(
alloc.try_alloc(BlockResources::new(&[0; 1], 0)),
Err(AllocFailure::Rejected { .. })
);
let mut alloc = alloc.next_state();
assert_matches!(
alloc.try_alloc(&[0; 1]),
Err(AllocFailure::OverflowsBin { .. })
);
}
proptest! {
#[test]
fn test_reject_tx_on_bin_cap_reached(max in prop::num::u64::ANY) {
proptest_reject_tx_on_bin_cap_reached(max)
}
#[test]
fn test_initial_bin_capacity(max in prop::num::u64::ANY) {
proptest_initial_bin_capacity(max)
}
#[test]
fn test_tx_dump_doesnt_fill_up_bin(args in arb_transactions()) {
proptest_tx_dump_doesnt_fill_up_bin(args)
}
}
fn proptest_reject_tx_on_bin_cap_reached(
tendermint_max_block_space_in_bytes: u64,
) {
let mut bins =
BsaNormalTxs::init(tendermint_max_block_space_in_bytes, 1_000);
bins.normal_txs.space.occupied = bins.normal_txs.space.allotted;
assert_matches!(
bins.try_alloc(BlockResources::new(b"arbitrary tx bytes", 0)),
Err(AllocFailure::Rejected { .. })
);
bins.normal_txs.space.occupied = 0;
bins.normal_txs.gas.occupied = bins.normal_txs.gas.allotted;
assert_matches!(
bins.try_alloc(BlockResources::new(b"arbitrary tx bytes", 1)),
Err(AllocFailure::Rejected { .. })
)
}
fn proptest_initial_bin_capacity(tendermint_max_block_space_in_bytes: u64) {
let bins = BsaInitialProtocolTxs::init(
tendermint_max_block_space_in_bytes,
1_000,
);
let expected = tendermint_max_block_space_in_bytes;
assert_eq!(
bins.protocol_txs.allotted,
threshold::ONE_HALF.over(tendermint_max_block_space_in_bytes)
);
assert_eq!(expected, bins.unoccupied_space_in_bytes());
}
fn proptest_tx_dump_doesnt_fill_up_bin(args: PropTx) {
let PropTx {
tendermint_max_block_space_in_bytes,
max_block_gas,
protocol_txs,
normal_txs,
} = args;
let mut bins = BsaInitialProtocolTxs::init(
tendermint_max_block_space_in_bytes,
max_block_gas,
);
let mut protocol_tx_iter = protocol_txs.iter();
let mut allocated_txs = vec![];
let mut new_size = 0;
for tx in protocol_tx_iter.by_ref() {
let bin = bins.protocol_txs;
if new_size + tx.len() as u64 >= bin.allotted {
break;
} else {
new_size += tx.len() as u64;
allocated_txs.push(tx);
}
}
for tx in allocated_txs {
assert!(bins.try_alloc(tx).is_ok());
}
let mut bins = bins.next_state();
let mut new_size = bins.normal_txs.space.allotted;
let mut decrypted_txs = vec![];
for tx in normal_txs {
let bin = bins.normal_txs.space;
if (new_size + tx.len() as u64) < bin.allotted {
new_size += tx.len() as u64;
decrypted_txs.push(tx);
} else {
break;
}
}
for tx in decrypted_txs {
assert!(bins.try_alloc(BlockResources::new(&tx, 0)).is_ok());
}
let mut bins = bins.next_state();
let mut allocated_txs = vec![];
let mut new_size = bins.protocol_txs.allotted;
for tx in protocol_tx_iter.by_ref() {
let bin = bins.protocol_txs;
if new_size + tx.len() as u64 >= bin.allotted {
break;
} else {
new_size += tx.len() as u64;
allocated_txs.push(tx);
}
}
for tx in allocated_txs {
assert!(bins.try_alloc(tx).is_ok());
}
}
prop_compose! {
fn arb_transactions()
(
(tendermint_max_block_space_in_bytes, max_block_gas, protocol_tx_max_bin_size,
decrypted_tx_max_bin_size) in arb_max_bin_sizes(),
)
(
tendermint_max_block_space_in_bytes in Just(tendermint_max_block_space_in_bytes),
max_block_gas in Just(max_block_gas),
protocol_txs in arb_tx_list(protocol_tx_max_bin_size),
normal_txs in arb_tx_list(decrypted_tx_max_bin_size),
)
-> PropTx {
PropTx {
tendermint_max_block_space_in_bytes,
max_block_gas,
protocol_txs: protocol_txs.into_iter().map(prost::bytes::Bytes::from).collect(),
normal_txs: normal_txs.into_iter().map(prost::bytes::Bytes::from).collect(),
}
}
}
fn arb_max_bin_sizes() -> impl Strategy<Value = (u64, u64, usize, usize)> {
const MAX_BLOCK_SIZE_BYTES: u64 = 1000;
(1..=MAX_BLOCK_SIZE_BYTES).prop_map(
|tendermint_max_block_space_in_bytes| {
(
tendermint_max_block_space_in_bytes,
tendermint_max_block_space_in_bytes,
threshold::ONE_HALF
.over(tendermint_max_block_space_in_bytes)
as usize,
threshold::ONE_HALF
.over(tendermint_max_block_space_in_bytes)
as usize,
)
},
)
}
fn arb_tx_list(max_bin_size: usize) -> impl Strategy<Value = Vec<Vec<u8>>> {
const MAX_TX_NUM: usize = 64;
let tx = prop::collection::vec(prop::num::u8::ANY, 0..=max_bin_size);
prop::collection::vec(tx, 0..=MAX_TX_NUM)
}
}