use super::Address;
use super::Object;
use super::TypeTag;
use std::borrow::Cow;
#[cfg(feature = "unstable")]
use super::Digest;
#[cfg(feature = "unstable")]
use super::U256;
#[derive(Debug, Clone)]
pub struct Coin<'a> {
coin_type: Cow<'a, TypeTag>,
id: Address,
balance: u64,
}
impl<'a> Coin<'a> {
pub fn coin_type(&self) -> &TypeTag {
&self.coin_type
}
pub fn id(&self) -> &Address {
&self.id
}
pub fn balance(&self) -> u64 {
self.balance
}
pub fn try_from_object(object: &'a Object) -> Option<Self> {
match &object.data {
super::ObjectData::Struct(move_struct) => {
let coin_type = move_struct.type_.is_coin()?;
let contents = &move_struct.contents;
if contents.len() != Address::LENGTH + std::mem::size_of::<u64>() {
return None;
}
let id = Address::new((&contents[..Address::LENGTH]).try_into().unwrap());
let balance =
u64::from_le_bytes((&contents[Address::LENGTH..]).try_into().unwrap());
Some(Self {
coin_type: Cow::Borrowed(coin_type),
id,
balance,
})
}
_ => None, }
}
pub fn into_owned(self) -> Coin<'static> {
Coin {
coin_type: Cow::Owned(self.coin_type.into_owned()),
id: self.id,
balance: self.balance,
}
}
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub struct EventCommitment {
pub checkpoint_seq: u64,
pub transaction_idx: u64,
pub event_idx: u64,
pub digest: Digest,
}
#[cfg(feature = "unstable")]
impl PartialOrd for EventCommitment {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[cfg(feature = "unstable")]
impl Ord for EventCommitment {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.checkpoint_seq, self.transaction_idx, self.event_idx).cmp(&(
other.checkpoint_seq,
other.transaction_idx,
other.event_idx,
))
}
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub struct EventStreamHead {
pub mmr: Vec<U256>,
pub checkpoint_seq: u64,
pub num_events: u64,
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
pub fn derive_event_stream_head_object_id(stream_id: Address) -> Address {
use super::Identifier;
use super::StructTag;
let accumulator_root = const { Address::from_static("0xacc") };
let value_type = StructTag::new(
Address::TWO,
Identifier::from_static("accumulator_settlement"),
Identifier::from_static("EventStreamHead"),
vec![],
);
let key_type_tag: TypeTag = StructTag::new(
Address::TWO,
Identifier::from_static("accumulator"),
Identifier::from_static("Key"),
vec![value_type.into()],
)
.into();
accumulator_root.derive_dynamic_child_id(&key_type_tag, stream_id.as_ref())
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
pub fn build_event_merkle_root(commitments: &[EventCommitment]) -> Digest {
debug_assert!(
commitments.windows(2).all(|w| w[0] <= w[1]),
"EventCommitments must be sorted by (checkpoint_seq, transaction_idx, event_idx)",
);
let tree = crate::merkle::MerkleTree::build_from_unserialized(commitments.iter())
.expect("EventCommitment BCS encoding is infallible");
Digest::new(tree.root().bytes())
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventBatch {
pub checkpoint_seq: u64,
pub commitments: Vec<EventCommitment>,
}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ApplyStreamError {
EmptyBatch {
batch_index: usize,
},
CommitmentCheckpointMismatch {
batch_index: usize,
commitment_index: usize,
batch_checkpoint_seq: u64,
commitment_checkpoint_seq: u64,
},
NonMonotonicCheckpoint {
batch_index: usize,
previous_checkpoint_seq: u64,
batch_checkpoint_seq: u64,
},
}
#[cfg(feature = "unstable")]
impl std::fmt::Display for ApplyStreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyBatch { batch_index } => {
write!(f, "batch {batch_index} is empty")
}
Self::CommitmentCheckpointMismatch {
batch_index,
commitment_index,
batch_checkpoint_seq,
commitment_checkpoint_seq,
} => write!(
f,
"batch {batch_index} declares checkpoint {batch_checkpoint_seq} \
but commitment {commitment_index} carries checkpoint \
{commitment_checkpoint_seq}",
),
Self::NonMonotonicCheckpoint {
batch_index,
previous_checkpoint_seq,
batch_checkpoint_seq,
} => write!(
f,
"batch {batch_index} at checkpoint {batch_checkpoint_seq} would \
regress the head's checkpoint {previous_checkpoint_seq}",
),
}
}
}
#[cfg(feature = "unstable")]
impl std::error::Error for ApplyStreamError {}
#[cfg(feature = "unstable")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "unstable")))]
pub fn apply_stream_updates(
head: EventStreamHead,
batches: &[EventBatch],
) -> Result<EventStreamHead, ApplyStreamError> {
let mut head = head;
for (batch_index, batch) in batches.iter().enumerate() {
if batch.commitments.is_empty() {
return Err(ApplyStreamError::EmptyBatch { batch_index });
}
for (commitment_index, commitment) in batch.commitments.iter().enumerate() {
if commitment.checkpoint_seq != batch.checkpoint_seq {
return Err(ApplyStreamError::CommitmentCheckpointMismatch {
batch_index,
commitment_index,
batch_checkpoint_seq: batch.checkpoint_seq,
commitment_checkpoint_seq: commitment.checkpoint_seq,
});
}
}
if head.num_events != 0 && batch.checkpoint_seq < head.checkpoint_seq {
return Err(ApplyStreamError::NonMonotonicCheckpoint {
batch_index,
previous_checkpoint_seq: head.checkpoint_seq,
batch_checkpoint_seq: batch.checkpoint_seq,
});
}
let root_digest = build_event_merkle_root(&batch.commitments);
let merkle_root = U256::from_digits(root_digest.into_inner());
fold_into_mmr(&mut head.mmr, merkle_root);
head.num_events += batch.commitments.len() as u64;
head.checkpoint_seq = batch.checkpoint_seq;
}
Ok(head)
}
#[cfg(feature = "unstable")]
fn fold_into_mmr(mmr: &mut Vec<U256>, mut carry: U256) {
let mut i = 0;
while i < mmr.len() {
if mmr[i] == U256::ZERO {
mmr[i] = carry;
return;
}
carry = hash_two_to_one(mmr[i], carry);
mmr[i] = U256::ZERO;
i += 1;
}
mmr.push(carry);
}
#[cfg(feature = "unstable")]
fn hash_two_to_one(left: U256, right: U256) -> U256 {
use super::hash::Hasher;
let mut hasher = Hasher::new();
hasher.update(left.digits());
hasher.update(right.digits());
U256::from_digits(hasher.finalize().into_inner())
}
#[cfg(test)]
#[cfg(feature = "unstable")]
mod test {
use super::*;
#[test]
fn event_commitment_bcs_shape() {
let commitment = EventCommitment {
checkpoint_seq: 0x0102030405060708,
transaction_idx: 0x1112131415161718,
event_idx: 0x2122232425262728,
digest: Digest::new([0xaa; 32]),
};
let mut expected = Vec::new();
expected.extend_from_slice(&0x0102030405060708u64.to_le_bytes());
expected.extend_from_slice(&0x1112131415161718u64.to_le_bytes());
expected.extend_from_slice(&0x2122232425262728u64.to_le_bytes());
expected.push(0x20);
expected.extend_from_slice(&[0xaa; 32]);
let bytes = bcs::to_bytes(&commitment).unwrap();
assert_eq!(bytes, expected);
assert_eq!(bytes.len(), 8 + 8 + 8 + 33);
let back: EventCommitment = bcs::from_bytes(&bytes).unwrap();
assert_eq!(back, commitment);
}
#[test]
fn event_commitment_ord_ignores_digest() {
let a = EventCommitment {
checkpoint_seq: 1,
transaction_idx: 2,
event_idx: 3,
digest: Digest::new([0x00; 32]),
};
let b = EventCommitment {
digest: Digest::new([0xff; 32]),
..a
};
assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
let c = EventCommitment { event_idx: 4, ..a };
assert!(a < c);
}
#[test]
fn event_stream_head_bcs_shape() {
let head = EventStreamHead {
mmr: vec![U256::ZERO, U256::ONE],
checkpoint_seq: 0x4142434445464748,
num_events: 0x5152535455565758,
};
let mut expected = Vec::new();
expected.push(0x02);
expected.extend_from_slice(&[0u8; 32]);
let mut one_le = [0u8; 32];
one_le[0] = 1;
expected.extend_from_slice(&one_le);
expected.extend_from_slice(&0x4142434445464748u64.to_le_bytes());
expected.extend_from_slice(&0x5152535455565758u64.to_le_bytes());
let bytes = bcs::to_bytes(&head).unwrap();
assert_eq!(bytes, expected);
let back: EventStreamHead = bcs::from_bytes(&bytes).unwrap();
assert_eq!(back, head);
}
#[test]
fn event_stream_head_default_is_empty() {
let head = EventStreamHead::default();
assert!(head.mmr.is_empty());
assert_eq!(head.checkpoint_seq, 0);
assert_eq!(head.num_events, 0);
}
#[test]
fn build_event_merkle_root_pinned_vector() {
let commitments = vec![
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([0x11; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 1,
digest: Digest::new([0x22; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 1,
event_idx: 0,
digest: Digest::new([0x33; 32]),
},
];
const EXPECTED: [u8; 32] = [
254, 183, 87, 247, 72, 14, 90, 116, 221, 195, 244, 87, 250, 236, 226, 161, 99, 106,
199, 246, 85, 138, 180, 110, 112, 50, 103, 77, 160, 104, 239, 61,
];
assert_eq!(build_event_merkle_root(&commitments).into_inner(), EXPECTED);
}
#[test]
fn build_event_merkle_root_empty_input_is_empty_node() {
assert_eq!(build_event_merkle_root(&[]).into_inner(), [0u8; 32]);
}
#[test]
fn derive_event_stream_head_object_id_pinned_vectors() {
let cases: &[(Address, Address)] = &[
(
Address::ZERO,
Address::from_static(
"0x9461a724d957b41485e094fdced6c668bd388070108dbfbdc12277ad68a2717f",
),
),
(
Address::TWO,
Address::from_static(
"0x1b877f5c7664df8957f127a95d1b2c8c1c239fd49566f9f69205df44133fc37f",
),
),
(
Address::from_static("0xacc"),
Address::from_static(
"0x452652326e8df295af20a4e0744acac9a74f87d93ba976dd97d3e93e1a542e37",
),
),
(
Address::from_static("0x42424242"),
Address::from_static(
"0xdbe2cd3f24c357c434991a4348e6ceb43bcb7d22696eb6797a6739e5351cb149",
),
),
];
for (stream_id, expected) in cases {
assert_eq!(
derive_event_stream_head_object_id(*stream_id),
*expected,
"mismatch for stream id {stream_id}",
);
}
}
fn u256_from_decimal(s: &str) -> U256 {
s.parse().expect("decimal U256 literal must parse")
}
fn single_event_batch(checkpoint_seq: u64, digest_byte: u8) -> EventBatch {
EventBatch {
checkpoint_seq,
commitments: vec![EventCommitment {
checkpoint_seq,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([digest_byte; 32]),
}],
}
}
#[test]
fn fold_into_mmr_matches_move_compat_fixture() {
let mut mmr = Vec::new();
for value in 50u64..58 {
fold_into_mmr(&mut mmr, U256::from(value));
}
assert_eq!(mmr.len(), 4);
assert_eq!(mmr[0], U256::ZERO);
assert_eq!(mmr[1], U256::ZERO);
assert_eq!(mmr[2], U256::ZERO);
assert_eq!(
mmr[3],
u256_from_decimal(
"69725770072863840208899320192042305265295220676851872214494910464384102654361",
),
);
}
#[test]
fn fold_into_mmr_two_inserts_collapse_to_level_one() {
let mut mmr = Vec::new();
fold_into_mmr(&mut mmr, U256::from(7u64));
assert_eq!(mmr, vec![U256::from(7u64)]);
fold_into_mmr(&mut mmr, U256::from(11u64));
assert_eq!(mmr.len(), 2);
assert_eq!(mmr[0], U256::ZERO);
assert_eq!(mmr[1], hash_two_to_one(U256::from(7u64), U256::from(11u64)));
}
#[test]
fn apply_stream_updates_single_batch_matches_upstream() {
let batch = EventBatch {
checkpoint_seq: 1,
commitments: vec![
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([0x11; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 1,
digest: Digest::new([0x22; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 1,
event_idx: 0,
digest: Digest::new([0x33; 32]),
},
],
};
let head = apply_stream_updates(EventStreamHead::default(), &[batch]).unwrap();
assert_eq!(head.checkpoint_seq, 1);
assert_eq!(head.num_events, 3);
assert_eq!(
head.mmr,
vec![u256_from_decimal(
"28014082315424315761761458464083312323394111104237010481447392654866601457662",
)],
);
}
#[test]
fn apply_stream_updates_two_batches_match_upstream() {
let batch_1 = EventBatch {
checkpoint_seq: 1,
commitments: vec![
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([0x11; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 0,
event_idx: 1,
digest: Digest::new([0x22; 32]),
},
EventCommitment {
checkpoint_seq: 1,
transaction_idx: 1,
event_idx: 0,
digest: Digest::new([0x33; 32]),
},
],
};
let batch_2 = EventBatch {
checkpoint_seq: 2,
commitments: vec![
EventCommitment {
checkpoint_seq: 2,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([0x44; 32]),
},
EventCommitment {
checkpoint_seq: 2,
transaction_idx: 0,
event_idx: 1,
digest: Digest::new([0x55; 32]),
},
],
};
let head = apply_stream_updates(EventStreamHead::default(), &[batch_1, batch_2]).unwrap();
assert_eq!(head.checkpoint_seq, 2);
assert_eq!(head.num_events, 5);
assert_eq!(head.mmr.len(), 2);
assert_eq!(head.mmr[0], U256::ZERO);
assert_eq!(
head.mmr[1],
u256_from_decimal(
"80180905428222716273420959625814881301112107405105460786291242224918309625423",
),
);
}
#[test]
fn apply_stream_updates_four_single_batches_match_upstream() {
let batches: Vec<EventBatch> = (1u64..=4)
.map(|cp| single_event_batch(cp, cp as u8))
.collect();
let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
assert_eq!(head.checkpoint_seq, 4);
assert_eq!(head.num_events, 4);
assert_eq!(head.mmr.len(), 3);
assert_eq!(head.mmr[0], U256::ZERO);
assert_eq!(head.mmr[1], U256::ZERO);
assert_eq!(
head.mmr[2],
u256_from_decimal(
"43434128249102587327404298804800250101556402749045331898264216785541514599480",
),
);
}
#[test]
fn apply_stream_updates_no_batches_is_identity() {
let head = EventStreamHead {
mmr: vec![U256::ONE, U256::ZERO, U256::from(42u64)],
checkpoint_seq: 17,
num_events: 9,
};
let out = apply_stream_updates(head.clone(), &[]).unwrap();
assert_eq!(out, head);
}
#[test]
fn apply_stream_updates_equal_checkpoint_seq_is_allowed() {
let head = apply_stream_updates(
EventStreamHead::default(),
&[single_event_batch(5, 0x01), single_event_batch(5, 0x02)],
)
.unwrap();
assert_eq!(head.checkpoint_seq, 5);
assert_eq!(head.num_events, 2);
}
#[test]
fn apply_stream_updates_rejects_empty_batch() {
let err = apply_stream_updates(
EventStreamHead::default(),
&[
single_event_batch(1, 0x01),
EventBatch {
checkpoint_seq: 2,
commitments: vec![],
},
],
)
.unwrap_err();
assert_eq!(err, ApplyStreamError::EmptyBatch { batch_index: 1 });
}
#[test]
fn apply_stream_updates_rejects_commitment_checkpoint_mismatch() {
let err = apply_stream_updates(
EventStreamHead::default(),
&[EventBatch {
checkpoint_seq: 5,
commitments: vec![
EventCommitment {
checkpoint_seq: 5,
transaction_idx: 0,
event_idx: 0,
digest: Digest::new([0x01; 32]),
},
EventCommitment {
checkpoint_seq: 6, transaction_idx: 0,
event_idx: 1,
digest: Digest::new([0x02; 32]),
},
],
}],
)
.unwrap_err();
assert_eq!(
err,
ApplyStreamError::CommitmentCheckpointMismatch {
batch_index: 0,
commitment_index: 1,
batch_checkpoint_seq: 5,
commitment_checkpoint_seq: 6,
},
);
}
#[test]
fn apply_stream_updates_rejects_non_monotonic_checkpoint() {
let err = apply_stream_updates(
EventStreamHead::default(),
&[single_event_batch(10, 0x01), single_event_batch(9, 0x02)],
)
.unwrap_err();
assert_eq!(
err,
ApplyStreamError::NonMonotonicCheckpoint {
batch_index: 1,
previous_checkpoint_seq: 10,
batch_checkpoint_seq: 9,
},
);
}
#[cfg(feature = "proptest")]
mod proptests {
use super::*;
use proptest::collection::vec;
use proptest::prelude::*;
use test_strategy::proptest;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn monotonic_batches() -> impl Strategy<Value = Vec<EventBatch>> {
vec((1u64..=10, 1usize..=4), 0..=8).prop_map(|deltas| {
let mut seq = 0u64;
let mut batches = Vec::with_capacity(deltas.len());
for (delta, n_events) in deltas {
seq += delta;
let commitments: Vec<EventCommitment> = (0..n_events)
.map(|i| EventCommitment {
checkpoint_seq: seq,
transaction_idx: 0,
event_idx: i as u64,
digest: {
let mut d = [0u8; 32];
d[..8].copy_from_slice(&seq.to_le_bytes());
d[8..16].copy_from_slice(&(i as u64).to_le_bytes());
Digest::new(d)
},
})
.collect();
batches.push(EventBatch {
checkpoint_seq: seq,
commitments,
});
}
batches
})
}
#[proptest]
fn mmr_popcount_invariant(#[strategy(vec(1u64..=u64::MAX, 0..=20))] carries: Vec<u64>) {
let mut mmr = Vec::new();
for c in &carries {
fold_into_mmr(&mut mmr, U256::from(*c));
}
let n = carries.len() as u64;
let expected_len = if n == 0 {
0
} else {
64 - n.leading_zeros() as usize
};
prop_assert_eq!(
mmr.len(),
expected_len,
"mmr length must match highest set bit"
);
for (i, slot) in mmr.iter().enumerate() {
let bit_set = (n >> i) & 1 == 1;
if bit_set {
prop_assert_ne!(
*slot,
U256::ZERO,
"slot {} should be non-zero (bit set in n={})",
i,
n,
);
} else {
prop_assert_eq!(
*slot,
U256::ZERO,
"slot {} should be zero (bit unset in n={})",
i,
n,
);
}
}
}
#[proptest]
fn apply_stream_updates_is_associative_over_batches(
#[strategy(monotonic_batches())] batches: Vec<EventBatch>,
) {
let one_shot = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
let mut step_by_step = EventStreamHead::default();
for batch in &batches {
step_by_step =
apply_stream_updates(step_by_step, std::slice::from_ref(batch)).unwrap();
}
prop_assert_eq!(one_shot, step_by_step);
}
#[proptest]
fn apply_stream_updates_num_events_is_sum_of_commitments(
#[strategy(monotonic_batches())] batches: Vec<EventBatch>,
) {
let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
let expected: u64 = batches.iter().map(|b| b.commitments.len() as u64).sum();
prop_assert_eq!(head.num_events, expected);
}
#[proptest]
fn apply_stream_updates_advances_checkpoint_seq(
#[strategy(monotonic_batches())] batches: Vec<EventBatch>,
) {
let head = apply_stream_updates(EventStreamHead::default(), &batches).unwrap();
let expected = batches.last().map(|b| b.checkpoint_seq).unwrap_or(0);
prop_assert_eq!(head.checkpoint_seq, expected);
}
}
}