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