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