Skip to main content

sui_types/
accumulator_root.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    MoveTypeTagTrait, MoveTypeTagTraitGeneric, SUI_ACCUMULATOR_ROOT_ADDRESS,
6    SUI_ACCUMULATOR_ROOT_OBJECT_ID, SUI_FRAMEWORK_ADDRESS, SUI_FRAMEWORK_PACKAGE_ID,
7    accumulator_event::AccumulatorEvent,
8    balance::Balance,
9    base_types::{ObjectID, SequenceNumber, SuiAddress},
10    digests::{Digest, TransactionDigest},
11    dynamic_field::{
12        BoundedDynamicFieldID, DYNAMIC_FIELD_FIELD_STRUCT_NAME, DYNAMIC_FIELD_MODULE_NAME,
13        DynamicFieldKey, DynamicFieldObject, Field, serialize_dynamic_field,
14    },
15    error::{SuiError, SuiErrorKind, SuiResult},
16    object::{MoveObject, Object, Owner},
17    storage::{ObjectStore, RuntimeObjectResolver},
18};
19use move_core_types::{
20    ident_str,
21    identifier::IdentStr,
22    language_storage::{StructTag, TypeTag},
23    u256::U256,
24};
25use serde::{Deserialize, Serialize, de::DeserializeOwned};
26use sui_protocol_config::ProtocolConfig;
27
28pub const ACCUMULATOR_ROOT_MODULE: &IdentStr = ident_str!("accumulator");
29pub const ACCUMULATOR_METADATA_MODULE: &IdentStr = ident_str!("accumulator_metadata");
30pub const ACCUMULATOR_SETTLEMENT_MODULE: &IdentStr = ident_str!("accumulator_settlement");
31pub const ACCUMULATOR_SETTLEMENT_EVENT_STREAM_HEAD: &IdentStr = ident_str!("EventStreamHead");
32pub const ACCUMULATOR_ROOT_CREATE_FUNC: &IdentStr = ident_str!("create");
33pub const ACCUMULATOR_ROOT_SETTLE_U128_FUNC: &IdentStr = ident_str!("settle_u128");
34pub const ACCUMULATOR_ROOT_SETTLEMENT_PROLOGUE_FUNC: &IdentStr = ident_str!("settlement_prologue");
35pub const ACCUMULATOR_ROOT_SETTLEMENT_SETTLE_EVENTS_FUNC: &IdentStr = ident_str!("settle_events");
36
37const ACCUMULATOR_KEY_TYPE: &IdentStr = ident_str!("Key");
38const ACCUMULATOR_U128_TYPE: &IdentStr = ident_str!("U128");
39
40pub fn check_accumulator_type_bounds(config: &ProtocolConfig, ty: &TypeTag) -> bool {
41    match config.max_accumulator_type_nodes_as_option() {
42        Some(max) => ty.node_count() <= max,
43        None => true,
44    }
45}
46
47pub fn get_accumulator_root_obj_initial_shared_version(
48    object_store: &dyn ObjectStore,
49) -> SuiResult<Option<SequenceNumber>> {
50    Ok(object_store
51        .get_object(&SUI_ACCUMULATOR_ROOT_OBJECT_ID)
52        .map(|obj| match obj.owner {
53            Owner::Shared {
54                initial_shared_version,
55            } => initial_shared_version,
56            _ => unreachable!("Accumulator root object must be shared"),
57        }))
58}
59
60/// Rust type for the Move type accumulator::Key used to derive the dynamic field id for the
61/// accumulator value.
62#[derive(Debug, Serialize, Deserialize, Clone)]
63pub struct AccumulatorKey {
64    pub owner: SuiAddress,
65}
66
67impl MoveTypeTagTraitGeneric for AccumulatorKey {
68    fn get_type_tag(type_params: &[TypeTag]) -> TypeTag {
69        TypeTag::Struct(Box::new(StructTag {
70            address: SUI_FRAMEWORK_PACKAGE_ID.into(),
71            module: ACCUMULATOR_ROOT_MODULE.to_owned(),
72            name: ACCUMULATOR_KEY_TYPE.to_owned(),
73            type_params: type_params.to_vec(),
74        }))
75    }
76}
77
78#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
79pub enum AccumulatorValue {
80    U128(U128),
81}
82
83#[derive(Default, Serialize, Deserialize, Debug, Eq, PartialEq)]
84pub struct U128 {
85    pub value: u128,
86}
87
88impl MoveTypeTagTrait for U128 {
89    fn get_type_tag() -> TypeTag {
90        TypeTag::Struct(Box::new(StructTag {
91            address: SUI_FRAMEWORK_ADDRESS,
92            module: ACCUMULATOR_ROOT_MODULE.to_owned(),
93            name: ACCUMULATOR_U128_TYPE.to_owned(),
94            type_params: vec![],
95        }))
96    }
97}
98
99/// New-type for ObjectIDs that are known to have been properly derived as a Balance accumulator field.
100#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Hash)]
101pub struct AccumulatorObjId(ObjectID);
102
103impl AccumulatorObjId {
104    pub fn new_unchecked(id: ObjectID) -> Self {
105        Self(id)
106    }
107
108    pub fn inner(&self) -> &ObjectID {
109        &self.0
110    }
111}
112
113impl std::fmt::Display for AccumulatorObjId {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{}", self.0)
116    }
117}
118
119pub trait UnsettledObjectFundsRead {
120    fn get_unsettled_object_withdraw(
121        &self,
122        account: &AccumulatorObjId,
123        accumulator_version: SequenceNumber,
124    ) -> u128;
125}
126
127pub struct EmptyUnsettledObjectFunds;
128
129impl UnsettledObjectFundsRead for EmptyUnsettledObjectFunds {
130    fn get_unsettled_object_withdraw(
131        &self,
132        _account: &AccumulatorObjId,
133        _accumulator_version: SequenceNumber,
134    ) -> u128 {
135        0
136    }
137}
138
139impl AccumulatorValue {
140    pub fn as_u128(&self) -> Option<u128> {
141        match self {
142            AccumulatorValue::U128(value) => Some(value.value),
143        }
144    }
145
146    pub fn get_field_id(owner: SuiAddress, type_: &TypeTag) -> SuiResult<AccumulatorObjId> {
147        if !Balance::is_balance_type(type_) {
148            return Err(SuiErrorKind::TypeError {
149                error: "only Balance<T> is supported".to_string(),
150            }
151            .into());
152        }
153
154        let key = AccumulatorKey { owner };
155        Ok(AccumulatorObjId(
156            DynamicFieldKey(
157                SUI_ACCUMULATOR_ROOT_OBJECT_ID,
158                key,
159                AccumulatorKey::get_type_tag(std::slice::from_ref(type_)),
160            )
161            .into_unbounded_id()?
162            .as_object_id(),
163        ))
164    }
165
166    pub fn exists(
167        runtime_object_resolver: &dyn RuntimeObjectResolver,
168        version_bound: Option<SequenceNumber>,
169        owner: SuiAddress,
170        type_: &TypeTag,
171    ) -> SuiResult<bool> {
172        if !Balance::is_balance_type(type_) {
173            return Err(SuiErrorKind::TypeError {
174                error: "only Balance<T> is supported".to_string(),
175            }
176            .into());
177        }
178
179        let key = AccumulatorKey { owner };
180        DynamicFieldKey(
181            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
182            key,
183            AccumulatorKey::get_type_tag(std::slice::from_ref(type_)),
184        )
185        .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
186        .exists(runtime_object_resolver)
187    }
188
189    pub fn load_by_id<T>(
190        runtime_object_resolver: &dyn RuntimeObjectResolver,
191        version_bound: Option<SequenceNumber>,
192        id: AccumulatorObjId,
193    ) -> SuiResult<Option<T>>
194    where
195        T: Serialize + DeserializeOwned,
196    {
197        BoundedDynamicFieldID::<AccumulatorKey>::new(
198            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
199            id.0,
200            version_bound.unwrap_or(SequenceNumber::MAX),
201        )
202        .load_object(runtime_object_resolver)?
203        .map(|o| o.load_value::<T>())
204        .transpose()
205    }
206
207    pub fn load(
208        runtime_object_resolver: &dyn RuntimeObjectResolver,
209        version_bound: Option<SequenceNumber>,
210        owner: SuiAddress,
211        type_: &TypeTag,
212    ) -> SuiResult<Option<Self>> {
213        if !Balance::is_balance_type(type_) {
214            return Err(SuiErrorKind::TypeError {
215                error: "only Balance<T> is supported".to_string(),
216            }
217            .into());
218        }
219
220        let key = AccumulatorKey { owner };
221        let key_type_tag = AccumulatorKey::get_type_tag(std::slice::from_ref(type_));
222
223        let Some(value) = DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
224            .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
225            .load_object(runtime_object_resolver)?
226            .map(|o| o.load_value::<U128>())
227            .transpose()?
228        else {
229            return Ok(None);
230        };
231
232        Ok(Some(Self::U128(value)))
233    }
234
235    pub fn load_object(
236        runtime_object_resolver: &dyn RuntimeObjectResolver,
237        version_bound: Option<SequenceNumber>,
238        owner: SuiAddress,
239        type_: &TypeTag,
240    ) -> SuiResult<Option<Object>> {
241        let key = AccumulatorKey { owner };
242        let key_type_tag = AccumulatorKey::get_type_tag(std::slice::from_ref(type_));
243
244        Ok(
245            DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
246                .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
247                .load_object(runtime_object_resolver)?
248                .map(|o| o.into_object()),
249        )
250    }
251
252    pub fn load_object_by_id(
253        runtime_object_resolver: &dyn RuntimeObjectResolver,
254        version_bound: Option<SequenceNumber>,
255        id: ObjectID,
256    ) -> SuiResult<Option<Object>> {
257        Ok(BoundedDynamicFieldID::<AccumulatorKey>::new(
258            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
259            id,
260            version_bound.unwrap_or(SequenceNumber::MAX),
261        )
262        .load_object(runtime_object_resolver)?
263        .map(|o| o.into_object()))
264    }
265
266    pub fn create_for_testing(owner: SuiAddress, type_tag: TypeTag, balance: u64) -> Object {
267        let key = AccumulatorKey { owner };
268        let value = U128 {
269            value: balance as u128,
270        };
271
272        let field_key = DynamicFieldKey(
273            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
274            key,
275            AccumulatorKey::get_type_tag(std::slice::from_ref(&type_tag)),
276        );
277        let field = field_key.into_field(value).unwrap();
278        let move_object = field
279            .into_move_object_unsafe_for_testing(SequenceNumber::new())
280            .unwrap();
281
282        Object::new_move(
283            move_object,
284            Owner::ObjectOwner(SUI_ACCUMULATOR_ROOT_ADDRESS.into()),
285            TransactionDigest::genesis_marker(),
286        )
287    }
288}
289
290/// Extract stream id from an accumulator event if it targets sui::accumulator_settlement::EventStreamHead
291pub fn stream_id_from_accumulator_event(ev: &AccumulatorEvent) -> Option<SuiAddress> {
292    if let TypeTag::Struct(tag) = &ev.write.address.ty
293        && tag.address == SUI_FRAMEWORK_ADDRESS
294        && tag.module.as_ident_str() == ACCUMULATOR_SETTLEMENT_MODULE
295        && tag.name.as_ident_str() == ACCUMULATOR_SETTLEMENT_EVENT_STREAM_HEAD
296    {
297        return Some(ev.write.address.address);
298    }
299    None
300}
301
302impl TryFrom<&MoveObject> for AccumulatorValue {
303    type Error = SuiError;
304    fn try_from(value: &MoveObject) -> Result<Self, Self::Error> {
305        let (_key, value): (AccumulatorKey, AccumulatorValue) = value.try_into()?;
306        Ok(value)
307    }
308}
309
310impl TryFrom<&MoveObject> for (AccumulatorKey, AccumulatorValue) {
311    type Error = SuiError;
312    fn try_from(value: &MoveObject) -> Result<Self, Self::Error> {
313        value
314            .type_()
315            .is_balance_accumulator_field()
316            .then(|| value.to_rust::<Field<AccumulatorKey, U128>>())
317            .flatten()
318            .map(|f| (f.name, AccumulatorValue::U128(f.value)))
319            .ok_or_else(|| {
320                SuiErrorKind::DynamicFieldReadError(format!(
321                    "Dynamic field {:?} is not a AccumulatorValue",
322                    value.id()
323                ))
324                .into()
325            })
326    }
327}
328
329pub fn update_account_balance_for_testing(account_object: &mut Object, balance_change: i128) {
330    let current_balance_field = DynamicFieldObject::<AccumulatorKey>::new(account_object.clone())
331        .load_field::<U128>()
332        .unwrap();
333
334    let current_balance = current_balance_field.value.value;
335
336    assert!(current_balance <= i128::MAX as u128);
337    assert!(current_balance as i128 >= balance_change.abs());
338
339    let new_balance = U128 {
340        value: (current_balance as i128 + balance_change) as u128,
341    };
342
343    let new_field = serialize_dynamic_field(
344        &current_balance_field.id,
345        &current_balance_field.name,
346        new_balance,
347    )
348    .unwrap();
349
350    let move_object = account_object.data.try_as_move_mut().unwrap();
351    move_object.set_contents_unsafe(new_field);
352}
353
354pub(crate) fn accumulator_value_balance_type_maybe(s: &StructTag) -> Option<TypeTag> {
355    if s.address == SUI_FRAMEWORK_ADDRESS
356        && s.module.as_ident_str() == DYNAMIC_FIELD_MODULE_NAME
357        && s.name.as_ident_str() == DYNAMIC_FIELD_FIELD_STRUCT_NAME
358        && s.type_params.len() == 2
359        && let Some(key_type) = accumulator_key_type_maybe(&s.type_params[0])
360        && is_accumulator_u128(&s.type_params[1])
361    {
362        Balance::maybe_get_balance_type_param(&key_type)
363    } else {
364        None
365    }
366}
367
368/// Check if a TypeTag is Key<Balance<T>>
369pub(crate) fn accumulator_key_type_maybe(t: &TypeTag) -> Option<TypeTag> {
370    if let TypeTag::Struct(s) = t
371        && s.address == SUI_FRAMEWORK_ADDRESS
372        && s.module.as_ident_str() == ACCUMULATOR_ROOT_MODULE
373        && s.name.as_ident_str() == ACCUMULATOR_KEY_TYPE
374        && s.type_params.len() == 1
375    {
376        Some(s.type_params[0].clone())
377    } else {
378        None
379    }
380}
381
382/// Check if a TypeTag is U128 from accumulator module
383pub(crate) fn is_accumulator_u128(t: &TypeTag) -> bool {
384    if let TypeTag::Struct(s) = t {
385        s.address == SUI_FRAMEWORK_ADDRESS
386            && s.module.as_ident_str() == ACCUMULATOR_ROOT_MODULE
387            && s.name.as_ident_str() == ACCUMULATOR_U128_TYPE
388            && s.type_params.is_empty()
389    } else {
390        false
391    }
392}
393
394/// Rust representation of the Move EventStreamHead struct from accumulator_settlement module.
395/// This represents the state of an authenticated event stream head stored on-chain.
396#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
397pub struct EventStreamHead {
398    /// The MMR (Merkle Mountain Range) digest representing the accumulated events
399    pub mmr: Vec<U256>,
400    /// The checkpoint sequence number when this stream head was last updated
401    pub checkpoint_seq: u64,
402    /// The total number of events accumulated in this stream
403    pub num_events: u64,
404}
405
406impl Default for EventStreamHead {
407    fn default() -> Self {
408        Self::new()
409    }
410}
411
412impl EventStreamHead {
413    pub fn new() -> Self {
414        Self {
415            mmr: vec![],
416            checkpoint_seq: 0,
417            num_events: 0,
418        }
419    }
420
421    pub fn num_events(&self) -> u64 {
422        self.num_events
423    }
424
425    pub fn checkpoint_seq(&self) -> u64 {
426        self.checkpoint_seq
427    }
428
429    pub fn mmr(&self) -> &Vec<U256> {
430        &self.mmr
431    }
432}
433
434pub fn derive_event_stream_head_object_id(stream_id: SuiAddress) -> SuiResult<ObjectID> {
435    let key = AccumulatorKey { owner: stream_id };
436
437    let value_type_tag = TypeTag::Struct(Box::new(StructTag {
438        address: SUI_FRAMEWORK_ADDRESS,
439        module: ACCUMULATOR_SETTLEMENT_MODULE.to_owned(),
440        name: ACCUMULATOR_SETTLEMENT_EVENT_STREAM_HEAD.to_owned(),
441        type_params: vec![],
442    }));
443
444    let key_type_tag = AccumulatorKey::get_type_tag(&[value_type_tag]);
445
446    DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
447        .into_unbounded_id()
448        .map(|id| id.as_object_id())
449}
450
451#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
452pub struct EventCommitment {
453    pub checkpoint_seq: u64,
454    pub transaction_idx: u64,
455    pub event_idx: u64,
456    pub digest: Digest,
457}
458
459impl EventCommitment {
460    pub fn new(checkpoint_seq: u64, transaction_idx: u64, event_idx: u64, digest: Digest) -> Self {
461        Self {
462            checkpoint_seq,
463            transaction_idx,
464            event_idx,
465            digest,
466        }
467    }
468}
469
470impl PartialOrd for EventCommitment {
471    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
472        Some(self.cmp(other))
473    }
474}
475
476impl Ord for EventCommitment {
477    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
478        (self.checkpoint_seq, self.transaction_idx, self.event_idx).cmp(&(
479            other.checkpoint_seq,
480            other.transaction_idx,
481            other.event_idx,
482        ))
483    }
484}
485
486pub fn build_event_merkle_root(events: &[EventCommitment]) -> Digest {
487    use fastcrypto::hash::Blake2b256;
488    use fastcrypto::merkle::MerkleTree;
489
490    debug_assert!(
491        events.windows(2).all(|w| w[0] <= w[1]),
492        "Events must be ordered by (checkpoint_seq, transaction_idx, event_idx)"
493    );
494
495    let merkle_tree = MerkleTree::<Blake2b256>::build_from_unserialized(events.to_vec())
496        .expect("failed to serialize event commitments for merkle root");
497    let root_node = merkle_tree.root();
498    let root_digest = root_node.bytes();
499    Digest::new(root_digest)
500}