Skip to content

Commit 6bfbdaf

Browse files
committed
runtime-sdk: Add support for incoming messages
1 parent a044341 commit 6bfbdaf

25 files changed

Lines changed: 909 additions & 29 deletions

File tree

client-sdk/go/modules/core/types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ type GasCosts struct {
2525
AuthSignature uint64 `json:"auth_signature"`
2626
AuthMultisigSigner uint64 `json:"auth_multisig_signer"`
2727
CallformatX25519Deoxysii uint64 `json:"callformat_x25519_deoxysii"`
28+
29+
// Fields below have omitempty set for backwards compatibility. Once there are no deployed
30+
// runtimes using an old version of the SDK, this should be removed.
31+
32+
InMsgBase uint64 `json:"inmsg_base,omitempty"`
2833
}
2934

3035
// Parameters are the parameters for the consensus accounts module.
@@ -38,7 +43,8 @@ type Parameters struct {
3843
// Fields below have omitempty set for backwards compatibility. Once there are no deployed
3944
// runtimes using an old version of the SDK, this should be removed.
4045

41-
MaxTxSize uint32 `json:"max_tx_size,omitempty"`
46+
MaxTxSize uint32 `json:"max_tx_size,omitempty"`
47+
MaxInMsgGas uint32 `json:"max_inmsg_gas,omitempty"`
4248
}
4349

4450
// ModuleName is the core module name.

