Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

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

Merged
merged 11 commits into from
Apr 10, 2022

Conversation

Rjected
Copy link
Contributor

@Rjected Rjected commented Mar 30, 2022

Motivation

Frequently users run into errors returned by a Provider that are caused by chain ID inconsistencies between the user's Signer and Provider. For example, if the user tries to sign/send a transaction using a SignerMiddleware instantiated with a Signer and Provider that have different chain IDs, they might get this error from the provider:

Error: (code: -32700, message: Invalid signature v value, data: None)

Since users often use a SignerMiddleware to interact with both a provider and signer, it would be useful to ensure that the Signer uses the same chain ID as the Provider, to prevent this type of user error. Other approaches were discussed in a different issue.

Solution

Update the SignerMiddleware constructor and with_signer method to set the passed Signer's chain ID to the ID retrieved from the provider.
This changes the following SignerMiddleware methods:

Old:

    pub fn new(inner: M, signer: S) -> Self {
        // constructor...
    }

    pub fn with_signer(&self, signer: S) -> Self {
        // implementation...
    }

New:

    pub async fn new(inner: M, signer: S) -> Result<Self, SignerMiddlewareError<M, S>> {
        // constructor...
        // pulls the chain id from the inner middleware
    }

    pub async fn with_signer(&self, signer: S) -> Result<Self, SignerMiddlewareError<M, S>> {
        // implementation...
    }

We also have removed the #[must_use] attribute from with_signer, since it doesn't do anything for async functions.

Importantly, if a chain_id cannot be retrieved from the provider (due to a provider error or some other reason), we bubble up the error.

Previously, the implementation details of the provider were not relevant to the creation of a SignerMiddleware.
Now, the SignerMiddleware must be constructed with a provider with an implemented and working get_chainid method, or new/with_signer will return an error rather than a new SignerMiddleware.

Additionally, this breaks current uses of SignerMiddleware, since the Result must be handled, and each method is now async, since we're now interacting with a provider.

Tests are added which set a custom chain ID for Ganache and make sure the signer properly pulls the custom chain ID. To set a chain ID on Ganache, it had to be updated. There may be some differences in behavior between the two versions, so we should watch for test failures after re-enabling the relevant section of CI.

PR Checklist

  • Added Tests
  • Added Documentation
  • Updated the changelog

@Rjected
Copy link
Contributor Author

Rjected commented Mar 30, 2022

Seems like the updated ganache behaves differently and caused the run to fail, the test probably needs to be updated:

failures:

---- eth_tests::deploy_and_call_contract stdout ----
thread 'eth_tests::deploy_and_call_contract' panicked at 'assertion failed: `(left == right)`
  left: `56568`,
 right: `38856`', ethers-contract/tests/contract.rs:72:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    eth_tests::deploy_and_call_contract

test result: FAILED. 34 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 14.91s

error: test failed, to rerun pass '-p ethers-contract --test contract'

@Rjected Rjected force-pushed the consistent_chain_id branch from 839baa0 to 3bf49b5 Compare March 30, 2022 01:34
Copy link
Collaborator

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

///
/// [`Middleware`] ethers_providers::Middleware
/// [`Signer`] ethers_signers::Signer
pub async fn new(inner: M, signer: S) -> Result<Self, SignerMiddlewareError<M, S>> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is definitely an improvement but this breaks, and will be redundant if the user manually sets the chain id without requesting it from the inner middleware.

I'd rather like to make this a separate asynchronous function that does this. basically with_signer

Comment on lines 37 to 39
let chain_id = provider.get_chainid().await.unwrap().as_u64();
let wallet = wallet.with_chain_id(chain_id);
let provider = SignerMiddleware::new(provider, wallet);
let provider = SignerMiddleware::new(provider, wallet).await.unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

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

here for example, in this scenario, we're now querying the chain id twice

@gakonst
Copy link
Owner

gakonst commented Apr 2, 2022

I think moving to a new method instead of a breaking change makes sense here. Not sure what the best name for the function is, something that declares that it auto detects chain id ideally

@Rjected Rjected force-pushed the consistent_chain_id branch from 3bf49b5 to 9d21591 Compare April 10, 2022 00:47
@Rjected
Copy link
Contributor Author

Rjected commented Apr 10, 2022

Just switched to a new method new_with_provider_chain:

/// 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 })
}

Edited the comment on new to point to this method & explain what might happen if the chainid is not consistent.
Also needed to remove an assert in deploy_and_call_contract since the gas used & estimation are no longer equal with the updated ganache.

Copy link
Owner

@gakonst gakonst left a comment

Choose a reason for hiding this comment

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

Need to remove the formatting change in the doc comments, otherwise lgtm

ethers-middleware/src/signer.rs Outdated Show resolved Hide resolved
Rjected added 11 commits April 10, 2022 12:55
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.
newer ganache version is needed to specify the ganache provider chain id
in tests
 * 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
@Rjected Rjected force-pushed the consistent_chain_id branch from 924acff to 6fbff4c Compare April 10, 2022 16:55
@Rjected
Copy link
Contributor Author

Rjected commented Apr 10, 2022

Removed indents and rebased

Copy link
Owner

@gakonst gakonst left a comment

Choose a reason for hiding this comment

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

gg thx!

@gakonst gakonst merged commit 1d14f9d into gakonst:master Apr 10, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants