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