Skip to main content

sui_core/accumulators/object_funds_checker/
mod.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 mysten_common::{assert_reachable, debug_fatal};
10use sui_types::{
11    accumulator_root::AccumulatorObjId,
12    base_types::SequenceNumber,
13    effects::{TransactionEffects, TransactionEffectsAPI},
14    executable_transaction::VerifiedExecutableTransaction,
15    execution_params::FundsWithdrawStatus,
16    transaction::TransactionDataAPI,
17};
18use tokio::{
19    sync::{oneshot, watch},
20    time::Instant,
21};
22use tracing::{debug, instrument};
23
24use crate::{
25    accumulators::{
26        funds_read::AccountFundsRead, unsettled_object_withdrawals::UnsettledObjectWithdrawals,
27    },
28    authority::{ExecutionEnv, authority_per_epoch_store::AuthorityPerEpochStore},
29    execution_scheduler::ExecutionScheduler,
30};
31
32#[cfg(test)]
33mod integration_tests;
34pub mod metrics;
35#[cfg(test)]
36mod unit_tests;
37
38/// Note that there is no need to have a separate InsufficientFunds variant.
39/// If the funds are insufficient, the execution would still have to abort and rely on
40/// a rescheduling to be able to execute again.
41pub enum ObjectFundsWithdrawStatus {
42    SufficientFunds,
43    // The receiver will be notified when the funds are determined to be sufficient or insufficient.
44    // The bool is true if the funds are sufficient, false if the funds are insufficient.
45    Pending(oneshot::Receiver<FundsWithdrawStatus>),
46}
47
48pub struct ObjectFundsChecker {
49    /// Watchers to keep track the last settled accumulator version.
50    /// This is updated whenever the settlement barrier transaction is executed.
51    last_settled_version_sender: watch::Sender<SequenceNumber>,
52    last_settled_version_receiver: watch::Receiver<SequenceNumber>,
53    unsettled: Arc<UnsettledObjectWithdrawals>,
54    metrics: Arc<metrics::ObjectFundsCheckerMetrics>,
55}
56
57impl ObjectFundsChecker {
58    pub fn new(
59        starting_accumulator_version: SequenceNumber,
60        unsettled: Arc<UnsettledObjectWithdrawals>,
61        metrics: Arc<metrics::ObjectFundsCheckerMetrics>,
62    ) -> Self {
63        let (last_settled_version_sender, last_settled_version_receiver) =
64            watch::channel(starting_accumulator_version);
65        Self {
66            last_settled_version_sender,
67            last_settled_version_receiver,
68            unsettled,
69            metrics,
70        }
71    }
72
73    #[cfg(test)]
74    pub fn new_for_testing(
75        starting_accumulator_version: SequenceNumber,
76        metrics: Arc<metrics::ObjectFundsCheckerMetrics>,
77    ) -> Self {
78        Self::new(
79            starting_accumulator_version,
80            Arc::new(UnsettledObjectWithdrawals::new(metrics.clone())),
81            metrics,
82        )
83    }
84
85    #[cfg(test)]
86    fn unsettled(&self) -> &Arc<UnsettledObjectWithdrawals> {
87        &self.unsettled
88    }
89
90    #[instrument(level = "debug", skip_all, fields(tx_digest = ?certificate.digest()))]
91    pub fn should_commit_object_funds_withdraws(
92        &self,
93        certificate: &VerifiedExecutableTransaction,
94        effects: &TransactionEffects,
95        accumulator_running_max_withdraws: &BTreeMap<AccumulatorObjId, u128>,
96        execution_env: &ExecutionEnv,
97        funds_read: &Arc<dyn AccountFundsRead>,
98        execution_scheduler: &Arc<ExecutionScheduler>,
99        epoch_store: &Arc<AuthorityPerEpochStore>,
100    ) -> bool {
101        if effects.status().is_err() {
102            // This transaction already failed. It does not matter any more
103            // whether it has sufficient object funds or not.
104            return true;
105        }
106        let address_funds_reservations: BTreeSet<_> = certificate
107            .transaction_data()
108            .process_funds_withdrawals_for_execution(epoch_store.get_chain_identifier())
109            .into_keys()
110            .collect();
111        // All withdraws will show up as accumulator events with integer values.
112        // Among them, addresses that do not have funds reservations are object
113        // withdraws.
114        let object_running_max_withdraws: BTreeMap<_, _> = accumulator_running_max_withdraws
115            .clone()
116            .into_iter()
117            .filter(|(account, _)| !address_funds_reservations.contains(account))
118            .collect();
119        // If there are no object withdraws, we can skip checking object funds.
120        if object_running_max_withdraws.is_empty() {
121            return true;
122        }
123        // A tx with object withdraws can only exist when accumulators are enabled
124        // for the epoch, and every production path that produces such a tx also
125        // assigns an accumulator version. The `None` paths (accumulator-disabled
126        // epoch, end-of-epoch tx) never produce withdraws and so never reach here.
127        let Some(accumulator_version) = execution_env.assigned_versions.accumulator_version()
128        else {
129            debug_fatal!("accumulator_version must be set for a tx with object withdraws");
130            return false;
131        };
132        // The sufficiency check must use the running max withdraws (the peak withdraw
133        // exposure at any point during execution), but the amount that settlement will
134        // actually deduct from each account is the net amount recorded in the effects.
135        // E.g. a tx that withdraws 10 and deposits 10 back has a running max of 10 but
136        // nets to 0. Recording the running max as unsettled would over-count against
137        // other withdraws in the same consensus commit.
138        let unsettled_withdraw_updates = if epoch_store
139            .protocol_config()
140            .record_net_unsettled_object_withdraws()
141        {
142            let updates: BTreeMap<_, _> = effects
143                .accumulator_events()
144                .into_iter()
145                .filter(|event| !address_funds_reservations.contains(&event.accumulator_obj))
146                .filter_map(|event| {
147                    event
148                        .write
149                        .get_fund_withdraw_amount()
150                        // A zero-amount withdraw emits a single Split(0) accumulator event,
151                        // which survives effects folding as a Split (the fold's Merge
152                        // tie-break only applies when an account has multiple writes).
153                        // It contributes nothing to the running max nor to settlement,
154                        // so recording it would be a no-op; skip it.
155                        .filter(|amount| *amount > 0)
156                        .map(|amount| (event.accumulator_obj, amount))
157                })
158                .collect();
159            // A positive net withdraw in effects implies a positive peak, so the account
160            // must have a running max entry that the net cannot exceed. Recording more
161            // than what the sufficiency check covered could break the
162            // funds >= unsettled_withdraw invariant in try_withdraw.
163            debug_assert!(
164                updates.iter().all(|(obj_id, net)| {
165                    object_running_max_withdraws
166                        .get(obj_id)
167                        .is_some_and(|max| net <= max)
168                }),
169                "net withdraw exceeds running max: tx={:?} updates={:?} running_max={:?}",
170                certificate.digest(),
171                updates,
172                object_running_max_withdraws,
173            );
174            updates
175        } else {
176            object_running_max_withdraws.clone()
177        };
178        match self.check_object_funds(
179            object_running_max_withdraws,
180            unsettled_withdraw_updates,
181            accumulator_version,
182            funds_read.as_ref(),
183        ) {
184            // Sufficient funds, we can go ahead and commit the execution results as it is.
185            ObjectFundsWithdrawStatus::SufficientFunds => {
186                assert_reachable!("object funds sufficient");
187                debug!("Object funds sufficient, committing effects");
188                self.metrics
189                    .check_result
190                    .with_label_values(&["sufficient"])
191                    .inc();
192                true
193            }
194            // Currently insufficient funds. We need to wait until it reach a deterministic state
195            // before we can determine if it is really insufficient (to include potential deposits)
196            // At that time we will have to re-enqueue the transaction for execution again.
197            // Re-enqueue is handled here so the caller does not need to worry about it.
198            ObjectFundsWithdrawStatus::Pending(receiver) => {
199                self.metrics.pending_checks.inc();
200                let timer = self.metrics.pending_check_latency.start_timer();
201                let pending_metrics = self.metrics.clone();
202                let scheduler = execution_scheduler.clone();
203                let cert = certificate.clone();
204                let mut execution_env = execution_env.clone();
205                let epoch_store = epoch_store.clone();
206                tokio::task::spawn(async move {
207                    // It is possible that checkpoint executor finished executing
208                    // the current epoch and went ahead with epoch change asynchronously,
209                    // while this is still waiting.
210                    let inner_metrics = pending_metrics.clone();
211                    let _ = epoch_store
212                        .within_alive_epoch(async move {
213                            let tx_digest = cert.digest();
214                            match receiver.await {
215                                Ok(FundsWithdrawStatus::MaybeSufficient) => {
216                                    assert_reachable!("object funds maybe sufficient");
217                                    // The withdraw state is now deterministically known,
218                                    // so we can enqueue the transaction again and it will check again
219                                    // whether it is sufficient or not in the next execution.
220                                    // TODO: We should be able to optimize this by avoiding re-execution.
221                                    debug!(?tx_digest, "Object funds possibly sufficient");
222                                }
223                                Ok(FundsWithdrawStatus::Insufficient) => {
224                                    assert_reachable!("object funds insufficient");
225                                    // Re-enqueue with insufficient funds status, so it will be executed
226                                    // in the next execution and fail through early error.
227                                    // FIXME: We need to also track the amount of gas that was used,
228                                    // so that we could charge properly in the next execution when we
229                                    // go through early error. Otherwise we would undercharge.
230                                    execution_env = execution_env.with_insufficient_funds();
231                                    inner_metrics
232                                        .check_result
233                                        .with_label_values(&["insufficient"])
234                                        .inc();
235                                    debug!(?tx_digest, "Object funds insufficient");
236                                }
237                                Err(e) => {
238                                    tracing::error!(
239                                        "Error receiving funds withdraw status: {:?}",
240                                        e
241                                    );
242                                }
243                            }
244                            scheduler.send_transaction_for_execution(
245                                &cert,
246                                execution_env,
247                                // TODO: Should the enqueue_time be the original enqueue time
248                                // of this transaction?
249                                Instant::now(),
250                            );
251                        })
252                        .await;
253                    timer.observe_duration();
254                    pending_metrics.pending_checks.dec();
255                });
256                false
257            }
258        }
259    }
260
261    fn check_object_funds(
262        &self,
263        object_running_max_withdraws: BTreeMap<AccumulatorObjId, u128>,
264        unsettled_withdraw_updates: BTreeMap<AccumulatorObjId, u128>,
265        accumulator_version: SequenceNumber,
266        funds_read: &dyn AccountFundsRead,
267    ) -> ObjectFundsWithdrawStatus {
268        let last_settled_version = *self.last_settled_version_receiver.borrow();
269        if accumulator_version <= last_settled_version {
270            // If the version we are withdrawing from is already settled, we have all the information
271            // we need to determine if the funds are sufficient or not.
272            if self.try_withdraw(
273                funds_read,
274                &object_running_max_withdraws,
275                &unsettled_withdraw_updates,
276                accumulator_version,
277            ) {
278                return ObjectFundsWithdrawStatus::SufficientFunds;
279            } else {
280                let (sender, receiver) = oneshot::channel();
281                // unwrap is safe because the receiver is defined right above.
282                sender.send(FundsWithdrawStatus::Insufficient).unwrap();
283                return ObjectFundsWithdrawStatus::Pending(receiver);
284            }
285        }
286
287        // Spawn a task to wait for the last settled version to become accumulator_version,
288        // before we could check again.
289        let last_settled_version_sender = self.last_settled_version_sender.clone();
290        let (sender, receiver) = oneshot::channel();
291        tokio::spawn(async move {
292            let mut version_receiver = last_settled_version_sender.subscribe();
293            // The wait is guaranteed to be notified because we update version after executing each settlement transaction,
294            // and every settlement transaction must eventually be executed.
295            let res = version_receiver
296                .wait_for(|v| *v >= accumulator_version)
297                .await;
298            if res.is_err() {
299                // This shouldn't happen, but just to be safe.
300                tracing::error!("Last settled accumulator version receiver channel closed");
301                return;
302            }
303            // We notify the waiter that the funds are now deterministically known,
304            // but we don't need to check here whether they are sufficient or not.
305            // Next time during execution we will check again.
306            let _ = sender.send(FundsWithdrawStatus::MaybeSufficient);
307        });
308        ObjectFundsWithdrawStatus::Pending(receiver)
309    }
310
311    /// Checks that each account can cover its running max withdraw (`object_running_max_withdraws`),
312    /// and if so, adds `unsettled_withdraw_updates` to the unsettled withdraws of each account.
313    fn try_withdraw(
314        &self,
315        funds_read: &dyn AccountFundsRead,
316        object_running_max_withdraws: &BTreeMap<AccumulatorObjId, u128>,
317        unsettled_withdraw_updates: &BTreeMap<AccumulatorObjId, u128>,
318        accumulator_version: SequenceNumber,
319    ) -> bool {
320        for (obj_id, amount) in object_running_max_withdraws {
321            let funds = funds_read.get_account_amount_at_version(obj_id, accumulator_version);
322            // Reading unsettled without a top-level lock is safe because no two transactions can be withdrawing
323            // from the same account at the same time.
324            let unsettled_withdraw = self
325                .unsettled
326                .get_unsettled_object_withdraw(obj_id, accumulator_version);
327            debug!(
328                ?obj_id,
329                ?funds,
330                ?accumulator_version,
331                ?unsettled_withdraw,
332                ?amount,
333                "Trying to withdraw"
334            );
335            assert!(funds >= unsettled_withdraw);
336            if funds - unsettled_withdraw < *amount {
337                return false;
338            }
339        }
340        self.unsettled
341            .record_unsettled_withdraws(unsettled_withdraw_updates.iter(), accumulator_version);
342        true
343    }
344
345    pub fn settle_accumulator_version(&self, next_accumulator_version: SequenceNumber) {
346        // unwrap is safe because a receiver is always alive as part of self.
347        self.last_settled_version_sender
348            .send(next_accumulator_version)
349            .unwrap();
350        self.metrics
351            .highest_settled_version
352            .set(next_accumulator_version.value() as i64);
353    }
354
355    #[cfg(test)]
356    pub fn get_current_accumulator_version(&self) -> SequenceNumber {
357        *self.last_settled_version_receiver.borrow()
358    }
359}