Skip to content

Commit

Permalink
invalidate on failed tx
Browse files Browse the repository at this point in the history
  • Loading branch information
MujkicA committed Nov 2, 2023
1 parent ba43b00 commit 7f0cb56
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 21 deletions.
5 changes: 3 additions & 2 deletions packages/fuels-accounts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ rand = { workspace = true, default-features = false }
semver = { workspace = true }
tai64 = { workspace = true, features = ["serde"] }
thiserror = { workspace = true, default-features = false }
tokio = { workspace = true, features = ["full", "time", "test-util"], optional = true}
tokio = { workspace = true, features = ["full"], optional = true}
tracing = { workspace = true }
zeroize = { workspace = true, features = ["derive"] }

[dev-dependencies]
tempfile = { workspace = true }
tokio = { workspace = true, features = ["test-util"]}

[features]
default = ["std"]
coin-cache = ["tokio?/time", "tokio?/test-util"]
coin-cache = ["tokio?/time"]
std = ["fuels-core/std", "dep:tokio", "fuel-core-client/default", "dep:eth-keystore"]
40 changes: 39 additions & 1 deletion packages/fuels-accounts/src/coin_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fuels_core::types::coin_type_id::CoinTypeId;
type CoinCacheKey = (Bech32Address, AssetId);

