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    num_deposits: u64,
225    num_withdrawals: u64,
226}
227
228impl AccumulatorSettlementTxBuilder {
229    pub fn new(
230        cache: Option<&dyn TransactionCacheRead>,
231        ckpt_effects: &[TransactionEffects],
232        checkpoint_seq: u64,
233        tx_index_offset: u64,
234    ) -> Self {
235        let mut updates = BTreeMap::<_, _>::new();
236        let mut addresses = HashMap::<_, _>::new();
237        let mut num_deposits = 0u64;
238        let mut num_withdrawals = 0u64;
239
240        for (tx_index, effect) in ckpt_effects.iter().enumerate() {
241            let tx = effect.transaction_digest();
242            // TransactionEffectsAPI::accumulator_events() uses a linear scan of all
243            // object changes and allocates a new vector. In the common case (on validators),
244            // we still have still have the original vector in the writeback cache, so
245            // we can avoid the unnecessary work by just taking it from the cache.
246            let events = match cache.and_then(|c| c.take_accumulator_events(tx)) {
247                Some(events) => events,
248                None => effect.accumulator_events(),
249            };
250
251            for event in events {
252                // The input to the settlement is the sum of the outputs of accumulator events (i.e. deposits).
253                // and the output of the settlement is the sum of the inputs (i.e. withdraws).
254                let (event_input_sui, event_output_sui) = event.total_sui_in_event();
255
256                let AccumulatorEvent {
257                    accumulator_obj,
258                    write:
259                        AccumulatorWriteV1 {
260                            operation,
261                            value,
262                            address,
263                        },
264                } = event;
265
266                if let Some(prev) = addresses.insert(accumulator_obj, address.clone()) {
267                    debug_assert_eq!(prev, address);
268                }
269
270                let entry = updates.entry(accumulator_obj).or_insert_with(|| {
271                    let zero = MergedValueIntermediate::zero(&value);
272                    Update {
273                        merge: zero.clone(),
274                        split: zero,
275                        input_sui: 0,
276                        output_sui: 0,
277                    }
278                });
279
280                // The output of the event is the input of the settlement, and vice versa.
281                entry.input_sui += event_output_sui;
282                entry.output_sui += event_input_sui;
283
284                match operation {
285                    AccumulatorOperation::Merge => {
286                        num_deposits += 1;
287                        entry.merge.accumulate_into(
288                            value,
289                            checkpoint_seq,
290                            tx_index as u64 + tx_index_offset,
291                        );
292                    }
293                    AccumulatorOperation::Split => {
294                        num_withdrawals += 1;
295                        entry.split.accumulate_into(
296                            value,
297                            checkpoint_seq,
298                            tx_index as u64 + tx_index_offset,
299                        );
300                    }
301                }
302            }
303        }
304
305        Self {
306            updates,
307            addresses,
308            num_deposits,
309            num_withdrawals,
310        }
311    }
312
313    pub fn num_updates(&self) -> usize {
314        self.updates.len()
315    }
316
317    pub fn num_deposits(&self) -> u64 {
318        self.num_deposits
319    }
320
321    pub fn num_withdrawals(&self) -> u64 {
322        self.num_withdrawals
323    }
324
325    /// Returns a unified map of funds changes for all accounts.
326    /// The funds change for each account is merged from the merge and split operations.
327    pub fn collect_funds_changes(&self) -> BTreeMap<AccumulatorObjId, i128> {
328        self.updates
329            .iter()
330            .filter_map(|(object_id, update)| match (&update.merge, &update.split) {
331                (
332                    MergedValueIntermediate::SumU128(merge),
333                    MergedValueIntermediate::SumU128(split),
334                ) => Some((*object_id, *merge as i128 - *split as i128)),
335                _ => None,
336            })
337            .collect()
338    }
339
340    /// Builds settlement transactions that apply accumulator updates.
341    pub fn build_tx(
342        self,
343        protocol_config: &ProtocolConfig,
344        epoch: u64,
345        accumulator_root_obj_initial_shared_version: SequenceNumber,
346        checkpoint_height: u64,
347        checkpoint_seq: u64,
348    ) -> Vec<TransactionKind> {
349        let Self {
350            updates, addresses, ..
351        } = self;
352
353        let build_one_settlement_txn = |idx: u64, updates: &mut Vec<(AccumulatorObjId, Update)>| {
354            let (total_input_sui, total_output_sui) =
355                updates
356                    .iter()
357                    .fold((0, 0), |(acc_input, acc_output), (_, update)| {
358                        (acc_input + update.input_sui, acc_output + update.output_sui)
359                    });
360
361            Self::build_one_settlement_txn(
362                &addresses,
363                epoch,
364                idx,
365                checkpoint_height,
366                accumulator_root_obj_initial_shared_version,
367                updates.drain(..),
368                total_input_sui,
369                total_output_sui,
370                checkpoint_seq,
371            )
372        };
373
374        let chunk_size = protocol_config
375            .max_updates_per_settlement_txn_as_option()
376            .unwrap_or(u32::MAX) as usize;
377
378        updates
379            .into_iter()
380            .chunks(chunk_size)
381            .into_iter()
382            .enumerate()
383            .map(|(idx, chunk)| {
384                build_one_settlement_txn(idx as u64, &mut chunk.collect::<Vec<_>>())
385            })
386            .collect()
387    }
388
389    fn add_prologue(
390        builder: &mut ProgrammableTransactionBuilder,
391        root: Argument,
392        epoch: u64,
393        checkpoint_height: u64,
394        idx: u64,
395        total_input_sui: u64,
396        total_output_sui: u64,
397    ) {
398        let epoch_arg = builder.pure(epoch).unwrap();
399        let checkpoint_height_arg = builder.pure(checkpoint_height).unwrap();
400        let idx_arg = builder.pure(idx).unwrap();
401        let total_input_sui_arg = builder.pure(total_input_sui).unwrap();
402        let total_output_sui_arg = builder.pure(total_output_sui).unwrap();
403
404        builder.programmable_move_call(
405            SUI_FRAMEWORK_PACKAGE_ID,
406            ACCUMULATOR_SETTLEMENT_MODULE.into(),
407            ACCUMULATOR_ROOT_SETTLEMENT_PROLOGUE_FUNC.into(),
408            vec![],
409            vec![
410                root,
411                epoch_arg,
412                checkpoint_height_arg,
413                idx_arg,
414                total_input_sui_arg,
415                total_output_sui_arg,
416            ],
417        );
418    }
419
420    fn build_one_settlement_txn(
421        addresses: &HashMap<AccumulatorObjId, AccumulatorAddress>,
422        epoch: u64,
423        idx: u64,
424        checkpoint_height: u64,
425        accumulator_root_obj_initial_shared_version: SequenceNumber,
426        updates: impl Iterator<Item = (AccumulatorObjId, Update)>,
427        total_input_sui: u64,
428        total_output_sui: u64,
429        checkpoint_seq: u64,
430    ) -> TransactionKind {
431        let mut builder = ProgrammableTransactionBuilder::new();
432
433        let root = builder
434            .input(CallArg::Object(ObjectArg::SharedObject {
435                id: SUI_ACCUMULATOR_ROOT_OBJECT_ID,
436                initial_shared_version: accumulator_root_obj_initial_shared_version,
437                mutability: SharedObjectMutability::NonExclusiveWrite,
438            }))
439            .unwrap();
440
441        Self::add_prologue(
442            &mut builder,
443            root,
444            epoch,
445            checkpoint_height,
446            idx,
447            total_input_sui,
448            total_output_sui,
449        );
450
451        for (accumulator_obj, update) in updates {
452            let Update { merge, split, .. } = update;
453            let address = addresses.get(&accumulator_obj).unwrap();
454            let merged_value = MergedValue::from(merge);
455            let split_value = MergedValue::from(split);
456            MergedValue::add_move_call(
457                merged_value,
458                split_value,
459                root,
460                address,
461                checkpoint_seq,
462                &mut builder,
463            );
464        }
465
466        TransactionKind::ProgrammableSystemTransaction(builder.finish())
467    }
468}
469
470/// Builds the barrier transaction that advances the version of the accumulator root object.
471/// This must be called after all settlement transactions have been executed.
472/// `settlement_effects` contains the effects of all settlement transactions.
473pub fn build_accumulator_barrier_tx(
474    epoch: u64,
475    accumulator_root_obj_initial_shared_version: SequenceNumber,
476    checkpoint_height: u64,
477    settlement_effects: &[TransactionEffects],
478) -> TransactionKind {
479    let num_settlements = settlement_effects.len() as u64;
480
481    let (objects_created, objects_destroyed) = count_accumulator_object_changes(settlement_effects);
482
483    let mut builder = ProgrammableTransactionBuilder::new();
484    let root = builder
485        .input(CallArg::Object(ObjectArg::SharedObject {
486            id: SUI_ACCUMULATOR_ROOT_OBJECT_ID,
487            initial_shared_version: accumulator_root_obj_initial_shared_version,
488            mutability: SharedObjectMutability::Mutable,
489        }))
490        .unwrap();
491
492    AccumulatorSettlementTxBuilder::add_prologue(
493        &mut builder,
494        root,
495        epoch,
496        checkpoint_height,
497        num_settlements,
498        0,
499        0,
500    );
501
502    let objects_created_arg = builder.pure(objects_created).unwrap();
503    let objects_destroyed_arg = builder.pure(objects_destroyed).unwrap();
504    builder.programmable_move_call(
505        SUI_FRAMEWORK_PACKAGE_ID,
506        ACCUMULATOR_METADATA_MODULE.into(),
507        ident_str!("record_accumulator_object_changes").into(),
508        vec![],
509        vec![root, objects_created_arg, objects_destroyed_arg],
510    );
511
512    TransactionKind::ProgrammableSystemTransaction(builder.finish())
513}
514
515pub(crate) fn count_accumulator_object_changes(
516    settlement_effects: &[TransactionEffects],
517) -> (u64, u64) {
518    settlement_effects
519        .iter()
520        .flat_map(|effects| effects.object_changes())
521        .fold((0u64, 0u64), |(created, destroyed), change| {
522            match change.id_operation {
523                IDOperation::Created => (created + 1, destroyed),
524                IDOperation::Deleted => (created, destroyed + 1),
525                IDOperation::None => (created, destroyed),
526            }
527        })
528}
529
530#[cfg(test)]
531mod barrier_settlement_key_tests {
532    use super::*;
533    use sui_types::transaction::TransactionKey;
534
535    #[test]
536    fn test_barrier_tx_returns_accumulator_settlement_key() {
537        let epoch = 5u64;
538        let checkpoint_height = 42u64;
539
540        let kind = build_accumulator_barrier_tx(
541            epoch,
542            SequenceNumber::from_u64(1),
543            checkpoint_height,
544            &[], // no settlement effects needed for key extraction
545        );
546
547        assert_eq!(
548            kind.accumulator_barrier_settlement_key(),
549            Some(TransactionKey::AccumulatorSettlement(
550                epoch,
551                checkpoint_height
552            ))
553        );
554        assert!(kind.is_accumulator_barrier_settle_tx());
555    }
556
557    #[test]
558    fn test_settlement_tx_has_no_barrier_key() {
559        // Non-barrier settlement transactions use ReadOnly access to the accumulator root,
560        // so they should not return an AccumulatorSettlement key.
561        let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
562        let builder = AccumulatorSettlementTxBuilder::new(None, &[], 0, 0);
563        let txns = builder.build_tx(&protocol_config, 5, SequenceNumber::from_u64(1), 42, 0);
564        for txn in txns {
565            assert_eq!(txn.accumulator_barrier_settlement_key(), None);
566            assert!(!txn.is_accumulator_barrier_settle_tx());
567        }
568    }
569}