Skip to main content

sui_sdk_types/
framework.rs

1//! Rust definitions of move/sui framework types.
2
3use super::Address;
4use super::Object;
5use super::TypeTag;
6use std::borrow::Cow;
7
8#[cfg(feature = "unstable")]
9use super::Digest;
10#[cfg(feature = "unstable")]
11use super::U256;
12
13#[derive(Debug, Clone)]
14pub struct Coin<'a> {
15    coin_type: Cow<'a, TypeTag>,
16    id: Address,
17    balance: u64,
18}
19
20impl<'a> Coin<'a> {
21    pub fn coin_type(&self) -> &TypeTag {
22        &self.coin_type
23    }
24
25    pub fn id(&self) -> &Address {
26        &self.id
27    }
28
29    pub fn balance(&self) -> u64 {
30        self.balance
31    }
32
33    pub fn try_from_object(object: &'a Object) -> Option<Self> {
34        match &object.data {
35            super::ObjectData::Struct(move_struct) => {
36                let coin_type = move_struct.type_.is_coin()?;
37
38                let contents = &move_struct.contents;
39                if contents.len() != Address::LENGTH + std::mem::size_of::<u64>() {
40                    return None;
41                }
42
43                let id = Address::new((&contents[..Address::LENGTH]).try_into().unwrap());
44                let balance =
45                    u64::from_le_bytes((&contents[Address::LENGTH..]).try_into().unwrap());
46
47                Some(Self {
48                    coin_type: Cow::Borrowed(coin_type),
49                    id,
50                    balance,
51                })
52            }
53            _ => None, // package
54        }
55    }
56
57    pub fn into_owned(self) -> Coin<'static> {
58        Coin {
59            coin_type: Cow::Owned(self.coin_type.into_owned()),
60            id: self.id,
61            balance: self.balance,
62        }
63    }
64}
65
66/// A commitment to a single event for inclusion in an authenticated event
67/// stream's Merkle Mountain Range.
68///
69/// Each leaf of the per-checkpoint merkle tree is the BCS encoding of this
70/// struct. The four fields together identify the event's position in the
71/// ledger and bind it to its content:
72///
73/// - `checkpoint_seq`: the checkpoint containing the emitting transaction.
74/// - `transaction_idx`: the emitting transaction's 0-based index within its
75///   checkpoint (user transactions are numbered first, settlement transactions
76///   continue the same sequence).
77/// - `event_idx`: the event's 0-based index within its transaction's event
78///   list.
79/// - `digest`: the per-event digest, `BLAKE2b-256(BCS of `[`Event`])`.
80///
81/// Ordering is lexicographic over `(checkpoint_seq, transaction_idx,
82/// event_idx)` only; the per-event digest is not part of the comparison since
83/// the positional tuple already uniquely identifies an event.
84///
85/// Mirrors `sui::accumulator_settlement::EventCommitment` on the Move side.
86///
87/// [`Event`]: crate::Event
88///
89/// # BCS
90///
91/// ```text
92/// event-commitment = u64 u64 u64 digest
93/// ```
94#[cfg(feature = "unstable")]
95#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
96#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
97#[cfg_attr(
98    feature = "serde",
99    derive(serde_derive::Serialize, serde_derive::Deserialize)
100)]
101pub struct EventCommitment {
102    pub checkpoint_seq: u64,
103    pub transaction_idx: u64,
104    pub event_idx: u64,
105    pub digest: Digest,
106}
107
108#[cfg(feature = "unstable")]
109impl PartialOrd for EventCommitment {
110    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
111        Some(self.cmp(other))
112    }
113}
114
115#[cfg(feature = "unstable")]
116impl Ord for EventCommitment {
117    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
118        (self.checkpoint_seq, self.transaction_idx, self.event_idx).cmp(&(
119            other.checkpoint_seq,
120            other.transaction_idx,
121            other.event_idx,
122        ))
123    }
124}
125
126/// On-chain head of an authenticated event stream.
127///
128/// The framework maintains one of these per event stream as a dynamic field
129/// on the accumulator root object (`0xacc`), keyed by
130/// `accumulator::Key<accumulator_settlement::EventStreamHead> { owner:
131/// stream_id }`. Use [`derive_event_stream_head_object_id`] to compute the
132/// dynamic field's object id from the stream id.
133///
134/// Each settlement transaction that processes events for the stream folds a
135/// per-checkpoint merkle tree root into the MMR using carry-propagation. The
136/// framework guarantees at most one such settlement per stream per
137/// checkpoint, so [`checkpoint_seq`] strictly identifies which checkpoint the
138/// head reflects.
139///
140/// Mirrors `sui::accumulator_settlement::EventStreamHead`.
141///
142/// [`checkpoint_seq`]: Self::checkpoint_seq
143///
144/// # BCS
145///
146/// ```text
147/// event-stream-head = vector u256 u64 u64
148/// ```
149#[cfg(feature = "unstable")]
150#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
151#[derive(Clone, Debug, Default, PartialEq, Eq)]
152#[cfg_attr(
153    feature = "serde",
154    derive(serde_derive::Serialize, serde_derive::Deserialize)
155)]
156pub struct EventStreamHead {
157    /// Merkle Mountain Range peaks, ordered from the lowest level upward.
158    /// Empty slots hold `U256::ZERO`; the vector grows by one slot whenever
159    /// the carry propagates past the current highest level.
160    pub mmr: Vec<U256>,
161    /// The latest checkpoint whose events have been folded into the MMR.
162    pub checkpoint_seq: u64,
163    /// Total number of events ever folded into the MMR.
164    pub num_events: u64,
165}
166
167/// Compute the object id of the [`EventStreamHead`] dynamic field for a
168/// given stream.
169///
170/// The framework stores each stream's head as a dynamic field on the
171/// accumulator root object (`0xacc`). The field is keyed by
172/// `sui::accumulator::Key<sui::accumulator_settlement::EventStreamHead>`,
173/// with the `owner` field of `Key` set to the stream id. This helper
174/// reproduces that derivation so a client can fetch the head via an OCS
175/// inclusion proof anchored to a verified checkpoint.
176///
177/// The BCS encoding of `Key { owner: stream_id }` is identical to that of
178/// the bare 32-byte address, since `Key` is a single-field struct over the
179/// owner.
180///
181/// ```
182/// use sui_sdk_types::Address;
183/// use sui_sdk_types::framework::derive_event_stream_head_object_id;
184///
185/// // Pinned interop vector cross-verified against
186/// // `sui_types::accumulator_root::derive_event_stream_head_object_id`.
187/// let stream_id = Address::ZERO;
188/// let object_id = derive_event_stream_head_object_id(stream_id);
189/// assert_eq!(
190///     object_id,
191///     Address::from_static("0x9461a724d957b41485e094fdced6c668bd388070108dbfbdc12277ad68a2717f"),
192/// );
193/// ```
194#[cfg(feature = "unstable")]
195#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
196pub fn derive_event_stream_head_object_id(stream_id: Address) -> Address {
197    use super::Identifier;
198    use super::StructTag;
199
200    // Parent: the accumulator root object at `0xacc`.
201    let accumulator_root = const { Address::from_static("0xacc") };
202
203    // Value type: `sui::accumulator_settlement::EventStreamHead`. Captured
204    // inline rather than via a dedicated constant since this is the only
205    // call site that needs it.
206    let value_type = StructTag::new(
207        Address::TWO,
208        Identifier::from_static("accumulator_settlement"),
209        Identifier::from_static("EventStreamHead"),
210        vec![],
211    );
212
213    // Key type: `sui::accumulator::Key<EventStreamHead>`.
214    let key_type_tag: TypeTag = StructTag::new(
215        Address::TWO,
216        Identifier::from_static("accumulator"),
217        Identifier::from_static("Key"),
218        vec![value_type.into()],
219    )
220    .into();
221
222    // Key bytes: BCS of `AccumulatorKey { owner: stream_id }` reduces to
223    // the 32 raw address bytes because BCS encodes a single-field struct
224    // identically to the bare field.
225    accumulator_root.derive_dynamic_child_id(&key_type_tag, stream_id.as_ref())
226}
227
228/// Build the per-checkpoint merkle root over an ordered slice of event
229/// commitments.
230///
231/// Each leaf is the BCS encoding of an [`EventCommitment`], hashed through
232/// the standard Blake2b256 leaf/inner-prefix scheme defined in
233/// [`crate::merkle`]. The output matches the root the framework computes
234/// when sealing a per-checkpoint event tree.
235///
236/// Callers must provide `commitments` pre-sorted by
237/// `(checkpoint_seq, transaction_idx, event_idx)`. The framework folds the
238/// sorted sequence into its MMR, so an out-of-order input would yield a
239/// root that fails reconciliation. Debug builds verify the ordering with a
240/// `debug_assert!`; in release builds, ordering is the caller's
241/// responsibility.
242///
243/// An empty input yields the all-zero "empty" root from [`crate::merkle`].
244/// This is a degenerate case the framework never produces — there is no
245/// merkle tree to fold when a checkpoint has no events for a stream — but
246/// the function itself is defined for any input length so it can be used
247/// as a primitive elsewhere.
248#[cfg(feature = "unstable")]
249#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
250pub fn build_event_merkle_root(commitments: &[EventCommitment]) -> Digest {
251    debug_assert!(
252        commitments.windows(2).all(|w| w[0] <= w[1]),
253        "EventCommitments must be sorted by (checkpoint_seq, transaction_idx, event_idx)",
254    );
255    let tree = crate::merkle::MerkleTree::build_from_unserialized(commitments.iter())
256        .expect("EventCommitment BCS encoding is infallible");
257    Digest::new(tree.root().bytes())
258}
259
260/// A non-empty group of event commitments folded into an [`EventStreamHead`]
261/// as a single MMR update.
262///
263/// The framework guarantees at most one accumulator settlement per
264/// `(checkpoint, stream)` pair, so callers grouping incoming events by
265/// `checkpoint_seq` reproduce the on-chain batching exactly. Within a
266/// batch, every commitment's `checkpoint_seq` must equal the batch's, and
267/// commitments must be sorted by their positional tuple
268/// `(checkpoint_seq, transaction_idx, event_idx)`.
269#[cfg(feature = "unstable")]
270#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
271#[derive(Clone, Debug, PartialEq, Eq)]
272pub struct EventBatch {
273    /// The checkpoint sequence number that produced these commitments.
274    pub checkpoint_seq: u64,
275    /// Commitments to fold, sorted by their positional tuple. Must be
276    /// non-empty and every commitment's `checkpoint_seq` must equal
277    /// [`Self::checkpoint_seq`].
278    pub commitments: Vec<EventCommitment>,
279}
280
281/// Reasons [`apply_stream_updates`] can reject a batch.
282#[cfg(feature = "unstable")]
283#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
284#[derive(Clone, Debug, PartialEq, Eq)]
285#[non_exhaustive]
286pub enum ApplyStreamError {
287    /// A batch contained no commitments. The framework never folds an
288    /// empty batch into the MMR, so a verifier should not produce one.
289    EmptyBatch {
290        /// Index into the `batches` slice where the empty batch was found.
291        batch_index: usize,
292    },
293
294    /// A commitment's `checkpoint_seq` field disagreed with the enclosing
295    /// batch's `checkpoint_seq`. A batch corresponds to a single settlement
296    /// transaction; all of its commitments share the settlement's
297    /// checkpoint.
298    CommitmentCheckpointMismatch {
299        /// Index into the `batches` slice.
300        batch_index: usize,
301        /// Index into the offending batch's `commitments` vector.
302        commitment_index: usize,
303        /// The batch's authoritative checkpoint sequence number.
304        batch_checkpoint_seq: u64,
305        /// The commitment's contradictory checkpoint sequence number.
306        commitment_checkpoint_seq: u64,
307    },
308
309    /// A batch's `checkpoint_seq` was strictly less than the head's last
310    /// folded checkpoint. Batches must be applied in monotonic order.
311    NonMonotonicCheckpoint {
312        /// Index into the `batches` slice.
313        batch_index: usize,
314        /// The head's `checkpoint_seq` before this batch was attempted.
315        previous_checkpoint_seq: u64,
316        /// The offending batch's `checkpoint_seq`.
317        batch_checkpoint_seq: u64,
318    },
319}
320
321#[cfg(feature = "unstable")]
322impl std::fmt::Display for ApplyStreamError {
323    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
324        match self {
325            Self::EmptyBatch { batch_index } => {
326                write!(f, "batch {batch_index} is empty")
327            }
328            Self::CommitmentCheckpointMismatch {
329                batch_index,
330                commitment_index,
331                batch_checkpoint_seq,
332                commitment_checkpoint_seq,
333            } => write!(
334                f,
335                "batch {batch_index} declares checkpoint {batch_checkpoint_seq} \
336                 but commitment {commitment_index} carries checkpoint \
337                 {commitment_checkpoint_seq}",
338            ),
339            Self::NonMonotonicCheckpoint {
340                batch_index,
341                previous_checkpoint_seq,
342                batch_checkpoint_seq,
343            } => write!(
344                f,
345                "batch {batch_index} at checkpoint {batch_checkpoint_seq} would \
346                 regress the head's checkpoint {previous_checkpoint_seq}",
347            ),
348        }
349    }
350}
351
352#[cfg(feature = "unstable")]
353impl std::error::Error for ApplyStreamError {}
354
355/// Fold a sequence of `batches` into `head`, returning the updated head.
356///
357/// For each batch in order, this:
358///
359/// 1. Computes the merkle root over the batch's commitments via
360///    [`build_event_merkle_root`].
361/// 2. Reinterprets the 32-byte root as a [`U256`] in little-endian.
362/// 3. Folds the result into `head.mmr` using BLAKE2b-256
363///    carry-propagation: while the lowest empty slot is occupied, hash
364///    that peak with the carry and try the next slot up; otherwise drop
365///    the carry into the slot. If the carry propagates past the highest
366///    occupied level, push a new slot.
367/// 4. Advances `head.checkpoint_seq` to the batch's checkpoint and
368///    increments `head.num_events` by the number of commitments folded.
369///
370/// The MMR fold matches the on-chain Move implementation byte-for-byte —
371/// see `test_mmr_digest_compat_with_rust` under
372/// `sui-framework/sources/accumulator_settlement.move`. Any divergence
373/// here breaks reconciliation against fetched [`EventStreamHead`]
374/// objects.
375#[cfg(feature = "unstable")]
376#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
377pub fn apply_stream_updates(
378    head: EventStreamHead,
379    batches: &[EventBatch],
380) -> Result<EventStreamHead, ApplyStreamError> {
381    let mut head = head;
382    for (batch_index, batch) in batches.iter().enumerate() {
383        if batch.commitments.is_empty() {
384            return Err(ApplyStreamError::EmptyBatch { batch_index });
385        }
386        for (commitment_index, commitment) in batch.commitments.iter().enumerate() {
387            if commitment.checkpoint_seq != batch.checkpoint_seq {
388                return Err(ApplyStreamError::CommitmentCheckpointMismatch {
389                    batch_index,
390                    commitment_index,
391                    batch_checkpoint_seq: batch.checkpoint_seq,
392                    commitment_checkpoint_seq: commitment.checkpoint_seq,
393                });
394            }
395        }
396        // The head has not been folded into yet when `num_events == 0`, so
397        // any starting checkpoint is acceptable; only subsequent batches
398        // must be monotonic relative to the prior fold.
399        if head.num_events != 0 && batch.checkpoint_seq < head.checkpoint_seq {
400            return Err(ApplyStreamError::NonMonotonicCheckpoint {
401                batch_index,
402                previous_checkpoint_seq: head.checkpoint_seq,
403                batch_checkpoint_seq: batch.checkpoint_seq,
404            });
405        }
406
407        let root_digest = build_event_merkle_root(&batch.commitments);
408        let merkle_root = U256::from_digits(root_digest.into_inner());
409        fold_into_mmr(&mut head.mmr, merkle_root);
410        head.num_events += batch.commitments.len() as u64;
411        head.checkpoint_seq = batch.checkpoint_seq;
412    }
413    Ok(head)
414}
415
416/// MMR carry-propagation fold.
417///
418/// Walks `mmr` from the lowest level upward. If a slot is empty
419/// (`U256::ZERO`), drop the carry into it and stop. Otherwise hash the
420/// existing peak together with the carry, clear the slot, and continue
421/// with the result as the new carry at the next level up. If the carry
422/// propagates past the highest occupied slot, append a new one.
423#[cfg(feature = "unstable")]
424fn fold_into_mmr(mmr: &mut Vec<U256>, mut carry: U256) {
425    let mut i = 0;
426    while i < mmr.len() {
427        if mmr[i] == U256::ZERO {
428            mmr[i] = carry;
429            return;
430        }
431        carry = hash_two_to_one(mmr[i], carry);
432        mmr[i] = U256::ZERO;
433        i += 1;
434    }
435    mmr.push(carry);
436}
437
438/// Compute `BLAKE2b-256(bcs(left) || bcs(right))` and reinterpret the
439/// 32-byte digest as a `U256` in little-endian.
440///
441/// `U256`'s BCS encoding is its 32 little-endian bytes verbatim (see
442/// [`U256`]'s `Serialize` impl), so `digits()` is byte-equivalent to
443/// `bcs::to_bytes(&u256)` and we avoid two throwaway allocations per
444/// fold by hashing the digit slices directly.
445#[cfg(feature = "unstable")]
446fn hash_two_to_one(left: U256, right: U256) -> U256 {
447    use super::hash::Hasher;
448
449    let mut hasher = Hasher::new();
450    hasher.update(left.digits());
451    hasher.update(right.digits());
452    U256::from_digits(hasher.finalize().into_inner())
453}
454
455#[cfg(test)]
456#[cfg(feature = "unstable")]
457mod test {
458    use super::*;
459
460    // Pinned BCS encoding of an `EventCommitment`. The shape must agree with
461    // `sui::accumulator_settlement::EventCommitment` upstream byte-for-byte,
462    // since this is what the framework hashes to produce the per-checkpoint
463    // merkle root.
464    #[test]
465    fn event_commitment_bcs_shape() {
466        let commitment = EventCommitment {
467            checkpoint_seq: 0x0102030405060708,
468            transaction_idx: 0x1112131415161718,
469            event_idx: 0x2122232425262728,
470            digest: Digest::new([0xaa; 32]),
471        };
472
473        let mut expected = Vec::new();
474        expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
475        expected.extend_from_slice(&0x1112131415161718u64.to_le_bytes());
476        expected.extend_from_slice(&0x2122232425262728u64.to_le_bytes());
477        // The `Digest` BCS shape is length-prefixed: a single ULEB128 byte
478        // `0x20` (=32) followed by the 32 digest bytes.
479        expected.push(0x20);
480        expected.extend_from_slice(&[0xaa; 32]);
481
482        let bytes = bcs::to_bytes(&commitment).unwrap();
483        assert_eq!(bytes, expected);
484        assert_eq!(bytes.len(), 8 + 8 + 8 + 33);
485
486        let back: EventCommitment = bcs::from_bytes(&bytes).unwrap();
487        assert_eq!(back, commitment);
488    }
489
490    // `EventCommitment` ordering matches the ledger-position tuple
491    // `(checkpoint_seq, transaction_idx, event_idx)` and ignores the digest
492    // so that two distinct digests sharing the same position compare equal
493    // under the ordering. This mirrors the upstream `Ord` impl, which is
494    // what the framework relies on when sorting commitments before
495    // building the per-checkpoint merkle tree.
496    #[test]
497    fn event_commitment_ord_ignores_digest() {
498        let a = EventCommitment {
499            checkpoint_seq: 1,
500            transaction_idx: 2,
501            event_idx: 3,
502            digest: Digest::new([0x00; 32]),
503        };
504        let b = EventCommitment {
505            digest: Digest::new([0xff; 32]),
506            ..a
507        };
508        assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
509
510        let c = EventCommitment { event_idx: 4, ..a };
511        assert!(a < c);
512    }
513
514    // Pinned BCS encoding of an `EventStreamHead`. As with
515    // `EventCommitment`, the framework reads/writes this struct byte-for-byte
516    // on chain; any divergence here breaks every consumer's ability to
517    // reconcile a locally-replayed MMR against a fetched head.
518    #[test]
519    fn event_stream_head_bcs_shape() {
520        let head = EventStreamHead {
521            mmr: vec![U256::ZERO, U256::ONE],
522            checkpoint_seq: 0x4142434445464748,
523            num_events: 0x5152535455565758,
524        };
525
526        let mut expected = Vec::new();
527        // `mmr`: ULEB128 length (=2), then two 32-byte little-endian `u256`s.
528        expected.push(0x02);
529        expected.extend_from_slice(&[0u8; 32]);
530        let mut one_le = [0u8; 32];
531        one_le[0] = 1;
532        expected.extend_from_slice(&one_le);
533        expected.extend_from_slice(&0x4142434445464748u64.to_le_bytes());
534        expected.extend_from_slice(&0x5152535455565758u64.to_le_bytes());
535
536        let bytes = bcs::to_bytes(&head).unwrap();
537        assert_eq!(bytes, expected);
538
539        let back: EventStreamHead = bcs::from_bytes(&bytes).unwrap();
540        assert_eq!(back, head);
541    }
542
543    #[test]
544    fn event_stream_head_default_is_empty() {
545        let head = EventStreamHead::default();
546        assert!(head.mmr.is_empty());
547        assert_eq!(head.checkpoint_seq, 0);
548        assert_eq!(head.num_events, 0);
549    }
550
551    // Cross-implementation pin: the expected merkle root was captured by
552    // running `sui_types::accumulator_root::build_event_merkle_root`
553    // upstream on the same three commitments. A regression in either the
554    // `EventCommitment` BCS shape or the underlying merkle tree
555    // construction (leaf/inner prefix, padding, hash function) would shift
556    // this digest. The `merkle` module's root construction and
557    // `event_commitment_bcs_shape` above already pin those pieces against
558    // their respective upstream sources, so this test seals the
559    // composition end-to-end.
560    #[test]
561    fn build_event_merkle_root_pinned_vector() {
562        let commitments = vec![
563            EventCommitment {
564                checkpoint_seq: 1,
565                transaction_idx: 0,
566                event_idx: 0,
567                digest: Digest::new([0x11; 32]),
568            },
569            EventCommitment {
570                checkpoint_seq: 1,
571                transaction_idx: 0,
572                event_idx: 1,
573                digest: Digest::new([0x22; 32]),
574            },
575            EventCommitment {
576                checkpoint_seq: 1,
577                transaction_idx: 1,
578                event_idx: 0,
579                digest: Digest::new([0x33; 32]),
580            },
581        ];
582        const EXPECTED: [u8; 32] = [
583            254, 183, 87, 247, 72, 14, 90, 116, 221, 195, 244, 87, 250, 236, 226, 161, 99, 106,
584            199, 246, 85, 138, 180, 110, 112, 50, 103, 77, 160, 104, 239, 61,
585        ];
586        assert_eq!(build_event_merkle_root(&commitments).into_inner(), EXPECTED);
587    }
588
589    #[test]
590    fn build_event_merkle_root_empty_input_is_empty_node() {
591        // Documented degenerate case: the merkle tree over zero leaves is the
592        // all-zero "empty" node. `apply_stream_updates` rejects this so a
593        // verifier would never reach it via the normal path, but the helper
594        // is defined for arbitrary input and should not panic.
595        assert_eq!(build_event_merkle_root(&[]).into_inner(), [0u8; 32]);
596    }
597
598    // Cross-implementation pin: each `(stream_id, object_id)` pair was
599    // captured by running upstream's
600    // `sui_types::accumulator_root::derive_event_stream_head_object_id`
601    // directly on the listed stream id. The derivation composes
602    // `Address::derive_dynamic_child_id` (validated against the Move-side
603    // snapshot test) with a fixed `StructTag`, so a regression here would
604    // most likely indicate an unintended change to either the type tag or
605    // the parent accumulator root address.
606    #[test]
607    fn derive_event_stream_head_object_id_pinned_vectors() {
608        let cases: &[(Address, Address)] = &[
609            (
610                Address::ZERO,
611                Address::from_static(
612                    "0x9461a724d957b41485e094fdced6c668bd388070108dbfbdc12277ad68a2717f",
613                ),
614            ),
615            (
616                Address::TWO,
617                Address::from_static(
618                    "0x1b877f5c7664df8957f127a95d1b2c8c1c239fd49566f9f69205df44133fc37f",
619                ),
620            ),
621            (
622                Address::from_static("0xacc"),
623                Address::from_static(
624                    "0x452652326e8df295af20a4e0744acac9a74f87d93ba976dd97d3e93e1a542e37",
625                ),
626            ),
627            (
628                Address::from_static("0x42424242"),
629                Address::from_static(
630                    "0xdbe2cd3f24c357c434991a4348e6ceb43bcb7d22696eb6797a6739e5351cb149",
631                ),
632            ),
633        ];
634        for (stream_id, expected) in cases {
635            assert_eq!(
636                derive_event_stream_head_object_id(*stream_id),
637                *expected,
638                "mismatch for stream id {stream_id}",
639            );
640        }
641    }
642
643    fn u256_from_decimal(s: &str) -> U256 {
644        s.parse().expect("decimal U256 literal must parse")
645    }
646
647    fn single_event_batch(checkpoint_seq: u64, digest_byte: u8) -> EventBatch {
648        EventBatch {
649            checkpoint_seq,
650            commitments: vec![EventCommitment {
651                checkpoint_seq,
652                transaction_idx: 0,
653                event_idx: 0,
654                digest: Digest::new([digest_byte; 32]),
655            }],
656        }
657    }
658
659    // Load-bearing interop pin against the Move test
660    // `test_mmr_digest_compat_with_rust` in
661    // `sui-framework/sources/accumulator_settlement.move`. Inserting
662    // `U256::from(50..58)` as eight successive carries must collapse to a
663    // single peak at level 3 with the exact decimal value below; the lower
664    // three slots are zeroed. Any divergence in the carry-propagation
665    // loop or the `BLAKE2b-256(bcs(left) || bcs(right))` two-to-one step
666    // breaks reconciliation against on-chain `EventStreamHead` objects.
667    #[test]
668    fn fold_into_mmr_matches_move_compat_fixture() {
669        let mut mmr = Vec::new();
670        for value in 50u64..58 {
671            fold_into_mmr(&mut mmr, U256::from(value));
672        }
673        assert_eq!(mmr.len(), 4);
674        assert_eq!(mmr[0], U256::ZERO);
675        assert_eq!(mmr[1], U256::ZERO);
676        assert_eq!(mmr[2], U256::ZERO);
677        assert_eq!(
678            mmr[3],
679            u256_from_decimal(
680                "69725770072863840208899320192042305265295220676851872214494910464384102654361",
681            ),
682        );
683    }
684
685    // Two carries collapse into one peak at level 1, leaving level 0
686    // empty. Stand-alone sanity check that the carry propagation is wired
687    // correctly even for tiny inputs.
688    #[test]
689    fn fold_into_mmr_two_inserts_collapse_to_level_one() {
690        let mut mmr = Vec::new();
691        fold_into_mmr(&mut mmr, U256::from(7u64));
692        assert_eq!(mmr, vec![U256::from(7u64)]);
693
694        fold_into_mmr(&mut mmr, U256::from(11u64));
695        assert_eq!(mmr.len(), 2);
696        assert_eq!(mmr[0], U256::ZERO);
697        assert_eq!(mmr[1], hash_two_to_one(U256::from(7u64), U256::from(11u64)));
698    }
699
700    // End-to-end interop pin: feed a 3-commitment batch through
701    // `apply_stream_updates` starting from an empty head and compare to
702    // the `EventStreamHead` produced by
703    // `sui_light_client::authenticated_events::mmr::apply_stream_updates`
704    // upstream on the identical input.
705    #[test]
706    fn apply_stream_updates_single_batch_matches_upstream() {
707        let batch = EventBatch {
708            checkpoint_seq: 1,
709            commitments: vec![
710                EventCommitment {
711                    checkpoint_seq: 1,
712                    transaction_idx: 0,
713                    event_idx: 0,
714                    digest: Digest::new([0x11; 32]),
715                },
716                EventCommitment {
717                    checkpoint_seq: 1,
718                    transaction_idx: 0,
719                    event_idx: 1,
720                    digest: Digest::new([0x22; 32]),
721                },
722                EventCommitment {
723                    checkpoint_seq: 1,
724                    transaction_idx: 1,
725                    event_idx: 0,
726                    digest: Digest::new([0x33; 32]),
727                },
728            ],
729        };
730        let head = apply_stream_updates(EventStreamHead::default(), &[batch]).unwrap();
731        assert_eq!(head.checkpoint_seq, 1);
732        assert_eq!(head.num_events, 3);
733        assert_eq!(
734            head.mmr,
735            vec![u256_from_decimal(
736                "28014082315424315761761458464083312323394111104237010481447392654866601457662",
737            )],
738        );
739    }
740
741    // End-to-end interop pin across two sequential batches at different
742    // checkpoints. The merkle root from batch 2 carries up through level 0
743    // and lands at level 1, vacating slot 0.
744    #[test]
745    fn apply_stream_updates_two_batches_match_upstream() {
746        let batch_1 = EventBatch {
747            checkpoint_seq: 1,
748            commitments: vec![
749                EventCommitment {
750                    checkpoint_seq: 1,
751                    transaction_idx: 0,
752                    event_idx: 0,
753                    digest: Digest::new([0x11; 32]),
754                },
755                EventCommitment {
756                    checkpoint_seq: 1,
757                    transaction_idx: 0,
758                    event_idx: 1,
759                    digest: Digest::new([0x22; 32]),
760                },
761                EventCommitment {
762                    checkpoint_seq: 1,
763                    transaction_idx: 1,
764                    event_idx: 0,
765                    digest: Digest::new([0x33; 32]),
766                },
767            ],
768        };
769        let batch_2 = EventBatch {
770            checkpoint_seq: 2,
771            commitments: vec![
772                EventCommitment {
773                    checkpoint_seq: 2,
774                    transaction_idx: 0,
775                    event_idx: 0,
776                    digest: Digest::new([0x44; 32]),
777                },
778                EventCommitment {
779                    checkpoint_seq: 2,
780                    transaction_idx: 0,
781                    event_idx: 1,
782                    digest: Digest::new([0x55; 32]),
783                },
784            ],
785        };
786        let head = apply_stream_updates(EventStreamHead::default(), &[batch_1, batch_2]).unwrap();
787        assert_eq!(head.checkpoint_seq, 2);
788        assert_eq!(head.num_events, 5);
789        assert_eq!(head.mmr.len(), 2);
790        assert_eq!(head.mmr[0], U256::ZERO);
791        assert_eq!(
792            head.mmr[1],
793            u256_from_decimal(
794                "80180905428222716273420959625814881301112107405105460786291242224918309625423",
795            ),
796        );
797    }
798
799    // Four single-event batches drive the carry up through two levels,
800    // leaving levels 0 and 1 empty and one peak at level 2. End-to-end pin
801    // against upstream.
802    #[test]
803    fn apply_stream_updates_four_single_batches_match_upstream() {
804        let batches: Vec<EventBatch> = (1u64..=4)
805            .map(|cp| single_event_batch(cp, cp as u8))
806            .collect();
807        let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
808        assert_eq!(head.checkpoint_seq, 4);
809        assert_eq!(head.num_events, 4);
810        assert_eq!(head.mmr.len(), 3);
811        assert_eq!(head.mmr[0], U256::ZERO);
812        assert_eq!(head.mmr[1], U256::ZERO);
813        assert_eq!(
814            head.mmr[2],
815            u256_from_decimal(
816                "43434128249102587327404298804800250101556402749045331898264216785541514599480",
817            ),
818        );
819    }
820
821    // Folding zero batches into an arbitrary head must return the head
822    // unchanged.
823    #[test]
824    fn apply_stream_updates_no_batches_is_identity() {
825        let head = EventStreamHead {
826            mmr: vec![U256::ONE, U256::ZERO, U256::from(42u64)],
827            checkpoint_seq: 17,
828            num_events: 9,
829        };
830        let out = apply_stream_updates(head.clone(), &[]).unwrap();
831        assert_eq!(out, head);
832    }
833
834    // Same-checkpoint re-application is permitted (a stream can produce
835    // multiple settlement transactions in one checkpoint in unrelated
836    // accumulator object spaces, although per-stream the framework
837    // guarantees only one — the SDK helper still allows equal
838    // `checkpoint_seq` to keep the contract symmetric with strict less-than
839    // being the violation).
840    #[test]
841    fn apply_stream_updates_equal_checkpoint_seq_is_allowed() {
842        let head = apply_stream_updates(
843            EventStreamHead::default(),
844            &[single_event_batch(5, 0x01), single_event_batch(5, 0x02)],
845        )
846        .unwrap();
847        assert_eq!(head.checkpoint_seq, 5);
848        assert_eq!(head.num_events, 2);
849    }
850
851    #[test]
852    fn apply_stream_updates_rejects_empty_batch() {
853        let err = apply_stream_updates(
854            EventStreamHead::default(),
855            &[
856                single_event_batch(1, 0x01),
857                EventBatch {
858                    checkpoint_seq: 2,
859                    commitments: vec![],
860                },
861            ],
862        )
863        .unwrap_err();
864        assert_eq!(err, ApplyStreamError::EmptyBatch { batch_index: 1 });
865    }
866
867    #[test]
868    fn apply_stream_updates_rejects_commitment_checkpoint_mismatch() {
869        let err = apply_stream_updates(
870            EventStreamHead::default(),
871            &[EventBatch {
872                checkpoint_seq: 5,
873                commitments: vec![
874                    EventCommitment {
875                        checkpoint_seq: 5,
876                        transaction_idx: 0,
877                        event_idx: 0,
878                        digest: Digest::new([0x01; 32]),
879                    },
880                    EventCommitment {
881                        checkpoint_seq: 6, // wrong
882                        transaction_idx: 0,
883                        event_idx: 1,
884                        digest: Digest::new([0x02; 32]),
885                    },
886                ],
887            }],
888        )
889        .unwrap_err();
890        assert_eq!(
891            err,
892            ApplyStreamError::CommitmentCheckpointMismatch {
893                batch_index: 0,
894                commitment_index: 1,
895                batch_checkpoint_seq: 5,
896                commitment_checkpoint_seq: 6,
897            },
898        );
899    }
900
901    #[test]
902    fn apply_stream_updates_rejects_non_monotonic_checkpoint() {
903        let err = apply_stream_updates(
904            EventStreamHead::default(),
905            &[single_event_batch(10, 0x01), single_event_batch(9, 0x02)],
906        )
907        .unwrap_err();
908        assert_eq!(
909            err,
910            ApplyStreamError::NonMonotonicCheckpoint {
911                batch_index: 1,
912                previous_checkpoint_seq: 10,
913                batch_checkpoint_seq: 9,
914            },
915        );
916    }
917
918    /// Property-based coverage for the MMR fold and the streaming
919    /// `apply_stream_updates` driver. Gated on `feature = "proptest"`
920    /// to match the rest of the crate's proptest surface.
921    #[cfg(feature = "proptest")]
922    mod proptests {
923        use super::*;
924
925        use proptest::collection::vec;
926        use proptest::prelude::*;
927        use test_strategy::proptest;
928
929        // See the matching comment in `merkle::tests::proptests` for why
930        // this explicit binding is needed on wasm.
931        #[cfg(target_arch = "wasm32")]
932        use wasm_bindgen_test::wasm_bindgen_test as test;
933
934        /// Generate a list of monotonically-increasing batches with at
935        /// least one commitment each. Strict monotonicity (delta >= 1)
936        /// keeps every batch on its own checkpoint, matching the
937        /// "one settlement per `(checkpoint, stream)`" rule the
938        /// framework enforces on-chain.
939        fn monotonic_batches() -> impl Strategy<Value = Vec<EventBatch>> {
940            vec((1u64..=10, 1usize..=4), 0..=8).prop_map(|deltas| {
941                let mut seq = 0u64;
942                let mut batches = Vec::with_capacity(deltas.len());
943                for (delta, n_events) in deltas {
944                    seq += delta;
945                    let commitments: Vec<EventCommitment> = (0..n_events)
946                        .map(|i| EventCommitment {
947                            checkpoint_seq: seq,
948                            transaction_idx: 0,
949                            event_idx: i as u64,
950                            // Mix the seq and index into the digest so
951                            // that distinct commitments produce distinct
952                            // leaves (and therefore distinct merkle
953                            // roots), which keeps the fold's hash
954                            // collisions vanishingly improbable.
955                            digest: {
956                                let mut d = [0u8; 32];
957                                d[..8].copy_from_slice(&seq.to_le_bytes());
958                                d[8..16].copy_from_slice(&(i as u64).to_le_bytes());
959                                Digest::new(d)
960                            },
961                        })
962                        .collect();
963                    batches.push(EventBatch {
964                        checkpoint_seq: seq,
965                        commitments,
966                    });
967                }
968                batches
969            })
970        }
971
972        /// After N non-zero carries are folded, the MMR's non-zero
973        /// peaks land at exactly the set-bit positions of N, and its
974        /// length is `floor(log2(N)) + 1`.
975        ///
976        /// Carries are constrained to `1..=u64::MAX` (mapped through
977        /// `U256::from`) so that no slot is accidentally zeroed by an
978        /// empty insert. Hash collisions to `U256::ZERO` are
979        /// possible in principle but require a 2^-256 birthday hit,
980        /// well outside the property's effective probability budget.
981        #[proptest]
982        fn mmr_popcount_invariant(#[strategy(vec(1u64..=u64::MAX, 0..=20))] carries: Vec<u64>) {
983            let mut mmr = Vec::new();
984            for c in &carries {
985                fold_into_mmr(&mut mmr, U256::from(*c));
986            }
987            let n = carries.len() as u64;
988            let expected_len = if n == 0 {
989                0
990            } else {
991                64 - n.leading_zeros() as usize
992            };
993            prop_assert_eq!(
994                mmr.len(),
995                expected_len,
996                "mmr length must match highest set bit"
997            );
998            for (i, slot) in mmr.iter().enumerate() {
999                let bit_set = (n >> i) & 1 == 1;
1000                if bit_set {
1001                    prop_assert_ne!(
1002                        *slot,
1003                        U256::ZERO,
1004                        "slot {} should be non-zero (bit set in n={})",
1005                        i,
1006                        n,
1007                    );
1008                } else {
1009                    prop_assert_eq!(
1010                        *slot,
1011                        U256::ZERO,
1012                        "slot {} should be zero (bit unset in n={})",
1013                        i,
1014                        n,
1015                    );
1016                }
1017            }
1018        }
1019
1020        /// `apply_stream_updates` is associative over the batch slice:
1021        /// folding all batches in one call must match folding them
1022        /// one at a time, starting from the same `EventStreamHead`.
1023        ///
1024        /// This is the contract a streaming client relies on when it
1025        /// applies batches as they arrive vs. when it replays a
1026        /// captured trace in bulk.
1027        #[proptest]
1028        fn apply_stream_updates_is_associative_over_batches(
1029            #[strategy(monotonic_batches())] batches: Vec<EventBatch>,
1030        ) {
1031            let one_shot = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
1032            let mut step_by_step = EventStreamHead::default();
1033            for batch in &batches {
1034                step_by_step =
1035                    apply_stream_updates(step_by_step, std::slice::from_ref(batch)).unwrap();
1036            }
1037            prop_assert_eq!(one_shot, step_by_step);
1038        }
1039
1040        /// The head's `num_events` after `apply_stream_updates` equals
1041        /// the sum of `commitments.len()` across the input batches
1042        /// (starting from a default head, which has `num_events = 0`).
1043        #[proptest]
1044        fn apply_stream_updates_num_events_is_sum_of_commitments(
1045            #[strategy(monotonic_batches())] batches: Vec<EventBatch>,
1046        ) {
1047            let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
1048            let expected: u64 = batches.iter().map(|b| b.commitments.len() as u64).sum();
1049            prop_assert_eq!(head.num_events, expected);
1050        }
1051
1052        /// `apply_stream_updates` carries the last batch's
1053        /// `checkpoint_seq` into the head verbatim. For an empty input
1054        /// the head's checkpoint is left unchanged at its default
1055        /// value of zero.
1056        #[proptest]
1057        fn apply_stream_updates_advances_checkpoint_seq(
1058            #[strategy(monotonic_batches())] batches: Vec<EventBatch>,
1059        ) {
1060            let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
1061            let expected = batches.last().map(|b| b.checkpoint_seq).unwrap_or(0);
1062            prop_assert_eq!(head.checkpoint_seq, expected);
1063        }
1064    }
1065}