#[derive(Debug)]
struct CoinsCache {
pub(crate) struct CoinsCache {
ttl: Duration,
items: HashMap<CoinCacheKey, HashSet<CoinCacheItem>>,
}
Expand Down Expand Up @@ -52,6 +52,24 @@ impl CoinsCache {
.collect()
}

pub fn remove_items(
&mut self,
inputs: impl IntoIterator<Item = ((Bech32Address, AssetId), Vec<CoinTypeId>)>,
) {
inputs.into_iter().for_each(|(key, ids)| {
ids.into_iter().for_each(|id| {
self.remove(&key, id);
})
})
}

fn remove(&mut self, key: &CoinCacheKey, id: CoinTypeId) {
if let Some(ids) = self.items.get_mut(key) {
let item = CoinCacheItem::new(id);
ids.remove(&item);
}
}

fn remove_expired_entries(&mut self, key: &CoinCacheKey) {
if let Some(entry) = self.items.get_mut(key) {
entry.retain(|item| item.is_valid(self.ttl));
Expand Down Expand Up @@ -155,4 +173,24 @@ mod tests {

assert!(active_coins.is_empty());
}

#[test]
fn test_remove_items() {
let mut cache = CoinsCache::new(Duration::from_secs(60));

let key: CoinCacheKey = Default::default();
let (item1, item2) = get_items();

let items_to_insert = [(key.clone(), vec![item1.clone(), item2.clone()])];
cache.insert_multiple(items_to_insert.iter().cloned());

let items_to_remove = [(key.clone(), vec![item1.clone()])];
cache.remove_items(items_to_remove.iter().cloned());

let active_coins = cache.get_active(&key);

assert_eq!(active_coins.len(), 1);
assert!(!active_coins.contains(&item1));
assert!(active_coins.contains(&item2));
}
}
12 changes: 11 additions & 1 deletion packages/fuels-accounts/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,17 @@ impl Provider {
/// Sends a transaction to the underlying Provider's client.
pub async fn send_transaction_and_await_commit<T: Transaction>(&self, tx: T) -> Result<TxId> {
let tx_id = self.send_transaction(tx.clone()).await?;
self.client.await_transaction_commit(&tx_id).await?;
let _status = self.client.await_transaction_commit(&tx_id).await?;

#[cfg(feature = "coin-cache")]
{
if matches!(
_status,
TransactionStatus::SqueezedOut { .. } | TransactionStatus::Failure { .. }
) {
self.cache.lock().await.remove_items(tx.used_coins())
}
}

Ok(tx_id)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/fuels-core/src/types/transaction_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ macro_rules! impl_tx_trait {
}

Ok(TransactionFee::checked_from_tx(
&self.network_info.consensus_parameters,
&self.consensus_parameters(),
&tx.tx,
))
}
Expand Down Expand Up @@ -293,7 +293,7 @@ impl ScriptTransactionBuilder {
}

fn base_offset(&self) -> usize {
offsets::base_offset_script(&self.network_info.consensus_parameters)
offsets::base_offset_script(&self.consensus_parameters())
+ padded_len_usize(self.script_data.len())
+ padded_len_usize(self.script.len())
}
Expand Down Expand Up @@ -455,7 +455,7 @@ impl CreateTransactionBuilder {
}

fn base_offset(&self) -> usize {
offsets::base_offset_create(&self.network_info.consensus_parameters)
offsets::base_offset_create(&self.consensus_parameters())
}

pub fn with_bytecode_length(mut self, bytecode_length: u64) -> Self {
Expand Down
101 changes: 87 additions & 14 deletions packages/fuels/tests/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,35 +877,108 @@ async fn create_transfer(
#[cfg(feature = "coin-cache")]
#[tokio::test]
async fn test_caching() -> Result<()> {
use fuels_core::types::tx_status::TxStatus;

let provider_config = Config {
block_production: Trigger::Interval {
block_time: std::time::Duration::from_secs(1),
},
manual_blocks_enabled: true,
..Config::local_node()
};
let amount = 1000;
let num_coins = 3;
let num_coins = 10;
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(Some(2), Some(num_coins), Some(amount)),
WalletsConfig::new(Some(1), Some(num_coins), Some(amount)),
Some(provider_config),
None,
)
.await?;
let wallet_1 = wallets.pop().unwrap();
let wallet_2 = wallets.pop().unwrap();
let provider = wallet_1.provider().unwrap();
let wallet_2 = WalletUnlocked::new_random(Some(provider.clone()));

// Consecutively send transfer txs. Without caching, the txs will
// end up trying to use the same input coins because 'get_spendable_coins()'
// won't filter out recently used coins.
let mut tx_ids = vec![];
for _ in 0..10 {
let tx = create_transfer(&wallet_1, 100, wallet_2.address()).await?;
let tx_id = provider.send_transaction(tx).await?;
tx_ids.push(tx_id);
}

provider.produce_blocks(10, None).await?;

for _ in 0..3 {
create_transfer(&wallet_1, 100, wallet_2.address()).await?;
// Confirm all txs are settled
for tx_id in tx_ids {
let status = provider.tx_status(&tx_id).await?;
assert!(matches!(status, TxStatus::Success { .. }));
}

// Advnace time until txs are executed
// Verify the transfers were succesful
assert_eq!(wallet_2.get_asset_balance(&BASE_ASSET_ID).await?, 1000);

Ok(())
}

#[cfg(feature = "coin-cache")]
async fn create_revert_tx(wallet: &WalletUnlocked) -> Result<ScriptTransaction> {
use fuel_core_types::fuel_asm::Opcode;

let amount = 1;
let inputs = wallet
.get_asset_inputs_for_amount(BASE_ASSET_ID, amount)
.await?;
let outputs =
wallet.get_asset_outputs_for_amount(&Bech32Address::default(), BASE_ASSET_ID, amount);
let network_info = wallet.try_provider()?.network_info().await?;

let mut tb = ScriptTransactionBuilder::prepare_transfer(
inputs,
outputs,
TxParameters::default(),
network_info,
)
.with_script(vec![Opcode::RVRT.into()]);

wallet.sign_transaction(&mut tb);
wallet.adjust_for_fee(&mut tb, amount).await?;
tb.build()
}

#[cfg(feature = "coin-cache")]
#[tokio::test]
async fn test_cache_invalidation_on_await() -> Result<()> {
use fuels_core::types::tx_status::TxStatus;

let provider_config = Config {
manual_blocks_enabled: true,
..Config::local_node()
};

// create wallet with 1 coin so that the cache prevents further
// spending unless the coin is invalidated from the cache
let mut wallets = launch_custom_provider_and_get_wallets(
WalletsConfig::new(Some(1), Some(1), Some(100)),
Some(provider_config),
None,
)
.await?;
let wallet = wallets.pop().unwrap();

let provider = wallet.provider().unwrap();
let tx = create_revert_tx(&wallet).await?;

// Pause time so that cache doesn't invalidate items base on TTL
tokio::time::pause();
tokio::time::advance(tokio::time::Duration::from_secs(3)).await;

assert_eq!(
wallet_2.get_asset_balance(&BASE_ASSET_ID).await?,
amount * num_coins
);
provider.produce_blocks(1, None).await?;

// tx inputs should be cached and invalidated due to the tx failing
let tx_id = provider.send_transaction_and_await_commit(tx).await?;
let status = provider.tx_status(&tx_id).await?;
assert!(matches!(status, TxStatus::Revert { .. }));

let coins = wallet.get_spendable_resources(BASE_ASSET_ID, 1).await?;
assert_eq!(coins.len(), 1);

Ok(())
}

0 comments on commit 7f0cb56

Please sign in to comment.