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