Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: caching of recently used coins #1105

Merged
merged 55 commits into from
Nov 6, 2023
Merged

feat: caching of recently used coins #1105

merged 55 commits into from
Nov 6, 2023

Conversation

MujkicA
Copy link
Contributor

@MujkicA MujkicA commented Aug 28, 2023

Checklist

  • I have linked to any relevant issues.
  • I have updated the documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added necessary labels.
  • I have done my best to ensure that my PR adheres to the Fuel Labs Code Review Standards.
  • I have requested a review from the relevant team or maintainers.

Adds a cache to the Provider for recently used coins. The cache is behind an optional feature flag coin-cache.

The cache holds a map with entries of (Bech32Address, AssetId) -> HashSet<CoinTypeId> where CoinTypeId is either a utxo or a nonce and Bech32Address is the address of the account that owns the coins.

Whenever a tx is submitted, every CoinTypeId is added to the cache entry corresponding to it's owner and asset.
When an account requests spendable_coins of a specific asset, the node is queried with recently used coins from the cache as a filtering condition.
The cache specifies a TTL for the items, once it expires the corresponding entries will be ignored and evicted the next time active entries are retrieved.

CoinCacheItem is a wrapper for a CoinTypeId that is stored in the cache. It includes the creation time so it can be verified against the TTL. It implements the Hash and PartialEq traits so that inserting them into the Set only compares against the id and not the creation time. This means that if a coin somehow is used again before it was evicted, the corresponding entry will just be updated with a new timestamp.
Tests make use of tokio::time to advance time manually instead of using sleep().

Other changes:
The strategy for adding fee coins is changed from completly recomputing the
base asset input set to just adding an amount of base asset that will cover the fee.
While this might lead to a bit more dust it gives us more flexibility and we can allow the user to specify base asset inputs at will.

@MujkicA MujkicA self-assigned this Aug 28, 2023
@MujkicA MujkicA mentioned this pull request Aug 28, 2023
5 tasks
@MujkicA MujkicA added the enhancement New feature or request label Oct 21, 2023
@MujkicA MujkicA marked this pull request as ready for review October 21, 2023 14:50
iqdecay
iqdecay previously approved these changes Nov 1, 2023
Copy link
Contributor

@iqdecay iqdecay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only have some minor remarks

packages/fuels-core/src/types/transaction_builders.rs Outdated Show resolved Hide resolved
packages/fuels/tests/providers.rs Outdated Show resolved Hide resolved
packages/fuels/tests/providers.rs Show resolved Hide resolved
@MujkicA
Copy link
Contributor Author

MujkicA commented Nov 2, 2023

I didn't review the full PR, but from a quick read of the PR description it seems like we should maybe try to invalidate cached UTXOs in the event a transaction is unsuccessful. Otherwise users may need to wait for the TTL before being able to try again. This would only work if the user submits_and_awaits so that the sdk can check the response.

Alternatively, a short TTL such as 5 * block_time_ms might be good enough in most cases (since we currently use 1s blocks).

I thought about it but decided against it because it works only partially, i.e. when they use send_and_await. I imagine using send_and_await is more likely in local tests then out in the wild or if it is used on a live network, I think it's unlikely they will encounter the problem of using the same inputs since they wait for completion anyways.
It doesn't seem like a big win, but it would add a bit more complexity. Let me know if my assumptions are wrong @Voxelot.

On the other hand, we could probably benefit from invalidating at places where we call submit_and_await internally like contract calls. So maybe it's worth it.

@MujkicA
Copy link
Contributor Author

MujkicA commented Nov 2, 2023

Update

I've adjusted it to invalidate on failed txs during submit_and_await and included a test. I've also fixed the general test for caching.

Do we still want to shorten the TTL and is there a way to retrieve the block-time from the network? @Voxelot

@MujkicA
Copy link
Contributor Author

MujkicA commented Nov 2, 2023

Great work. Clean overall. Two notes: One is regarding the CI. It might be that the feature isn't being tested by the CI due to us choosing features manually to avoid the rocksdb feature:

          - cargo_command: nextest
            args: run --all-targets --features "default fuel-core-lib test-type-paths" --workspace
            download_sway_artifacts: sway-examples-w-type-paths

And another note: I have a suggestion for the Provider.

Rather than riddle it with cfg compile-time conditional code why not separate the logic into a strategy? It would concern itself with allowing or disallowing resources. Something like this:

#[async_trait::async_trait]
pub trait ResourcePicker {
    async fn register_used_resources(
        &self,
        coin_ids: impl IntoIterator<Item = (CoinCacheKey, Vec<CoinTypeId>)> + Send + Sync,
    );

    async fn modify_filter(&self, filter: &mut ResourceFilter);
}

And have two implementations, choose the correct one over in the provider:

    #[cfg(not(feature = "coin-cache"))]
    resource_picker: NoRestrictions,
    #[cfg(feature = "coin-cache")]
    resource_picker: ReusageAvoider,

NoRestrictions would just be a No-op impl of the trait. ReusageAvoider can house the logic now present in the provider:

#[derive(Default, Clone, Debug)]
pub struct ReusageAvoider {
    cache: Arc<Mutex<CoinsCache>>,
}

#[async_trait::async_trait]
impl ResourcePicker for ReusageAvoider {
    async fn register_used_resources(
        &self,
        coin_ids: impl IntoIterator<Item = (CoinCacheKey, Vec<CoinTypeId>)> + Send + Sync,
    ) {
        self.cache.lock().await.insert_multiple(coin_ids);
    }

    async fn modify_filter(&self, filter: &mut ResourceFilter) {
        let mut cache = self.cache.lock().await;
        let used_coins = cache.get_active(&(filter.from.clone(), filter.asset_id));

        let excluded_utxos = used_coins
            .iter()
            .filter_map(|coin_id| match coin_id {
                CoinTypeId::UtxoId(utxo_id) => Some(utxo_id),
                _ => None,
            })
            .cloned()
            .collect::<Vec<_>>();

        let excluded_message_nonces = used_coins
            .iter()
            .filter_map(|coin_id| match coin_id {
                CoinTypeId::Nonce(nonce) => Some(nonce),
                _ => None,
            })
            .cloned()
            .collect::<Vec<_>>();

        filter.excluded_utxos.extend(excluded_utxos);
        filter
            .excluded_message_nonces
            .extend(excluded_message_nonces);
    }
}

Since the choice is a compile-time one, we should feel very little if any impact when using the noop implementation (after optimizations).

I agree it would make it cleaner, but I'd rather we do that in a separate PR since updating this with the master branch is getting cumbersome.

Copy link
Member

@digorithm digorithm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a first pass, and it's looking solid. I'll do another pass once the CI is green (I believe this is test-related so it shouldn't change much).

packages/fuels/Cargo.toml Show resolved Hide resolved
iqdecay
iqdecay previously approved these changes Nov 3, 2023
Copy link
Contributor

@iqdecay iqdecay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good, a non-blocking question on naming.

packages/fuels-accounts/src/coin_cache.rs Outdated Show resolved Hide resolved
segfault-magnet
segfault-magnet previously approved these changes Nov 6, 2023
Copy link
Contributor

@hal3e hal3e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

@MujkicA MujkicA merged commit 24e79f3 into master Nov 6, 2023
39 checks passed
@MujkicA MujkicA deleted the feature/utxo_caching branch November 6, 2023 14:23
@MujkicA MujkicA mentioned this pull request Nov 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants