Skip to main content

sui_core/accumulators/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::{BTreeMap, HashMap};
5
6use itertools::Itertools;
7use move_core_types::ident_str;
8use move_core_types::u256::U256;
9use mysten_common::fatal;
10use sui_protocol_config::ProtocolConfig;
11use sui_types::accumulator_event::AccumulatorEvent;
12use sui_types::accumulator_root::{
13    ACCUMULATOR_ROOT_SETTLE_U128_FUNC, ACCUMULATOR_ROOT_SETTLEMENT_PROLOGUE_FUNC,
14    ACCUMULATOR_SETTLEMENT_MODULE, AccumulatorObjId, EventCommitment, build_event_merkle_root,
15};
16use sui_types::balance::{BALANCE_MODULE_NAME, BALANCE_STRUCT_NAME};
17use sui_types::base_types::SequenceNumber;
18
19use sui_types::accumulator_root::ACCUMULATOR_METADATA_MODULE;
20use sui_types::digests::Digest;
21use sui_types::effects::{
22    AccumulatorAddress, AccumulatorOperation, AccumulatorValue, AccumulatorWriteV1, IDOperation,
23    TransactionEffects, TransactionEffectsAPI,
24};
25use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
26use sui_types::transaction::{
27    Argument, CallArg, ObjectArg, SharedObjectMutability, TransactionKind,
28};
29use sui_types::{
30    SUI_ACCUMULATOR_ROOT_OBJECT_ID, SUI_FRAMEWORK_ADDRESS, SUI_FRAMEWORK_PACKAGE_ID, TypeTag,
31};
32
33use crate::execution_cache::TransactionCacheRead;
34
35// provides balance read functionality for the scheduler
36pub mod funds_read;
37// provides balance read functionality for RPC
38pub mod balances;
39pub mod coin_reservations;
40pub mod object_funds_checker;
41pub(crate) mod transaction_rewriting;
42pub mod unsettled_object_withdrawals;
43
44/// Merged value is the value stored inside accumulator objects.
45/// Each mergeable Move type will map to a single variant as its representation.
46///
47/// For instance, Balance<T> stores a single u64 value, so it will map to SumU128.
48/// A clawback Balance<T> will map to SumU128U128 since it also needs to represent
49/// the amount of the balance that has been frozen.
50#[derive(Debug, Copy, Clone)]
51enum MergedValue {
52    SumU128(u128),
53    SumU128U128(u128, u128),
54    /// Merkle root of events in this checkpoint and event count.
55    EventDigest(/* merkle root */ Digest, /* event count */ u64),
56}
57
58enum ClassifiedType {
59    Balance,
60    Unknown,
61}
62
63impl ClassifiedType {
64    fn classify(ty: &TypeTag) -> Self {
65        let TypeTag::Struct(struct_tag) = ty else {
66            return Self::Unknown;
67        };
68
69        if struct_tag.address == SUI_FRAMEWORK_ADDRESS
70            && struct_tag.module.as_ident_str() == BALANCE_MODULE_NAME
71            && struct_tag.name.as_ident_str() == BALANCE_STRUCT_NAME
72        {
73            return Self::Balance;
74        }
75
76        Self::Unknown
77    }
78}
79
80impl MergedValue {
81    fn add_move_call(
82        merge: Self,
83        split: Self,
84        root: Argument,
85        address: &AccumulatorAddress,
86        checkpoint_seq: u64,
87        builder: &mut ProgrammableTransactionBuilder,
88    ) {
89        let ty = ClassifiedType::classify(&address.ty);
90        let address_arg = builder.pure(address.address).unwrap();
91
92        match (ty, merge, split) {
93            (
94                ClassifiedType::Balance,
95                MergedValue::SumU128(merge_amount),
96                MergedValue::SumU128(split_amount),
97            ) => {
98                // Net out the merge and split amounts.
99                let (merge_amount, split_amount) = if merge_amount >= split_amount {
100                    (merge_amount - split_amount, 0)
101                } else {
102                    (0, split_amount - merge_amount)
103                };
104
105                if merge_amount != 0 || split_amount != 0 {
106                    let merge_amount = builder.pure(merge_amount).unwrap();
107                    let split_amount = builder.pure(split_amount).unwrap();
108                    builder.programmable_move_call(
109                        SUI_FRAMEWORK_PACKAGE_ID,
110                        ACCUMULATOR_SETTLEMENT_MODULE.into(),
111                        ACCUMULATOR_ROOT_SETTLE_U128_FUNC.into(),
112                        vec![address.ty.clone()],
113                        vec![root, address_arg, merge_amount, split_amount],
114                    );
115                }
116            }
117            (_, MergedValue::SumU128U128(_v1, _v2), MergedValue::SumU128U128(_w1, _w2)) => todo!(),
118            (_, MergedValue::EventDigest(digest, event_count), MergedValue::EventDigest(_, _)) => {
119                let args = vec![
120                    root,
121                    builder.pure(address.address).unwrap(),
122                    builder
123                        .pure(U256::from_le_bytes(&digest.into_inner()))
124                        .unwrap(),
125                    builder.pure(event_count).unwrap(),
126                    builder.pure(checkpoint_seq).unwrap(),
127                ];
128                builder.programmable_move_call(
129                    SUI_FRAMEWORK_PACKAGE_ID,
130                    ACCUMULATOR_SETTLEMENT_MODULE.into(),
131                    sui_types::accumulator_root::ACCUMULATOR_ROOT_SETTLEMENT_SETTLE_EVENTS_FUNC
132                        .into(),
133                    vec![],
134                    args,
135                );
136            }
137            _ => fatal!("invalid merge {:?} {:?}", merge, split),
138        }
139    }
140}
141
142impl From<MergedValueIntermediate> for MergedValue {
143    fn from(value: MergedValueIntermediate) -> Self {
144        match value {
145            MergedValueIntermediate::SumU128(v) => MergedValue::SumU128(v),
146            MergedValueIntermediate::SumU128U128(v1, v2) => MergedValue::SumU128U128(v1, v2),
147            MergedValueIntermediate::Events(events) => {
148                let event_count = events.len() as u64;
149                MergedValue::EventDigest(build_event_merkle_root(&events), event_count)
150            }
151        }
152    }
153}
154
155/// MergedValueIntermediate is an intermediate / in-memory representation of the for
156/// accumulators. It is used to store the merged result of all accumulator writes in a single
157/// checkpoint.
158///
159/// This pattern is not necessary for fully commutative operations, since those could use MergedValue directly.
160///
161/// However, this supports the commutative-merge + non-commutative-update pattern, which will be used by event
162/// streams. In this pattern, everything within a checkpoint is merged commutatively, and then a single
163/// non-commutative update is applied to the accumulator at the end of the checkpoint.
164#[derive(Debug, Clone)]
165enum MergedValueIntermediate {
166    SumU128(u128),
167    SumU128U128(u128, u128),
168    Events(Vec<EventCommitment>),
169}
170
171impl MergedValueIntermediate {
172    // Create a zero value with the appropriate type for the accumulator value.
173    fn zero(value: &AccumulatorValue) -> Self {
174        match value {
175            AccumulatorValue::Integer(_) => Self::SumU128(0),
176            AccumulatorValue::IntegerTuple(_, _) => Self::SumU128U128(0, 0),
177            AccumulatorValue::EventDigest(_) => Self::Events(vec![]),
178        }
179    }
180
181    fn accumulate_into(
182        &mut self,
183        value: AccumulatorValue,
184        checkpoint_seq: u64,
185        transaction_idx: u64,
186    ) {
187        match (self, value) {
188            (Self::SumU128(v1), AccumulatorValue::Integer(v2)) => *v1 += v2 as u128,
189            (Self::SumU128U128(v1, v2), AccumulatorValue::IntegerTuple(w1, w2)) => {
190                *v1 += w1 as u128;
191                *v2 += w2 as u128;
192            }
193            (Self::Events(commitments), AccumulatorValue::EventDigest(event_digests)) => {
194                for (event_idx, digest) in event_digests {
195                    commitments.push(EventCommitment::new(
196                        checkpoint_seq,
197                        transaction_idx,
198                        event_idx,
199                        digest,
200                    ));
201                }
202            }
203            _ => {
204                fatal!("invalid merge");
205            }
206        }
207    }
208}
209
210struct Update {
211    merge: MergedValueIntermediate,
212    split: MergedValueIntermediate,
213    // Track input and output SUI for each update. Necessary so that when we construct
214    // a settlement transaction from a collection of Updates, they can accurately
215    // track the net SUI flows.
216    input_sui: u64,
217    output_sui: u64,
218}
219
220pub(crate) struct AccumulatorSettlementTxBuilder {
221    // updates is iterated over, must be a BTreeMap
222    updates: BTreeMap<AccumulatorObjId, Update>,
223    // addresses is only used for lookups.
224    addresses: HashMap<AccumulatorObjId, AccumulatorAddress>,
225    num_deposits: u64,
226    num_withdrawals: u64,
227}
228
229impl AccumulatorSettlementTxBuilder {
230    pub fn new(
231        cache: Option<&dyn TransactionCacheRead>,
232        ckpt_effects: &[TransactionEffects],
233        checkpoint_seq: u64,
234        tx_index_offset: u64,
235    ) -> Self {
236        let mut updates = BTreeMap::<_, _>::new();
237        let mut addresses = HashMap::<_, _>::new();
238        let mut num_deposits = 0u64;
239        let mut num_withdrawals = 0u64;
240
241        for (tx_index, effect) in ckpt_effects.iter().enumerate() {
242            let tx = effect.transaction_digest();
243            // TransactionEffectsAPI::accumulator_events() uses a linear scan of all
244            // object changes and allocates a new vector. In the common case (on validators),
245            // we still have still have the original vector in the writeback cache, so
246            // we can avoid the unnecessary work by just taking it from the cache.
247            let events = match cache.and_then(|c| c.take_accumulator_events(tx)) {
248                Some(events) => events,
249                None => effect.accumulator_events(),
250            };
251
252            for event in events {
253                // The input to the settlement is the sum of the outputs of accumulator events (i.e. deposits).
254                // and the output of the settlement is the sum of the inputs (i.e. withdraws).
255                let (event_input_sui, event_output_sui) = event.total_sui_in_event();
256
257                let AccumulatorEvent {
258                    accumulator_obj,
259                    write:
260                        AccumulatorWriteV1 {
261                            operation,
262                            value,
263                            address,
264                        },
265                } = event;
266
267                if let Some(prev) = addresses.insert(accumulator_obj, address.clone()) {
268                    debug_assert_eq!(prev, address);
269                }
270
271                let entry = updates.entry(accumulator_obj).or_insert_with(|| {
272                    let zero = MergedValueIntermediate::zero(&value);
273                    Update {
274                        merge: zero.clone(),
275                        split: zero,
276                        input_sui: 0,
277                        output_sui: 0,
278                    }
279                });
280
281                // The output of the event is the input of the settlement, and vice versa.
282                entry.input_sui += event_output_sui;
283                entry.output_sui += event_input_sui;
284
285                match operation {
286                    AccumulatorOperation::Merge => {
287                        num_deposits += 1;
288                        entry.merge.accumulate_into(
289                            value,
290                            checkpoint_seq,
291                            tx_index as u64 + tx_index_offset,
292                        );
293                    }
294                    AccumulatorOperation::Split => {
295                        num_withdrawals += 1;
296                        entry.split.accumulate_into(
297                            value,
298                            checkpoint_seq,
299                            tx_index as u64 + tx_index_offset,
300                        );
301                    }
302                }
303            }
304        }
305
306        Self {
307            updates,
308            addresses,
309            num_deposits,
310            num_withdrawals,
311        }
312    }
313
314    pub fn num_deposits(&self) -> u64 {
315        self.num_deposits
316    }
317
318    pub fn num_withdrawals(&self) -> u64 {
319        self.num_withdrawals
320    }
321
322    /// Returns a unified map of funds changes for all accounts.
323    /// The funds change for each account is merged from the merge and split operations.
324    pub fn collect_funds_changes(&self) -> BTreeMap<AccumulatorObjId, i128> {
325        self.updates
326            .iter()
327            .filter_map(|(object_id, update)| match (&update.merge, &update.split) {
328                (
329                    MergedValueIntermediate::SumU128(merge),
330                    MergedValueIntermediate::SumU128(split),
331                ) => Some((*object_id, *merge as i128 - *split as i128)),
332                _ => None,
333            })
334            .collect()
335    }
336
337    /// Builds settlement transactions that apply accumulator updates.
338    pub fn build_tx(
339        self,
340        protocol_config: &ProtocolConfig,
341        epoch: u64,
342        accumulator_root_obj_initial_shared_version: SequenceNumber,
343        checkpoint_height: u64,
344        checkpoint_seq: u64,
345    ) -> Vec<TransactionKind> {
346        let Self {
347            updates, addresses, ..
348        } = self;
349
350        let build_one_settlement_txn = |idx: u64, updates: &mut Vec<(AccumulatorObjId, Update)>| {
351            let (total_input_sui, total_output_sui) =
352                updates
353                    .iter()
354                    .fold((0, 0), |(acc_input, acc_output), (_, update)| {
355                        (acc_input + update.input_sui, acc_output + update.output_sui)
356                    });
357
358            Self::build_one_settlement_txn(
359                &addresses,
360                epoch,
361                idx,
362                checkpoint_height,
363                accumulator_root_obj_initial_shared_version,
364                updates.drain(..),
365                total_input_sui,
366                total_output_sui,
367                checkpoint_seq,
368            )
369        };
370
371        let chunk_size = protocol_config
372            .max_updates_per_settlement_txn_as_option()
373            .unwrap_or(u32::MAX) as usize;
374
375        updates
376            .into_iter()
377            .chunks(chunk_size)
378            .into_iter()
379            .enumerate()
380            .map(|(idx, chunk)| {
381                build_one_settlement_txn(idx as u64, &mut chunk.collect::<Vec<_>>())
382            })
383            .collect()
384    }
385
386    fn add_prologue(
387        builder: &mut ProgrammableTransactionBuilder,
388        root: Argument,
389        epoch: u64,
390        checkpoint_height: u64,
391        idx: u64,
392        total_input_sui: u64,
393        total_output_sui: u64,
394    ) {
395        let epoch_arg = builder.pure(epoch).unwrap();
396        let checkpoint_height_arg = builder.pure(checkpoint_height).unwrap();
397        let idx_arg = builder.pure(idx).unwrap();
398        let total_input_sui_arg = builder.pure(total_input_sui).unwrap();
399        let total_output_sui_arg = builder.pure(total_output_sui).unwrap();
400
401        builder.programmable_move_call(
402            SUI_FRAMEWORK_PACKAGE_ID,
403            ACCUMULATOR_SETTLEMENT_MODULE.into(),
404            ACCUMULATOR_ROOT_SETTLEMENT_PROLOGUE_FUNC.into(),
405            vec![],
406            vec![
407                root,
408                epoch_arg,
409                checkpoint_height_arg,
410                idx_arg,
411                total_input_sui_arg,
412                total_output_sui_arg,
413            ],
414        );
415    }
416
417    fn build_one_settlement_txn(
418        addresses: &HashMap<AccumulatorObjId, AccumulatorAddress>,
419        epoch: u64,
420        idx: u64,
421        checkpoint_height: u64,
422        accumulator_root_obj_initial_shared_version: SequenceNumber,
423        updates: impl Iterator<Item = (AccumulatorObjId, Update)>,
424        total_input_sui: u64,
425        total_output_sui: u64,
426        checkpoint_seq: u64,
427    ) -> TransactionKind {
428        let mut builder = ProgrammableTransactionBuilder::new();
429
430        let root = builder
431            .input(CallArg::Object(ObjectArg::SharedObject {
432                id: SUI_ACCUMULATOR_ROOT_OBJECT_ID,
433                initial_shared_version: accumulator_root_obj_initial_shared_version,
434                mutability: SharedObjectMutability::NonExclusiveWrite,
435            }))
436            .unwrap();
437
438        Self::add_prologue(
439            &mut builder,
440            root,
441            epoch,
442            checkpoint_height,
443            idx,
444            total_input_sui,
445            total_output_sui,
446        );
447
448        for (accumulator_obj, update) in updates {
449            let Update { merge, split, .. } = update;
450            let address = addresses.get(&accumulator_obj).unwrap();
451            let merged_value = MergedValue::from(merge);
452            let split_value = MergedValue::from(split);
453            MergedValue::add_move_call(
454                merged_value,
455                split_value,
456                root,
457                address,
458                checkpoint_seq,
459                &mut builder,
460            );
461        }
462
463        TransactionKind::ProgrammableSystemTransaction(builder.finish())
464    }
465}
466
467/// Builds the barrier transaction that advances the version of the accumulator root object.
468/// This must be called after all settlement transactions have been executed.
469/// `settlement_effects` contains the effects of all settlement transactions.
470pub fn build_accumulator_barrier_tx(
471    epoch: u64,
472    accumulator_root_obj_initial_shared_version: SequenceNumber,
473    checkpoint_height: u64,
474    settlement_effects: &[TransactionEffects],
475) -> TransactionKind {
476    let num_settlements = settlement_effects.len() as u64;
477
478    let (objects_created, objects_destroyed) = count_accumulator_object_changes(settlement_effects);
479
480    let mut builder = ProgrammableTransactionBuilder::new();
481    let root = builder
482        .input(CallArg::Object(ObjectArg::SharedObject {
483            id: SUI_ACCUMULATOR_ROOT_OBJECT_ID,
484            initial_shared_version: accumulator_root_obj_initial_shared_version,
485            mutability: SharedObjectMutability::Mutable,
486        }))
487        .unwrap();
488
489    AccumulatorSettlementTxBuilder::add_prologue(
490        &mut builder,
491        root,
492        epoch,
493        checkpoint_height,
494        num_settlements,
495        0,
496        0,
497    );
498
499    let objects_created_arg = builder.pure(objects_created).unwrap();
500    let objects_destroyed_arg = builder.pure(objects_destroyed).unwrap();
501    builder.programmable_move_call(
502        SUI_FRAMEWORK_PACKAGE_ID,
503        ACCUMULATOR_METADATA_MODULE.into(),
504        ident_str!("record_accumulator_object_changes").into(),
505        vec![],
506        vec![root, objects_created_arg, objects_destroyed_arg],
507    );
508
509    TransactionKind::ProgrammableSystemTransaction(builder.finish())
510}
511
512pub(crate) fn count_accumulator_object_changes(
513    settlement_effects: &[TransactionEffects],
514) -> (u64, u64) {
515    settlement_effects
516        .iter()
517        .flat_map(|effects| effects.object_changes())
518        .fold((0u64, 0u64), |(created, destroyed), change| {
519            match change.id_operation {
520                IDOperation::Created => (created + 1, destroyed),
521                IDOperation::Deleted => (created, destroyed + 1),
522                IDOperation::None => (created, destroyed),
523            }
524        })
525}
526
527#[cfg(test)]
528mod barrier_settlement_key_tests {
529    use super::*;
530    use sui_types::transaction::TransactionKey;
531
532    #[test]
533    fn test_barrier_tx_returns_accumulator_settlement_key() {
534        let epoch = 5u64;
535        let checkpoint_height = 42u64;
536
537        let kind = build_accumulator_barrier_tx(
538            epoch,
539            SequenceNumber::from_u64(1),
540            checkpoint_height,
541            &[], // no settlement effects needed for key extraction
542        );
543
544        assert_eq!(
545            kind.accumulator_barrier_settlement_key(),
546            Some(TransactionKey::AccumulatorSettlement(
547                epoch,
548                checkpoint_height
549            ))
550        );
551        assert!(kind.is_accumulator_barrier_settle_tx());
552    }
553
554    #[test]
555    fn test_settlement_tx_has_no_barrier_key() {
556        // Non-barrier settlement transactions use ReadOnly access to the accumulator root,
557        // so they should not return an AccumulatorSettlement key.
558        let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
559        let builder = AccumulatorSettlementTxBuilder::new(None, &[], 0, 0);
560        let txns = builder.build_tx(&protocol_config, 5, SequenceNumber::from_u64(1), 42, 0);
561        for txn in txns {
562            assert_eq!(txn.accumulator_barrier_settlement_key(), None);
563            assert!(!txn.is_accumulator_barrier_settle_tx());
564        }
565    }
566}