Skip to content

Commit c57d8fc

Browse files
committed
runtime-sdk: Better handle nonces in the future
1 parent 1965b2f commit c57d8fc

4 files changed

Lines changed: 119 additions & 28 deletions

File tree

runtime-sdk/src/context.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ pub enum Mode {
3636
ExecuteTx,
3737
CheckTx,
3838
SimulateTx,
39+
PreScheduleTx,
3940
}
4041

4142
const MODE_CHECK_TX: &str = "check_tx";
4243
const MODE_EXECUTE_TX: &str = "execute_tx";
4344
const MODE_SIMULATE_TX: &str = "simulate_tx";
45+
const MODE_PRE_SCHEDULE_TX: &str = "pre_schedule_tx";
4446

4547
impl fmt::Display for Mode {
4648
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -54,6 +56,7 @@ impl From<&Mode> for &'static str {
5456
Mode::CheckTx => MODE_CHECK_TX,
5557
Mode::ExecuteTx => MODE_EXECUTE_TX,
5658
Mode::SimulateTx => MODE_SIMULATE_TX,
59+
Mode::PreScheduleTx => MODE_PRE_SCHEDULE_TX,
5760
}
5861
}
5962
}
@@ -89,7 +92,12 @@ pub trait Context {
8992

9093
/// Whether the transaction is just being checked for validity.
9194
fn is_check_only(&self) -> bool {
92-
self.mode() == Mode::CheckTx
95+
self.mode() == Mode::CheckTx || self.mode() == Mode::PreScheduleTx
96+
}
97+
98+
/// Whether the transaction is just being checked for validity before being scheduled.
99+
fn is_pre_schedule(&self) -> bool {
100+
self.mode() == Mode::PreScheduleTx
93101
}
94102

95103
/// Whether the transaction is just being simulated.
@@ -122,7 +130,7 @@ pub trait Context {
122130
.unwrap_or_default()
123131
}
124132
// When just checking a transaction, we always want to be fast and skip contracts.
125-
Mode::CheckTx => false,
133+
Mode::CheckTx | Mode::PreScheduleTx => false,
126134
}
127135
}
128136

runtime-sdk/src/dispatcher.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ pub struct DispatchOptions<'a> {
115115
pub tx_index: usize,
116116
/// Optionally only allow methods for which the provided authorizer closure returns true.
117117
pub method_authorizer: Option<&'a dyn Fn(&str) -> bool>,
118+
/// Optionally skip authentication.
119+
pub skip_authentication: bool,
118120
}
119121

