sui_inverted_index/
dimensions.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use sui_types::accumulator_root::stream_id_from_accumulator_event;
5use sui_types::balance::Balance;
6use sui_types::base_types::SuiAddress;
7use sui_types::effects::AccumulatorValue;
8use sui_types::effects::TransactionEffects;
9use sui_types::effects::TransactionEffectsAPI;
10use sui_types::effects::TransactionEvents;
11use sui_types::full_checkpoint_content::ObjectSet;
12use sui_types::object::Owner;
13use sui_types::storage::ObjectKey;
14use sui_types::transaction::TransactionData;
15use sui_types::transaction::TransactionDataAPI;
16
17/// A queryable dimension for the checkpoint inverted index.
18///
19/// Each variant has a unique single-byte tag used as a prefix in row keys,
20/// ensuring no two dimensions can produce the same encoded bytes.
21///
22/// Compound dimensions (MoveCall, EmitModule, EventType) use hierarchical
23/// keys: each prefix level is a valid, independently queryable key. For
24/// example, MoveCall encodes `[pkg_32]`, `[pkg_32][module]`, or
25/// `[pkg_32][module\x00function]` depending on the query specificity.
26/// The 32-byte address/package prefix is fixed-width (no separator needed),
27/// and `\x00` separates variable-length components (safe because Move
28/// identifiers cannot contain null bytes).
29#[repr(u8)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum IndexDimension {
32    Sender = 0x01,
33    /// Any address whose state moved as a side effect of the transaction:
34    /// addresses that own an object after the txn (transfers in / new
35    /// owned objects), addresses that owned an object before the txn that
36    /// was mutated, transferred away, deleted, or wrapped, and addresses
37    /// whose address-balance changed via an accumulator `Balance<T>` event.
38    AffectedAddress = 0x02,
39    AffectedObject = 0x03,
40    /// Compound: `[package_32]` | `[package_32][module]` | `[package_32][module\x00function]`
41    MoveCall = 0x04,
42    /// Compound: `[package_id_32]` | `[package_id_32][module]`
43    EmitModule = 0x05,
44    /// Compound: `[type_address_32]` | `[..][module]` | `[..\x00name]` | `[..\x00name\x00instantiation_bcs]`
45    EventType = 0x06,
46    /// Authenticated event stream head id (the `SuiAddress` keying an
47    /// `accumulator_settlement::EventStreamHead` accumulator). Tx-space matches
48    /// any transaction that wrote to the stream; event-space matches the
49    /// individual events committed to the stream.
50    EventStreamHead = 0x07,
51    /// Internal existence marker (not user-queryable, not exposed via
52    /// `filter.rs`): one bit per real event, recording that an `event_seq`
53    /// position is occupied. Event-space only — it gives the read side a
54    /// concrete universe of real events to subtract from when evaluating
55    /// unanchored negation (`NOT D` == `E \ D`), since the packed `event_seq`
56    /// namespace is far too sparse to synthesize a dense complement. Every real
57    /// event maps to the same singleton key; it carries a single placeholder
58    /// value byte so the encoded key keeps the standard `[tag, value...]` shape.
59    EventExtant = 0x08,
60    /// Internal query-only universe marker for tx-space unanchored negation
61    /// (not user-queryable, never persisted): the tx-seq namespace is dense —
62    /// every integer up to the indexed tip is a real transaction — so backends
63    /// synthesize full buckets for this key at scan time instead of reading
64    /// storage. The filter layer anchors exclude-only tx terms with an include
65    /// on this key, resolving `NOT D` as `range \ D` without an extra
66    /// evaluator code path. No write path emits this dimension.
67    TxUniverse = 0x09,
68    /// Global marker for every Move package write — first publishes and upgrades
69    /// alike, regardless of package. Singleton key (a placeholder byte): every
70    /// package write maps to the same row, so the read side can enumerate all
71    /// package writes chain-wide in checkpoint order without an id to key on.
72    /// Backs the global `Query.packages` listing.
73    AnyPackageWrite = 0x0a,
74}
75
76impl IndexDimension {
77    pub fn tag_byte(self) -> u8 {
78        self as u8
79    }
80
81    pub fn from_tag_byte(tag: u8) -> Option<Self> {
82        match tag {
83            tag if tag == Self::Sender.tag_byte() => Some(Self::Sender),
84            tag if tag == Self::AffectedAddress.tag_byte() => Some(Self::AffectedAddress),
85            tag if tag == Self::AffectedObject.tag_byte() => Some(Self::AffectedObject),
86            tag if tag == Self::MoveCall.tag_byte() => Some(Self::MoveCall),
87            tag if tag == Self::EmitModule.tag_byte() => Some(Self::EmitModule),
88            tag if tag == Self::EventType.tag_byte() => Some(Self::EventType),
89            tag if tag == Self::EventStreamHead.tag_byte() => Some(Self::EventStreamHead),
90            tag if tag == Self::EventExtant.tag_byte() => Some(Self::EventExtant),
91            tag if tag == Self::TxUniverse.tag_byte() => Some(Self::TxUniverse),
92            tag if tag == Self::AnyPackageWrite.tag_byte() => Some(Self::AnyPackageWrite),
93            _ => None,
94        }
95    }
96}
97
98const COMPOUND_VALUE_SEPARATOR: u8 = 0x00;
99
100/// Singleton value for the internal [`IndexDimension::EventExtant`] marker:
101/// every real event maps to the same key. It carries a single placeholder byte
102/// so the encoded row key has the standard `[tag, value...]` shape and passes
103/// `BitmapKey::new`'s length invariant (dimension keys must be at least two
104/// bytes) without a per-tag carve-out. The marker is a singleton — one row per
105/// bucket, not per event — so the extra byte costs ~1 byte per bucket-row.
106pub const EVENT_EXTANT_VALUE: &[u8] = &[0x00];
107
108/// Singleton value for the internal [`IndexDimension::TxUniverse`] marker.
109/// Like [`EVENT_EXTANT_VALUE`], it is a single placeholder byte so the encoded
110/// key has the standard `[tag, value...]` shape and passes `BitmapKey::new`'s
111/// length invariant. The key never reaches storage — backends recognize the
112/// tag at scan time and synthesize full buckets over the requested range.
113pub const TX_UNIVERSE_VALUE: &[u8] = &[0x00];
114
115/// Singleton value for the global [`IndexDimension::AnyPackageWrite`] marker:
116/// every package write maps to the same key. Like [`EVENT_EXTANT_VALUE`], it is
117/// a single placeholder byte so the encoded key keeps the standard
118/// `[tag, value...]` shape and passes `BitmapKey::new`'s length invariant.
119pub const ANY_PACKAGE_VALUE: &[u8] = &[0x00];
120
121/// Visit all tx-space dimensions for a transaction.
122///
123/// `object_set` is used to resolve owners for the input and output states
124/// referenced by `effects.object_changes()`. Callers typically pass either
125/// `&checkpoint.object_set` (archival) or an `ObjectSet` built from the
126/// transaction's input + output objects (fullnode).
127///
128/// The callback is invoked once per logical tx-space dimension candidate as
129/// `f(dimension, key)`, where `key` is the encoded value bytes for that
130/// dimension (the lookup key, without the dimension's tag byte prefix).
131/// Compound dimensions are emitted at every prefix level so queries at any
132/// specificity remain a single key lookup.
133///
134/// This uses a visitor rather than returning owned dimension values so the
135/// extractor can reuse one scratch buffer for all compound keys emitted from a
136/// transaction instead of allocating one `Vec<u8>` per dimension.
137pub fn for_each_transaction_dimension(
138    tx_data: &TransactionData,
139    effects: &TransactionEffects,
140    events: Option<&TransactionEvents>,
141    object_set: &ObjectSet,
142    mut f: impl FnMut(IndexDimension, &[u8]),
143) {
144    let mut scratch = Vec::new();
145    let sender = tx_data.sender();
146
147    f(IndexDimension::Sender, sender.as_ref());
148
149    for change in effects.object_changes() {
150        for version in [change.input_version, change.output_version]
151            .into_iter()
152            .flatten()
153        {
154            let Some(obj) = object_set.get(&ObjectKey(change.id, version)) else {
155                continue;
156            };
157            if let Some(addr) = owner_as_affected_address(obj.owner()) {
158                f(IndexDimension::AffectedAddress, addr);
159            }
160        }
161
162        f(IndexDimension::AffectedObject, change.id.as_ref());
163
164        // Move package writes — first publishes and upgrades alike — emit a
165        // single global marker so the read side can enumerate every package
166        // write chain-wide in checkpoint order.
167        if let Some(sui_types::object::Data::Package(_)) = change
168            .output_version
169            .and_then(|v| object_set.get(&ObjectKey(change.id, v)))
170            .map(|obj| &obj.data)
171        {
172            f(IndexDimension::AnyPackageWrite, ANY_PACKAGE_VALUE);
173        }
174    }
175
176    for (_, package_id, module, function) in tx_data.move_calls() {
177        let pkg = package_id.as_ref();
178
179        scratch.clear();
180        scratch.reserve(pkg.len() + module.len() + 1 + function.len());
181        append_dimension_value_component(&mut scratch, pkg);
182        f(IndexDimension::MoveCall, &scratch);
183
184        append_dimension_value_component(&mut scratch, module.as_bytes());
185        f(IndexDimension::MoveCall, &scratch);
186
187        append_separated_dimension_value_component(&mut scratch, function.as_bytes());
188        f(IndexDimension::MoveCall, &scratch);
189    }
190
191    // Event attributes are also indexed in tx-space so callers can find the
192    // transactions that emitted a given event. The EventExtant existence marker
193    // is event-space only (it backs event negation), so don't forward it here.
194    for_each_event_dimension(sender, effects, events, |_idx, dim, key| {
195        if dim != IndexDimension::EventExtant {
196            f(dim, key);
197        }
198    });
199
200    for acc in effects.accumulator_events() {
201        if Balance::is_balance_type(&acc.write.address.ty)
202            && matches!(&acc.write.value, AccumulatorValue::Integer(_))
203        {
204            f(
205                IndexDimension::AffectedAddress,
206                acc.write.address.address.as_ref(),
207            );
208        }
209    }
210}
211
212/// Visit all event-space dimensions for a transaction.
213///
214/// The callback is invoked as `f(event_idx, dimension, key)` once per logical
215/// event-space dimension candidate, where `key` is the encoded value bytes
216/// for that dimension (the lookup key, without the dimension's tag byte
217/// prefix).
218///
219/// Like [`for_each_transaction_dimension`], this keeps the ownership boundary
220/// inside the visitor call so compound event keys can borrow a reused scratch
221/// buffer while the caller consumes each value synchronously.
222///
223/// Every real event also yields one [`IndexDimension::EventExtant`] candidate
224/// (a singleton existence marker keyed by a placeholder byte) at its own
225/// `event_idx`.
226pub fn for_each_event_dimension(
227    sender: SuiAddress,
228    effects: &TransactionEffects,
229    events: Option<&TransactionEvents>,
230    mut f: impl FnMut(u32, IndexDimension, &[u8]),
231) {
232    let mut scratch = Vec::new();
233    let event_count = events.map(|e| e.data.len()).unwrap_or(0);
234
235    for (idx, ev) in events.iter().flat_map(|evs| evs.data.iter()).enumerate() {
236        let event_idx = u32::try_from(idx).expect("event index exceeds u32::MAX");
237
238        // Existence marker: one bit per real event, independent of its
239        // attributes. Records that this `event_seq` position is occupied so the
240        // read side has a universe `E` to subtract from for event negation.
241        f(event_idx, IndexDimension::EventExtant, EVENT_EXTANT_VALUE);
242
243        f(event_idx, IndexDimension::Sender, sender.as_ref());
244
245        let pkg = ev.package_id.as_ref();
246        let type_addr = ev.type_.address.as_ref();
247        let emit_mod: &str = ev.transaction_module.as_str();
248        let type_mod: &str = ev.type_.module.as_str();
249        let type_name: &str = ev.type_.name.as_str();
250
251        scratch.clear();
252        scratch.reserve(pkg.len() + emit_mod.len());
253        append_dimension_value_component(&mut scratch, pkg);
254        f(event_idx, IndexDimension::EmitModule, &scratch);
255
256        append_dimension_value_component(&mut scratch, emit_mod.as_bytes());
257        f(event_idx, IndexDimension::EmitModule, &scratch);
258
259        scratch.clear();
260        scratch.reserve(type_addr.len() + type_mod.len() + type_name.len() + 2);
261        append_dimension_value_component(&mut scratch, type_addr);
262        f(event_idx, IndexDimension::EventType, &scratch);
263
264        append_dimension_value_component(&mut scratch, type_mod.as_bytes());
265        f(event_idx, IndexDimension::EventType, &scratch);
266
267        append_separated_dimension_value_component(&mut scratch, type_name.as_bytes());
268        f(event_idx, IndexDimension::EventType, &scratch);
269
270        if !ev.type_.type_params.is_empty() {
271            let params_bcs =
272                bcs::to_bytes(&ev.type_.type_params).expect("BCS encoding of type params");
273            append_separated_dimension_value_component(&mut scratch, &params_bcs);
274            f(event_idx, IndexDimension::EventType, &scratch);
275        }
276    }
277
278    for acc in effects.accumulator_events() {
279        let AccumulatorValue::EventDigest(event_digests) = &acc.write.value else {
280            continue;
281        };
282        let Some(stream_id) = stream_id_from_accumulator_event(&acc) else {
283            continue;
284        };
285        for (idx, _digest) in event_digests {
286            let event_idx = u32::try_from(*idx).expect("accumulator event index exceeds u32::MAX");
287            assert!(
288                (*idx as usize) < event_count,
289                "accumulator event references event idx {} but txn emitted only {} events",
290                idx,
291                event_count,
292            );
293            f(
294                event_idx,
295                IndexDimension::EventStreamHead,
296                stream_id.as_ref(),
297            );
298        }
299    }
300}
301
302/// Encode a dimension value into a row key component: `[tag_byte][value_bytes]`.
303pub fn encode_dimension_key(dim: IndexDimension, value: &[u8]) -> Vec<u8> {
304    let mut key = Vec::with_capacity(1 + value.len());
305    write_dimension_key(&mut key, dim, value);
306    key
307}
308
309/// Build a MoveCall compound value at the desired specificity.
310pub fn move_call_value(package: &[u8], module: Option<&str>, function: Option<&str>) -> Vec<u8> {
311    let mut v = Vec::with_capacity(32 + 32);
312    write_move_call_value(&mut v, package, module, function);
313    v
314}
315
316/// Build an EmitModule compound value at the desired specificity.
317pub fn emit_module_value(package_id: &[u8], module: Option<&str>) -> Vec<u8> {
318    let mut v = Vec::with_capacity(32 + 16);
319    write_emit_module_value(&mut v, package_id, module);
320    v
321}
322
323/// Build an EventType compound value at the desired specificity.
324/// `instantiation_bcs` is the BCS encoding of `Vec<TypeTag>`, used only
325/// when matching a fully instantiated generic type.
326pub fn event_type_value(
327    type_address: &[u8],
328    module: Option<&str>,
329    name: Option<&str>,
330    instantiation_bcs: Option<&[u8]>,
331) -> Vec<u8> {
332    let mut v = Vec::with_capacity(32 + 32);
333    write_event_type_value(&mut v, type_address, module, name, instantiation_bcs);
334    v
335}
336
337/// Append a dimension key into `out` using the `[tag_byte][value_bytes]` format.
338pub fn write_dimension_key(out: &mut Vec<u8>, dim: IndexDimension, value: &[u8]) {
339    out.clear();
340    out.reserve(1 + value.len());
341    out.push(dim.tag_byte());
342    out.extend_from_slice(value);
343}
344
345/// Append one component to a compound dimension value.
346pub fn append_dimension_value_component(out: &mut Vec<u8>, component: &[u8]) {
347    out.extend_from_slice(component);
348}
349
350/// Append one separator-prefixed component to a compound dimension value.
351pub fn append_separated_dimension_value_component(out: &mut Vec<u8>, component: &[u8]) {
352    out.push(COMPOUND_VALUE_SEPARATOR);
353    append_dimension_value_component(out, component);
354}
355
356/// Append a MoveCall compound value at the desired specificity into `out`.
357pub fn write_move_call_value(
358    out: &mut Vec<u8>,
359    package: &[u8],
360    module: Option<&str>,
361    function: Option<&str>,
362) {
363    out.clear();
364    out.reserve(32 + 32);
365    append_dimension_value_component(out, package);
366    if let Some(m) = module {
367        append_dimension_value_component(out, m.as_bytes());
368        if let Some(f) = function {
369            append_separated_dimension_value_component(out, f.as_bytes());
370        }
371    }
372}
373
374/// Append an EmitModule compound value at the desired specificity into `out`.
375pub fn write_emit_module_value(out: &mut Vec<u8>, package_id: &[u8], module: Option<&str>) {
376    out.clear();
377    out.reserve(32 + 16);
378    append_dimension_value_component(out, package_id);
379    if let Some(m) = module {
380        append_dimension_value_component(out, m.as_bytes());
381    }
382}
383
384/// Append an EventType compound value at the desired specificity into `out`.
385pub fn write_event_type_value(
386    out: &mut Vec<u8>,
387    type_address: &[u8],
388    module: Option<&str>,
389    name: Option<&str>,
390    instantiation_bcs: Option<&[u8]>,
391) {
392    out.clear();
393    out.reserve(32 + 32);
394    append_dimension_value_component(out, type_address);
395    if let Some(m) = module {
396        append_dimension_value_component(out, m.as_bytes());
397        if let Some(n) = name {
398            append_separated_dimension_value_component(out, n.as_bytes());
399            if let Some(bcs) = instantiation_bcs {
400                append_separated_dimension_value_component(out, bcs);
401            }
402        }
403    }
404}
405
406fn owner_as_affected_address(owner: &Owner) -> Option<&[u8]> {
407    match owner {
408        Owner::AddressOwner(addr) => Some(addr.as_ref()),
409        Owner::ConsensusAddressOwner { owner, .. } => Some(owner.as_ref()),
410        _ => None,
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use std::collections::HashSet;
417
418    use move_core_types::ident_str;
419    use sui_types::accumulator_event::AccumulatorEvent;
420    use sui_types::base_types::ObjectID;
421    use sui_types::effects::TestEffectsBuilder;
422    use sui_types::event::Event;
423    use sui_types::gas_coin::GAS;
424    use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
425    use sui_types::transaction::SenderSignedData;
426
427    use super::*;
428
429    #[test]
430    fn transaction_visitor_emits_tx_and_event_dimensions() {
431        let sender = TestCheckpointBuilder::derive_address(1);
432        let recipient = TestCheckpointBuilder::derive_address(2);
433        let affected = TestCheckpointBuilder::derive_object_id(10);
434        let package = ObjectID::ZERO;
435        let event_type = GAS::type_();
436        let checkpoint = TestCheckpointBuilder::new(0)
437            .start_transaction(1)
438            .create_coin_object(10, 2, 100, GAS::type_tag())
439            .add_move_call(package, "coin", "transfer")
440            .with_events(vec![Event::new(
441                &package,
442                ident_str!("emit_mod"),
443                sender,
444                event_type.clone(),
445                vec![],
446            )])
447            .finish_transaction()
448            .build_checkpoint();
449        let tx = &checkpoint.transactions[0];
450
451        let mut keys = HashSet::new();
452        for_each_transaction_dimension(
453            &tx.transaction,
454            &tx.effects,
455            tx.events.as_ref(),
456            &checkpoint.object_set,
457            |dim, value| {
458                keys.insert(encode_dimension_key(dim, value));
459            },
460        );
461
462        assert!(keys.contains(&encode_dimension_key(
463            IndexDimension::Sender,
464            sender.as_ref()
465        )));
466        assert!(keys.contains(&encode_dimension_key(
467            IndexDimension::AffectedAddress,
468            recipient.as_ref()
469        )));
470        assert!(keys.contains(&encode_dimension_key(
471            IndexDimension::AffectedObject,
472            affected.as_ref()
473        )));
474        assert!(keys.contains(&encode_dimension_key(
475            IndexDimension::MoveCall,
476            &move_call_value(package.as_ref(), Some("coin"), Some("transfer"))
477        )));
478        assert!(keys.contains(&encode_dimension_key(
479            IndexDimension::EmitModule,
480            &emit_module_value(package.as_ref(), Some("emit_mod"))
481        )));
482        assert!(keys.contains(&encode_dimension_key(
483            IndexDimension::EventType,
484            &event_type_value(
485                event_type.address.as_ref(),
486                Some(event_type.module.as_str()),
487                Some(event_type.name.as_str()),
488                None,
489            )
490        )));
491    }
492
493    #[test]
494    fn event_visitor_emits_supported_dimensions_per_event() {
495        let sender = TestCheckpointBuilder::derive_address(1);
496        let affected = TestCheckpointBuilder::derive_object_id(10);
497        let package = ObjectID::ZERO;
498        let event_type = GAS::type_();
499        let checkpoint = TestCheckpointBuilder::new(0)
500            .start_transaction(1)
501            .create_coin_object(10, 2, 100, GAS::type_tag())
502            .add_move_call(package, "coin", "transfer")
503            .with_events(vec![Event::new(
504                &package,
505                ident_str!("emit_mod"),
506                sender,
507                event_type.clone(),
508                vec![],
509            )])
510            .finish_transaction()
511            .build_checkpoint();
512        let tx = &checkpoint.transactions[0];
513
514        let mut keys = HashSet::new();
515        for_each_event_dimension(
516            tx.transaction.sender(),
517            &tx.effects,
518            tx.events.as_ref(),
519            |event_idx, dim, value| {
520                keys.insert((event_idx, encode_dimension_key(dim, value)));
521            },
522        );
523
524        for expected in [
525            encode_dimension_key(IndexDimension::Sender, sender.as_ref()),
526            encode_dimension_key(
527                IndexDimension::EmitModule,
528                &emit_module_value(package.as_ref(), Some("emit_mod")),
529            ),
530            encode_dimension_key(
531                IndexDimension::EventType,
532                &event_type_value(
533                    event_type.address.as_ref(),
534                    Some(event_type.module.as_str()),
535                    Some(event_type.name.as_str()),
536                    None,
537                ),
538            ),
539        ] {
540            assert!(keys.contains(&(0, expected)));
541        }
542
543        let move_call_key = encode_dimension_key(
544            IndexDimension::MoveCall,
545            &move_call_value(package.as_ref(), Some("coin"), Some("transfer")),
546        );
547        let affected_object_key =
548            encode_dimension_key(IndexDimension::AffectedObject, affected.as_ref());
549
550        assert!(!keys.iter().any(|(_, k)| k == &move_call_key));
551        assert!(!keys.iter().any(|(_, k)| k == &affected_object_key));
552    }
553
554    #[test]
555    fn event_visitor_emits_existence_marker_per_event() {
556        let sender = TestCheckpointBuilder::derive_address(1);
557        let package = ObjectID::ZERO;
558        let event_type = GAS::type_();
559        let ev = || {
560            Event::new(
561                &package,
562                ident_str!("emit_mod"),
563                sender,
564                event_type.clone(),
565                vec![],
566            )
567        };
568        let checkpoint = TestCheckpointBuilder::new(0)
569            .start_transaction(1)
570            .with_events(vec![ev(), ev(), ev()])
571            .finish_transaction()
572            .build_checkpoint();
573        let tx = &checkpoint.transactions[0];
574
575        let mut extant_idxs = Vec::new();
576        for_each_event_dimension(
577            tx.transaction.sender(),
578            &tx.effects,
579            tx.events.as_ref(),
580            |event_idx, dim, value| {
581                if dim == IndexDimension::EventExtant {
582                    assert_eq!(
583                        value, EVENT_EXTANT_VALUE,
584                        "existence marker carries the singleton placeholder value"
585                    );
586                    extant_idxs.push(event_idx);
587                }
588            },
589        );
590
591        // Exactly one existence bit per real event, at each event's own index.
592        assert_eq!(extant_idxs, vec![0, 1, 2]);
593        // The encoded key is the tag byte followed by the placeholder value byte.
594        assert_eq!(
595            encode_dimension_key(IndexDimension::EventExtant, EVENT_EXTANT_VALUE),
596            vec![IndexDimension::EventExtant.tag_byte(), 0x00]
597        );
598    }
599
600    #[test]
601    fn transaction_visitor_omits_event_existence_marker() {
602        let sender = TestCheckpointBuilder::derive_address(1);
603        let package = ObjectID::ZERO;
604        let checkpoint = TestCheckpointBuilder::new(0)
605            .start_transaction(1)
606            .with_events(vec![Event::new(
607                &package,
608                ident_str!("emit_mod"),
609                sender,
610                GAS::type_(),
611                vec![],
612            )])
613            .finish_transaction()
614            .build_checkpoint();
615        let tx = &checkpoint.transactions[0];
616
617        let mut saw_extant = false;
618        for_each_transaction_dimension(
619            &tx.transaction,
620            &tx.effects,
621            tx.events.as_ref(),
622            &checkpoint.object_set,
623            |dim, _value| {
624                saw_extant |= dim == IndexDimension::EventExtant;
625            },
626        );
627        assert!(
628            !saw_extant,
629            "EventExtant is event-space only and must not appear in tx-space"
630        );
631    }
632
633    #[test]
634    fn event_extant_tag_byte_round_trips() {
635        assert_eq!(
636            IndexDimension::from_tag_byte(IndexDimension::EventExtant.tag_byte()),
637            Some(IndexDimension::EventExtant)
638        );
639    }
640
641    #[test]
642    fn affected_address_captures_prior_owner_on_transfer() {
643        let alice = TestCheckpointBuilder::derive_address(1);
644        let bob = TestCheckpointBuilder::derive_address(2);
645        let checkpoint = TestCheckpointBuilder::new(0)
646            .start_transaction(1)
647            .create_owned_object(10)
648            .finish_transaction()
649            .start_transaction(1)
650            .transfer_object(10, 2)
651            .finish_transaction()
652            .build_checkpoint();
653        let transfer_tx = &checkpoint.transactions[1];
654
655        let mut keys = HashSet::new();
656        for_each_transaction_dimension(
657            &transfer_tx.transaction,
658            &transfer_tx.effects,
659            transfer_tx.events.as_ref(),
660            &checkpoint.object_set,
661            |dim, value| {
662                keys.insert(encode_dimension_key(dim, value));
663            },
664        );
665
666        assert!(
667            keys.contains(&encode_dimension_key(
668                IndexDimension::AffectedAddress,
669                bob.as_ref()
670            )),
671            "new owner Bob should be captured via object_changes output state"
672        );
673        assert!(
674            keys.contains(&encode_dimension_key(
675                IndexDimension::AffectedAddress,
676                alice.as_ref()
677            )),
678            "prior owner Alice should be captured via object_changes input state"
679        );
680    }
681
682    #[test]
683    fn affected_address_captures_address_balance_accumulator() {
684        let balance_owner = TestCheckpointBuilder::derive_address(2);
685        let mut checkpoint = TestCheckpointBuilder::new(0)
686            .start_transaction(1)
687            .finish_transaction()
688            .build_checkpoint();
689        let tx = &checkpoint.transactions[0];
690        let signed = SenderSignedData::new(tx.transaction.clone(), tx.signatures.clone());
691        let accumulator_event = AccumulatorEvent::from_balance_change(
692            balance_owner,
693            Balance::type_tag(GAS::type_tag()),
694            100,
695        )
696        .unwrap();
697        checkpoint.transactions[0].effects = TestEffectsBuilder::new(&signed)
698            .with_accumulator_events([accumulator_event])
699            .build();
700        let tx = &checkpoint.transactions[0];
701
702        let mut keys = HashSet::new();
703        for_each_transaction_dimension(
704            &tx.transaction,
705            &tx.effects,
706            tx.events.as_ref(),
707            &checkpoint.object_set,
708            |dim, value| {
709                keys.insert(encode_dimension_key(dim, value));
710            },
711        );
712
713        assert!(keys.contains(&encode_dimension_key(
714            IndexDimension::AffectedAddress,
715            balance_owner.as_ref()
716        )));
717    }
718
719    #[test]
720    fn transaction_visitor_emits_package_write_marker() {
721        use std::collections::HashSet;
722        use sui_types::digests::TransactionDigest;
723        use sui_types::effects::TestEffectsBuilder;
724        use sui_types::full_checkpoint_content::ObjectSet;
725        use sui_types::move_package::MovePackage;
726        use sui_types::object::{Data, Object};
727
728        // A BCS-encoded `MovePackage` (id 0x0..0, version 2) holding one module
729        // "DUMMY" whose declared self-address is 0x0..0. We patch the id byte
730        // (offset 31) and version byte (offset 32) to model each write below.
731        let pkg_bytes = vec![
732            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
733            0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 5, 68, 85, 77, 77, 89, 63, 161, 28, 235, 11, 7, 0,
734            0, 5, 4, 1, 0, 2, 5, 2, 1, 7, 3, 6, 8, 9, 32, 0, 0, 0, 5, 68, 85, 77, 77, 89, 0, 0, 0,
735            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
736            0, 0, 0,
737        ];
738
739        let tx = TestCheckpointBuilder::new(0)
740            .start_transaction(1)
741            .finish_transaction()
742            .build_checkpoint()
743            .transactions[0]
744            .transaction
745            .clone();
746        let signed_data = sui_types::transaction::SenderSignedData::new(tx.clone(), vec![]);
747
748        // Run the extractor over a single package write installed at `id_byte`
749        // and `version`. Returns the emitted keys.
750        let run = |id_byte: u8, version: u8| {
751            let mut bytes = pkg_bytes.clone();
752            bytes[31] = id_byte;
753            bytes[32] = version;
754            let pkg: MovePackage = bcs::from_bytes(&bytes).unwrap();
755            let pkg_id = pkg.id();
756            let pkg_version = pkg.version();
757
758            let mut object_set = ObjectSet::default();
759            object_set.insert(Object::new_package_from_data(
760                Data::Package(pkg),
761                TransactionDigest::random(),
762            ));
763
764            let effects = TestEffectsBuilder::new(&signed_data)
765                .with_package_writes(vec![(pkg_id, pkg_version)])
766                .build();
767
768            let mut keys = HashSet::new();
769            for_each_transaction_dimension(&tx, &effects, None, &object_set, |dim, value| {
770                keys.insert(encode_dimension_key(dim, value));
771            });
772            keys
773        };
774
775        let any_write = encode_dimension_key(IndexDimension::AnyPackageWrite, ANY_PACKAGE_VALUE);
776
777        // Every package write emits the single global marker regardless of
778        // publish-vs-upgrade or whether the upgrade reuses its id: a first
779        // publish (version 1), a user upgrade that mints a new id (version 2,
780        // new id), and a reused-id upgrade (version 2, original == id).
781        for (id_byte, version) in [(0, 1), (1, 2), (0, 2)] {
782            assert!(
783                run(id_byte, version).contains(&any_write),
784                "package write (id_byte={id_byte}, version={version}) emits AnyPackageWrite"
785            );
786        }
787    }
788}