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