From c816a7bd9d9df3671d7e5075ce4f552c72b2aa9f Mon Sep 17 00:00:00 2001 From: Martin Beckmann Date: Tue, 9 Jan 2024 13:20:25 +0100 Subject: [PATCH] Support simulating using the pending timestamp with enso (#2185) # Description When we simulate a transaction in the driver we only care whether it would work on the `pending` block and not on the `latest` block. Of course we can't magically predict the entire state of the blockchain on the next block but at least we could catch problems that arise of the next block number, timestamp and basefee. This is pretty easy to do with `tenderly` and `web3` but unfortunately the `enso` simulator does not really support simulating on the `pending` block. To get closer to the desired behavior I opened a [PR](https://github.com/EnsoFinance/temper/pull/27) (not yet merged) on the enso simulator which allows users to provide the timestamp they want to simulate on and changed our code to produce the needed timestamp in the `enso` simulator. # Changes - added `block_timestamp` to the `enso` dto - added `current_block_stream` and `network_block_interval` to `enso` simulator to get the needed timestamp - added plumbing to configure `network_block_interval` in a config file - extended `FetchBlock.sol` to also return the blocks timestamp ## How to test Did a manual test locally. Created a test order and checked that the enso simulator can handle the timestamp. Unfortunately the `enso` team did not build a new docker image so I also tested that our code doesn't break with the old `temper` version so we should be able to merge this PR without any issues. Will contact the team to request a new build from them. Fixes #2174 --- crates/contracts/artifacts/FetchBlock.json | 2 +- crates/contracts/solidity/FetchBlock.sol | 3 +- crates/driver/example.toml | 4 ++ crates/driver/src/infra/config/file/load.rs | 1 + crates/driver/src/infra/config/file/mod.rs | 4 ++ crates/driver/src/infra/simulator/enso/dto.rs | 2 + crates/driver/src/infra/simulator/enso/mod.rs | 38 +++++++++++++++++-- crates/driver/src/infra/simulator/mod.rs | 6 ++- crates/driver/src/run.rs | 1 + crates/ethrpc/src/current_block/mod.rs | 2 + crates/ethrpc/src/current_block/retriever.rs | 5 ++- 11 files changed, 61 insertions(+), 7 deletions(-) diff --git a/crates/contracts/artifacts/FetchBlock.json b/crates/contracts/artifacts/FetchBlock.json index f42c413206..a952327598 100644 --- a/crates/contracts/artifacts/FetchBlock.json +++ b/crates/contracts/artifacts/FetchBlock.json @@ -1 +1 @@ -{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"}],"bytecode":"0x6080604052348015600f57600080fd5b506000804311601e5760006027565b6027600143607c565b9050804060008260375760006042565b6040600184607c565b405b6040805160208101869052908101849052606081018290529091506000906080016040516020818303038152906040529050805181602001f35b81810381811115609c57634e487b7160e01b600052601160045260246000fd5b9291505056fe","deployedBytecode":"0x6080604052600080fdfea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} +{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"}],"bytecode":"0x6080604052348015600f57600080fd5b506000804311601e5760006027565b60276001436084565b9050804060008260375760006042565b60406001846084565b405b604080516020810186905290810184905260608101829052426080820181905291925060009060a0016040516020818303038152906040529050805181602001f35b8181038181111560a457634e487b7160e01b600052601160045260246000fd5b9291505056fe","deployedBytecode":"0x6080604052600080fdfea164736f6c6343000811000a","devdoc":{"methods":{}},"userdoc":{"methods":{}}} diff --git a/crates/contracts/solidity/FetchBlock.sol b/crates/contracts/solidity/FetchBlock.sol index d9a478e4df..a38458028a 100644 --- a/crates/contracts/solidity/FetchBlock.sol +++ b/crates/contracts/solidity/FetchBlock.sol @@ -15,8 +15,9 @@ contract FetchBlock { bytes32 parentHash = blockNumber > 0 ? blockhash(blockNumber - 1) : bytes32(0); + uint timestamp = block.timestamp; - bytes memory result = abi.encode(blockNumber, blockHash, parentHash); + bytes memory result = abi.encode(blockNumber, blockHash, parentHash, timestamp); assembly { return(add(32, result), mload(result)) } diff --git a/crates/driver/example.toml b/crates/driver/example.toml index 4db4cc011b..6595f5dd21 100644 --- a/crates/driver/example.toml +++ b/crates/driver/example.toml @@ -75,3 +75,7 @@ graph-api-base-url = "https://api.thegraph.com/subgraphs/name/" # [[liquidity.uniswap-v3]] # Custom Uniswap V3 configuration # router = "0xE592427A0AEce92De3Edee1F18E0157C05861564" # max_pools_to_initialize = 100 # how many of the deepest pools to initialise on startup + +# [enso] +# url = "http://localhost:8454" +# network-block-interval = "12s" diff --git a/crates/driver/src/infra/config/file/load.rs b/crates/driver/src/infra/config/file/load.rs index 5d75387842..1a370877cc 100644 --- a/crates/driver/src/infra/config/file/load.rs +++ b/crates/driver/src/infra/config/file/load.rs @@ -302,6 +302,7 @@ pub async fn load(network: &blockchain::Network, path: &Path) -> infra::Config { } (None, Some(config)) => Some(simulator::Config::Enso(simulator::enso::Config { url: config.url, + network_block_interval: config.network_block_interval, })), (None, None) => None, (Some(_), Some(_)) => panic!("Cannot configure both Tenderly and Enso"), diff --git a/crates/driver/src/infra/config/file/mod.rs b/crates/driver/src/infra/config/file/mod.rs index 0e50a83308..434c172ea7 100644 --- a/crates/driver/src/infra/config/file/mod.rs +++ b/crates/driver/src/infra/config/file/mod.rs @@ -260,6 +260,10 @@ struct TenderlyConfig { struct EnsoConfig { /// URL at which the trade simulator is hosted url: Url, + /// How often the network produces a new block. If this is not set the + /// system assumes an unpredictable network like proof-of-work. + #[serde(default, with = "humantime_serde")] + network_block_interval: Option, } #[derive(Clone, Debug, Default, Deserialize)] diff --git a/crates/driver/src/infra/simulator/enso/dto.rs b/crates/driver/src/infra/simulator/enso/dto.rs index 6687cb4b2a..065bcf03a4 100644 --- a/crates/driver/src/infra/simulator/enso/dto.rs +++ b/crates/driver/src/infra/simulator/enso/dto.rs @@ -20,6 +20,8 @@ pub struct Request { #[serde(skip_serializing_if = "Option::is_none")] pub block_number: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub block_timestamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub access_list: Option, } diff --git a/crates/driver/src/infra/simulator/enso/mod.rs b/crates/driver/src/infra/simulator/enso/mod.rs index e5076f481e..b9ff7b53c8 100644 --- a/crates/driver/src/infra/simulator/enso/mod.rs +++ b/crates/driver/src/infra/simulator/enso/mod.rs @@ -1,4 +1,10 @@ -use {crate::domain::eth, reqwest::ClientBuilder, thiserror::Error}; +use { + crate::domain::eth, + ethrpc::current_block::CurrentBlockStream, + reqwest::ClientBuilder, + std::time::Duration, + thiserror::Error, +}; mod dto; @@ -8,23 +14,48 @@ const GAS_LIMIT: u64 = 30_000_000; pub(super) struct Enso { url: reqwest::Url, chain_id: eth::ChainId, + current_block: CurrentBlockStream, + network_block_interval: Option, } #[derive(Debug, Clone)] pub struct Config { /// The URL of the Transaction Simulator API. pub url: reqwest::Url, + /// The time between new blocks in the network. + pub network_block_interval: Option, } impl Enso { - pub(super) fn new(config: Config, chain_id: eth::ChainId) -> Self { + pub(super) fn new( + config: Config, + chain_id: eth::ChainId, + current_block: CurrentBlockStream, + ) -> Self { Self { url: reqwest::Url::parse(&format!("{}api/v1/simulate", config.url)).unwrap(), chain_id, + current_block, + network_block_interval: config.network_block_interval, } } pub(super) async fn simulate(&self, tx: eth::Tx) -> Result { + let current_block = *self.current_block.borrow(); + + let (block_number, block_timestamp) = match self.network_block_interval { + None => (None, None), // use default values which result in simulation on `latest` + Some(duration) => { + // We would like to simulate on the `pending` block instead of the `latest` + // block. Unfortunately `enso` does not support that so to get closer to + // the actual behavior of the `pending` block we use the block number of + // the `latest` block but the timestamp of the `pending` block. + let block_number = current_block.number; + let next_timestamp = current_block.timestamp + duration.as_secs(); + (Some(block_number), Some(next_timestamp)) + } + }; + let res: dto::Response = ClientBuilder::new() .build() .unwrap() @@ -36,7 +67,8 @@ impl Enso { data: tx.input.into(), value: tx.value.into(), gas_limit: GAS_LIMIT, - block_number: None, + block_number, + block_timestamp, access_list: if tx.access_list.is_empty() { None } else { diff --git a/crates/driver/src/infra/simulator/mod.rs b/crates/driver/src/infra/simulator/mod.rs index e91d09a8c4..9a4d345e74 100644 --- a/crates/driver/src/infra/simulator/mod.rs +++ b/crates/driver/src/infra/simulator/mod.rs @@ -53,7 +53,11 @@ impl Simulator { /// Uses Ethereum RPC API to generate access lists. pub fn enso(config: enso::Config, eth: Ethereum) -> Self { Self { - inner: Inner::Enso(enso::Enso::new(config, eth.network().chain)), + inner: Inner::Enso(enso::Enso::new( + config, + eth.network().chain, + eth.current_block().clone(), + )), eth, disable_access_lists: false, disable_gas: None, diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index 2fa0e35a4f..cd407fcf4e 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -108,6 +108,7 @@ fn simulator(config: &infra::Config, eth: &Ethereum) -> Simulator { Some(infra::simulator::Config::Enso(enso)) => Simulator::enso( simulator::enso::Config { url: enso.url.to_owned(), + network_block_interval: enso.network_block_interval.to_owned(), }, eth.to_owned(), ), diff --git a/crates/ethrpc/src/current_block/mod.rs b/crates/ethrpc/src/current_block/mod.rs index 854d972f88..622f3e5c08 100644 --- a/crates/ethrpc/src/current_block/mod.rs +++ b/crates/ethrpc/src/current_block/mod.rs @@ -51,6 +51,7 @@ pub struct BlockInfo { pub number: u64, pub hash: H256, pub parent_hash: H256, + pub timestamp: u64, } impl TryFrom> for BlockInfo { @@ -61,6 +62,7 @@ impl TryFrom> for BlockInfo { number: value.number.context("block missing number")?.as_u64(), hash: value.hash.context("block missing hash")?, parent_hash: value.parent_hash, + timestamp: value.timestamp.as_u64(), }) } } diff --git a/crates/ethrpc/src/current_block/retriever.rs b/crates/ethrpc/src/current_block/retriever.rs index e17227a9ec..b17851913a 100644 --- a/crates/ethrpc/src/current_block/retriever.rs +++ b/crates/ethrpc/src/current_block/retriever.rs @@ -76,6 +76,7 @@ impl BlockRetrieving for BlockRetriever { Ok(BlockInfo { number: fetch.number.saturating_sub(1), hash: fetch.parent_hash, + timestamp: fetch.timestamp, parent_hash: call.hash, }) } else { @@ -93,17 +94,19 @@ impl BlockRetrieving for BlockRetriever { } /// Decodes the return data from the `FetchBlock` contract. -fn decode(return_data: [u8; 96]) -> Result { +fn decode(return_data: [u8; 128]) -> Result { let number = u64::try_from(U256::from_big_endian(&return_data[0..32])) .ok() .context("block number overflows u64")?; let hash = H256::from_slice(&return_data[32..64]); let parent_hash = H256::from_slice(&return_data[64..96]); + let timestamp = U256::from_big_endian(&return_data[96..128]).as_u64(); Ok(BlockInfo { number, hash, parent_hash, + timestamp, }) }