120122
/// The runtime dispatcher.
@@ -244,8 +246,10 @@ impl<R: Runtime> Dispatcher<R> {
244246
opts: &DispatchOptions<'_>,
245247
) -> Result<DispatchResult, Error> {
246248
// Run pre-processing hooks.
247-
if let Err(err) = R::Modules::authenticate_tx(ctx, &tx) {
248-
return Ok(err.into_call_result().into());
249+
if !opts.skip_authentication {
250+
if let Err(err) = R::Modules::authenticate_tx(ctx, &tx) {
251+
return Ok(err.into_call_result().into());
252+
}
249253
}
250254
let tx_auth_info = tx.auth_info.clone();
251255
let is_read_only = tx.call.read_only;
@@ -656,21 +660,50 @@ impl<R: Runtime + Send + Sync> transaction::dispatcher::Dispatcher for Dispatche
656660
}
657661

658662
// Determine the current transaction index.
659-
let index = new_batch.len();
663+
let tx_index = new_batch.len();
660664

661665
// First run the transaction in check tx mode in a separate subcontext. If
662-
// that fails, skip and reject transaction.
663-
let check_result = ctx.with_child(Mode::CheckTx, |mut ctx| {
664-
Self::dispatch_tx(&mut ctx, tx_size, tx.clone(), index)
665-
})?;
666-
if let module::CallResult::Failed { .. } = check_result.result {
667-
// Skip and reject transaction.
668-
tx_reject_hashes.push(Hash::digest_bytes(&raw_tx));
666+
// that fails, skip and (sometimes) reject transaction.
667+
let skip =
668+
ctx.with_child(Mode::PreScheduleTx, |mut ctx| -> Result<_, Error> {
669+
// First authenticate the transaction to get any nonce related errors.
670+
match R::Modules::authenticate_tx(&mut ctx, &tx) {
671+
Err(modules::core::Error::FutureNonce) => {
672+
// Only skip transaction as it may become valid in the future.
673+
return Ok(true);
674+
}
675+
Err(_) => {
676+
// Skip and reject the transaction.
677+
}
678+
Ok(_) => {
679+
// Run additional checks on the transaction.
680+
let check_result = Self::dispatch_tx_opts(
681+
&mut ctx,
682+
tx.clone(),
683+
&DispatchOptions {
684+
tx_size,
685+
tx_index,
686+
skip_authentication: true, // Already done.
687+
..Default::default()
688+
},
689+
)?;
690+
if check_result.result.is_success() {
691+
// Checks successful, execute transaction as usual.
692+
return Ok(false);
693+
}
694+
}
695+
}
696+
697+
// Skip and reject the transaction.
698+
tx_reject_hashes.push(Hash::digest_bytes(&raw_tx));
699+
Ok(true)
700+
})?;
701+
if skip {
669702
continue;
670703
}
671704

672705
new_batch.push(raw_tx);
673-
results.push(Self::execute_tx(ctx, tx_size, tx, index)?);
706+
results.push(Self::execute_tx(ctx, tx_size, tx, tx_index)?);
674707
}
675708

676709
// If there's more room in the block and we got the maximum number of

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

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Accounts module.
22
use std::{
3+
cmp::Ordering,
34
collections::{BTreeMap, BTreeSet},
45
convert::TryInto,
56
};
@@ -16,7 +17,9 @@ use crate::{
1617
modules,
1718
modules::core::{Error as CoreError, API as _},
1819
runtime::Runtime,
19-
sdk_derive, storage,
20+
sdk_derive,
21+
sender::SenderMeta,
22+
storage,
2023
storage::Prefix,
2124
types::{
2225
address::{Address, SignatureAddressSpec},
@@ -32,6 +35,10 @@ pub mod types;
3235
/// Unique module name.
3336
const MODULE_NAME: &str = "accounts";
3437

38+
/// Maximum delta that the transaction nonce can be in the future from the current nonce to still
39+
/// be accepted during transaction checks.
40+
const MAX_CHECK_NONCE_FUTURE_DELTA: u64 = 10;
41+
3542
/// Errors emitted by the accounts module.
3643
#[derive(Error, Debug, oasis_runtime_sdk_macros::Error)]
3744
pub enum Error {
@@ -230,7 +237,7 @@ pub trait API {
230237
fn check_signer_nonces<C: Context>(
231238
ctx: &mut C,
232239
tx_auth_info: &AuthInfo,
233-
) -> Result<Option<Address>, modules::core::Error>;
240+
) -> Result<Address, modules::core::Error>;
234241

235242
/// Update transaction signer account nonces.
236243
fn update_signer_nonces<C: Context>(
@@ -623,31 +630,73 @@ impl API for Module {
623630
fn check_signer_nonces<C: Context>(
624631
ctx: &mut C,
625632
auth_info: &AuthInfo,
626-
) -> Result<Option<Address>, modules::core::Error> {
633+
) -> Result<Address, modules::core::Error> {
634+
let is_pre_schedule = ctx.is_pre_schedule();
635+
let is_check_only = ctx.is_check_only();
636+
627637
// TODO: Optimize the check/update pair so that the accounts are
628638
// fetched only once.
629639
let params = Self::params(ctx.runtime_state());
630640
// Fetch information about each signer.
631641
let mut store = storage::PrefixStore::new(ctx.runtime_state(), &MODULE_NAME);
632642
let accounts =
633643
storage::TypedStore::new(storage::PrefixStore::new(&mut store, &state::ACCOUNTS));
634-
let mut payer = None;
644+
let mut sender = None;
635645
for si in auth_info.signer_info.iter() {
636646
let address = si.address_spec.address();
637647
let account: types::Account = accounts.get(&address).unwrap_or_default();
638-
if account.nonce != si.nonce {
639-
// Reject unles nonce checking is disabled.
640-
if !params.debug_disable_nonce_check {
648+
649+
// First signer pays for the fees and is considered the sender.
650+
if sender.is_none() {
651+
sender = Some(SenderMeta {
652+
address,
653+
tx_nonce: si.nonce,
654+
state_nonce: account.nonce,
655+
});
656+
}
657+
658+
// When nonce checking is disabled, skip the rest of the checks.
659+
if params.debug_disable_nonce_check {
660+
continue;
661+
}
662+
663+
// Check signer nonce against the corresponding account nonce.
664+
match si.nonce.cmp(&account.nonce) {
665+
Ordering::Less => {
666+
// In the past and will never become valid, reject.
641667
return Err(modules::core::Error::InvalidNonce);
642668
}
643-
}
669+
Ordering::Equal => {} // Ok.
670+
Ordering::Greater => {
671+
// If too much in the future, reject.
672+
if si.nonce - account.nonce > MAX_CHECK_NONCE_FUTURE_DELTA {
673+
return Err(modules::core::Error::InvalidNonce);
674+
}
644675

645-
// First signer pays for the fees.
646-
if payer.is_none() {
647-
payer = Some(address);
676+
// If in the future and this is before scheduling, reject with separate error
677+
// that will make the scheduler skip the transaction.
678+
if is_pre_schedule {
679+
return Err(modules::core::Error::FutureNonce);
680+
}
681+
682+
// If in the future and this is during execution, reject.
683+
if !is_check_only {
684+
return Err(modules::core::Error::InvalidNonce);
685+
}
686+
687+
// If in the future and this is during checks, accept.
688+
}
648689
}
649690
}
650-
Ok(payer)
691+
692+
// Configure the sender.
693+
let sender = sender.expect("at least one signer is always present");
694+
let sender_address = sender.address;
695+
if is_check_only {
696+
<C::Runtime as Runtime>::Core::set_sender_meta(ctx, sender);
697+
}
698+
699+
Ok(sender_address)
651700
}
652701

653702
fn update_signer_nonces<C: Context>(
@@ -869,8 +918,6 @@ impl module::TransactionHandler for Module {
869918

870919
// Charge the specified amount of fees.
871920
if !tx.auth_info.fee.amount.amount().is_zero() {
872-
let payer = payer.expect("at least one signer is always present");
873-
874921
if ctx.is_check_only() {
875922
// Do not update balances during transaction checks. In case of checks, only do it
876923
// after all the other checks have already passed as otherwise retrying the
@@ -918,7 +965,6 @@ impl module::TransactionHandler for Module {
918965

919966
// Update payer balance.
920967
let payer = Self::check_signer_nonces(ctx, tx_auth_info).unwrap(); // Already checked.
921-
let payer = payer.unwrap(); // Already checked.
922968
let amount = &tx_auth_info.fee.amount;
923969
Self::sub_amount(ctx.runtime_state(), payer, amount).unwrap(); // Already checked.
924970

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ pub enum Error {
130130
#[sdk_error(code = 25)]
131131
ReadOnlyTransaction,
132132

133+
#[error("future nonce")]
134+
#[sdk_error(code = 26)]
135+
FutureNonce,
136+
133137
#[error("{0}")]
134138
#[sdk_error(transparent)]
135139
TxSimulationFailed(#[from] TxSimulationFailure),

0 commit comments

Comments
 (0)