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