oasis_core_runtime/consensus/state/
staking.rs

1//! Staking state in the consensus layer.
2use std::collections::BTreeMap;
3
4use anyhow::anyhow;
5
6use crate::{
7    common::{
8        key_format::{KeyFormat, KeyFormatAtom},
9        quantity::Quantity,
10    },
11    consensus::{
12        address::Address,
13        beacon::EpochTime,
14        staking::{Account, DebondingDelegation, Delegation},
15        state::StateError,
16    },
17    key_format,
18    storage::mkvs::ImmutableMKVS,
19};
20
21/// Consensus staking state wrapper.
22pub struct ImmutableState<'a, T: ImmutableMKVS> {
23    mkvs: &'a T,
24}
25
26impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> {
27    /// Constructs a new ImmutableMKVS.
28    pub fn new(mkvs: &'a T) -> ImmutableState<'a, T> {
29        ImmutableState { mkvs }
30    }
31}
32
33key_format!(AccountsKeyFmt, 0x50, Address);
34key_format!(TotalSupplyKeyFmt, 0x51, ());
35key_format!(CommonPoolKeyFmt, 0x52, ());
36key_format!(DelegationKeyFmt, 0x53, (Address, Address));
37key_format!(
38    DebondingDelegationKeyFmt,
39    0x54,
40    (Address, Address, EpochTime)
41);
42key_format!(LastBlockFees, 0x57, ());
43key_format!(GovernanceDepositsKeyFmt, 0x59, ());
44
45impl<'a, T: ImmutableMKVS> ImmutableState<'a, T> {
46    /// Returns the staking account for the given account address.
47    pub fn account(&self, address: Address) -> Result<Account, StateError> {
48        match self.mkvs.get(&AccountsKeyFmt(address).encode()) {
49            Ok(Some(b)) => {
50                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)))
51            }
52            Ok(None) => Ok(Account::default()),
53            Err(err) => Err(StateError::Unavailable(anyhow!(err))),
54        }
55    }
56
57    fn load_stored_balance<K: KeyFormat>(&self, key_format: K) -> Result<Quantity, StateError> {
58        match self.mkvs.get(&key_format.encode()) {
59            Ok(Some(b)) => {
60                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)))
61            }
62            Ok(None) => Ok(Quantity::default()),
63            Err(err) => Err(StateError::Unavailable(anyhow!(err))),
64        }
65    }
66
67    /// Returns the total supply.
68    pub fn total_supply(&self) -> Result<Quantity, StateError> {
69        self.load_stored_balance(TotalSupplyKeyFmt(()))
70    }
71
72    /// Returns the balance of the global common pool.
73    pub fn common_pool(&self) -> Result<Quantity, StateError> {
74        self.load_stored_balance(CommonPoolKeyFmt(()))
75    }
76
77    /// Returns the last block fees balance.
78    pub fn last_block_fees(&self) -> Result<Quantity, StateError> {
79        self.load_stored_balance(LastBlockFees(()))
80    }
81
82    /// Returns the governance deposits balance.
83    pub fn governance_deposits(&self) -> Result<Quantity, StateError> {
84        self.load_stored_balance(GovernanceDepositsKeyFmt(()))
85    }
86
87    /// Returns the non-empty addresses from the staking ledger.
88    pub fn addresses(&self) -> Result<Vec<Address>, StateError> {
89        let mut it = self.mkvs.iter();
90        it.seek(&AccountsKeyFmt::default().encode_partial(0));
91
92        Ok(it
93            .map_while(|(key, _)| AccountsKeyFmt::decode(&key))
94            .map(|AccountsKeyFmt(addr)| addr)
95            .collect())
96    }
97
98    /// Returns the delegation.
99    pub fn delegation(
100        &self,
101        delegator_addr: Address,
102        escrow_addr: Address,
103    ) -> Result<Delegation, StateError> {
104        match self
105            .mkvs
106            .get(&DelegationKeyFmt((escrow_addr, delegator_addr)).encode())
107        {
108            Ok(Some(b)) => {
109                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)))
110            }
111            Ok(None) => Ok(Delegation::default()),
112            Err(err) => Err(StateError::Unavailable(anyhow!(err))),
113        }
114    }
115
116    /// Returns all active delegations.
117    pub fn delegations(
118        &self,
119    ) -> Result<BTreeMap<Address, BTreeMap<Address, Delegation>>, StateError> {
120        let mut it = self.mkvs.iter();
121        it.seek(&DelegationKeyFmt::default().encode_partial(0));
122
123        let mut result: BTreeMap<Address, BTreeMap<Address, Delegation>> = BTreeMap::new();
124
125        while let Some((DelegationKeyFmt((escrow_addr, delegator_addr)), value)) = it
126            .next()
127            .and_then(|(key, value)| DelegationKeyFmt::decode(&key).zip(value.into()))
128        {
129            if !result.contains_key(&escrow_addr) {
130                result.insert(escrow_addr.clone(), BTreeMap::new());
131            }
132            let inner = result.get_mut(&escrow_addr).expect("inner map must exist");
133
134            inner.insert(
135                delegator_addr,
136                cbor::from_slice(&value).map_err(|err| StateError::Unavailable(anyhow!(err)))?,
137            );
138        }
139
140        Ok(result)
141    }
142
143    /// Returns the debonding delegation.
144    pub fn debonding_delegation(
145        &self,
146        delegator_addr: Address,
147        escrow_addr: Address,
148        epoch: EpochTime,
149    ) -> Result<DebondingDelegation, StateError> {
150        match self
151            .mkvs
152            .get(&DebondingDelegationKeyFmt((delegator_addr, escrow_addr, epoch)).encode())
153        {
154            Ok(Some(b)) => {
155                cbor::from_slice(&b).map_err(|err| StateError::Unavailable(anyhow!(err)))
156            }
157            Ok(None) => Ok(DebondingDelegation::default()),
158            Err(err) => Err(StateError::Unavailable(anyhow!(err))),
159        }
160    }
161
162    /// Returns all debonding delegations.
163    pub fn debonding_delegations(
164        &self,
165    ) -> Result<BTreeMap<Address, BTreeMap<Address, Vec<DebondingDelegation>>>, StateError> {
166        let mut it = self.mkvs.iter();
167        it.seek(&DebondingDelegationKeyFmt::default().encode_partial(0));
168
169        let mut result: BTreeMap<Address, BTreeMap<Address, Vec<DebondingDelegation>>> =
170            BTreeMap::new();
171
172        while let Some((DebondingDelegationKeyFmt((delegator_addr, escrow_addr, _)), value)) = it
173            .next()
174            .and_then(|(key, value)| DebondingDelegationKeyFmt::decode(&key).zip(value.into()))
175        {
176            if !result.contains_key(&escrow_addr) {
177                result.insert(escrow_addr.clone(), BTreeMap::new());
178            }
179            let inner = result.get_mut(&escrow_addr).expect("inner map must exist");
180            if !inner.contains_key(&delegator_addr) {
181                inner.insert(delegator_addr.clone(), Vec::new());
182            }
183            let in_inner = inner
184                .get_mut(&delegator_addr)
185                .expect("inner vec in inner map must exist");
186            in_inner.push(
187                cbor::from_slice(&value).map_err(|err| StateError::Unavailable(anyhow!(err)))?,
188            );
189        }
190
191        Ok(result)
192    }
193}
194
195#[cfg(test)]
196mod test {
197    use crate::{
198        common::crypto::{hash::Hash, signature::PublicKey},
199        consensus::staking::{EscrowAccount, GeneralAccount, SharePool},
200        storage::mkvs::{
201            interop::{Fixture, ProtocolServer},
202            Root, RootType, Tree,
203        },
204    };
205
206    use super::*;
207
208    #[test]
209    fn test_staking_state_interop() {
210        // Keep in sync with go/consensus/cometbft/apps/staking/state/interop/interop.go.
211        // If mock consensus state changes, update the root hash bellow.
212        // See protocol server stdout for hash.
213        // To make the hash show up during tests, run "cargo test" as
214        // "cargo test -- --nocapture".
215
216        // Setup protocol server with initialized mock consensus state.
217        let server = ProtocolServer::new(Fixture::ConsensusMock.into());
218        let mock_consensus_root = Root {
219            version: 1,
220            root_type: RootType::State,
221            hash: Hash::from("8e39bf193f8a954ab8f8d7cb6388c591fd0785ea060bbd8e3752e266b54499d3"),
222            ..Default::default()
223        };
224        let mkvs = Tree::builder()
225            .with_capacity(100_000, 10_000_000)
226            .with_root(mock_consensus_root)
227            .build(server.read_sync());
228        let staking_state = ImmutableState::new(&mkvs);
229
230        let pk =
231            PublicKey::from("7e57baaad01fffffffffffffffffffffffffffffffffffffffffffffffffffff");
232        let pk2 =
233            PublicKey::from("7e57baaad02fffffffffffffffffffffffffffffffffffffffffffffffffffff");
234        let pk3 =
235            PublicKey::from("7e57baaad03fffffffffffffffffffffffffffffffffffffffffffffffffffff");
236        let expected_addrs = vec![
237            Address::from_pk(&pk),
238            Address::from_pk(&pk2),
239            Address::from_pk(&pk3),
240        ];
241
242        // Test all addresses and accounts.
243        let addrs = staking_state
244            .addresses()
245            .expect("addresses query should work");
246        assert_eq!(expected_addrs, addrs, "expected addresses should match");
247
248        let mut accounts = Vec::new();
249        for addr in &addrs {
250            let acc = staking_state
251                .account(addr.clone())
252                .expect("accounts query should work");
253            accounts.push(acc);
254        }
255
256        let expected_accounts = vec![
257            Account {
258                general: GeneralAccount {
259                    balance: Quantity::from(23u32),
260                    nonce: 13,
261                    ..Default::default()
262                },
263                escrow: EscrowAccount {
264                    active: SharePool {
265                        balance: Quantity::from(100u32),
266                        total_shares: Quantity::from(10u32),
267                    },
268                    debonding: SharePool {
269                        balance: Quantity::from(5u32),
270                        total_shares: Quantity::from(5u32),
271                    },
272                    ..Default::default()
273                },
274                ..Default::default()
275            },
276            Account {
277                general: GeneralAccount {
278                    balance: Quantity::from(23u32),
279                    nonce: 1,
280                    ..Default::default()
281                },
282                escrow: EscrowAccount {
283                    active: SharePool {
284                        balance: Quantity::from(500u32),
285                        total_shares: Quantity::from(5u32),
286                    },
287                    ..Default::default()
288                },
289                ..Default::default()
290            },
291            Account {
292                general: GeneralAccount {
293                    balance: Quantity::from(113u32),
294                    nonce: 17,
295                    ..Default::default()
296                },
297                escrow: EscrowAccount {
298                    active: SharePool {
299                        balance: Quantity::from(400u32),
300                        total_shares: Quantity::from(35u32),
301                    },
302                    ..Default::default()
303                },
304                ..Default::default()
305            },
306        ];
307        assert_eq!(
308            expected_accounts, accounts,
309            "expected addresses should match"
310        );
311
312        // Test all delegations.
313        let delegations = staking_state
314            .delegations()
315            .expect("delegations query should work");
316        for (escrow_addr, dels) in &delegations {
317            for (delegator_addr, del) in dels.clone() {
318                let d = staking_state
319                    .delegation(delegator_addr, escrow_addr.clone())
320                    .expect("delegation query should work");
321                assert_eq!(del, d, "delegation should match")
322            }
323        }
324
325        let mut expected_delegations: BTreeMap<Address, BTreeMap<Address, Delegation>> =
326            BTreeMap::new();
327        // Delegations to address[0].
328        expected_delegations.insert(
329            addrs[0].clone(),
330            [
331                (
332                    addrs[0].clone(),
333                    Delegation {
334                        shares: Quantity::from(5u32),
335                    },
336                ),
337                (
338                    addrs[1].clone(),
339                    Delegation {
340                        shares: Quantity::from(5u32),
341                    },
342                ),
343            ]
344            .iter()
345            .cloned()
346            .collect(),
347        );
348        // Delegations to address[1].
349        expected_delegations.insert(
350            addrs[1].clone(),
351            [(
352                addrs[2].clone(),
353                Delegation {
354                    shares: Quantity::from(5u32),
355                },
356            )]
357            .iter()
358            .cloned()
359            .collect(),
360        );
361        // Delegations to address[2].
362        expected_delegations.insert(
363            addrs[2].clone(),
364            [
365                (
366                    addrs[0].clone(),
367                    Delegation {
368                        shares: Quantity::from(20u32),
369                    },
370                ),
371                (
372                    addrs[1].clone(),
373                    Delegation {
374                        shares: Quantity::from(6u32),
375                    },
376                ),
377                (
378                    addrs[2].clone(),
379                    Delegation {
380                        shares: Quantity::from(10u32),
381                    },
382                ),
383            ]
384            .iter()
385            .cloned()
386            .collect(),
387        );
388        assert_eq!(
389            expected_delegations, delegations,
390            "expected delegations should match"
391        );
392
393        // Test all debonding delegations.
394        let debonding_delegations = staking_state
395            .debonding_delegations()
396            .expect("debonding delegations query should work");
397        for (escrow_addr, debss) in &debonding_delegations {
398            for (delegator_addr, debs) in debss {
399                for deb in debs {
400                    let d = staking_state
401                        .debonding_delegation(
402                            delegator_addr.clone(),
403                            escrow_addr.clone(),
404                            deb.debond_end_time,
405                        )
406                        .expect("debonding delegation query should work");
407                    assert_eq!(deb.clone(), d, "debonding delegation should match")
408                }
409            }
410        }
411        let mut expected_debonding: BTreeMap<Address, BTreeMap<Address, Vec<DebondingDelegation>>> =
412            BTreeMap::new();
413        // Debonding delegations in address[0].
414        expected_debonding.insert(
415            addrs[0].clone(),
416            [
417                (
418                    addrs[0].clone(),
419                    vec![DebondingDelegation {
420                        shares: Quantity::from(1u32),
421                        debond_end_time: 33,
422                    }],
423                ),
424                (
425                    addrs[1].clone(),
426                    vec![
427                        DebondingDelegation {
428                            shares: Quantity::from(1u32),
429                            debond_end_time: 15,
430                        },
431                        DebondingDelegation {
432                            shares: Quantity::from(1u32),
433                            debond_end_time: 21,
434                        },
435                    ],
436                ),
437                (
438                    addrs[2].clone(),
439                    vec![DebondingDelegation {
440                        shares: Quantity::from(2u32),
441                        debond_end_time: 100,
442                    }],
443                ),
444            ]
445            .iter()
446            .cloned()
447            .collect(),
448        );
449        assert_eq!(
450            expected_debonding, debonding_delegations,
451            "expected debonding delegations should match"
452        );
453
454        // Test all stored balances.
455        let total_supply = staking_state
456            .total_supply()
457            .expect("total supply query should work");
458        assert_eq!(
459            Quantity::from(10000u32),
460            total_supply,
461            "total supply should match expected"
462        );
463
464        let common_pool = staking_state
465            .common_pool()
466            .expect("common pool query should work");
467        assert_eq!(
468            Quantity::from(1000u32),
469            common_pool,
470            "common pool should match expected"
471        );
472
473        let last_block_fees = staking_state
474            .last_block_fees()
475            .expect("last block fees query should work");
476        assert_eq!(
477            Quantity::from(33u32),
478            last_block_fees,
479            "last block fees should match expected"
480        );
481
482        let governance_deposits = staking_state
483            .governance_deposits()
484            .expect("governance deposits query should work");
485        assert_eq!(
486            Quantity::from(12u32),
487            governance_deposits,
488            "governance deposits should match expected"
489        );
490    }
491}