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
132pub trait UnsettledObjectFundsRead {
133    fn get_unsettled_object_withdraw(
134        &self,
135        account: &AccumulatorObjId,
136        accumulator_version: SequenceNumber,
137    ) -> u128;
138}
139
140pub struct EmptyUnsettledObjectFunds;
141
142impl UnsettledObjectFundsRead for EmptyUnsettledObjectFunds {
143    fn get_unsettled_object_withdraw(
144        &self,
145        _account: &AccumulatorObjId,
146        _accumulator_version: SequenceNumber,
147    ) -> u128 {
148        0
149    }
150}
151
152impl AccumulatorValue {
153    pub fn as_u128(&self) -> Option<u128> {
154        match self {
155            AccumulatorValue::U128(value) => Some(value.value),
156        }
157    }
158
159    pub fn get_field_id(owner: SuiAddress, type_: &TypeTag) -> SuiResult<AccumulatorObjId> {
160        if !Balance::is_balance_type(type_) {
161            return Err(SuiErrorKind::TypeError {
162                error: "only Balance<T> is supported".to_string(),
163            }
164            .into());
165        }
166
167        let key = AccumulatorKey { owner };
168        Ok(AccumulatorObjId(
169            DynamicFieldKey(
170                SUI_ACCUMULATOR_ROOT_OBJECT_ID,
171                key,
172                AccumulatorKey::get_type_tag(std::slice::from_ref(type_)),
173            )
174            .into_unbounded_id()?
175            .as_object_id(),
176        ))
177    }
178
179    pub fn exists(
180        runtime_object_resolver: &dyn RuntimeObjectResolver,
181        version_bound: Option<SequenceNumber>,
182        owner: SuiAddress,
183        type_: &TypeTag,
184    ) -> SuiResult<bool> {
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        DynamicFieldKey(
194            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
195            key,
196            AccumulatorKey::get_type_tag(std::slice::from_ref(type_)),
197        )
198        .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
199        .exists(runtime_object_resolver)
200    }
201
202    pub fn load_by_id<T>(
203        runtime_object_resolver: &dyn RuntimeObjectResolver,
204        version_bound: Option<SequenceNumber>,
205        id: AccumulatorObjId,
206    ) -> SuiResult<Option<T>>
207    where
208        T: Serialize + DeserializeOwned,
209    {
210        BoundedDynamicFieldID::<AccumulatorKey>::new(
211            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
212            id.0,
213            version_bound.unwrap_or(SequenceNumber::MAX),
214        )
215        .load_object(runtime_object_resolver)?
216        .map(|o| o.load_value::<T>())
217        .transpose()
218    }
219
220    pub fn load(
221        runtime_object_resolver: &dyn RuntimeObjectResolver,
222        version_bound: Option<SequenceNumber>,
223        owner: SuiAddress,
224        type_: &TypeTag,
225    ) -> SuiResult<Option<Self>> {
226        if !Balance::is_balance_type(type_) {
227            return Err(SuiErrorKind::TypeError {
228                error: "only Balance<T> is supported".to_string(),
229            }
230            .into());
231        }
232
233        let key = AccumulatorKey { owner };
234        let key_type_tag = AccumulatorKey::get_type_tag(std::slice::from_ref(type_));
235
236        let Some(value) = DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
237            .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
238            .load_object(runtime_object_resolver)?
239            .map(|o| o.load_value::<U128>())
240            .transpose()?
241        else {
242            return Ok(None);
243        };
244
245        Ok(Some(Self::U128(value)))
246    }
247
248    pub fn load_object(
249        runtime_object_resolver: &dyn RuntimeObjectResolver,
250        version_bound: Option<SequenceNumber>,
251        owner: SuiAddress,
252        type_: &TypeTag,
253    ) -> SuiResult<Option<Object>> {
254        let key = AccumulatorKey { owner };
255        let key_type_tag = AccumulatorKey::get_type_tag(std::slice::from_ref(type_));
256
257        Ok(
258            DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
259                .into_id_with_bound(version_bound.unwrap_or(SequenceNumber::MAX))?
260                .load_object(runtime_object_resolver)?
261                .map(|o| o.into_object()),
262        )
263    }
264
265    pub fn load_object_by_id(
266        runtime_object_resolver: &dyn RuntimeObjectResolver,
267        version_bound: Option<SequenceNumber>,
268        id: ObjectID,
269    ) -> SuiResult<Option<Object>> {
270        Ok(BoundedDynamicFieldID::<AccumulatorKey>::new(
271            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
272            id,
273            version_bound.unwrap_or(SequenceNumber::MAX),
274        )
275        .load_object(runtime_object_resolver)?
276        .map(|o| o.into_object()))
277    }
278
279    pub fn create_for_testing(owner: SuiAddress, type_tag: TypeTag, balance: u64) -> Object {
280        let key = AccumulatorKey { owner };
281        let value = U128 {
282            value: balance as u128,
283        };
284
285        let field_key = DynamicFieldKey(
286            SUI_ACCUMULATOR_ROOT_OBJECT_ID,
287            key,
288            AccumulatorKey::get_type_tag(std::slice::from_ref(&type_tag)),
289        );
290        let field = field_key.into_field(value).unwrap();
291        let move_object = field
292            .into_move_object_unsafe_for_testing(SequenceNumber::new())
293            .unwrap();
294
295        Object::new_move(
296            move_object,
297            Owner::ObjectOwner(SUI_ACCUMULATOR_ROOT_ADDRESS.into()),
298            TransactionDigest::genesis_marker(),
299        )
300    }
301}
302
303/// Extract stream id from an accumulator event if it targets sui::accumulator_settlement::EventStreamHead
304pub fn stream_id_from_accumulator_event(ev: &AccumulatorEvent) -> Option<SuiAddress> {
305    if let TypeTag::Struct(tag) = &ev.write.address.ty
306        && tag.address == SUI_FRAMEWORK_ADDRESS
307        && tag.module.as_ident_str() == ACCUMULATOR_SETTLEMENT_MODULE
308        && tag.name.as_ident_str() == ACCUMULATOR_SETTLEMENT_EVENT_STREAM_HEAD
309    {
310        return Some(ev.write.address.address);
311    }
312    None
313}
314
315impl TryFrom<&MoveObject> for AccumulatorValue {
316    type Error = SuiError;
317    fn try_from(value: &MoveObject) -> Result<Self, Self::Error> {
318        let (_key, value): (AccumulatorKey, AccumulatorValue) = value.try_into()?;
319        Ok(value)
320    }
321}
322
323impl TryFrom<&MoveObject> for (AccumulatorKey, AccumulatorValue) {
324    type Error = SuiError;
325    fn try_from(value: &MoveObject) -> Result<Self, Self::Error> {
326        value
327            .type_()
328            .is_balance_accumulator_field()
329            .then(|| value.to_rust::<Field<AccumulatorKey, U128>>())
330            .flatten()
331            .map(|f| (f.name, AccumulatorValue::U128(f.value)))
332            .ok_or_else(|| {
333                SuiErrorKind::DynamicFieldReadError(format!(
334                    "Dynamic field {:?} is not a AccumulatorValue",
335                    value.id()
336                ))
337                .into()
338            })
339    }
340}
341
342pub fn update_account_balance_for_testing(account_object: &mut Object, balance_change: i128) {
343    let current_balance_field = DynamicFieldObject::<AccumulatorKey>::new(account_object.clone())
344        .load_field::<U128>()
345        .unwrap();
346
347    let current_balance = current_balance_field.value.value;
348
349    assert!(current_balance <= i128::MAX as u128);
350    assert!(current_balance as i128 >= balance_change.abs());
351
352    let new_balance = U128 {
353        value: (current_balance as i128 + balance_change) as u128,
354    };
355
356    let new_field = serialize_dynamic_field(
357        &current_balance_field.id,
358        &current_balance_field.name,
359        new_balance,
360    )
361    .unwrap();
362
363    let move_object = account_object.data.try_as_move_mut().unwrap();
364    move_object.set_contents_unsafe(new_field);
365}
366
367pub(crate) fn accumulator_value_balance_type_maybe(s: &StructTag) -> Option<TypeTag> {
368    if s.address == SUI_FRAMEWORK_ADDRESS
369        && s.module.as_ident_str() == DYNAMIC_FIELD_MODULE_NAME
370        && s.name.as_ident_str() == DYNAMIC_FIELD_FIELD_STRUCT_NAME
371        && s.type_params.len() == 2
372        && let Some(key_type) = accumulator_key_type_maybe(&s.type_params[0])
373        && is_accumulator_u128(&s.type_params[1])
374    {
375        Balance::maybe_get_balance_type_param(&key_type)
376    } else {
377        None
378    }
379}
380
381/// Check if a TypeTag is Key<Balance<T>>
382pub(crate) fn accumulator_key_type_maybe(t: &TypeTag) -> Option<TypeTag> {
383    if let TypeTag::Struct(s) = t
384        && s.address == SUI_FRAMEWORK_ADDRESS
385        && s.module.as_ident_str() == ACCUMULATOR_ROOT_MODULE
386        && s.name.as_ident_str() == ACCUMULATOR_KEY_TYPE
387        && s.type_params.len() == 1
388    {
389        Some(s.type_params[0].clone())
390    } else {
391        None
392    }
393}
394
395/// Check if a TypeTag is U128 from accumulator module
396pub(crate) fn is_accumulator_u128(t: &TypeTag) -> bool {
397    if let TypeTag::Struct(s) = t {
398        s.address == SUI_FRAMEWORK_ADDRESS
399            && s.module.as_ident_str() == ACCUMULATOR_ROOT_MODULE
400            && s.name.as_ident_str() == ACCUMULATOR_U128_TYPE
401            && s.type_params.is_empty()
402    } else {
403        false
404    }
405}
406
407/// Rust representation of the Move EventStreamHead struct from accumulator_settlement module.
408/// This represents the state of an authenticated event stream head stored on-chain.
409#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
410pub struct EventStreamHead {
411    /// The MMR (Merkle Mountain Range) digest representing the accumulated events
412    pub mmr: Vec<U256>,
413    /// The checkpoint sequence number when this stream head was last updated
414    pub checkpoint_seq: u64,
415    /// The total number of events accumulated in this stream
416    pub num_events: u64,
417}
418
419impl Default for EventStreamHead {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425impl EventStreamHead {
426    pub fn new() -> Self {
427        Self {
428            mmr: vec![],
429            checkpoint_seq: 0,
430            num_events: 0,
431        }
432    }
433
434    pub fn num_events(&self) -> u64 {
435        self.num_events
436    }
437
438    pub fn checkpoint_seq(&self) -> u64 {
439        self.checkpoint_seq
440    }
441
442    pub fn mmr(&self) -> &Vec<U256> {
443        &self.mmr
444    }
445}
446
447pub fn derive_event_stream_head_object_id(stream_id: SuiAddress) -> SuiResult<ObjectID> {
448    let key = AccumulatorKey { owner: stream_id };
449
450    let value_type_tag = TypeTag::Struct(Box::new(StructTag {
451        address: SUI_FRAMEWORK_ADDRESS,
452        module: ACCUMULATOR_SETTLEMENT_MODULE.to_owned(),
453        name: ACCUMULATOR_SETTLEMENT_EVENT_STREAM_HEAD.to_owned(),
454        type_params: vec![],
455    }));
456
457    let key_type_tag = AccumulatorKey::get_type_tag(&[value_type_tag]);
458
459    DynamicFieldKey(SUI_ACCUMULATOR_ROOT_OBJECT_ID, key, key_type_tag)
460        .into_unbounded_id()
461        .map(|id| id.as_object_id())
462}
463
464#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
465pub struct EventCommitment {
466    pub checkpoint_seq: u64,
467    pub transaction_idx: u64,
468    pub event_idx: u64,
469    pub digest: Digest,
470}
471
472impl EventCommitment {
473    pub fn new(checkpoint_seq: u64, transaction_idx: u64, event_idx: u64, digest: Digest) -> Self {
474        Self {
475            checkpoint_seq,
476            transaction_idx,
477            event_idx,
478            digest,
479        }
480    }
481}
482
483impl PartialOrd for EventCommitment {
484    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
485        Some(self.cmp(other))
486    }
487}
488
489impl Ord for EventCommitment {
490    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
491        (self.checkpoint_seq, self.transaction_idx, self.event_idx).cmp(&(
492            other.checkpoint_seq,
493            other.transaction_idx,
494            other.event_idx,
495        ))
496    }
497}
498
499pub fn build_event_merkle_root(events: &[EventCommitment]) -> Digest {
500    use fastcrypto::hash::Blake2b256;
501    use fastcrypto::merkle::MerkleTree;
502
503    debug_assert!(
504        events.windows(2).all(|w| w[0] <= w[1]),
505        "Events must be ordered by (checkpoint_seq, transaction_idx, event_idx)"
506    );
507
508    let merkle_tree = MerkleTree::<Blake2b256>::build_from_unserialized(events.to_vec())
509        .expect("failed to serialize event commitments for merkle root");
510    let root_node = merkle_tree.root();
511    let root_digest = root_node.bytes();
512    Digest::new(root_digest)
513}