cap_common/
transaction_list.rs

1use crate::transaction::Event;
2use certified_vars::hashtree::{fork, fork_hash};
3use certified_vars::Paged;
4use certified_vars::{rbtree::RbTree, AsHashTree, Hash, HashTree};
5use ic_kit::candid::types::{Compound, Type};
6use ic_kit::candid::CandidType;
7use ic_kit::Principal;
8use serde::ser::{SerializeSeq, SerializeTuple};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use std::alloc::{dealloc, Layout};
11use std::ptr;
12use std::ptr::NonNull;
13
14/// A list contains a series of transactions and appropriate indexers.
15///
16/// This structure exposes a virtual merkle-tree in the following form:
17///
18/// 0: event_hashes
19/// 1: offset
20/// 3: user_indexer
21/// 4: contract_indexer
22/// 5: token_indexer
23///
24/// ```text
25///       ROOT
26///      /    \
27///     /      \
28///    V        V
29///   /  \     /  \
30///  0    1   3    V
31///               / \
32///              4   5
33/// ```
34pub struct TransactionList {
35    /// Map each local Transaction ID to its hash.
36    event_hashes: RbTree<u32, Hash>,
37    /// ID of the current contract.
38    contract: Principal,
39    /// The offset of this list, i.e the actual id of the first event in the list.
40    global_offset: u64,
41    /// Maps each user principal id to the vector of events they have.
42    user_indexer: Paged<Principal, NonNull<Event>, 64>,
43    /// Maps contract id to each transaction page.
44    contract_indexer: Paged<Principal, NonNull<Event>, 64>,
45    /// Map each token id to a map of transactions for that token.
46    token_indexer: Paged<u64, NonNull<Event>, 64>,
47    /// All of the events in this list, we store a pointer to an allocated memory. Which is used
48    /// only internally in this struct. And this Vec should be considered the actual owner of this
49    /// pointers.
50    /// So this should be the last thing that will be dropped.
51    events: Vec<NonNull<Event>>,
52}
53
54impl TransactionList {
55    /// Create a new list with the given global offset.
56    #[inline]
57    pub fn new(contract: Principal, offset: u64) -> Self {
58        TransactionList {
59            events: vec![],
60            contract,
61            event_hashes: RbTree::new(),
62            global_offset: offset,
63            user_indexer: Paged::new(),
64            contract_indexer: Paged::new(),
65            token_indexer: Paged::new(),
66        }
67    }
68
69    /// Return the principal id of the contract we're storing transactions for.
70    #[inline]
71    pub fn contract_id(&self) -> &Principal {
72        &self.contract
73    }
74
75    /// Return the total number of transactions.
76    #[inline]
77    pub fn size(&self) -> u64 {
78        self.global_offset + (self.events.len() as u64)
79    }
80
81    /// Return the total number of items in this list.
82    #[inline]
83    pub fn len(&self) -> usize {
84        self.events.len()
85    }
86
87    /// Returns `tru` if there are no events in this list.
88    #[inline]
89    pub fn is_empty(&self) -> bool {
90        self.events.is_empty()
91    }
92
93    /// Try to insert an event into the list.
94    pub fn insert(&mut self, event: Event) -> u64 {
95        let local_index = self.events.len() as u32;
96        let hash = event.hash();
97        let event: NonNull<Event> = Box::leak(Box::new(event)).into();
98        let eve = unsafe { event.as_ref() };
99
100        // Update the indexers for the transaction.
101        self.contract_indexer.insert(self.contract, event);
102        for user in eve.extract_principal_ids() {
103            self.user_indexer.insert(*user, event);
104        }
105        for token_id in eve.extract_token_ids() {
106            self.token_indexer.insert(token_id, event);
107        }
108
109        // Insert the event itself.
110        self.event_hashes.insert(local_index, hash);
111        self.events.push(event);
112
113        self.global_offset + (local_index as u64)
114    }
115
116    /// Return the transactions associated with a user's principal id at the given page.
117    #[inline]
118    pub fn get_transactions_for_user(&self, principal: &Principal, page: u32) -> Vec<&Event> {
119        if let Some(data) = self.user_indexer.get(principal, page as usize) {
120            data.iter().map(|v| unsafe { v.as_ref() }).collect()
121        } else {
122            vec![]
123        }
124    }
125
126    /// Return the last page number associated with the given user.
127    #[inline]
128    pub fn last_page_for_user(&self, principal: &Principal) -> u32 {
129        self.user_indexer
130            .get_last_page_number(principal)
131            .unwrap_or(0) as u32
132    }
133
134    /// Return the transactions associated with a token's principal id at the given page.
135    #[inline]
136    pub fn get_transactions_for_contract(&self, principal: &Principal, page: u32) -> Vec<&Event> {
137        if let Some(data) = self.contract_indexer.get(principal, page as usize) {
138            data.iter().map(|v| unsafe { v.as_ref() }).collect()
139        } else {
140            vec![]
141        }
142    }
143
144    /// Return the last page number associated with the given token contract.
145    #[inline]
146    pub fn last_page_for_contract(&self, principal: &Principal) -> u32 {
147        self.contract_indexer
148            .get_last_page_number(principal)
149            .unwrap_or(0) as u32
150    }
151
152    /// Return the transactions for a specific token.
153    #[inline]
154    pub fn get_transactions_for_token(&self, token_id: &u64, page: u32) -> Vec<&Event> {
155        if let Some(data) = self.token_indexer.get(token_id, page as usize) {
156            data.iter().map(|v| unsafe { v.as_ref() }).collect()
157        } else {
158            vec![]
159        }
160    }
161
162    #[inline]
163    pub fn last_page_for_token(&self, token_id: &u64) -> u32 {
164        self.token_indexer
165            .get_last_page_number(token_id)
166            .unwrap_or(0) as u32
167    }
168
169    /// Return the witness that can be used to prove the response from get_transactions_for_user.
170    #[inline]
171    pub fn witness_transactions_for_user(&self, principal: &Principal, page: u32) -> HashTree {
172        fork(
173            HashTree::Pruned(fork_hash(
174                &self.event_hashes.root_hash(),
175                &self.global_offset.root_hash(),
176            )),
177            fork(
178                self.user_indexer.witness(principal, page as usize),
179                HashTree::Pruned(fork_hash(
180                    &self.contract_indexer.root_hash(),
181                    &self.token_indexer.root_hash(),
182                )),
183            ),
184        )
185    }
186
187    /// Return the witness that can be used to prove the response from get_transactions_for_token.
188    #[inline]
189    pub fn witness_transactions_for_contract(&self, principal: &Principal, page: u32) -> HashTree {
190        fork(
191            HashTree::Pruned(fork_hash(
192                &self.event_hashes.root_hash(),
193                &self.global_offset.root_hash(),
194            )),
195            fork(
196                HashTree::Pruned(self.user_indexer.root_hash()),
197                fork(
198                    self.contract_indexer.witness(principal, page as usize),
199                    HashTree::Pruned(self.token_indexer.root_hash()),
200                ),
201            ),
202        )
203    }
204
205    /// Return the witness that can be used to prove the response from get_transactions_for_token.
206    #[inline]
207    pub fn witness_transactions_for_token(&self, token_id: &u64, page: u32) -> HashTree {
208        fork(
209            HashTree::Pruned(fork_hash(
210                &self.event_hashes.root_hash(),
211                &self.global_offset.root_hash(),
212            )),
213            fork(
214                HashTree::Pruned(self.user_indexer.root_hash()),
215                fork(
216                    HashTree::Pruned(self.contract_indexer.root_hash()),
217                    self.token_indexer.witness(token_id, page as usize),
218                ),
219            ),
220        )
221    }
222
223    /// Return a transaction by its global id.
224    #[inline]
225    pub fn get_transaction(&self, id: u64) -> Option<&Event> {
226        if id < self.global_offset {
227            None
228        } else {
229            let local = (id - self.global_offset) as usize;
230            if local < self.events.len() {
231                Some(unsafe { self.events[local].as_ref() })
232            } else {
233                None
234            }
235        }
236    }
237
238    /// Return a witness which proves the response returned by get_transaction.
239    #[inline]
240    pub fn witness_transaction(&self, id: u64) -> HashTree {
241        let left = if id < self.global_offset {
242            fork(
243                HashTree::Pruned(self.event_hashes.root_hash()),
244                self.global_offset.as_hash_tree(),
245            )
246        } else {
247            let local = (id - self.global_offset) as u32;
248            fork(
249                self.event_hashes.witness(&local),
250                self.global_offset.as_hash_tree(),
251            )
252        };
253
254        fork(
255            left,
256            HashTree::Pruned(fork_hash(
257                &self.user_indexer.root_hash(),
258                &fork_hash(
259                    &self.contract_indexer.root_hash(),
260                    &self.token_indexer.root_hash(),
261                ),
262            )),
263        )
264    }
265}
266
267impl AsHashTree for TransactionList {
268    fn root_hash(&self) -> Hash {
269        fork_hash(
270            &fork_hash(
271                &self.event_hashes.root_hash(),
272                &self.global_offset.root_hash(),
273            ),
274            &fork_hash(
275                &self.user_indexer.root_hash(),
276                &fork_hash(
277                    &self.contract_indexer.root_hash(),
278                    &self.token_indexer.root_hash(),
279                ),
280            ),
281        )
282    }
283
284    fn as_hash_tree(&self) -> HashTree<'_> {
285        fork(
286            fork(
287                self.event_hashes.as_hash_tree(),
288                self.global_offset.as_hash_tree(),
289            ),
290            fork(
291                self.user_indexer.as_hash_tree(),
292                fork(
293                    self.contract_indexer.as_hash_tree(),
294                    self.token_indexer.as_hash_tree(),
295                ),
296            ),
297        )
298    }
299}
300
301impl Drop for TransactionList {
302    fn drop(&mut self) {
303        unsafe {
304            for event in &self.events {
305                let as_mut_ref = &mut (*event.as_ptr());
306                ptr::drop_in_place(as_mut_ref);
307                dealloc(event.cast().as_ptr(), Layout::for_value(event.as_ref()));
308            }
309        }
310    }
311}
312
313struct EventsWrapper<'a>(&'a Vec<NonNull<Event>>);
314
315impl<'a> Serialize for EventsWrapper<'a> {
316    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
317    where
318        S: Serializer,
319    {
320        let mut s = serializer.serialize_seq(Some(self.0.len()))?;
321        for ev in self.0 {
322            s.serialize_element(unsafe { ev.as_ref() })?;
323        }
324        s.end()
325    }
326}
327
328impl<'a> CandidType for EventsWrapper<'a> {
329    fn _ty() -> Type {
330        <Vec<Event>>::_ty()
331    }
332
333    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
334    where
335        S: ic_kit::candid::types::Serializer,
336    {
337        let mut ser = serializer.serialize_vec(self.0.len())?;
338        for e in self.0.iter() {
339            Compound::serialize_element(&mut ser, unsafe { e.as_ref() })?;
340        }
341        Ok(())
342    }
343}
344
345impl Serialize for TransactionList {
346    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
347    where
348        S: Serializer,
349    {
350        let mut s = serializer.serialize_tuple(3)?;
351        s.serialize_element(&self.global_offset)?;
352        s.serialize_element(&self.contract)?;
353        s.serialize_element(&EventsWrapper(&self.events))?;
354        s.end()
355    }
356}
357
358impl<'de> Deserialize<'de> for TransactionList {
359    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
360    where
361        D: Deserializer<'de>,
362    {
363        #[derive(Deserialize)]
364        struct BucketDe(u64, Principal, Vec<Event>);
365
366        let data = BucketDe::deserialize(deserializer)?;
367        let mut list = TransactionList::new(data.1, data.0);
368
369        for event in data.2 {
370            list.insert(event);
371        }
372
373        Ok(list)
374    }
375}
376
377impl CandidType for TransactionList {
378    fn _ty() -> Type {
379        <(u64, Principal, EventsWrapper)>::_ty()
380    }
381
382    fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
383    where
384        S: ic_kit::candid::types::Serializer,
385    {
386        (
387            &self.global_offset,
388            &self.contract,
389            &EventsWrapper(&self.events),
390        )
391            .idl_serialize(serializer)
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use ic_kit::candid::{decode_one, encode_one};
399    use ic_kit::mock_principals;
400
401    fn e(time: u64, caller: Principal) -> Event {
402        Event {
403            time,
404            caller,
405            operation: "transfer".into(),
406            details: vec![],
407        }
408    }
409
410    /// root_hash and as_hash_tree should use the same tree layout.
411    #[test]
412    fn test_hash_tree() {
413        let mut list = TransactionList::new(mock_principals::xtc(), 0);
414        list.insert(e(0, mock_principals::alice()));
415        list.insert(e(1, mock_principals::alice()));
416        list.insert(e(2, mock_principals::alice()));
417        list.insert(e(3, mock_principals::alice()));
418        assert_eq!(list.as_hash_tree().reconstruct(), list.root_hash());
419    }
420
421    /// This test tires to see if the witness created for a lookup is minimal
422    /// and reconstructs to the root_hash.
423    #[test]
424    fn test_witness_transaction() {
425        let mut list = TransactionList::new(mock_principals::xtc(), 0);
426        list.insert(e(0, mock_principals::alice()));
427        list.insert(e(1, mock_principals::alice()));
428        list.insert(e(2, mock_principals::alice()));
429        list.insert(e(3, mock_principals::alice()));
430
431        let event = list.get_transaction(1).unwrap();
432        let witness = list.witness_transaction(1);
433        assert_eq!(event.time, 1);
434        assert_eq!(witness.reconstruct(), list.root_hash());
435    }
436
437    #[test]
438    fn test_witness_transaction_large() {
439        let mut list = TransactionList::new(mock_principals::xtc(), 0);
440        list.insert(e(0, mock_principals::alice()));
441        list.insert(e(1, mock_principals::alice()));
442        list.insert(e(2, mock_principals::alice()));
443        list.insert(e(3, mock_principals::alice()));
444
445        assert_eq!(list.get_transaction(4).is_none(), true);
446
447        let witness = list.witness_transaction(4);
448        assert_eq!(witness.reconstruct(), list.root_hash());
449    }
450
451    #[test]
452    fn test_witness_transaction_below_offset() {
453        let mut list = TransactionList::new(mock_principals::xtc(), 10);
454        list.insert(e(10, mock_principals::alice()));
455        list.insert(e(11, mock_principals::alice()));
456        list.insert(e(12, mock_principals::alice()));
457        list.insert(e(13, mock_principals::alice()));
458
459        assert_eq!(list.get_transaction(5).is_none(), true);
460        let witness = list.witness_transaction(5);
461        assert_eq!(witness.reconstruct(), list.root_hash());
462    }
463
464    #[test]
465    fn test_witness_user_transactions() {
466        let mut list = TransactionList::new(mock_principals::xtc(), 0);
467
468        for i in 0..5000 {
469            if i % 27 == 0 {
470                list.insert(e(i, mock_principals::bob()));
471            } else {
472                list.insert(e(i, mock_principals::alice()));
473            }
474        }
475
476        let mut count = 0;
477
478        for page in 0.. {
479            let principal = mock_principals::bob();
480            let data = list.get_transactions_for_user(&principal, page);
481            let witness = list.witness_transactions_for_user(&principal, page);
482            let len = data.len();
483
484            assert_eq!(witness.reconstruct(), list.root_hash());
485
486            count += len;
487
488            if len == 0 {
489                break;
490            }
491        }
492
493        // floor(5000 / 27) + 1 = 186
494        assert_eq!(count, 186);
495    }
496
497    #[test]
498    fn serde() {
499        let mut list = TransactionList::new(mock_principals::xtc(), 0);
500        list.insert(e(0, mock_principals::alice()));
501        list.insert(e(1, mock_principals::alice()));
502        list.insert(e(2, mock_principals::alice()));
503        list.insert(e(3, mock_principals::alice()));
504        let expected = list.root_hash();
505
506        let data: Vec<u8> = serde_cbor::to_vec(&list).unwrap();
507        let list: TransactionList = serde_cbor::from_slice(&data).unwrap();
508        assert_eq!(list.root_hash(), expected);
509    }
510
511    #[test]
512    fn candid() {
513        let mut list = TransactionList::new(mock_principals::xtc(), 0);
514        list.insert(e(0, mock_principals::alice()));
515        list.insert(e(1, mock_principals::alice()));
516        list.insert(e(2, mock_principals::alice()));
517        list.insert(e(3, mock_principals::alice()));
518        let expected = list.root_hash();
519
520        let encoded = encode_one(&list).unwrap();
521        let decoded: TransactionList = decode_one(&encoded).unwrap();
522        assert_eq!(decoded.root_hash(), expected);
523    }
524}