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}