1use 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#[repr(u8)]
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum IndexDimension {
32 Sender = 0x01,
33 AffectedAddress = 0x02,
39 AffectedObject = 0x03,
40 MoveCall = 0x04,
42 EmitModule = 0x05,
44 EventType = 0x06,
46 EventStreamHead = 0x07,
51 EventExtant = 0x08,
60 TxUniverse = 0x09,
68 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
100pub const EVENT_EXTANT_VALUE: &[u8] = &[0x00];
107
108pub const TX_UNIVERSE_VALUE: &[u8] = &[0x00];
114
115pub const ANY_PACKAGE_VALUE: &[u8] = &[0x00];
120
121pub 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 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 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
212pub 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 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, ¶ms_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
302pub 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
309pub 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
316pub 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
323pub 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
337pub 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
345pub fn append_dimension_value_component(out: &mut Vec<u8>, component: &[u8]) {
347 out.extend_from_slice(component);
348}
349
350pub 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
356pub 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
374pub 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
384pub 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 assert_eq!(extant_idxs, vec![0, 1, 2]);
593 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 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 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 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}