ethereum / py-evm

A Python implementation of the Ethereum Virtual Machine
https://py-evm.readthedocs.io/en/latest/
MIT License
2.27k stars 654 forks source link

MuirGlacierTransaction incorrectly decodes sender in raw transaction #1956

Closed Pet3ris closed 4 years ago

Pet3ris commented 4 years ago
py-evm==0.3.0a19
web3==5.12.1
eth-tester==0.5.0b2

What is wrong?

I'm running the following code on a recent mainnet transaction hash.

import pytest
import web3
import eth_account
import rlp
from eth.vm.forks.muir_glacier.transactions import MuirGlacierTransaction
def test_extract_from_account():
    w3 = web3.Web3(web3.HTTPProvider("<insert your favorite mainnet provider>"))
    tx = w3.eth.getTransaction("0x2133167f62f03f65e07210640530373aad2e743ee9e2c025b9abec4e8fe2133d")
    raw_tx = recover_raw_transaction(tx)
    TransactionClass = MuirGlacierTransaction
    evm_transaction = rlp.decode(raw_tx, TransactionClass)
    sender = evm_transaction.sender
    print(w3.eth.getBalance(sender))

If you see this transaction on Etherscan, you will see that the sender is actually 0x14b95ed55c0825a30c5bf6d4905379e06749b117, however, when I run this locally, it prints out b'b\xecR$c4\x98\xc1$i\x0cUt\x8f\xe4\x88\xa01\xa95', which is 0x62ec5224633498c124690c55748fe488a031a935.

There is one custom function I'm using here, which is recover_raw_transaction, I've adapted the implementation from eth_accounts and tested it using the following test. All this function does is recovers the signed raw transaction hash using transaction information from mainnet.

def recover_raw_transaction(tx) -> str:
    """Recover raw transaction for replay.

    Adapted from: https://github.com/ethereum/eth-account/blob/1d26f44f6075d6f283aeaeff879f4508c9a228dc/eth_account/_utils/signing.py#L28-L42
    """
    transaction = {
        "to": tx["to"],
        "gas": tx["gas"],
        "gasPrice": tx["gasPrice"],
        "value": tx["value"],
        "nonce": tx["nonce"],
    }
    if "data" in tx:
        transaction["data"] = tx["data"]

    v = tx["v"]
    r = int.from_bytes(tx["r"], "big")
    s = int.from_bytes(tx["s"], "big")
    unsigned_transaction = serializable_unsigned_transaction_from_dict(transaction)
    return encode_transaction(unsigned_transaction, vrs=(v, r, s))
def test_signing():
    # Setup env and fund our account
    acct = eth_account.Account.create("SPLASH")
    w3 = web3.Web3(web3.EthereumTesterProvider())
    w3.eth.sendTransaction({"from": w3.eth.coinbase, "to": acct.address, "value": 10 ** 8})
    assert w3.eth.getBalance(acct.address) == 10 ** 8

    # Sign transaction and send it
    signed_tx = w3.eth.account.sign_transaction(
        {
            "from": acct.address,
            "to": w3.eth.coinbase,
            "data": b"",
            "value": 10 ** 7,
            "nonce": w3.eth.getTransactionCount(acct.address),
            "gasPrice": w3.eth.gasPrice,
            "gas": 10 ** 5,
        },
        acct.key,
    )
    raw_tx = signed_tx.rawTransaction
    tx_hash = w3.eth.sendRawTransaction(raw_tx)

    # Attempt to recover the signed transaction from its arguments
    tx = w3.eth.getTransaction(tx_hash)
    raw_tx2 = recover_raw_transaction(tx)

    # The reconstructed raw transaction should match the signed one
    assert raw_tx == raw_tx2

Here is the full MuirGlacierTransaction - I've checked that the to address and other arguments match this mainnet transaction well, but sender simply doesn't.

MuirGlacierTransaction(nonce=3788, gas_price=500000000000, gas=190894, to=b'z%\rV0\xb4\xcfS\x979\xdf,]\xac\xb4\xc6Y\xf2H\x8d', value=400000000000000000, data=b'', v=38, r=74143883448002018754475343141839442092018387336910689797702689452511837451914, s=46215258636012427298027329700625499290324342672107021033224234713237025620081)

How can it be fixed

The problem is likely with the .sender extraction? Am I using the wrong transaction class for this block?

Pet3ris commented 4 years ago

Found the following fix (the mainnet node was returning data as input):

def recover_raw_transaction(tx) -> str:
    """Recover raw transaction for replay.

    Inspired by: https://github.com/ethereum/eth-account/blob/1d26f44f6075d6f283aeaeff879f4508c9a228dc/eth_account/_utils/signing.py#L28-L42
    """
    transaction = {
        "to": tx["to"],
        "gas": tx["gas"],
        "gasPrice": tx["gasPrice"],
        "value": tx["value"],
        "nonce": tx["nonce"],
    }
    if "data" in tx:
        transaction["data"] = tx["data"]
    if "input" in tx:
        transaction["data"] = tx["input"]

    v = tx["v"]
    r = int.from_bytes(tx["r"], "big")
    s = int.from_bytes(tx["s"], "big")
    unsigned_transaction = serializable_unsigned_transaction_from_dict(transaction)
    return encode_transaction(unsigned_transaction, vrs=(v, r, s))