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