Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
da1baf8
add eth_transact_at_time
mokita-j Oct 31, 2025
bd927ad
add: local timestamp in estimate_gas
mokita-j Oct 31, 2025
b75f9ff
refactor: merge eth_transact_at_time into eth_transact
mokita-j Oct 31, 2025
c4ea78f
change estimate_gas block default to pending
mokita-j Oct 31, 2025
de1a294
add: timestamp to eth_call
mokita-j Oct 31, 2025
3ec16d5
refactor timestamp source & fix tests
mokita-j Nov 3, 2025
ae99522
remove redundant logs and comments
mokita-j Nov 3, 2025
0db67ae
add: dry_run timestamp override tests
mokita-j Nov 3, 2025
8d39a29
Update from github-actions[bot] running command 'prdoc --audience run…
github-actions[bot] Nov 3, 2025
fa6888b
format with fmt
mokita-j Nov 3, 2025
0b9825f
fix prdoc bump
mokita-j Nov 3, 2025
e107143
refactor dry_run api
mokita-j Nov 3, 2025
e4a3dae
format with fmt
mokita-j Nov 3, 2025
4b353ed
remove log
mokita-j Nov 4, 2025
fa5d203
remove unused imports
mokita-j Nov 4, 2025
2fd4af1
refactor with_dry_run
mokita-j Nov 4, 2025
d9f24fe
fix: increment block iff timestamp_override is provided
mokita-j Nov 5, 2025
1d1170f
refactor: dry_run_override into is_dry_run
mokita-j Nov 5, 2025
8ca05c5
Merge remote-tracking branch 'origin/master' into mj/fix-dry-run-time…
mokita-j Nov 5, 2025
aa531f9
update: tests-evm hash
mokita-j Nov 5, 2025
71ff4fb
slight adjustments
pgherveou Nov 5, 2025
e1a46e5
fix dry run config
mokita-j Nov 5, 2025
de9475c
fix
pgherveou Nov 5, 2025
7b77659
move things around
pgherveou Nov 5, 2025
2e6c186
add: fallback to eth_transact if eth_transact_config not available
mokita-j Nov 5, 2025
fd0746f
fix
pgherveou Nov 5, 2025
68bb7ff
Update substrate/frame/revive/src/evm/api/rpc_types.rs
pgherveou Nov 5, 2025
73c7bcf
fix clippy
pgherveou Nov 5, 2025
239edb3
refactor to saturating add & add DryRunConfig constructor
mokita-j Nov 5, 2025
056c19d
apply suggestion
pgherveou Nov 5, 2025
d5b5a0b
add name to print
pgherveou Nov 5, 2025
906f12e
fix
pgherveou Nov 5, 2025
82c5714
address historical block use case
pgherveou Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests-evm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
repository: paritytech/evm-test-suite
ref: 460b2c9aa3a3019d3508bb5a34a2498ea86035ff
ref: 372e9b48d375a63297d6e553a70a89c4e9dfa14e
path: evm-test-suite

- uses: denoland/setup-deno@v1
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions prdoc/pr_10191.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
title: 'pallet_revive: Add dry-run timestamp override support'
doc:
- audience: Runtime User
description: |-
# Description

This PR updates `pallet-revive` to **support overriding the block timestamp during dry-run calls**.
The dry-run execution now uses the following configuration for `eth_estimateGas` and `eth_call` when the block tag is `pending`:

```text
block.timestamp = max(rpc_timestamp, latest_block.timestamp + 1)
block.number = latest_block.number + 1
```

