gakonst / ethers-rs

Complete Ethereum & Celo library and wallet implementation in Rust. https://docs.rs/ethers
Apache License 2.0
2.48k stars 796 forks source link

`Transaction::recover_from` is buggy for legacy txs #1388

Open meetmangukiya opened 2 years ago

meetmangukiya commented 2 years ago

Version

├── ethers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   ├── ethers-addressbook v0.1.0 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   ├── ethers-contract v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   ├── ethers-contract-abigen v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-contract-derive v0.6.3 (proc-macro) (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   ├── ethers-contract-abigen v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-providers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   ├── ethers-etherscan v0.2.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   ├── ethers-middleware v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   ├── ethers-contract v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-etherscan v0.2.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-providers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   │   ├── ethers-signers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│   │   │   ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   ├── ethers-providers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   ├── ethers-signers v0.6.2 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)
│   └── ethers-solc v0.3.0 (https://github.com/gakonst/ethers-rs?branch=master#974441e9)
│       ├── ethers-core v0.6.3 (https://github.com/gakonst/ethers-rs?branch=master#974441e9) (*)

Platform

Darwin mac-mini 21.5.0 Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:29 PDT 2022; root:xnu-8020.121.3~4/RELEASE_ARM64_T8101 arm64

Description

Transaction::recover_from doesn't work well for recovering from for legacy txs. Have to look into it. Opening to track the bug.

mattsse commented 2 years ago

probably something's off with the conversion to typedtransaction and/or its hash

gakonst commented 2 years ago

cc @Rjected maybe related to the rlp ser/de

Rjected commented 2 years ago

hmm, it would be helpful to have a tx example to test conversion / hashing if you have one

meetmangukiya commented 2 years ago
use ethers::types::H256;
use ethers::providers::Provider;
use std::sync::Arc;
use ethers::providers::Middleware;

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let provider = Arc::new(Provider::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?);
    let tx = provider.get_transaction("0x655357c04593c85e84cadd945bde83386d6519b6cbc90380a120ecb2f5930253".parse::<H256>()?).await?.unwrap();
    let from = tx.recover_from()?;
    println!("recovered: {:?}", from);
    println!("original: {:?}", tx.from);
    assert_eq!(from, tx.from);
    Ok(())
}
Rjected commented 2 years ago

Ok I figured it out, it is once again the chain id not being serialized properly before computing the sighash. The rlp encoding methods currently encode the chain_id in the following way before hashing:

    /// Gets the transaction's RLP encoding, prepared with the chain_id and extra fields for
    /// signing. Assumes the chainid exists.
    pub fn rlp(&self) -> Bytes {
        let mut rlp = RlpStream::new();
        rlp.begin_list(NUM_TX_FIELDS);
        self.rlp_base(&mut rlp);

        // Only hash the 3 extra fields when preparing the
        // data to sign if chain_id is present
        rlp_opt(&mut rlp, &self.chain_id);
        rlp.append(&0u8);
        rlp.append(&0u8);
        rlp.out().freeze().into()
    }

EIP155 means transactions need a populated chain id parameter for any signing or recovery related operations to work. As an aside, maybe chain_id should not be an Option because of this? The problem is that rlp_opt will serialize the chain id as an empty string (0x80) if it is None. This one byte difference causes an incorrect sighash and recovery failure. Populating the chain_id in the example causes the code example to succeed:

use ethers::types::H256;
use ethers::providers::Provider;
use std::sync::Arc;
use ethers::providers::Middleware;

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let provider = Arc::new(Provider::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?);
    let tx = provider.get_transaction("0x655357c04593c85e84cadd945bde83386d6519b6cbc90380a120ecb2f5930253".parse::<H256>()?).await?.unwrap();
    tx.chain_id = Some(provider.get_chainid().await?);
    let from = tx.recover_from()?;
    println!("recovered: {:?}", from);
    println!("original: {:?}", tx.from);
    assert_eq!(from, tx.from);
    Ok(())
}
recovered: 0xc4b732fd121f2f3783a9ac2a6c62fd535fd13fda
original: 0xc4b732fd121f2f3783a9ac2a6c62fd535fd13fda

So maybe the solution here is to change get_transaction to populate the chain id, since the provider is responsible for filling the transaction and there is no way to populate it later.

mattsse commented 2 years ago

wait, why's there even a chain_id field in the Transaction response object?

mmm, this doesn't look very sound to me to recover from the response directly because of this

what ethers has is

but what's missing is something like

TypedTransaction that represents the Transaction Response type, we use this in anvil

https://github.com/foundry-rs/foundry/blob/8ffdf37dda1b5d6c4366ad3b9877a8dd6fc9e6ca/anvil/core/src/eth/transaction/mod.rs#L353-L361

which anvil uses to recover the sender, this is what eth_sendRawTransaction decodes to

so perhaps we can do

Rjected commented 2 years ago

I could be wrong, but I'm not sure that any provider actually sends a chainId in a Transaction response, so I also feel like some of the types should be revised here. We should definitely introduce a TypedTransaction, or generally a typed signed transaction type like in anvil into ethers.

Encoding and decoding methods on Request types could also be moved to their appropriate place: