Skip to main content

sui_core/accumulators/
unsettled_object_withdrawals.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, BTreeSet},
6    sync::Arc,
7};
8
9use parking_lot::RwLock;
10use sui_protocol_config::assert_reachable_gated;
11use sui_types::{
12    accumulator_root::{AccumulatorObjId, UnsettledObjectFundsRead},
13    base_types::SequenceNumber,
14    digests::ChainIdentifier,
15    effects::{TransactionEffects, TransactionEffectsAPI},
16    transaction::{TransactionData, TransactionDataAPI},
17};
18
19use crate::accumulators::object_funds_checker::metrics::ObjectFundsCheckerMetrics;
20
21pub struct UnsettledObjectWithdrawals {
22    inner: RwLock<Inner>,
23    metrics: Arc<ObjectFundsCheckerMetrics>,
24}
25
26#[derive(Default)]
27struct Inner {
28    /// Tracks the amount of pending unsettled withdraws for each account at each accumulator version.
29    /// When we check object funds sufficiency, we read the balance bounded by the withdraw accumulator version.
30    /// Balance are updated only by settlement transactions, not when we withdraw funds.
31    /// Hence when we are checking object funds, on top of the settled balance, we also need to account for
32    /// the amount of withdraws from the same consensus commit (that all reads from the same accumulator version).
33    /// When `record_net_unsettled_object_withdraws` is enabled, the recorded amounts are the per-account
34    /// net withdraws from effects (what settlement will actually deduct); otherwise they are the
35    /// running max withdraws.
36    unsettled_withdraws: BTreeMap<AccumulatorObjId, BTreeMap<SequenceNumber, u128>>,
37    /// Tracks the accounts that have pending withdraws at each accumulator version.
38    /// This information is not required for functional correctness, but needed to garbage collect
39    /// unused entries in unsettled_withdraws that are now fully committed. Without doing so unsettled_withdraws
40    /// may grow unbounded.
41    unsettled_accounts: BTreeMap<SequenceNumber, BTreeSet<AccumulatorObjId>>,
42}
43
44impl UnsettledObjectFundsRead for UnsettledObjectWithdrawals {
45    fn get_unsettled_object_withdraw(
46        &self,
47        account: &AccumulatorObjId,
48        accumulator_version: SequenceNumber,
49    ) -> u128 {
50        UnsettledObjectWithdrawals::get_unsettled_object_withdraw(
51            self,
52            account,
53            accumulator_version,
54        )
55    }
56}
57
58impl UnsettledObjectWithdrawals {
59    pub fn new(metrics: Arc<ObjectFundsCheckerMetrics>) -> Self {
60        Self {
61            inner: RwLock::new(Inner::default()),
62            metrics,
63        }
64    }
65
66    /// Get the current unsettled withdraw amount for an account at a given accumulator version.
67    pub(crate) fn get_unsettled_object_withdraw(
68        &self,
69        account: &AccumulatorObjId,
70        accumulator_version: SequenceNumber,
71    ) -> u128 {
72        self.inner
73            .read()
74            .unsettled_withdraws
75            .get(account)
76            .and_then(|withdraws| withdraws.get(&accumulator_version))
77            .copied()
78            .unwrap_or_default()
79    }
80
81    /// Record a new unsettled withdraw for an account at a given accumulator version.
82    /// This updates the unsettled withdraws map, and is called after a transaction successfully executed
83    /// that withdraws funds from an object balance account.
84    pub(crate) fn record_unsettled_withdraws(
85        &self,
86        withdraws: impl IntoIterator<Item = (AccumulatorObjId, u128)>,
87        accumulator_version: SequenceNumber,
88    ) {
89        let mut inner = self.inner.write();
90        for (account, amount) in withdraws {
91            let entry = inner
92                .unsettled_withdraws
93                .entry(account)
94                .or_default()
95                .entry(accumulator_version)
96                .or_default();
97            *entry = entry.checked_add(amount).unwrap();
98            inner
99                .unsettled_accounts
100                .entry(accumulator_version)
101                .or_default()
102                .insert(account);
103        }
104        self.update_unsettled_metrics(&inner);
105    }
106
107    pub fn record_object_funds_withdraws(
108        &self,
109        tx_data: &TransactionData,
110        effects: &TransactionEffects,
111        accumulator_running_max_withdraws: &BTreeMap<AccumulatorObjId, u128>,
112        accumulator_version: SequenceNumber,
113        chain_identifier: ChainIdentifier,
114    ) {
115        if accumulator_running_max_withdraws.is_empty() {
116            return;
117        }
118        let address_funds_reservations: BTreeSet<_> = tx_data
119            .process_funds_withdrawals_for_execution(chain_identifier)
120            .into_keys()
121            .collect();
122        let updates: Vec<_> = effects
123            .accumulator_events()
124            .into_iter()
125            .filter(|event| !address_funds_reservations.contains(&event.accumulator_obj))
126            .filter_map(|event| {
127                event
128                    .write
129                    .get_fund_withdraw_amount()
130                    .filter(|amount| *amount > 0)
131                    .map(|amount| (event.accumulator_obj, amount))
132            })
133            .collect();
134        if updates.is_empty() {
135            return;
136        }
137        debug_assert!(
138            updates.iter().all(|(obj_id, net)| {
139                accumulator_running_max_withdraws
140                    .get(obj_id)
141                    .is_some_and(|max| net <= max)
142            }),
143            "net withdraw exceeds running max: tx={:?} updates={:?} running_max={:?}",
144            tx_data.digest(),
145            updates,
146            accumulator_running_max_withdraws,
147        );
148        self.record_unsettled_withdraws(updates, accumulator_version);
149        assert_reachable_gated!(
150            "record unsettled object withdraws from in-execution check",
151            |pc| pc.check_object_funds_withdraw_in_execution()
152        );
153        self.metrics
154            .in_execution_check_result
155            .with_label_values(&["sufficient"])
156            .inc();
157    }
158
159    fn update_unsettled_metrics(&self, inner: &Inner) {
160        self.metrics
161            .unsettled_accounts
162            .set(inner.unsettled_withdraws.len() as i64);
163        self.metrics
164            .unsettled_versions
165            .set(inner.unsettled_accounts.len() as i64);
166    }
167
168    /// Garbage collect tracking of unsettled withdraws for committed accumulator versions.
169    /// This isn't required for functional correctness, but ensures that the tracking data structure doesn't grow unbounded.
170    pub fn commit_accumulator_versions(&self, committed_accumulator_versions: Vec<SequenceNumber>) {
171        let mut inner = self.inner.write();
172        for accumulator_version in committed_accumulator_versions {
173            let accounts = inner
174                .unsettled_accounts
175                .remove(&accumulator_version)
176                .unwrap_or_default();
177            for account in accounts {
178                if let Some(withdraws) = inner.unsettled_withdraws.get_mut(&account) {
179                    withdraws.remove(&accumulator_version);
180                    if withdraws.is_empty() {
181                        inner.unsettled_withdraws.remove(&account);
182                    }
183                }
184            }
185        }
186        self.update_unsettled_metrics(&inner);
187    }
188
189    #[cfg(test)]
190    pub fn is_empty(&self) -> bool {
191        let inner = self.inner.read();
192        inner.unsettled_withdraws.is_empty() && inner.unsettled_accounts.is_empty()
193    }
194}