use crate::consts::*;
use crate::interpreter::{ExecutableTransaction, InitialBalances, Interpreter};
use fuel_asm::{RegId, Word};
use fuel_tx::CheckError;
use fuel_types::AssetId;
use itertools::Itertools;
use std::collections::HashMap;
use std::ops::Index;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct Balance {
value: Word,
offset: usize,
}
impl Balance {
pub const fn new(value: Word, offset: usize) -> Self {
Self { value, offset }
}
pub const fn offset(&self) -> usize {
self.offset
}
pub const fn value(&self) -> Word {
self.value
}
pub fn checked_add(&mut self, value: Word) -> Option<&mut Self> {
self.value.checked_add(value).map(|v| self.value = v).map(|_| self)
}
pub fn checked_sub(&mut self, value: Word) -> Option<&mut Self> {
self.value.checked_sub(value).map(|v| self.value = v).map(|_| self)
}
}
#[derive(Debug, Default, Clone)]
pub struct RuntimeBalances {
state: HashMap<AssetId, Balance>,
}
impl From<InitialBalances> for RuntimeBalances {
fn from(balances: InitialBalances) -> Self {
Self::try_from_iter(balances.into_iter()).expect(
r#"This is a bug!
A checked transaction shouldn't produce a malformed initial free balances set.
Please, file a report mentioning an incorrect transaction validation implementation that allowed a type-safe checked transaction to be created from a malformed inputs set."#)
}
}
impl RuntimeBalances {
pub fn try_from_iter<T>(iter: T) -> Result<Self, CheckError>
where
T: IntoIterator<Item = (AssetId, Word)>,
{
iter.into_iter()
.sorted_by_key(|k| k.0)
.enumerate()
.try_fold(HashMap::new(), |mut state, (i, (asset, balance))| {
let offset = VM_MEMORY_BALANCES_OFFSET + i * (AssetId::LEN + WORD_SIZE);
state
.entry(asset)
.or_insert_with(|| Balance::new(0, offset))
.checked_add(balance)
.ok_or(CheckError::ArithmeticOverflow)?;
Ok(state)
})
.map(|state| Self { state })
}
pub fn balance(&self, asset: &AssetId) -> Option<Word> {
self.state.get(asset).map(Balance::value)
}
fn _set_memory_balance(balance: &Balance, memory: &mut [u8]) -> Word {
let value = balance.value();
let offset = balance.offset();
let offset = offset + AssetId::LEN;
let memory = &mut memory[offset..offset + WORD_SIZE];
memory.copy_from_slice(&value.to_be_bytes());
value
}
pub fn checked_balance_add(&mut self, memory: &mut [u8], asset: &AssetId, value: Word) -> Option<Word> {
self.state
.get_mut(asset)
.and_then(|b| b.checked_add(value))
.map(|balance| Self::_set_memory_balance(balance, memory))
.or_else(|| (value == 0).then_some(0))
}
pub fn checked_balance_sub(&mut self, memory: &mut [u8], asset: &AssetId, value: Word) -> Option<Word> {
self.state
.get_mut(asset)
.and_then(|b| b.checked_sub(value))
.map(|balance| Self::_set_memory_balance(balance, memory))
.or_else(|| (value == 0).then_some(0))
}
pub fn to_vm<S, Tx>(self, vm: &mut Interpreter<S, Tx>)
where
Tx: ExecutableTransaction,
{
let len = vm.params().max_inputs * (AssetId::LEN + WORD_SIZE) as Word;
vm.registers[RegId::SP] += len;
vm.reserve_stack(len)
.expect("consensus parameters won't allow stack overflow for VM initialization");
self.state.iter().for_each(|(asset, balance)| {
let value = balance.value();
let ofs = balance.offset();
vm.memory[ofs..ofs + AssetId::LEN].copy_from_slice(asset.as_ref());
vm.memory[ofs + AssetId::LEN..ofs + AssetId::LEN + WORD_SIZE].copy_from_slice(&value.to_be_bytes());
});
vm.balances = self;
}
}
impl Index<&AssetId> for RuntimeBalances {
type Output = Word;
fn index(&self, index: &AssetId) -> &Self::Output {
&self.state[index].value
}
}
impl AsMut<HashMap<AssetId, Balance>> for RuntimeBalances {
fn as_mut(&mut self) -> &mut HashMap<AssetId, Balance> {
&mut self.state
}
}
impl AsRef<HashMap<AssetId, Balance>> for RuntimeBalances {
fn as_ref(&self) -> &HashMap<AssetId, Balance> {
&self.state
}
}
impl PartialEq for RuntimeBalances {
fn eq(&self, other: &Self) -> bool {
self.state == other.state
}
}
#[test]
fn writes_to_memory_correctly() {
use crate::prelude::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let rng = &mut StdRng::seed_from_u64(2322u64);
let mut interpreter = Interpreter::<_, Script>::without_storage();
let base = AssetId::zeroed();
let base_balance = 950;
let assets = vec![
(rng.gen(), 10),
(rng.gen(), 25),
(rng.gen(), 50),
(base, base_balance),
(rng.gen(), 100),
];
let mut assets_sorted = assets.clone();
assets_sorted.as_mut_slice().sort_by(|a, b| a.0.cmp(&b.0));
assert_ne!(assets_sorted, assets);
let balances = assets.into_iter();
RuntimeBalances::try_from_iter(balances)
.expect("failed to generate balances")
.to_vm(&mut interpreter);
let memory = interpreter.memory();
assets_sorted
.iter()
.fold(VM_MEMORY_BALANCES_OFFSET, |ofs, (asset, value)| {
assert_eq!(asset.as_ref(), &memory[ofs..ofs + AssetId::LEN]);
assert_eq!(
&value.to_be_bytes(),
&memory[ofs + AssetId::LEN..ofs + AssetId::LEN + WORD_SIZE]
);
ofs + AssetId::LEN + WORD_SIZE
});
}
#[test]
fn try_from_iter_wont_overflow() {
use crate::prelude::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let rng = &mut StdRng::seed_from_u64(2322u64);
let a: AssetId = rng.gen();
let b: AssetId = rng.gen();
let c: AssetId = rng.gen();
let balances = vec![(a, u64::MAX), (b, 15), (c, 0)];
let runtime_balances = RuntimeBalances::try_from_iter(balances.clone()).expect("failed to create balance set");
balances.iter().for_each(|(asset, val)| {
let bal = runtime_balances.balance(asset).expect("failed to fetch balance");
assert_eq!(val, &bal);
});
let balances = vec![(a, u64::MAX), (b, 15), (c, 0), (b, 1)];
let balances_aggregated = vec![(a, u64::MAX), (b, 16), (c, 0)];
let runtime_balances = RuntimeBalances::try_from_iter(balances).expect("failed to create balance set");
balances_aggregated.iter().for_each(|(asset, val)| {
let bal = runtime_balances.balance(asset).expect("failed to fetch balance");
assert_eq!(val, &bal);
});
let balances = vec![(a, u64::MAX), (b, 15), (c, 0), (a, 1)];
let err = RuntimeBalances::try_from_iter(balances).expect_err("overflow set should fail");
assert_eq!(CheckError::ArithmeticOverflow, err);
}
#[test]
fn checked_add_and_sub_works() {
use crate::prelude::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
let rng = &mut StdRng::seed_from_u64(2322u64);
let mut memory = Interpreter::<_, Script>::without_storage().memory;
let asset: AssetId = rng.gen();
let balances = vec![(asset, 0)];
let mut balances = RuntimeBalances::try_from_iter(balances).expect("failed to create set");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(bal, 0);
let asset_b: AssetId = rng.gen();
assert_ne!(asset, asset_b);
let val = balances
.checked_balance_add(&mut memory, &asset_b, 0)
.expect("failed to add balance");
assert_eq!(val, 0);
assert!(balances.balance(&asset_b).is_none());
let val = balances
.checked_balance_add(&mut memory, &asset, 150)
.expect("failed to add balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, 150);
assert_eq!(bal, 150);
let val = balances
.checked_balance_add(&mut memory, &asset, 75)
.expect("failed to add balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, 225);
assert_eq!(bal, 225);
let val = balances
.checked_balance_sub(&mut memory, &asset, 30)
.expect("failed to sub balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, 195);
assert_eq!(bal, 195);
let val = balances
.checked_balance_sub(&mut memory, &asset, 120)
.expect("failed to sub balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, 75);
assert_eq!(bal, 75);
let val = balances
.checked_balance_sub(&mut memory, &asset, 70)
.expect("failed to sub balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, 5);
assert_eq!(bal, 5);
assert!(balances.checked_balance_sub(&mut memory, &asset, 10).is_none());
let val = balances
.checked_balance_add(&mut memory, &asset, u64::MAX - 5)
.expect("failed to add balance");
let bal = balances.balance(&asset).expect("failed to fetch balance");
assert_eq!(val, u64::MAX);
assert_eq!(bal, u64::MAX);
assert!(balances.checked_balance_add(&mut memory, &asset, 1).is_none());
}