oasis_core_runtime/consensus/roothash/
mod.rs

1//! Consensus roothash structures.
2//!
3//! # Note
4//!
5//! This **MUST** be kept in sync with go/roothash/api.
6//!
7use thiserror::Error;
8
9use crate::{
10    common::{
11        crypto::{hash::Hash, signature::PublicKey},
12        namespace::Namespace,
13    },
14    consensus::{registry::Runtime, scheduler::Committee, state::StateError},
15};
16
17// Modules.
18mod block;
19mod commitment;
20mod message;
21
22// Re-exports.
23pub use block::*;
24pub use commitment::*;
25pub use message::*;
26
27/// Errors emitted by the roothash module.
28#[derive(Debug, Error)]
29pub enum Error {
30    #[error("roothash: invalid runtime {0}")]
31    InvalidRuntime(Namespace),
32
33    #[error(transparent)]
34    State(#[from] StateError),
35
36    #[error("roothash/commitment: no runtime configured")]
37    NoRuntime,
38
39    #[error("roothash/commitment: no committee configured")]
40    NoCommittee,
41
42    #[error("roothash/commitment: invalid committee kind")]
43    InvalidCommitteeKind,
44
45    #[error("roothash/commitment: batch RAK signature invalid")]
46    RakSigInvalid,
47
48    #[error("roothash/commitment: node not part of committee")]
49    NotInCommittee,
50
51    #[error("roothash/commitment: node already sent commitment")]
52    AlreadyCommitted,
53
54    #[error("roothash/commitment: submitted commitment is not based on correct block")]
55    NotBasedOnCorrectBlock,
56
57    #[error("roothash/commitment: discrepancy detected")]
58    DiscrepancyDetected,
59
60    #[error("roothash/commitment: still waiting for commits")]
61    StillWaiting,
62
63    #[error("roothash/commitment: insufficient votes to finalize discrepancy resolution round")]
64    InsufficientVotes,
65
66    #[error("roothash/commitment: bad executor commitment")]
67    BadExecutorCommitment,
68
69    #[error("roothash/commitment: invalid messages")]
70    InvalidMessages,
71
72    #[error("roothash/commitment: invalid round")]
73    InvalidRound,
74
75    #[error("roothash/commitment: no proposer commitment")]
76    NoProposerCommitment,
77
78    #[error("roothash/commitment: bad proposer commitment")]
79    BadProposerCommitment,
80}
81
82/// Runtime block annotated with consensus information.
83#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
84pub struct AnnotatedBlock {
85    /// Consensus height at which this runtime block was produced.
86    pub consensus_height: i64,
87    /// Runtime block.
88    pub block: Block,
89}
90
91/// Result of a message being processed by the consensus layer.
92#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
93pub struct MessageEvent {
94    #[cbor(optional)]
95    pub module: String,
96
97    #[cbor(optional)]
98    pub code: u32,
99
100    #[cbor(optional)]
101    pub index: u32,
102
103    #[cbor(optional)]
104    pub result: Option<cbor::Value>,
105}
106
107impl MessageEvent {
108    /// Returns true if the event indicates that the message was successfully processed.
109    pub fn is_success(&self) -> bool {
110        self.code == 0
111    }
112}
113
114/// Per-runtime state.
115#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
116#[cbor(allow_unknown)]
117pub struct RuntimeState {
118    /// Latest per-epoch runtime descriptor.
119    pub runtime: Runtime,
120    /// Flag indicating whether the runtime is currently suspended.
121    #[cbor(optional)]
122    pub suspended: bool,
123
124    // Runtime's first block.
125    pub genesis_block: Block,
126
127    /// Runtime's most recently finalized block.
128    pub last_block: Block,
129    /// Height at which the runtime's most recent block was finalized.
130    pub last_block_height: i64,
131
132    /// Runtime round which was normally processed by the runtime. This is also the round that
133    /// contains the message results for the last processed runtime messages.
134    pub last_normal_round: u64,
135    /// Consensus block height corresponding to `last_normal_round`.
136    pub last_normal_height: i64,
137
138    /// Committee the executor pool is collecting commitments for.
139    #[cbor(optional)]
140    pub commitee: Option<Committee>,
141    // NOTE: Commitment pool deserialization is currently not supported.
142    /// Consensus height at which the round is scheduled for forced finalization.
143    #[cbor(optional)]
144    pub next_timeout: i64,
145
146    /// Liveness statistics for the current epoch.
147    pub liveness_stats: Option<LivenessStatistics>,
148}
149
150/// Per-epoch liveness statistics for nodes.
151#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
152pub struct LivenessStatistics {
153    /// Total number of rounds in the last epoch, excluding any rounds generated by the roothash
154    /// service itself.
155    pub total_rounds: u64,
156
157    /// A list of counters, specified in committee order (e.g. counter at index i has the value for
158    /// node i in the committee).
159    pub good_rounds: Vec<u64>,
160
161    /// A list that records the number of finalized rounds when a node acted as a proposed with the
162    /// highest rank.
163    ///
164    /// The list is ordered according to the committee arrangement (i.e., the counter at index i
165    /// holds the value for the node at index i in the committee).
166    pub finalized_proposals: Vec<u64>,
167
168    /// A list that records the number of failed rounds when a node/ acted as a proposer with the
169    /// highest rank.
170    ///
171    /// The list is ordered according to the committee arrangement (i.e., the counter at index i
172    /// holds the value for the node at index i in the committee).
173    pub missed_proposals: Vec<u64>,
174}
175
176/// Information about how a particular round was executed by the consensus layer.
177#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
178pub struct RoundResults {
179    /// Results of executing emitted runtime messages.
180    #[cbor(optional)]
181    pub messages: Vec<MessageEvent>,
182
183    /// Public keys of compute nodes' controlling entities that positively contributed to the round
184    /// by replicating the computation correctly.
185    #[cbor(optional)]
186    pub good_compute_entities: Vec<PublicKey>,
187    /// Public keys of compute nodes' controlling entities that negatively contributed to the round
188    /// by causing discrepancies.
189    #[cbor(optional)]
190    pub bad_compute_entities: Vec<PublicKey>,
191}
192
193/// Per-round state and I/O roots that are stored in consensus state.
194#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, cbor::Encode, cbor::Decode)]
195#[cbor(as_array)]
196pub struct RoundRoots {
197    pub state_root: Hash,
198    pub io_root: Hash,
199}
200
201#[cfg(test)]
202mod tests {
203    use base64::prelude::*;
204
205    use super::*;
206
207    #[test]
208    fn test_consistent_round_results() {
209        let tcs = vec![
210            ("oA==", RoundResults::default()),
211            ("oWhtZXNzYWdlc4GiZGNvZGUBZm1vZHVsZWR0ZXN0", RoundResults {
212                messages: vec![MessageEvent{module: "test".to_owned(), code: 1, index: 0, result: None}],
213                ..Default::default()
214            }),
215            ("omhtZXNzYWdlc4GkZGNvZGUYKmVpbmRleAFmbW9kdWxlZHRlc3RmcmVzdWx0a3Rlc3QtcmVzdWx0dWdvb2RfY29tcHV0ZV9lbnRpdGllc4NYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=",
216                RoundResults {
217                    messages: vec![MessageEvent{module: "test".to_owned(), code: 42, index: 1, result: Some(cbor::Value::TextString("test-result".to_string()))}],
218                    good_compute_entities: vec![
219                        "0000000000000000000000000000000000000000000000000000000000000000".into(),
220                        "0000000000000000000000000000000000000000000000000000000000000001".into(),
221                        "0000000000000000000000000000000000000000000000000000000000000002".into(),
222                    ],
223                    ..Default::default()
224                }),
225            ("o2htZXNzYWdlc4GkZGNvZGUYKmVpbmRleAFmbW9kdWxlZHRlc3RmcmVzdWx0a3Rlc3QtcmVzdWx0dGJhZF9jb21wdXRlX2VudGl0aWVzgVggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF1Z29vZF9jb21wdXRlX2VudGl0aWVzglggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC",
226                RoundResults {
227                    messages: vec![MessageEvent{module: "test".to_owned(), code: 42, index: 1, result: Some(cbor::Value::TextString("test-result".to_string()))}],
228                    good_compute_entities: vec![
229                        "0000000000000000000000000000000000000000000000000000000000000000".into(),
230                        "0000000000000000000000000000000000000000000000000000000000000002".into(),
231                    ],
232                    bad_compute_entities: vec![
233                        "0000000000000000000000000000000000000000000000000000000000000001".into(),
234                    ],
235                }),
236        ];
237        for (encoded_base64, rr) in tcs {
238            let dec: RoundResults =
239                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
240                    .expect("round results should deserialize correctly");
241            assert_eq!(dec, rr, "decoded results should match the expected value");
242        }
243    }
244
245    #[test]
246    fn test_consistent_round_roots() {
247        let tcs = vec![
248            ("glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", RoundRoots::default()),
249            ("glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", RoundRoots {
250                state_root: Hash::digest_bytes(b"test"),
251                ..Default::default()
252            }),
253            ("glggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/", RoundRoots {
254                io_root: Hash::digest_bytes(b"test"),
255                ..Default::default()
256            }),
257            ("glggPTf+WENeDYcyPe5KLBsznvlU3mNxbuefV0f5TZdPkT9YID03/lhDXg2HMj3uSiwbM575VN5jcW7nn1dH+U2XT5E/",
258                RoundRoots {
259                    state_root: Hash::digest_bytes(b"test"),
260                    io_root: Hash::digest_bytes(b"test"),
261                }),
262            ("glggC4+lzfqNgLxCHLxwDp+Bf5PLLb0DILrUZWwF+lp6Z/NYIJ3seczGUDFDvmAEdVCeep6Xsn8XRosTKWpu9wZ3mQRq",
263                RoundRoots {
264                    state_root: Hash::digest_bytes(b"test1"),
265                    io_root: Hash::digest_bytes(b"test2"),
266                }),
267            ("glggnex5zMZQMUO+YAR1UJ56npeyfxdGixMpam73BneZBGpYIAuPpc36jYC8Qhy8cA6fgX+Tyy29AyC61GVsBfpaemfz",
268                RoundRoots {
269                    state_root: Hash::digest_bytes(b"test2"),
270                    io_root: Hash::digest_bytes(b"test1"),
271                }),
272        ];
273
274        for (encoded_base64, rr) in tcs {
275            let dec: RoundRoots =
276                cbor::from_slice(&BASE64_STANDARD.decode(encoded_base64).unwrap())
277                    .expect("round roots should deserialize correctly");
278            assert_eq!(
279                dec, rr,
280                "decoded round roots should match the expected value"
281            );
282        }
283    }
284}