Ensure a consistent chain ID between a signer and provider in SignerMiddleware (#1095)

* feat(middleware): fetch chainid from middleware

Require SignerMiddleware to fetch the inner middleware's to set for the
signer. SignerMiddleware now requires an instantiated middleware with
an accessible get_chainid method.

* ci: update ganache

newer ganache version is needed to specify the ganache provider chain id
in tests

* set SignerMiddleware constructor return to result

* create new method for pulling chainid

* add consistent chainid CHANGELOG entry

* remove awaits

* switch test_derive_eip712 to use consistent signer

 * remove gas estimation equality assert in deploy_and_call_contract -
   updated version of ganache no longer returns the same value for gas
   estimation and gas_used

* revert with_signer to non-async non-result

* cargo fmt

* expand SignerMiddleware::new comment

* remove doc indent
This commit is contained in:
Dan Cline 2022-04-10 13:55:30 -04:00 committed by GitHub
parent 9206efa46c
commit 1d14f9d26e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 19 deletions

View File

@ -21,12 +21,12 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install ganache-cli - name: Set up node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 node-version: 10
- name: Install ganache - name: Install ganache
run: npm install -g ganache-cli run: npm install -g ganache
- name: Install Solc - name: Install Solc
run: | run: |
mkdir -p "$HOME/bin" mkdir -p "$HOME/bin"
@ -66,13 +66,13 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install ganache-cli - name: Set up node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10 node-version: 10
# TODO: can we combine these shared steps in github actions? # TODO: can we combine these shared steps in github actions?
- name: Install ganache - name: Install ganache
run: npm install -g ganache-cli run: npm install -g ganache
- name: Install Solc - name: Install Solc
run: | run: |
mkdir -p "$HOME/bin" mkdir -p "$HOME/bin"
@ -183,12 +183,12 @@ jobs:
# steps: # steps:
# - name: Checkout sources # - name: Checkout sources
# uses: actions/checkout@v2 # uses: actions/checkout@v2
# - name: Install ganache-cli # - name: Set up node
# uses: actions/setup-node@v1 # uses: actions/setup-node@v1
# with: # with:
# node-version: 10 # node-version: 10
# - name: Install ganache # - name: Install ganache
# run: npm install -g ganache-cli # run: npm install -g ganache
# - name: Install Solc # - name: Install Solc
# run: | # run: |
# mkdir -p "$HOME/bin" # mkdir -p "$HOME/bin"

View File

@ -234,6 +234,9 @@
### Unreleased ### Unreleased
- Ensure a consistent chain ID between a Signer and Provider in SignerMiddleware
[#1095](https://gakonst/ethers-rs/pull/1095)
### 0.6.0 ### 0.6.0
- add the missing constructor for `Timelag` middleware via - add the missing constructor for `Timelag` middleware via

View File

@ -69,7 +69,6 @@ mod eth_tests {
assert_eq!(last_sender.clone().call().await.unwrap(), addr2); assert_eq!(last_sender.clone().call().await.unwrap(), addr2);
assert_eq!(get_value.clone().call().await.unwrap(), "hi"); assert_eq!(get_value.clone().call().await.unwrap(), "hi");
assert_eq!(tx.input, calldata); assert_eq!(tx.input, calldata);
assert_eq!(tx_receipt.gas_used.unwrap(), gas_estimate);
// we can also call contract methods at other addresses with the `at` call // we can also call contract methods at other addresses with the `at` call
// (useful when interacting with multiple ERC20s for example) // (useful when interacting with multiple ERC20s for example)
@ -579,7 +578,8 @@ mod eth_tests {
.expect("failed to instantiate provider from ganache endpoint") .expect("failed to instantiate provider from ganache endpoint")
.interval(Duration::from_millis(10u64)); .interval(Duration::from_millis(10u64));
let client = SignerMiddleware::new(provider, wallet.clone()); let client =
SignerMiddleware::new_with_provider_chain(provider, wallet.clone()).await.unwrap();
let client = Arc::new(client); let client = Arc::new(client);
let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone()); let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone());
@ -597,8 +597,8 @@ mod eth_tests {
let contract = DeriveEip712Test::new(addr, client.clone()); let contract = DeriveEip712Test::new(addr, client.clone());
let foo_bar = FooBar { let foo_bar = FooBar {
foo: I256::from(10), foo: I256::from(10u64),
bar: U256::from(20), bar: U256::from(20u64),
fizz: b"fizz".into(), fizz: b"fizz".into(),
buzz: keccak256("buzz"), buzz: keccak256("buzz"),
far: String::from("space"), far: String::from("space"),

View File

@ -59,7 +59,7 @@ use thiserror::Error;
/// # } /// # }
/// ``` /// ```
/// ///
/// [`Provider`]: ethers_providers::Provider /// [`Signer`]: ethers_signers::Signer
pub struct SignerMiddleware<M, S> { pub struct SignerMiddleware<M, S> {
pub(crate) inner: M, pub(crate) inner: M,
pub(crate) signer: S, pub(crate) signer: S,
@ -107,12 +107,23 @@ where
S: Signer, S: Signer,
{ {
/// Creates a new client from the provider and signer. /// Creates a new client from the provider and signer.
/// Sets the address of this middleware to the address of the signer.
/// The chain_id of the signer will not be set to the chain id of the provider. If the signer
/// passed here is initialized with a different chain id, then the client may throw errors, or
/// methods like `sign_transaction` may error.
/// To automatically set the signer's chain id, see `new_with_provider_chain`.
///
/// [`Middleware`] ethers_providers::Middleware
/// [`Signer`] ethers_signers::Signer
pub fn new(inner: M, signer: S) -> Self { pub fn new(inner: M, signer: S) -> Self {
let address = signer.address(); let address = signer.address();
SignerMiddleware { inner, signer, address } SignerMiddleware { inner, signer, address }
} }
/// Signs and returns the RLP encoding of the signed transaction /// Signs and returns the RLP encoding of the signed transaction.
/// If the transaction does not have a chain id set, it sets it to the signer's chain id.
/// Returns an error if the transaction's existing chain id does not match the signer's chain
/// id.
async fn sign_transaction( async fn sign_transaction(
&self, &self,
tx: TypedTransaction, tx: TypedTransaction,
@ -148,6 +159,7 @@ where
&self.signer &self.signer
} }
/// Builds a SignerMiddleware with the given Signer.
#[must_use] #[must_use]
pub fn with_signer(&self, signer: S) -> Self pub fn with_signer(&self, signer: S) -> Self
where where
@ -160,6 +172,24 @@ where
this this
} }
/// Creates a new client from the provider and signer.
/// Sets the address of this middleware to the address of the signer.
/// Sets the chain id of the signer to the chain id of the inner [`Middleware`] passed in,
/// using the [`Signer`]'s implementation of with_chain_id.
///
/// [`Middleware`] ethers_providers::Middleware
/// [`Signer`] ethers_signers::Signer
pub async fn new_with_provider_chain(
inner: M,
signer: S,
) -> Result<Self, SignerMiddlewareError<M, S>> {
let address = signer.address();
let chain_id =
inner.get_chainid().await.map_err(|e| SignerMiddlewareError::MiddlewareError(e))?;
let signer = signer.with_chain_id(chain_id.as_u64());
Ok(SignerMiddleware { inner, signer, address })
}
fn set_tx_from_if_none(&self, tx: &TypedTransaction) -> TypedTransaction { fn set_tx_from_if_none(&self, tx: &TypedTransaction) -> TypedTransaction {
let mut tx = tx.clone(); let mut tx = tx.clone();
if tx.from().is_none() { if tx.from().is_none() {
@ -329,7 +359,12 @@ mod tests {
.into(); .into();
let chain_id = 1u64; let chain_id = 1u64;
let provider = Provider::try_from("http://localhost:8545").unwrap(); // Signer middlewares now rely on a working provider which it can query the chain id from,
// so we make sure ganache is started with the chain id that the expected tx was signed
// with
let ganache =
Ganache::new().args(vec!["--chain.chainId".to_string(), chain_id.to_string()]).spawn();
let provider = Provider::try_from(ganache.endpoint()).unwrap();
let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap() .unwrap()
@ -348,6 +383,50 @@ mod tests {
assert_eq!(tx, expected_rlp); assert_eq!(tx, expected_rlp);
} }
#[tokio::test]
async fn ganache_consistent_chainid() {
let ganache = Ganache::new().spawn();
let provider = Provider::try_from(ganache.endpoint()).unwrap();
let chain_id = provider.get_chainid().await.unwrap();
assert_eq!(chain_id, U256::from(1337));
// Intentionally do not set the chain id here so we ensure that the signer pulls the
// provider's chain id.
let key = LocalWallet::new(&mut rand::thread_rng());
// combine the provider and wallet and test that the chain id is the same for both the
// signer returned by the middleware and through the middleware itself.
let client = SignerMiddleware::new_with_provider_chain(provider, key).await.unwrap();
let middleware_chainid = client.get_chainid().await.unwrap();
assert_eq!(chain_id, middleware_chainid);
let signer = client.signer();
let signer_chainid = signer.chain_id();
assert_eq!(chain_id.as_u64(), signer_chainid);
}
#[tokio::test]
async fn ganache_consistent_chainid_not_default() {
let ganache = Ganache::new().args(vec!["--chain.chainId", "13371337"]).spawn();
let provider = Provider::try_from(ganache.endpoint()).unwrap();
let chain_id = provider.get_chainid().await.unwrap();
assert_eq!(chain_id, U256::from(13371337));
// Intentionally do not set the chain id here so we ensure that the signer pulls the
// provider's chain id.
let key = LocalWallet::new(&mut rand::thread_rng());
// combine the provider and wallet and test that the chain id is the same for both the
// signer returned by the middleware and through the middleware itself.
let client = SignerMiddleware::new_with_provider_chain(provider, key).await.unwrap();
let middleware_chainid = client.get_chainid().await.unwrap();
assert_eq!(chain_id, middleware_chainid);
let signer = client.signer();
let signer_chainid = signer.chain_id();
assert_eq!(chain_id.as_u64(), signer_chainid);
}
#[tokio::test] #[tokio::test]
async fn handles_tx_from_field() { async fn handles_tx_from_field() {
let ganache = Ganache::new().spawn(); let ganache = Ganache::new().spawn();
@ -361,7 +440,7 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
let client = SignerMiddleware::new(provider, key); let client = SignerMiddleware::new_with_provider_chain(provider, key).await.unwrap();
let request = TransactionRequest::new(); let request = TransactionRequest::new();

View File

@ -38,8 +38,10 @@ async fn using_gas_oracle() {
// connect to the network // connect to the network
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap(); let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
// this is set because ganache now sets 875000000 as the first block's base fee
let base_fee = 875000000;
// assign a gas oracle to use // assign a gas oracle to use
let gas_oracle = FakeGasOracle { gas_price: 1337.into() }; let gas_oracle = FakeGasOracle { gas_price: (base_fee + 1337).into() };
let expected_gas_price = gas_oracle.fetch().await.unwrap(); let expected_gas_price = gas_oracle.fetch().await.unwrap();
let provider = GasOracleMiddleware::new(provider, gas_oracle); let provider = GasOracleMiddleware::new(provider, gas_oracle);

View File

@ -35,8 +35,7 @@ async fn send_eth() {
.unwrap() .unwrap()
.interval(Duration::from_millis(10u64)); .interval(Duration::from_millis(10u64));
let chain_id = provider.get_chainid().await.unwrap().as_u64(); let chain_id = provider.get_chainid().await.unwrap().as_u64();
let wallet = wallet.with_chain_id(chain_id); let provider = SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap();
let provider = SignerMiddleware::new(provider, wallet);
// craft the transaction // craft the transaction
let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id); let tx = TransactionRequest::new().to(wallet2.address()).value(10000).chain_id(chain_id);
@ -168,7 +167,8 @@ async fn send_transaction_handles_tx_from_field() {
// connect to the network // connect to the network
let provider = Provider::try_from(ganache.endpoint()).unwrap(); let provider = Provider::try_from(ganache.endpoint()).unwrap();
let provider = SignerMiddleware::new(provider, signer.clone()); let provider =
SignerMiddleware::new_with_provider_chain(provider, signer.clone()).await.unwrap();
// sending a TransactionRequest with a from field of None should result // sending a TransactionRequest with a from field of None should result
// in a transaction from the signer address // in a transaction from the signer address
@ -231,7 +231,7 @@ async fn deploy_and_call_contract() {
.parse::<LocalWallet>() .parse::<LocalWallet>()
.unwrap() .unwrap()
.with_chain_id(chain_id); .with_chain_id(chain_id);
let client = SignerMiddleware::new(provider, wallet); let client = SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap();
let client = Arc::new(client); let client = Arc::new(client);
let factory = ContractFactory::new(abi, bytecode, client); let factory = ContractFactory::new(abi, bytecode, client);