oasis_core_runtime/consensus/roothash/commitment/
executor.rs1use 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
21pub const COMPUTE_RESULTS_HEADER_SIGNATURE_CONTEXT: &[u8] =
23 b"oasis-core/roothash: compute results header";
24
25pub 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#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)]
46pub struct ComputeResultsHeader {
47 pub round: u64,
49 pub previous_hash: Hash,
51
52 #[cbor(optional)]
54 pub io_root: Option<Hash>,
55 #[cbor(optional)]
57 pub state_root: Option<Hash>,
58 #[cbor(optional)]
60 pub messages_hash: Option<Hash>,
61
62 #[cbor(optional)]
64 pub in_msgs_hash: Option<Hash>,
65 #[cbor(optional)]
67 pub in_msgs_count: u32,
68}
69
70impl ComputeResultsHeader {
71 pub fn encoded_hash(&self) -> Hash {
73 Hash::digest_bytes(&cbor::to_vec(self.clone()))
74 }
75
76 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#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
87#[repr(u8)]
88pub enum ExecutorCommitmentFailure {
89 #[default]
91 FailureNone = 0,
92
93 FailureUnknown = 1,
95
96 FailureStateUnavailable = 2,
99}
100
101#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
103pub struct ExecutorCommitmentHeader {
104 pub header: ComputeResultsHeader,
106
107 #[cbor(optional)]
109 pub failure: ExecutorCommitmentFailure,
110
111 #[cbor(optional, rename = "rak_sig")]
113 pub rak_signature: Option<Signature>,
114}
115
116impl ExecutorCommitmentHeader {
117 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 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#[derive(Clone, Debug, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)]
142pub struct ExecutorCommitment {
143 pub node_id: PublicKey,
145
146 pub header: ExecutorCommitmentHeader,
148
149 #[cbor(rename = "sig")]
151 pub signature: Signature,
152
153 #[cbor(optional)]
158 pub messages: Vec<Message>,
159}
160
161impl ExecutorCommitment {
162 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 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 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 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 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 if self.header.rak_signature.is_some() {
239 return Err(anyhow!("failure indicating body includes RAK signature"));
240 }
241 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 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 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.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.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.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 },
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}