oasis_core_runtime/consensus/
staking.rs

1//! Consensus staking structures.
2//!
3//! # Note
4//!
5//! This **MUST** be kept in sync with go/staking/api.
6//!
7use std::collections::BTreeMap;
8
9use crate::{
10    common::{crypto::hash::Hash, quantity::Quantity},
11    consensus::{address::Address, beacon::EpochTime},
12};
13
14/// A stake transfer.
15#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
16pub struct Transfer {
17    pub to: Address,
18    pub amount: Quantity,
19}
20
21/// A withdrawal from an account.
22#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
23pub struct Withdraw {
24    pub from: Address,
25    pub amount: Quantity,
26}
27
28/// A stake escrow.
29#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
30pub struct Escrow {
31    pub account: Address,
32    pub amount: Quantity,
33}
34
35/// A reclaim escrow.
36#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
37pub struct ReclaimEscrow {
38    pub account: Address,
39    pub shares: Quantity,
40}
41
42/// Kind of staking threshold.
43#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, cbor::Encode, cbor::Decode)]
44#[repr(i32)]
45pub enum ThresholdKind {
46    /// Entity staking threshold.
47    KindEntity = 0,
48    /// Validator node staking threshold.
49    KindNodeValidator = 1,
50    /// Compute node staking threshold.
51    KindNodeCompute = 2,
52    /// Keymanager node staking threshold.
53    KindNodeKeyManager = 4,
54    /// Compute runtime staking threshold.
55    KindRuntimeCompute = 5,
56    /// Keymanager runtime staking threshold.
57    KindRuntimeKeyManager = 6,
58}
59
60/// Entry in the staking ledger.
61#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
62pub struct Account {
63    #[cbor(optional)]
64    pub general: GeneralAccount,
65
66    #[cbor(optional)]
67    pub escrow: EscrowAccount,
68}
69
70/// General purpose account.
71#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
72pub struct GeneralAccount {
73    #[cbor(optional)]
74    pub balance: Quantity,
75
76    #[cbor(optional)]
77    pub nonce: u64,
78
79    #[cbor(optional)]
80    pub allowances: BTreeMap<Address, Quantity>,
81}
82
83/// Escrow account.
84#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
85pub struct EscrowAccount {
86    #[cbor(optional)]
87    pub active: SharePool,
88
89    #[cbor(optional)]
90    pub debonding: SharePool,
91
92    #[cbor(optional)]
93    pub commission_schedule: CommissionSchedule,
94
95    #[cbor(optional)]
96    pub stake_accumulator: StakeAccumulator,
97}
98
99/// Combined balance of serval entries, the relative sizes of which are tracked through shares.
100#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
101pub struct SharePool {
102    #[cbor(optional)]
103    pub balance: Quantity,
104
105    #[cbor(optional)]
106    pub total_shares: Quantity,
107}
108
109/// Defines a list of commission rates and commission rate bounds with their starting times.
110#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
111pub struct CommissionSchedule {
112    #[cbor(optional)]
113    pub rates: Vec<CommissionRateStep>,
114
115    #[cbor(optional)]
116    pub bounds: Vec<CommissionRateBoundStep>,
117}
118
119/// Commission rate and its starting time.
120#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
121pub struct CommissionRateStep {
122    pub start: EpochTime,
123    pub rate: Quantity,
124}
125
126/// Commission rate bound and its starting time.
127#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
128pub struct CommissionRateBoundStep {
129    #[cbor(optional)]
130    pub start: EpochTime,
131
132    #[cbor(optional)]
133    pub rate_min: Quantity,
134
135    #[cbor(optional)]
136    pub rate_max: Quantity,
137}
138
139/// Per escrow account stake accumulator.
140#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
141pub struct StakeAccumulator {
142    #[cbor(optional)]
143    pub claims: BTreeMap<StakeClaim, Vec<StakeThreshold>>,
144}
145
146/// Unique stake claim identifier.
147pub type StakeClaim = String;
148
149/// Stake threshold used in the stake accumulator.
150#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
151pub struct StakeThreshold {
152    #[cbor(optional)]
153    pub global: Option<ThresholdKind>,
154
155    #[cbor(optional, rename = "const")]
156    pub constant: Option<Quantity>,
157}
158
159/// Delegation descriptor.
160#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
161pub struct Delegation {
162    pub shares: Quantity,
163}
164
165/// Debonding delegation descriptor.
166#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
167pub struct DebondingDelegation {
168    pub shares: Quantity,
169
170    #[cbor(rename = "debond_end")]
171    pub debond_end_time: EpochTime,
172}
173
174/// Reason for slashing an entity.
175#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, cbor::Encode, cbor::Decode)]
176#[repr(u8)]
177pub enum SlashReason {
178    /// Slashing due to submission of incorrect results in runtime executor commitments.
179    RuntimeIncorrectResults = 0x80,
180    /// Slashing due to signing two different executor commits or proposed batches for the same
181    /// round.
182    RuntimeEquivocation = 0x81,
183    /// Slashing due to not doing the required work.
184    RuntimeLiveness = 0x82,
185}
186
187/// Per-reason slashing configuration.
188#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
189pub struct Slash {
190    pub amount: Quantity,
191    pub freeze_interval: EpochTime,
192}
193
194/// Transfer result.
195#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
196pub struct TransferResult {
197    pub from: Address,
198    pub to: Address,
199    pub amount: Quantity,
200}
201
202/// Add escrow result.
203#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
204pub struct AddEscrowResult {
205    pub owner: Address,
206    pub escrow: Address,
207    pub amount: Quantity,
208    pub new_shares: Quantity,
209}
210
211/// Reclaim escrow result.
212#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
213pub struct ReclaimEscrowResult {
214    pub owner: Address,
215    pub escrow: Address,
216    pub amount: Quantity,
217    pub remaining_shares: Quantity,
218    pub debonding_shares: Quantity,
219    pub debond_end_time: EpochTime,
220}
221
222/// Withdraw result.
223#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
224pub struct WithdrawResult {
225    pub owner: Address,
226    pub beneficiary: Address,
227    pub allowance: Quantity,
228    pub amount_change: Quantity,
229}
230
231/// A staking-related event.
232#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
233pub struct Event {
234    #[cbor(optional)]
235    pub height: i64,
236    #[cbor(optional)]
237    pub tx_hash: Hash,
238
239    // TODO: Consider refactoring this to be an enum.
240    #[cbor(optional)]
241    pub transfer: Option<TransferEvent>,
242    #[cbor(optional)]
243    pub burn: Option<BurnEvent>,
244    #[cbor(optional)]
245    pub escrow: Option<EscrowEvent>,
246    #[cbor(optional)]
247    pub allowance_change: Option<AllowanceChangeEvent>,
248}
249
250/// Event emitted when stake is transferred, either by a call to Transfer or Withdraw.
251#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
252pub struct TransferEvent {
253    pub from: Address,
254    pub to: Address,
255    pub amount: Quantity,
256}
257
258/// Event emitted when stake is destroyed via a call to Burn.
259#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
260pub struct BurnEvent {
261    pub owner: Address,
262    pub amount: Quantity,
263}
264
265/// Escrow-related events.
266#[derive(Clone, Debug, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
267pub enum EscrowEvent {
268    /// Event emitted when stake is transferred into an escrow account.
269    #[cbor(rename = "add")]
270    Add {
271        owner: Address,
272        escrow: Address,
273        amount: Quantity,
274        new_shares: Quantity,
275    },
276
277    /// Event emitted when stake is taken from an escrow account (i.e. stake is slashed).
278    #[cbor(rename = "take")]
279    Take {
280        owner: Address,
281        // The sum of amounts slashed from active and debonding escrow balances.
282        amount: Quantity,
283        // The amount slashed from the debonding escrow balance.
284        debonding_amount: Quantity,
285    },
286
287    /// Event emitted when the debonding process has started and the given number of active shares
288    /// have been moved into the debonding pool and started debonding.
289    ///
290    /// Note that the given amount is valid at the time of debonding start and may not correspond to
291    /// the final debonded amount in case any escrowed stake is subject to slashing.
292    #[cbor(rename = "debonding_start")]
293    DebondingStart {
294        owner: Address,
295        escrow: Address,
296        amount: Quantity,
297        active_shares: Quantity,
298        debonding_shares: Quantity,
299        debond_end_time: EpochTime,
300    },
301
302    /// Event emitted when stake is reclaimed from an escrow account back into owner's general
303    /// account.
304    #[cbor(rename = "reclaim")]
305    Reclaim {
306        owner: Address,
307        escrow: Address,
308        amount: Quantity,
309        shares: Quantity,
310    },
311}
312
313/// Event emitted when allowance is changed for a beneficiary.
314#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
315pub struct AllowanceChangeEvent {
316    pub owner: Address,
317    pub beneficiary: Address,
318    pub allowance: Quantity,
319    #[cbor(optional)]
320    pub negative: bool,
321    pub amount_change: Quantity,
322}
323
324#[cfg(test)]
325mod tests {
326    use base64::prelude::*;
327
328    use super::*;
329    use crate::{
330        common::crypto::signature::PublicKey,
331        consensus::address::{COMMON_POOL_ADDRESS, GOVERNANCE_DEPOSITS_ADDRESS},
332    };
333
334    #[test]
335    fn test_consistent_accounts() {
336        let tcs = vec![
337        ("oA==", Account::default()),
338        (
339            "oWdnZW5lcmFsomVub25jZRghZ2JhbGFuY2VBCg==",
340            Account {
341                general: GeneralAccount {
342                    balance: Quantity::from(10u32),
343                    nonce: 33,
344                    ..Default::default()
345                },
346                ..Default::default()
347            },
348        ),
349        (
350            "oWdnZW5lcmFsoWphbGxvd2FuY2VzolUAdU/0RxQ6XsX0cbMPhna5TVaxV1BBIVUA98Te1iET4sKC6oZyI6VE7VXWum5BZA==",
351            {
352                Account {
353                    general: GeneralAccount {
354                        allowances: [
355                            (COMMON_POOL_ADDRESS.clone(), Quantity::from(100u32)),
356                            (GOVERNANCE_DEPOSITS_ADDRESS.clone(), Quantity::from(33u32))
357                        ].iter().cloned().collect(),
358                        ..Default::default()
359                        },
360                    ..Default::default()
361                }
362                },
363        ),
364        (
365            "oWZlc2Nyb3ejZmFjdGl2ZaJnYmFsYW5jZUIETGx0b3RhbF9zaGFyZXNBC3FzdGFrZV9hY2N1bXVsYXRvcqFmY2xhaW1zoWZlbnRpdHmCoWVjb25zdEFNoWZnbG9iYWwCc2NvbW1pc3Npb25fc2NoZWR1bGWhZmJvdW5kc4GjZXN0YXJ0GCFocmF0ZV9tYXhCA+hocmF0ZV9taW5BCg==",
366            Account {
367                escrow: EscrowAccount {
368                    active: SharePool{
369                        balance: Quantity::from(1100u32),
370                        total_shares: Quantity::from(11u32),
371                    },
372                    debonding: SharePool::default(),
373                    commission_schedule: CommissionSchedule {
374                        bounds: vec![CommissionRateBoundStep{
375                            start: 33,
376                            rate_min: Quantity::from(10u32),
377                            rate_max: Quantity::from(1000u32),
378                        }],
379                        ..Default::default()
380                    },
381                    stake_accumulator: StakeAccumulator {
382                        claims: [
383                            (
384                                "entity".to_string(),
385                                vec![
386                                    StakeThreshold{
387                                        constant: Some(Quantity::from(77u32)),
388                                        ..Default::default()
389                                    },
390                                    StakeThreshold{
391                                        global: Some(ThresholdKind::KindNodeCompute),
392                                        ..Default::default()
393                                    },
394                                ]
395                            )
396                        ].iter().cloned().collect()
397                    }
398                },
399                ..Default::default()
400            },
401        )
402    ];
403        for (encoded_base64, rr) in tcs {
404            let dec: Account = cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
405                .expect("account should deserialize correctly");
406            assert_eq!(dec, rr, "decoded account should match the expected value");
407        }
408    }
409
410    #[test]
411    fn test_consistent_delegations() {
412        let tcs = vec![
413            ("oWZzaGFyZXNA", Delegation::default()),
414            (
415                "oWZzaGFyZXNBZA==",
416                Delegation {
417                    shares: Quantity::from(100u32),
418                },
419            ),
420        ];
421        for (encoded_base64, rr) in tcs {
422            let dec: Delegation =
423                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
424                    .expect("delegation should deserialize correctly");
425            assert_eq!(dec, rr, "decoded account should match the expected value");
426        }
427    }
428
429    #[test]
430    fn test_consistent_debonding_delegations() {
431        let tcs = vec![
432            (
433                "omZzaGFyZXNAamRlYm9uZF9lbmQA",
434                DebondingDelegation::default(),
435            ),
436            (
437                "omZzaGFyZXNBZGpkZWJvbmRfZW5kFw==",
438                DebondingDelegation {
439                    shares: Quantity::from(100u32),
440                    debond_end_time: 23,
441                },
442            ),
443        ];
444        for (encoded_base64, rr) in tcs {
445            let dec: DebondingDelegation =
446                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
447                    .expect("debonding delegation should deserialize correctly");
448            assert_eq!(dec, rr, "decoded account should match the expected value");
449        }
450    }
451
452    #[test]
453    fn test_consistent_transfer_results() {
454        let addr1 = Address::from_pk(&PublicKey::from(
455            "aaafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
456        ));
457        let addr2 = Address::from_pk(&PublicKey::from(
458            "bbbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
459        ));
460
461        let tcs = vec![
462            ("o2J0b1UAAAAAAAAAAAAAAAAAAAAAAAAAAABkZnJvbVUAAAAAAAAAAAAAAAAAAAAAAAAAAABmYW1vdW50QA==", TransferResult::default()),
463            (
464                "o2J0b1UAuRI5eJXmRwxR+r7MndyD9wrthqFkZnJvbVUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNmYW1vdW50QWQ=",
465                TransferResult {
466                    amount: Quantity::from(100u32),
467                    from: addr1,
468                    to: addr2,
469                },
470            ),
471        ];
472        for (encoded_base64, rr) in tcs {
473            let dec: TransferResult =
474                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
475                    .expect("transfer result should deserialize correctly");
476            assert_eq!(dec, rr, "decoded result should match the expected value");
477        }
478    }
479
480    #[test]
481    fn test_consistent_withdraw_results() {
482        let addr1 = Address::from_pk(&PublicKey::from(
483            "aaafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
484        ));
485        let addr2 = Address::from_pk(&PublicKey::from(
486            "bbbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
487        ));
488
489        let tcs = vec![
490            ("pGVvd25lclUAAAAAAAAAAAAAAAAAAAAAAAAAAABpYWxsb3dhbmNlQGtiZW5lZmljaWFyeVUAAAAAAAAAAAAAAAAAAAAAAAAAAABtYW1vdW50X2NoYW5nZUA=", WithdrawResult::default()),
491            (
492                "pGVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNpYWxsb3dhbmNlQQprYmVuZWZpY2lhcnlVALkSOXiV5kcMUfq+zJ3cg/cK7YahbWFtb3VudF9jaGFuZ2VBBQ==",
493                WithdrawResult {
494                    owner: addr1,
495                    beneficiary: addr2,
496                    allowance: Quantity::from(10u32),
497                    amount_change: Quantity::from(5u32),
498                },
499            ),
500        ];
501        for (encoded_base64, rr) in tcs {
502            let dec: WithdrawResult =
503                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
504                    .expect("withdraw result should deserialize correctly");
505            assert_eq!(dec, rr, "decoded result should match the expected value");
506        }
507    }
508
509    #[test]
510    fn test_consistent_add_escrow_results() {
511        let addr1 = Address::from_pk(&PublicKey::from(
512            "aaafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
513        ));
514        let addr2 = Address::from_pk(&PublicKey::from(
515            "bbbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
516        ));
517
518        let tcs = vec![
519            ("pGVvd25lclUAAAAAAAAAAAAAAAAAAAAAAAAAAABmYW1vdW50QGZlc2Nyb3dVAAAAAAAAAAAAAAAAAAAAAAAAAAAAam5ld19zaGFyZXNA", AddEscrowResult::default()),
520            (
521                "pGVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNmYW1vdW50QWRmZXNjcm93VQC5Ejl4leZHDFH6vsyd3IP3Cu2GoWpuZXdfc2hhcmVzQQU=",
522                AddEscrowResult {
523                    owner: addr1,
524                    escrow: addr2,
525                    amount: Quantity::from(100u32),
526                    new_shares: Quantity::from(5u32),
527                },
528            ),
529        ];
530        for (encoded_base64, rr) in tcs {
531            let dec: AddEscrowResult =
532                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
533                    .expect("add escrow result should deserialize correctly");
534            assert_eq!(dec, rr, "decoded result should match the expected value");
535        }
536    }
537
538    #[test]
539    fn test_consistent_reclaim_escrow_results() {
540        let addr1 = Address::from_pk(&PublicKey::from(
541            "aaafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
542        ));
543        let addr2 = Address::from_pk(&PublicKey::from(
544            "bbbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
545        ));
546
547        let tcs = vec![
548            ("pmVvd25lclUAAAAAAAAAAAAAAAAAAAAAAAAAAABmYW1vdW50QGZlc2Nyb3dVAAAAAAAAAAAAAAAAAAAAAAAAAAAAb2RlYm9uZF9lbmRfdGltZQBwZGVib25kaW5nX3NoYXJlc0BwcmVtYWluaW5nX3NoYXJlc0A=", ReclaimEscrowResult::default()),
549            (
550                "pmVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNmYW1vdW50QWRmZXNjcm93VQC5Ejl4leZHDFH6vsyd3IP3Cu2GoW9kZWJvbmRfZW5kX3RpbWUYKnBkZWJvbmRpbmdfc2hhcmVzQRlwcmVtYWluaW5nX3NoYXJlc0Ey",
551                ReclaimEscrowResult {
552                    owner: addr1,
553                    escrow: addr2,
554                    amount: Quantity::from(100u32),
555                    remaining_shares: Quantity::from(50u32),
556                    debonding_shares: Quantity::from(25u32),
557                    debond_end_time: 42,
558                },
559            ),
560        ];
561        for (encoded_base64, rr) in tcs {
562            let dec: ReclaimEscrowResult =
563                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
564                    .expect("reclaim escrow result should deserialize correctly");
565            assert_eq!(dec, rr, "decoded result should match the expected value");
566        }
567    }
568
569    #[test]
570    fn test_consistent_events() {
571        let addr1 = Address::from_pk(&PublicKey::from(
572            "aaafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
573        ));
574        let addr2 = Address::from_pk(&PublicKey::from(
575            "bbbfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
576        ));
577        let tx_hash = Hash::empty_hash();
578
579        let tcs = vec![
580            (
581                "oWd0eF9oYXNoWCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
582                Event::default(),
583            ),
584            (
585                "omZoZWlnaHQYKmd0eF9oYXNoWCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
586                Event {
587                    height: 42,
588                    ..Default::default()
589                },
590            ),
591            (
592                "omZoZWlnaHQYKmd0eF9oYXNoWCDGcrjR71btKKuHw2IsURQGm90617j5c3SY0MAezvCWeg==",
593                Event {
594                    height: 42,
595                    tx_hash,
596                    ..Default::default()
597                },
598            ),
599
600            // Transfer.
601            (
602                "o2ZoZWlnaHQYKmd0eF9oYXNoWCDGcrjR71btKKuHw2IsURQGm90617j5c3SY0MAezvCWemh0cmFuc2ZlcqNidG9VALkSOXiV5kcMUfq+zJ3cg/cK7YahZGZyb21VACByFDSJP2FsCYFI4s+fmePigm4TZmFtb3VudEFk",
603                Event {
604                    height: 42,
605                    tx_hash,
606                    transfer: Some(TransferEvent {
607                        from: addr1.clone(),
608                        to: addr2.clone(),
609                        amount: 100u32.into(),
610                    }),
611                    ..Default::default()
612                },
613            ),
614
615            // Burn.
616            (
617                "o2RidXJuomVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNmYW1vdW50QWRmaGVpZ2h0GCpndHhfaGFzaFggxnK40e9W7Sirh8NiLFEUBpvdOte4+XN0mNDAHs7wlno=",
618                Event {
619                    height: 42,
620                    tx_hash,
621                    burn: Some(BurnEvent {
622                        owner: addr1.clone(),
623                        amount: 100u32.into(),
624                    }),
625                    ..Default::default()
626                },
627            ),
628
629            // Escrow.
630            (
631                "o2Zlc2Nyb3ehY2FkZKRlb3duZXJVACByFDSJP2FsCYFI4s+fmePigm4TZmFtb3VudEFkZmVzY3Jvd1UAuRI5eJXmRwxR+r7MndyD9wrthqFqbmV3X3NoYXJlc0EyZmhlaWdodBgqZ3R4X2hhc2hYIMZyuNHvVu0oq4fDYixRFAab3TrXuPlzdJjQwB7O8JZ6",
632                Event {
633                    height: 42,
634                    tx_hash,
635                    escrow: Some(EscrowEvent::Add {
636                        owner: addr1.clone(),
637                        escrow: addr2.clone(),
638                        amount: 100u32.into(),
639                        new_shares: 50u32.into(),
640                    }),
641                    ..Default::default()
642                },
643            ),
644            (
645                "o2Zlc2Nyb3ehZHRha2WjZW93bmVyVQAgchQ0iT9hbAmBSOLPn5nj4oJuE2ZhbW91bnRBZHBkZWJvbmRpbmdfYW1vdW50QRRmaGVpZ2h0GCpndHhfaGFzaFggxnK40e9W7Sirh8NiLFEUBpvdOte4+XN0mNDAHs7wlno=",
646                Event {
647                    height: 42,
648                    tx_hash,
649                    escrow: Some(EscrowEvent::Take {
650                        owner: addr1.clone(),
651                        amount: 100u32.into(),
652                        debonding_amount: 20u32.into()
653                    }),
654                    ..Default::default()
655                },
656            ),
657            (
658                "o2Zlc2Nyb3ehb2RlYm9uZGluZ19zdGFydKZlb3duZXJVACByFDSJP2FsCYFI4s+fmePigm4TZmFtb3VudEFkZmVzY3Jvd1UAuRI5eJXmRwxR+r7MndyD9wrthqFtYWN0aXZlX3NoYXJlc0Eyb2RlYm9uZF9lbmRfdGltZRgqcGRlYm9uZGluZ19zaGFyZXNBGWZoZWlnaHQYKmd0eF9oYXNoWCDGcrjR71btKKuHw2IsURQGm90617j5c3SY0MAezvCWeg==",
659                Event {
660                    height: 42,
661                    tx_hash,
662                    escrow: Some(EscrowEvent::DebondingStart {
663                        owner: addr1.clone(),
664                        escrow: addr2.clone(),
665                        amount: 100u32.into(),
666                        active_shares: 50u32.into(),
667                        debonding_shares: 25u32.into(),
668                        debond_end_time: 42,
669                    }),
670                    ..Default::default()
671                },
672            ),
673            (
674                "o2Zlc2Nyb3ehZ3JlY2xhaW2kZW93bmVyVQAgchQ0iT9hbAmBSOLPn5nj4oJuE2ZhbW91bnRBZGZlc2Nyb3dVALkSOXiV5kcMUfq+zJ3cg/cK7YahZnNoYXJlc0EZZmhlaWdodBgqZ3R4X2hhc2hYIMZyuNHvVu0oq4fDYixRFAab3TrXuPlzdJjQwB7O8JZ6",
675                Event {
676                    height: 42,
677                    tx_hash,
678                    escrow: Some(EscrowEvent::Reclaim {
679                        owner: addr1.clone(),
680                        escrow: addr2.clone(),
681                        amount: 100u32.into(),
682                        shares: 25u32.into(),
683                    }),
684                    ..Default::default()
685                },
686            ),
687
688            // Allowance change.
689            (
690                "o2ZoZWlnaHQYKmd0eF9oYXNoWCDGcrjR71btKKuHw2IsURQGm90617j5c3SY0MAezvCWenBhbGxvd2FuY2VfY2hhbmdlpGVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNpYWxsb3dhbmNlQWRrYmVuZWZpY2lhcnlVALkSOXiV5kcMUfq+zJ3cg/cK7YahbWFtb3VudF9jaGFuZ2VBMg==",
691                Event {
692                    height: 42,
693                    tx_hash,
694                    allowance_change: Some(AllowanceChangeEvent {
695                        owner: addr1.clone(),
696                        beneficiary: addr2.clone(),
697                        allowance: 100u32.into(),
698                        negative: false,
699                        amount_change: 50u32.into(),
700                    }),
701                    ..Default::default()
702                },
703            ),
704            (
705                "o2ZoZWlnaHQYKmd0eF9oYXNoWCDGcrjR71btKKuHw2IsURQGm90617j5c3SY0MAezvCWenBhbGxvd2FuY2VfY2hhbmdlpWVvd25lclUAIHIUNIk/YWwJgUjiz5+Z4+KCbhNobmVnYXRpdmX1aWFsbG93YW5jZUFka2JlbmVmaWNpYXJ5VQC5Ejl4leZHDFH6vsyd3IP3Cu2GoW1hbW91bnRfY2hhbmdlQTI=",
706                Event {
707                    height: 42,
708                    tx_hash,
709                    allowance_change: Some(AllowanceChangeEvent {
710                        owner: addr1.clone(),
711                        beneficiary: addr2.clone(),
712                        allowance: 100u32.into(),
713                        negative: true,
714                        amount_change: 50u32.into(),
715                    }),
716                    ..Default::default()
717                },
718            ),
719        ];
720        for (encoded_base64, ev) in tcs {
721            let dec: Event = cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
722                .expect("event should deserialize correctly");
723            assert_eq!(dec, ev, "decoded event should match the expected value");
724        }
725    }
726}