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