oasis_core_runtime/consensus/roothash/commitment/
executor.rs

1use std::any::Any;
2
3use anyhow::{anyhow, Result};
4
5use crate::{
6    common::{
7        crypto::{
8            hash::Hash,
9            signature::{
10                signature_context_with_chain_separation, signature_context_with_runtime_separation,
11                PublicKey, Signature, Signer,
12            },
13        },
14        namespace::Namespace,
15    },
16    consensus::roothash::{Header, Message},
17};
18
19use super::OpenCommitment;
20
21/// The signature context used to sign compute results headers with RAK.
22pub const COMPUTE_RESULTS_HEADER_SIGNATURE_CONTEXT: &[u8] =
23    b"oasis-core/roothash: compute results header";
24
25/// The signature context used to sign executor worker commitments.
26pub const EXECUTOR_COMMITMENT_SIGNATURE_CONTEXT: &[u8] =
27    b"oasis-core/roothash: executor commitment";
28
29fn executor_commitment_signature_context(
30    runtime_id: &Namespace,
31    chain_context: &String,
32) -> Vec<u8> {
33    let context = EXECUTOR_COMMITMENT_SIGNATURE_CONTEXT.to_vec();
34    let context = signature_context_with_runtime_separation(context, runtime_id);
35    signature_context_with_chain_separation(context, chain_context)
36}
37
38/// The header of a computed batch output by a runtime. This header is a
39/// compressed representation (e.g., hashes instead of full content) of
40/// the actual results.
41///
42/// # Note
43///
44/// This should be kept in sync with go/roothash/api/commitment/executor.go.
45#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
46pub struct ComputeResultsHeader {
47    /// Round number.
48    pub round: u64,
49    /// Hash of the previous block header this batch was computed against.
50    pub previous_hash: Hash,
51
52    /// The I/O merkle root.
53    #[cbor(optional)]
54    pub io_root: Option<Hash>,
55    /// The root hash of the state after computing this batch.
56    #[cbor(optional)]
57    pub state_root: Option<Hash>,
58    /// Hash of messages sent from this batch.
59    #[cbor(optional)]
60    pub messages_hash: Option<Hash>,
61
62    /// The hash of processed incoming messages.
63    #[cbor(optional)]
64    pub in_msgs_hash: Option<Hash>,
65    /// The number of processed incoming messages.
66    #[cbor(optional)]
67    pub in_msgs_count: u32,
68}
69
70impl ComputeResultsHeader {
71    /// Returns a hash of an encoded header.
72    pub fn encoded_hash(&self) -> Hash {
73        Hash::digest_bytes(&cbor::to_vec(self.clone()))
74    }
75
76    /// Returns true iff the header is the parent of a child header.
77    pub fn is_parent_of(&self, child: &Header) -> bool {
78        if self.round != child.round + 1 {
79            return false;
80        }
81        self.previous_hash == child.encoded_hash()
82    }
83}
84
85/// The executor commitment failure reason.
86#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
87#[repr(u8)]
88pub enum ExecutorCommitmentFailure {
89    /// Indicates that no failure has occurred.
90    #[default]
91    FailureNone = 0,
92
93    /// Indicates a generic failure.
94    FailureUnknown = 1,
95
96    /// Indicates that batch processing failed due to the state being
97    /// unavailable.
98    FailureStateUnavailable = 2,
99}
100
101/// The header of an executor commitment.
102#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
103pub struct ExecutorCommitmentHeader {
104    /// The compute results header.
105    pub header: ComputeResultsHeader,
106
107    /// The executor commitment failure reason.
108    #[cbor(optional)]
109    pub failure: ExecutorCommitmentFailure,
110
111    // Optional fields (may be absent for failure indication).
112    #[cbor(optional, rename = "rak_sig")]
113    pub rak_signature: Option<Signature>,
114}
115
116impl ExecutorCommitmentHeader {
117    /// Signs the executor commitment header.
118    pub fn sign(
119        &self,
120        signer: &impl Signer,
121        runtime_id: &Namespace,
122        chain_context: &String,
123    ) -> Result<Signature> {
124        let context = executor_commitment_signature_context(runtime_id, chain_context);
125        let message = cbor::to_vec(self.clone());
126
127        signer.sign(&context, &message)
128    }
129
130    /// Verifies the RAK signature.
131    pub fn verify_rak(&self, rak: PublicKey) -> Result<()> {
132        let sig = self.rak_signature.ok_or(anyhow!("missing RAK signature"))?;
133        let message = cbor::to_vec(self.header.clone());
134
135        sig.verify(&rak, COMPUTE_RESULTS_HEADER_SIGNATURE_CONTEXT, &message)
136            .map_err(|_| anyhow!("RAK signature verification failed"))
137    }
138}
139
140/// A commitment to results of processing a proposed runtime block.
141#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
142pub struct ExecutorCommitment {
143    // The public key of the node that generated this commitment.
144    pub node_id: PublicKey,
145
146    // The commitment header.
147    pub header: ExecutorCommitmentHeader,
148
149    // The commitment header signature.
150    #[cbor(rename = "sig")]
151    pub signature: Signature,
152
153    // The messages emitted by the runtime.
154    //
155    // This field is only present in case this commitment belongs to the proposer. In case of
156    // the commitment being submitted as equivocation evidence, this field should be omitted.
157    #[cbor(optional)]
158    pub messages: Vec<Message>,
159}
160
161impl ExecutorCommitment {
162    /// Signs the executor commitment header and sets the signature on the commitment.
163    pub fn sign(
164        &mut self,
165        signer: &impl Signer,
166        runtime_id: &Namespace,
167        chain_context: &String,
168    ) -> Result<()> {
169        let pk = signer.public();
170        if self.node_id != pk {
171            return Err(anyhow!(
172                "node ID does not match signer (ID: {} signer: {})",
173                self.node_id,
174                pk,
175            ));
176        }
177
178        self.signature = self.header.sign(signer, runtime_id, chain_context)?;
179
180        Ok(())
181    }
182
183    /// Verifies that the header signature is valid.
184    pub fn verify(&self, runtime_id: &Namespace, chain_context: &String) -> Result<()> {
185        let context = executor_commitment_signature_context(runtime_id, chain_context);
186        let message = cbor::to_vec(self.header.clone());
187
188        self.signature
189            .verify(&self.node_id, &context, &message)
190            .map_err(|_| anyhow!("roothash/commitment: signature verification failed"))
191    }
192
193    pub fn validate_basic(&self) -> Result<()> {
194        match self.header.failure {
195            ExecutorCommitmentFailure::FailureNone => {
196                // Ensure header fields are present.
197                if self.header.header.io_root.is_none() {
198                    return Err(anyhow!("missing IORoot"));
199                }
200                if self.header.header.state_root.is_none() {
201                    return Err(anyhow!("missing StateRoot"));
202                }
203                if self.header.header.messages_hash.is_none() {
204                    return Err(anyhow!("missing messages hash"));
205                }
206                if self.header.header.in_msgs_hash.is_none() {
207                    return Err(anyhow!("missing incoming messages hash"));
208                }
209
210                // Validate any included runtime messages.
211                for msg in self.messages.iter() {
212                    msg.validate_basic()
213                        .map_err(|err| anyhow!("bad runtime message: {:?}", err))?;
214                }
215            }
216            ExecutorCommitmentFailure::FailureUnknown
217            | ExecutorCommitmentFailure::FailureStateUnavailable => {
218                // Ensure header fields are empty.
219                if self.header.header.io_root.is_some() {
220                    return Err(anyhow!("failure indicating body includes IORoot"));
221                }
222                if self.header.header.state_root.is_some() {
223                    return Err(anyhow!("failure indicating commitment includes StateRoot"));
224                }
225                if self.header.header.messages_hash.is_some() {
226                    return Err(anyhow!(
227                        "failure indicating commitment includes MessagesHash"
228                    ));
229                }
230                if self.header.header.in_msgs_hash.is_some()
231                    || self.header.header.in_msgs_count != 0
232                {
233                    return Err(anyhow!(
234                        "failure indicating commitment includes InMessagesHash/Count"
235                    ));
236                }
237                // In case of failure indicating commitment make sure RAK signature is empty.
238                if self.header.rak_signature.is_some() {
239                    return Err(anyhow!("failure indicating body includes RAK signature"));
240                }
241                // In case of failure indicating commitment make sure messages are empty.
242                if !self.messages.is_empty() {
243                    return Err(anyhow!("failure indicating body includes messages"));
244                }
245            }
246        }
247
248        Ok(())
249    }
250}
251
252impl OpenCommitment for ExecutorCommitment {
253    fn mostly_equal(&self, other: &Self) -> bool {
254        self.to_vote() == other.to_vote()
255    }
256
257    fn is_indicating_failure(&self) -> bool {
258        self.header.failure != ExecutorCommitmentFailure::FailureNone
259    }
260
261    fn to_vote(&self) -> Hash {
262        self.header.header.encoded_hash()
263    }
264
265    fn to_dd_result(&self) -> &dyn Any {
266        self
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_consistent_hash() {
276        // NOTE: These hashes MUST be synced with go/roothash/api/commitment/executor_test.go.
277        let empty = ComputeResultsHeader::default();
278        assert_eq!(
279            empty.encoded_hash(),
280            Hash::from("57d73e02609a00fcf4ca43cbf8c9f12867c46942d246fb2b0bce42cbdb8db844")
281        );
282
283        let populated = ComputeResultsHeader {
284            round: 42,
285            previous_hash: empty.encoded_hash(),
286            io_root: Some(Hash::empty_hash()),
287            state_root: Some(Hash::empty_hash()),
288            messages_hash: Some(Hash::empty_hash()),
289            in_msgs_hash: Some(Hash::empty_hash()),
290            in_msgs_count: 0,
291        };
292        assert_eq!(
293            populated.encoded_hash(),
294            Hash::from("8459a9e6e3341cd2df5ada5737469a505baf92397aaa88b7100915324506d843")
295        );
296    }
297
298    #[test]
299    fn test_validate_basic() {
300        // NOTE: These hashes MUST be synced with go/roothash/api/commitment/executor_test.go.
301        let empty = ComputeResultsHeader::default();
302        assert_eq!(
303            empty.encoded_hash(),
304            Hash::from("57d73e02609a00fcf4ca43cbf8c9f12867c46942d246fb2b0bce42cbdb8db844")
305        );
306
307        let body = ExecutorCommitment {
308            header: ExecutorCommitmentHeader {
309                header: ComputeResultsHeader {
310                    round: 42,
311                    previous_hash: empty.encoded_hash(),
312                    io_root: Some(Hash::empty_hash()),
313                    state_root: Some(Hash::empty_hash()),
314                    messages_hash: Some(Hash::empty_hash()),
315                    in_msgs_hash: Some(Hash::empty_hash()),
316                    in_msgs_count: 0,
317                },
318                failure: ExecutorCommitmentFailure::FailureNone,
319                rak_signature: None,
320            },
321            messages: vec![],
322            node_id: PublicKey::default(),
323            signature: Signature::default(),
324        };
325
326        let tcs: Vec<(&str, fn(&mut ExecutorCommitment), bool)> = vec![
327            (
328                "Ok",
329                |ec: &mut ExecutorCommitment| {
330                    ec.header.header.round -= 1;
331                },
332                false,
333            ),
334            (
335                "Bad io_root",
336                |ec: &mut ExecutorCommitment| ec.header.header.io_root = None,
337                true,
338            ),
339            (
340                "Bad state_root",
341                |ec: &mut ExecutorCommitment| ec.header.header.state_root = None,
342                true,
343            ),
344            (
345                "Bad messages_hash",
346                |ec: &mut ExecutorCommitment| ec.header.header.messages_hash = None,
347                true,
348            ),
349            (
350                "Bad Failure (existing io_root)",
351                |ec: &mut ExecutorCommitment| {
352                    ec.header.failure = ExecutorCommitmentFailure::FailureUnknown;
353                    // ec.header.compute_results_header.io_root is set.
354                    ec.header.header.state_root = None;
355                    ec.header.header.messages_hash = None;
356                    ec.header.header.in_msgs_hash = None;
357                },
358                true,
359            ),
360            (
361                "Bad Failure (existing state_root)",
362                |ec: &mut ExecutorCommitment| {
363                    ec.header.failure = ExecutorCommitmentFailure::FailureUnknown;
364                    ec.header.header.io_root = None;
365                    // ec.header.compute_results_header.state_root is set.
366                    ec.header.header.messages_hash = None;
367                    ec.header.header.in_msgs_hash = None;
368                },
369                true,
370            ),
371            (
372                "Bad Failure (existing messages_hash)",
373                |ec: &mut ExecutorCommitment| {
374                    ec.header.failure = ExecutorCommitmentFailure::FailureUnknown;
375                    ec.header.header.io_root = None;
376                    ec.header.header.state_root = None;
377                    // ec.header.compute_results_header.messages_hash is set.
378                    ec.header.header.in_msgs_hash = None;
379                },
380                true,
381            ),
382            (
383                "Bad Failure (existing in_msgs_hash)",
384                |ec: &mut ExecutorCommitment| {
385                    ec.header.failure = ExecutorCommitmentFailure::FailureUnknown;
386                    ec.header.header.io_root = None;
387                    ec.header.header.state_root = None;
388                    ec.header.header.messages_hash = None;
389                    // ec.header.compute_results_header.in_msgs_hash is set.
390                },
391                true,
392            ),
393            (
394                "Ok Failure",
395                |ec: &mut ExecutorCommitment| {
396                    ec.header.failure = ExecutorCommitmentFailure::FailureUnknown;
397                },
398                true,
399            ),
400        ];
401
402        for (name, f, should_err) in tcs {
403            let mut b = body.clone();
404            f(&mut b);
405            let res = b.validate_basic();
406            assert_eq!(res.is_err(), should_err, "validate_basic({})", name)
407        }
408    }
409}