runtime-sdk/src/dispatcher.rs

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use crate::{
3131
error::{Error as _, RuntimeError},
3232
event::IntoTags,
3333
keymanager::{KeyManagerClient, KeyManagerError},
34-
module::{self, BlockHandler, MethodHandler, TransactionHandler},
34+
module::{self, BlockHandler, InMsgHandler, InMsgResult, MethodHandler, TransactionHandler},
3535
modules,
3636
modules::core::API as _,
3737
runtime::Runtime,
@@ -550,7 +550,7 @@ impl<R: Runtime> Dispatcher<R> {
550550
messages,
551551
block_tags: block_tags.into_tags(),
552552
tx_reject_hashes: vec![],
553-
in_msgs_count: 0, // TODO: Support processing incoming messages.
553+
in_msgs_count: 0,
554554
})
555555
}
556556
}
@@ -560,17 +560,63 @@ impl<R: Runtime + Send + Sync> transaction::dispatcher::Dispatcher for Dispatche
560560
&self,
561561
rt_ctx: transaction::Context<'_>,
562562
batch: &TxnBatch,
563-
_in_msgs: &[roothash::IncomingMessage],
563+
in_msgs: &[roothash::IncomingMessage],
564564
) -> Result<ExecuteBatchResult, RuntimeError> {
565-
self.execute_batch_common(
565+
let mut in_msgs_count = 0;
566+
567+
let mut result = self.execute_batch_common(
566568
rt_ctx,
567569
|ctx| -> Result<Vec<ExecuteTxResult>, RuntimeError> {
568570
// If prefetch limit is set enable prefetch.
569571
let prefetch_enabled = R::PREFETCH_LIMIT > 0;
572+
let mut results = Vec::with_capacity(batch.len());
573+
574+
// Process incoming messages first.
575+
let mut batch_it = batch.iter();
576+
'inmsg: for in_msg in in_msgs {
577+
match R::IncomingMessagesHandler::process_in_msg(ctx, &in_msg) {
578+
InMsgResult::Skip => {
579+
// Skip, but treat as processed.
580+
in_msgs_count += 1;
581+
}
582+
InMsgResult::Execute(raw_tx, tx) => {
583+
// Verify that the transaction has been included in the batch.
584+
match batch_it.next() {
585+
None => {
586+
// Nothing in the batch when there should be an incoming message.
587+
return Err(Error::MalformedTransactionInBatch(anyhow!(
588+
"missing incoming message"
589+
))
590+
.into());
591+
}
592+
Some(batch_tx) if batch_tx != raw_tx => {
593+
// Incoming message does not match what is in the batch.
594+
return Err(Error::MalformedTransactionInBatch(anyhow!(
595+
"mismatched incoming message"
596+
))
597+
.into());
598+
}
599+
_ => {
600+
// Everything is ok.
601+
}
602+
}
603+
604+
// Further execute the inner transaction. The transaction has already
605+
// passed checks so it is ok to include in a block.
606+
let tx_size = raw_tx.len().try_into().unwrap();
607+
let index = results.len();
608+
results.push(Self::execute_tx(ctx, tx_size, tx, index)?);
609+
610+
in_msgs_count += 1;
611+
}
612+
InMsgResult::Stop => break 'inmsg,
613+
}
614+
}
570615

616+
let inmsg_txs = results.len();
571617
let mut txs = Vec::with_capacity(batch.len());
572618
let mut prefixes: BTreeSet<Prefix> = BTreeSet::new();
573-
for tx in batch.iter() {
619+
for tx in batch.iter().skip(inmsg_txs) {
574620
let tx_size = tx.len().try_into().map_err(|_| {
575621
Error::MalformedTransactionInBatch(anyhow!("transaction too large"))
576622
})?;
@@ -593,24 +639,29 @@ impl<R: Runtime + Send + Sync> transaction::dispatcher::Dispatcher for Dispatche
593639
}
594640

595641
// Execute the batch.
596-
let mut results = Vec::with_capacity(batch.len());
597-
for (index, (tx_size, tx)) in txs.into_iter().enumerate() {
642+
for (index, (tx_size, tx)) in txs.into_iter().skip(inmsg_txs).enumerate() {
598643
results.push(Self::execute_tx(ctx, tx_size, tx, index)?);
599644
}
600645

601646
Ok(results)
602647
},
603-
)
648+
)?;
649+
650+
// Include number of processed incoming messages in the final result.
651+
result.in_msgs_count = in_msgs_count;
652+
653+
Ok(result)
604654
}
605655

606656
fn schedule_and_execute_batch(
607657
&self,
608658
rt_ctx: transaction::Context<'_>,
609659
batch: &mut TxnBatch,
610-
_in_msgs: &[roothash::IncomingMessage],
660+
in_msgs: &[roothash::IncomingMessage],
611661
) -> Result<ExecuteBatchResult, RuntimeError> {
612662
let cfg = R::SCHEDULE_CONTROL;
613663
let mut tx_reject_hashes = Vec::new();
664+
let mut in_msgs_count = 0;
614665

615666
let mut result = self.execute_batch_common(
616667
rt_ctx,
@@ -620,13 +671,35 @@ impl<R: Runtime + Send + Sync> transaction::dispatcher::Dispatcher for Dispatche
620671
// The idea is to keep scheduling transactions as long as we have some space
621672
// available in the block as determined by gas use.
622673
let mut new_batch = Vec::new();
623-
let mut results = Vec::with_capacity(batch.len());
674+
let mut results = Vec::with_capacity(in_msgs.len() + batch.len());
624675
let mut requested_batch_len = cfg.initial_batch_size;
676+
677+
// Process incoming messages first.
678+
'inmsg: for in_msg in in_msgs {
679+
match R::IncomingMessagesHandler::process_in_msg(ctx, &in_msg) {
680+
InMsgResult::Skip => {
681+
// Skip, but treat as processed.
682+
in_msgs_count += 1;
683+
}
684+
InMsgResult::Execute(raw_tx, tx) => {
685+
// Further execute the inner transaction. The transaction has already
686+
// passed checks so it is ok to include in a block.
687+
let tx_size = raw_tx.len().try_into().unwrap();
688+
let index = new_batch.len();
689+
new_batch.push(raw_tx.to_owned());
690+
results.push(Self::execute_tx(ctx, tx_size, tx, index)?);
691+
692+
in_msgs_count += 1;
693+
}
694+
InMsgResult::Stop => break 'inmsg,
695+
}
696+
}
697+
698+
// Process regular transactions.
625699
'batch: loop {
626700
// Remember length of last batch.
627701
let last_batch_len = batch.len();
628702
let last_batch_tx_hash = batch.last().map(|raw_tx| Hash::digest_bytes(raw_tx));
629-
630703
for raw_tx in batch.drain(..) {
631704
// If we don't have enough gas for processing even the cheapest transaction
632705
// we are done. Same if we reached the runtime-imposed maximum tx count.
@@ -731,8 +804,10 @@ impl<R: Runtime + Send + Sync> transaction::dispatcher::Dispatcher for Dispatche
731804
},
732805
)?;
733806

734-
// Include rejected transaction hashes in the final result.
807+
// Include rejected transaction hashes and number of processed incoming messages in the
808+
// final result.
735809
result.tx_reject_hashes = tx_reject_hashes;
810+
result.in_msgs_count = in_msgs_count;
736811

737812
Ok(result)
738813
}
@@ -919,6 +994,7 @@ mod test {
919994
core::Genesis {
920995
parameters: core::Parameters {
921996
max_batch_gas: u64::MAX,
997+
max_inmsg_gas: 0,
922998
max_tx_size: 32 * 1024,
923999
max_tx_signers: 1,
9241000
max_multisig_signers: 8,
@@ -927,6 +1003,7 @@ mod test {
9271003
auth_signature: 0,
9281004
auth_multisig_signer: 0,
9291005
callformat_x25519_deoxysii: 0,
1006+
inmsg_base: 0,
9301007
},
9311008
min_gas_price: BTreeMap::from([(token::Denomination::NATIVE, 0)]),
9321009
},

runtime-sdk/src/error.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
//! Error types for runtimes.
2+
use std::fmt::Display;
3+
24
pub use oasis_core_runtime::types::Error as RuntimeError;
35

46
use crate::{dispatcher, module::CallResult};
@@ -56,6 +58,18 @@ pub trait Error: std::error::Error {
5658
{
5759
Err(self)
5860
}
61+
62+
/// Converts the error into a serializable error.
63+
fn into_serializable(self) -> SerializableError
64+
where
65+
Self: Sized,
66+
{
67+
SerializableError {
68+
module: self.module_name().to_owned(),
69+
code: self.code(),
70+
message: self.to_string(),
71+
}
72+
}
5973
}
6074

6175
impl Error for std::convert::Infallible {
@@ -68,6 +82,47 @@ impl Error for std::convert::Infallible {
6882
}
6983
}
7084

85+
/// A standardized serialized implementation for an error.
86+
#[derive(Debug, Default, Clone, thiserror::Error, cbor::Encode, cbor::Decode)]
87+
pub struct SerializableError {
88+
pub module: String,
89+
pub code: u32,
90+
pub message: String,
91+
}
92+
93+
impl Display for SerializableError {
94+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95+
write!(f, "{}", self.message)
96+
}
97+
}
98+
99+
impl Error for SerializableError {
100+
fn module_name(&self) -> &str {
101+
&self.module
102+
}
103+
104+
fn code(&self) -> u32 {
105+
self.code
106+
}
107+
}
108+
109+
impl From<CallResult> for SerializableError {
110+
fn from(result: CallResult) -> Self {
111+
match result {
112+
CallResult::Failed {
113+
module,
114+
code,
115+
message,
116+
} => Self {
117+
module,
118+
code,
119+
message,
120+
},
121+
_ => Default::default(),
122+
}
123+
}
124+
}
125+
71126
#[cfg(test)]
72127
mod test {
73128
use super::*;

runtime-sdk/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![deny(rust_2018_idioms, unreachable_pub)]
44
#![forbid(unsafe_code)]
55
#![feature(int_log)]
6+
#![feature(associated_type_defaults)]
67

78
pub mod callformat;
89
pub mod config;

runtime-sdk/src/module.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use impl_trait_for_tuples::impl_for_tuples;
99

1010
use crate::{
1111
context::{Context, TxContext},
12+
core::consensus::roothash,
1213
dispatcher, error,
1314
error::Error as _,
1415
event, modules,
@@ -565,6 +566,39 @@ impl ModuleInfoHandler for Tuple {
565566
}
566567
}
567568

569+
/// Incoming message handler.
570+
pub trait InMsgHandler {
571+
/// Process an incoming message.
572+
fn process_in_msg<'a, C: Context>(
573+
ctx: &mut C,
574+
in_msg: &'a roothash::IncomingMessage,
575+
) -> InMsgResult<'a>;
576+
}
577+
578+
/// Result of processing an incoming message.
579+
#[derive(Debug)]
580+
pub enum InMsgResult<'a> {
581+
/// Skip to next incoming message, but count as processed.
582+
Skip,
583+
/// Add to batch/verify inclusion and execute.
584+
Execute(&'a [u8], Transaction),
585+
/// Stop processing incoming messages.
586+
Stop,
587+
}
588+
589+
/// An incoming message handler which discards all incoming messages.
590+
pub struct InMsgDiscard;
591+
592+
impl InMsgHandler for InMsgDiscard {
593+
fn process_in_msg<'a, C: Context>(
594+
_ctx: &mut C,
595+
_in_msg: &'a roothash::IncomingMessage,
596+
) -> InMsgResult<'a> {
597+
// Just skip all messages without doing anything.
598+
InMsgResult::Skip
599+
}
600+
}
601+
568602
/// A runtime module.
569603
pub trait Module {
570604
/// Module name.
@@ -591,6 +625,11 @@ pub trait Module {
591625

592626
/// Set the module's parameters.
593627
fn set_params<S: Store>(store: S, params: Self::Parameters) {
628+
params
629+
.validate_basic()
630+
.map_err(|_| ())
631+
.expect("module parameters are invalid");
632+
594633
let store = storage::PrefixStore::new(store, &Self::NAME);
595634
let mut store = storage::TypedStore::new(store);
596635
store.insert(Self::Parameters::STORE_KEY, params);

runtime-sdk/src/modules/consensus/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Consensus module.
22
//!
33
//! Low level consensus module for communicating with the consensus layer.
4-
use std::str::FromStr;
5-
64
use thiserror::Error;
75

86
use oasis_core_runtime::{
@@ -44,7 +42,7 @@ pub struct Parameters {
4442
impl Default for Parameters {
4543
fn default() -> Self {
4644
Self {
47-
consensus_denomination: token::Denomination::from_str("TEST").unwrap(),
45+
consensus_denomination: "TEST".parse().unwrap(),
4846
consensus_scaling_factor: 1,
4947
}
5048
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::modules;
2+
3+
/// Incoming message handler configuration.
4+
pub trait Config: 'static {
5+
/// The accounts module to use.
6+
type Accounts: modules::accounts::API;
7+
/// The consensus module to use.
8+
type Consensus: modules::consensus::API;
9+
10+
/// Maximum number of outgoing consensus message slots that an incoming message can claim.
11+
///
12+
/// When this is configured to be greater than zero it allows incoming messages to also emit
13+
/// consensus messages as a result of executing a transaction.
14+
const MAX_CONSENSUS_MSG_SLOTS_PER_TX: u32 = 1;
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use super::MODULE_NAME;
2+
use crate::error;
3+
4+
/// Events emitted by the consensus incoming message handler module.
5+
#[derive(Debug, cbor::Encode, oasis_runtime_sdk_macros::Event)]
6+
#[cbor(untagged)]
7+
pub enum Event {
8+
#[sdk_event(code = 1)]
9+
Processed {
10+
id: u64,
11+
#[cbor(optional)]
12+
tag: u64,
13+
#[cbor(optional)]
14+
error: Option<error::SerializableError>,
15+
},
16+
}

0 commit comments

Comments
 (0)