Fixes [#153](https://github.com/paritytech/contract-issues/issues/153), [#205](https://github.com/paritytech/contract-issues/issues/205)

## Integration

Downstream projects using the `ReviveApi::eth_transact` runtime API should either provide a `timestamp` or pass `None`.

## Review Notes
- Added dry run timestamp to `ExecConfig`.
- Added a new parameter to `ReviveApi::eth_transact` for passing the current RPC timestamp.
- `eth_estimateGas` defaults to the `pending` block tag.
- `eth_estimateGas` and `eth_call` with `pending` block tag will dry run the transaction with the block timestamp set to `max(rpc_timestamp, latest_block.timestamp + 1)` and block number set to `latest_block.number + 1`.
crates:
- name: pallet-revive-eth-rpc
bump: major
- name: pallet-revive
bump: major
1 change: 1 addition & 0 deletions substrate/frame/revive/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ sp-core = { workspace = true, default-features = true }
sp-crypto-hashing = { workspace = true }
sp-rpc = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
sp-timestamp = { workspace = true }
sp-weights = { workspace = true, default-features = true }
sqlx = { workspace = true, features = ["macros", "runtime-tokio", "sqlite"] }
subxt = { workspace = true, default-features = true, features = ["reconnecting-rpc-client"] }
Expand Down
54 changes: 49 additions & 5 deletions substrate/frame/revive/rpc/src/client/runtime_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ use crate::{
subxt_client::{self, SrcChainConfig},
ClientError,
};
use futures::TryFutureExt;
use pallet_revive::{
evm::{Block as EthBlock, GenericTransaction, ReceiptGasInfo, Trace, H160, U256},
EthTransactInfo,
evm::{
Block as EthBlock, BlockNumberOrTagOrHash, BlockTag, GenericTransaction, ReceiptGasInfo,
Trace, H160, U256,
},
DryRunConfig, EthTransactInfo,
};
use sp_core::H256;
use subxt::OnlineClient;
use sp_timestamp::Timestamp;
use subxt::{error::MetadataError, ext::subxt_rpcs::UserError, Error::Metadata, OnlineClient};

const LOG_TARGET: &str = "eth-rpc::runtime_api";

Expand Down Expand Up @@ -65,9 +70,48 @@ impl RuntimeApi {
pub async fn dry_run(
&self,
tx: GenericTransaction,
block: BlockNumberOrTagOrHash,
) -> Result<EthTransactInfo<Balance>, ClientError> {
let payload = subxt_client::apis().revive_api().eth_transact(tx.into());
let result = self.0.call(payload).await?;
let timestamp_override = match block {
BlockNumberOrTagOrHash::BlockTag(BlockTag::Pending) =>
Some(Timestamp::current().as_millis()),
_ => None,
};

let payload = subxt_client::apis()
.revive_api()
.eth_transact_with_config(
tx.clone().into(),
DryRunConfig::new(timestamp_override).into(),
)
.unvalidated();

let result = self
.0
.call(payload)
.or_else(|err| async {
match err {
// This will be hit if subxt metadata (subxt uses the latest finalized block
// metadata when the eth-rpc starts) does not contain the new method
Metadata(MetadataError::RuntimeMethodNotFound(name)) => {
log::debug!(target: LOG_TARGET, "Method {name:?} not found falling back to eth_transact");
let payload = subxt_client::apis().revive_api().eth_transact(tx.into());
self.0.call(payload).await
},
// This will be hit if we are trying to hit a block where the runtime did not
// have this new runtime `eth_transact_with_config` defined
subxt::Error::Rpc(subxt::error::RpcError::ClientError(
subxt::ext::subxt_rpcs::Error::User(UserError { message, .. }),
)) if message.contains("eth_transact_with_config is not found") => {
log::debug!(target: LOG_TARGET, "{message:?} not found falling back to eth_transact");
let payload = subxt_client::apis().revive_api().eth_transact(tx.into());
self.0.call(payload).await
},
e => Err(e),
}
})
.await?;

match result {
Err(err) => {
log::debug!(target: LOG_TARGET, "Dry run failed {err:?}");
Expand Down
10 changes: 6 additions & 4 deletions substrate/frame/revive/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ impl EthRpcServer for EthRpcServerImpl {
block: Option<BlockNumberOrTag>,
) -> RpcResult<U256> {
log::trace!(target: LOG_TARGET, "estimate_gas transaction={transaction:?} block={block:?}");
let hash = self.client.block_hash_for_tag(block.unwrap_or_default().into()).await?;
let block = block.unwrap_or_default();
Copy link
Copy Markdown
Member

@athei athei Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK this defaults to latest. But estimate_gas should default to pending. call should continue to default to latest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PG and I tried this test here and it seems that geth is defaulting to latest

let hash = self.client.block_hash_for_tag(block.clone().into()).await?;
let runtime_api = self.client.runtime_api(hash);
let dry_run = runtime_api.dry_run(transaction).await?;
let dry_run = runtime_api.dry_run(transaction, block.into()).await?;
log::trace!(target: LOG_TARGET, "estimate_gas result={dry_run:?}");
Ok(dry_run.eth_gas)
}
Expand All @@ -157,9 +158,10 @@ impl EthRpcServer for EthRpcServerImpl {
transaction: GenericTransaction,
block: Option<BlockNumberOrTagOrHash>,
) -> RpcResult<Bytes> {
let hash = self.client.block_hash_for_tag(block.unwrap_or_default()).await?;
let block = block.unwrap_or_default();
let hash = self.client.block_hash_for_tag(block.clone()).await?;
let runtime_api = self.client.runtime_api(hash);
let dry_run = runtime_api.dry_run(transaction).await?;
let dry_run = runtime_api.dry_run(transaction, block).await?;
Ok(dry_run.data.into())
}

Expand Down
4 changes: 4 additions & 0 deletions substrate/frame/revive/rpc/src/subxt_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ pub use subxt::config::PolkadotConfig as SrcChainConfig;
path = "pallet_revive::evm::api::rpc_types_gen::GenericTransaction",
with = "::subxt::utils::Static<::pallet_revive::evm::GenericTransaction>"
),
substitute_type(
path = "pallet_revive::evm::api::rpc_types::DryRunConfig<M>",
with = "::subxt::utils::Static<::pallet_revive::evm::DryRunConfig<M>>"
),
substitute_type(
path = "pallet_revive::primitives::EthTransactInfo<B>",
with = "::subxt::utils::Static<::pallet_revive::EthTransactInfo<B>>"
Expand Down
2 changes: 2 additions & 0 deletions substrate/frame/revive/src/evm/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ mod debug_rpc_types;
pub use debug_rpc_types::*;

mod rpc_types;
pub use rpc_types::DryRunConfig;

mod rpc_types_gen;
pub use rpc_types_gen::*;

Expand Down
21 changes: 21 additions & 0 deletions substrate/frame/revive/src/evm/api/rpc_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,29 @@
//! Utility impl for the RPC types.
use super::*;
use alloc::vec::Vec;
use codec::{Decode, Encode};
use frame_support::DefaultNoBound;
use scale_info::TypeInfo;
use sp_core::{H160, U256};

/// Configuration specific to a dry-run execution.
#[derive(Debug, Encode, Decode, TypeInfo, Clone, DefaultNoBound)]
pub struct DryRunConfig<Moment> {
/// Optional timestamp override for dry-run in pending block.
pub timestamp_override: Option<Moment>,
Comment thread
pgherveou marked this conversation as resolved.
/// Used for future extensions without breaking encoding.
pub reserved: Option<()>,
}
impl<Moment> DryRunConfig<Moment> {
/// Create a new `DryRunConfig` with an optional timestamp override.
pub fn new(timestamp_override: Option<Moment>) -> Self {
Self {
timestamp_override,
reserved: None, // default value
}
}
}

impl From<BlockNumberOrTag> for BlockNumberOrTagOrHash {
fn from(b: BlockNumberOrTag) -> Self {
match b {
Expand Down
3 changes: 2 additions & 1 deletion substrate/frame/revive/src/evm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ mod test {
let account = Account::default();
Self::fund_account(&account);

let dry_run = crate::Pallet::<Test>::dry_run_eth_transact(self.tx.clone());
let dry_run =
crate::Pallet::<Test>::dry_run_eth_transact(self.tx.clone(), Default::default());
self.tx.gas_price = Some(<Pallet<Test>>::evm_base_fee());

match dry_run {
Expand Down
22 changes: 16 additions & 6 deletions substrate/frame/revive/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use alloc::{
collections::{BTreeMap, BTreeSet},
vec::Vec,
};
use core::{fmt::Debug, marker::PhantomData, mem, ops::ControlFlow};
use core::{cmp, fmt::Debug, marker::PhantomData, mem, ops::ControlFlow};
use frame_support::{
crypto::ecdsa::ECDSAExt,
dispatch::DispatchResult,
Expand Down Expand Up @@ -951,12 +951,24 @@ where
return Ok(None);
};

let mut timestamp = T::Time::now();
let mut block_number = <frame_system::Pallet<T>>::block_number();
// if dry run with timestamp override is provided we simulate the run in a `pending` block
if let Some(timestamp_override) =
exec_config.is_dry_run.as_ref().and_then(|cfg| cfg.timestamp_override)
{
block_number = block_number.saturating_add(1u32.into());
// Delta is in milliseconds; increment timestamp by one second
let delta = 1000u32.into();
timestamp = cmp::max(timestamp.saturating_add(delta), timestamp_override);
}

let stack = Self {
origin,
gas_meter,
storage_meter,
timestamp: T::Time::now(),
block_number: <frame_system::Pallet<T>>::block_number(),
timestamp,
block_number,
first_frame,
frames: Default::default(),
transient_storage: TransientStorage::new(limits::TRANSIENT_STORAGE_BYTES),
Expand All @@ -965,7 +977,6 @@ where
contracts_to_be_destroyed: BTreeMap::new(),
_phantom: Default::default(),
};

Ok(Some((stack, executable)))
}

Expand Down Expand Up @@ -1324,7 +1335,7 @@ where
// When a dry-run simulates contract deployment, keep the execution result's
// data.
let data = if crate::tracing::if_tracing(|_| {}).is_none() &&
!self.exec_config.is_dry_run
self.exec_config.is_dry_run.is_none()
{
core::mem::replace(&mut output.data, Default::default())
} else {
Expand Down Expand Up @@ -1411,7 +1422,6 @@ where
} else {
self.transient_storage.rollback_transaction();
}

log::trace!(target: LOG_TARGET, "frame finished with: {output:?}");

self.pop_frame(success);
Expand Down
Loading
Loading