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