Skip to content

Commit

Permalink
Merge pull request #1674 from oasisprotocol/kostko/fix/eip-1559-tx
Browse files Browse the repository at this point in the history
runtime-sdk/modules/evm: Fix EIP-1559 gas price calculation
  • Loading branch information
kostko authored Apr 3, 2024
2 parents a9c70cb + 74b5090 commit 5bec25f
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 14 deletions.
19 changes: 15 additions & 4 deletions runtime-sdk/modules/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,15 +751,26 @@ impl<Cfg: Config> Module<Cfg> {

impl<Cfg: Config> module::TransactionHandler for Module<Cfg> {
fn decode_tx<C: Context>(
_ctx: &C,
ctx: &C,
scheme: &str,
body: &[u8],
) -> Result<Option<Transaction>, CoreError> {
match scheme {
"evm.ethereum.v0" => Ok(Some(
raw_tx::decode(body, Some(Cfg::CHAIN_ID))
"evm.ethereum.v0" => {
let min_gas_price =
<C::Runtime as Runtime>::Core::min_gas_price(ctx, &Cfg::TOKEN_DENOMINATION)
.unwrap_or_default();

Ok(Some(
raw_tx::decode(
body,
Some(Cfg::CHAIN_ID),
min_gas_price,
&Cfg::TOKEN_DENOMINATION,
)
.map_err(CoreError::MalformedTransaction)?,
)),
))
}
_ => Ok(None),
}
}
Expand Down
71 changes: 61 additions & 10 deletions runtime-sdk/modules/evm/src/raw_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ pub fn recover_low(
pub fn decode(
body: &[u8],
expected_chain_id: Option<u64>,
min_gas_price: u128,
denom: &token::Denomination,
) -> Result<transaction::Transaction, anyhow::Error> {
let (
chain_id,
Expand Down Expand Up @@ -99,9 +101,20 @@ pub fn decode(
let sig_recid = k256::ecdsa::RecoveryId::new(eth_tx.odd_y_parity, false);
let message = ethereum::EIP1559TransactionMessage::from(eth_tx);

// Base fee is zero. Allocate only priority fee.
let resolved_gas_price =
std::cmp::min(message.max_fee_per_gas, message.max_priority_fee_per_gas);
if message.max_fee_per_gas < message.max_priority_fee_per_gas {
return Err(anyhow!("invalid gas price"));
}
let base_fee_per_gas = min_gas_price.into();
if message.max_fee_per_gas < base_fee_per_gas {
return Err(anyhow!("gas price too low"));
}

let priority_fee_per_gas = std::cmp::min(
message.max_priority_fee_per_gas,
message.max_fee_per_gas.saturating_sub(base_fee_per_gas),
);
let effective_gas_price = priority_fee_per_gas.saturating_add(base_fee_per_gas);

(
Some(message.chain_id),
sig,
Expand All @@ -111,7 +124,7 @@ pub fn decode(
message.value,
message.input,
message.nonce,
resolved_gas_price,
effective_gas_price,
message.gas_limit,
)
}
Expand Down Expand Up @@ -175,7 +188,7 @@ pub fn decode(
nonce,
}],
fee: transaction::Fee {
amount: token::BaseUnits(resolved_fee_amount, token::Denomination::NATIVE),
amount: token::BaseUnits::new(resolved_fee_amount, denom.clone()),
gas: gas_limit,
consensus_messages: 0, // Dynamic number of consensus messages, limited by gas.
},
Expand Down Expand Up @@ -207,8 +220,15 @@ mod test {
expected_gas_price: u128,
expected_from: &str,
expected_nonce: u64,
min_gas_price: u128,
) {
let tx = decode(&Vec::from_hex(raw).unwrap(), expected_chain_id).unwrap();
let tx = decode(
&Vec::from_hex(raw).unwrap(),
expected_chain_id,
min_gas_price,
&token::Denomination::NATIVE,
)
.unwrap();
println!("{:?}", &tx);
assert_eq!(tx.call.method, "evm.Call");
let body: types::Call = cbor::from_value(tx.call.body).unwrap();
Expand Down Expand Up @@ -239,8 +259,15 @@ mod test {
expected_gas_price: u128,
expected_from: &str,
expected_nonce: u64,
min_gas_price: u128,
) {
let tx = decode(&Vec::from_hex(raw).unwrap(), expected_chain_id).unwrap();
let tx = decode(
&Vec::from_hex(raw).unwrap(),
expected_chain_id,
min_gas_price,
&token::Denomination::NATIVE,
)
.unwrap();
println!("{:?}", &tx);
assert_eq!(tx.call.method, "evm.Create");
let body: types::Create = cbor::from_value(tx.call.body).unwrap();
Expand All @@ -261,7 +288,13 @@ mod test {
}

fn decode_expect_invalid(raw: &str, expected_chain_id: Option<u64>) {
let e = decode(&Vec::from_hex(raw).unwrap(), expected_chain_id).unwrap_err();
let e = decode(
&Vec::from_hex(raw).unwrap(),
expected_chain_id,
0,
&token::Denomination::NATIVE,
)
.unwrap_err();
eprintln!("Decoding error (expected): {:?}", e);
}

Expand All @@ -270,7 +303,12 @@ mod test {
expected_chain_id: Option<u64>,
unexpected_from: &str,
) {
match decode(&Vec::from_hex(raw).unwrap(), expected_chain_id) {
match decode(
&Vec::from_hex(raw).unwrap(),
expected_chain_id,
0,
&token::Denomination::NATIVE,
) {
Ok(tx) => {
assert_ne!(
derive_caller::from_tx_auth_info(&tx.auth_info).unwrap(),
Expand Down Expand Up @@ -298,6 +336,7 @@ mod test {
// "cow" test account
"cd2a3d9f938e13cd947ec05abc7fe734df8dd826",
0,
1_000,
);
decode_expect_create(
// We're using a transaction normalized from the original (below) to have low `s`.
Expand All @@ -311,6 +350,7 @@ mod test {
// "horse" test account
"13978aee95f38490e9769c39b2773ed763d9cd5f",
0,
1_000,
);
}

Expand All @@ -327,6 +367,8 @@ mod test {
#[test]
fn test_decode_types() {
// https://github.com/ethereum/tests/blob/v10.0/BlockchainTests/ValidBlocks/bcEIP1559/transType.json

// Legacy.
decode_expect_call(
"f861018203e882c35094cccccccccccccccccccccccccccccccccccccccc80801ca021539ef96c70ab75350c594afb494458e211c8c722a7a0ffb7025c03b87ad584a01d5395fe48edb306f614f0cd682b8c2537537f5fd3e3275243c42e9deff8e93d",
None,
Expand All @@ -337,7 +379,10 @@ mod test {
1_000,
"d02d72e067e77158444ef2020ff2d325f929b363",
1,
1_000,
);

// Legacy.
decode_expect_call(
"01f86301028203e882c35094cccccccccccccccccccccccccccccccccccccccc8080c080a0260f95e555a1282ef49912ff849b2007f023c44529dc8fb7ecca7693cccb64caa06252cf8af2a49f4cb76fd7172feaece05124edec02db242886b36963a30c2606",
Some(1),
Expand All @@ -348,17 +393,23 @@ mod test {
1_000,
"d02d72e067e77158444ef2020ff2d325f929b363",
2,
1_000,
);

// EIP-1559
// maxFeePerGas = 1000
// maxPriorityFeePerGas = 100
decode_expect_call(
"02f8640103648203e882c35094cccccccccccccccccccccccccccccccccccccccc8080c001a08480e6848952a15ae06192b8051d213d689bdccdf8f14cf69f61725e44e5e80aa057c2af627175a2ac812dab661146dfc7b9886e885c257ad9c9175c3fcec2202e",
Some(1),
"cccccccccccccccccccccccccccccccccccccccc",
0,
"",
50_000,
100,
500, // min(100, 1000 - 400) + 400
"d02d72e067e77158444ef2020ff2d325f929b363",
3,
400,
);
}

Expand Down
94 changes: 94 additions & 0 deletions tests/e2e/evmtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

ethMath "github.com/ethereum/go-ethereum/common/math"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"google.golang.org/grpc"

Expand Down Expand Up @@ -1554,6 +1555,99 @@ func EVMParametersTest(_ *RuntimeScenario, _ *logging.Logger, _ *grpc.ClientConn
return nil
}

func submitEthereumTx(rtc client.RuntimeClient, txData ethTypes.TxData) (cbor.RawMessage, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

c := core.NewV1(rtc)
ac := accounts.NewV1(rtc)

mgp, err := c.MinGasPrice(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get min gas price: %w", err)
}
gasPrice := mgp[types.NativeDenomination]

nonce, err := ac.Nonce(ctx, client.RoundLatest, testing.Dave.Address)
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}

switch txData := txData.(type) {
case *ethTypes.LegacyTx:
txData.Nonce = nonce
txData.GasPrice = gasPrice.ToBigInt()
case *ethTypes.AccessListTx:
txData.Nonce = nonce
txData.GasPrice = gasPrice.ToBigInt()
case *ethTypes.DynamicFeeTx:
txData.Nonce = nonce
txData.GasFeeCap = gasPrice.ToBigInt()
default:
return nil, fmt.Errorf("unsupported tx type: %T", txData)
}

tx := ethTypes.NewTx(txData)

sk, err := crypto.ToECDSA(testing.Dave.SecretKey)
if err != nil {
return nil, fmt.Errorf("failed to prepare signer key: %w", err)
}
signer := ethTypes.LatestSignerForChainID(big.NewInt(0xa515))
signature, err := crypto.Sign(signer.Hash(tx).Bytes(), sk)
if err != nil {
return nil, fmt.Errorf("failed to sign ethereum tx: %w", err)
}

signedTx, err := tx.WithSignature(signer, signature)
if err != nil {
return nil, fmt.Errorf("failed to compose tx: %w", err)
}
rawTx, err := signedTx.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("failed to marshal tx: %w", err)
}

sdkTx := &types.UnverifiedTransaction{
Body: rawTx,
AuthProofs: []types.AuthProof{
{Module: "evm.ethereum.v0"},
},
}

return rtc.SubmitTx(ctx, sdkTx)
}

// EthereumTxTest tests Ethereum-encoded transaction support.
func EthereumTxTest(_ *RuntimeScenario, _ *logging.Logger, _ *grpc.ClientConn, rtc client.RuntimeClient) error {
for i, txData := range []ethTypes.TxData{
&ethTypes.LegacyTx{
To: testing.Dave.EthAddress,
Value: big.NewInt(0),
Gas: 100_000,
Data: nil,
},
&ethTypes.AccessListTx{
To: testing.Dave.EthAddress,
Value: big.NewInt(0),
Gas: 100_000,
Data: nil,
},
&ethTypes.DynamicFeeTx{
To: testing.Dave.EthAddress,
Value: big.NewInt(0),
Gas: 100_000,
Data: nil,
},
} {
_, err := submitEthereumTx(rtc, txData)
if err != nil {
return fmt.Errorf("transaction %d: %w", i, err)
}
}
return nil
}

// EVMRuntimeFixture prepares the runtime fixture for the EVM tests.
func EVMRuntimeFixture(ff *oasis.NetworkFixture) {
// The EVM runtime has 110_000 TEST tokens already minted internally. Since we connect it to the
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/scenarios.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
DelegationReceiptsTest,
EVMParametersTest,
SubcallRoundRootTest,
EthereumTxTest,
}, WithCustomFixture(EVMRuntimeFixture))

// C10lEVMRuntime is the c10l-evm runtime test.
Expand All @@ -70,6 +71,7 @@ var (
C10lEVMRNGTest,
C10lEVMMessageSigningTest,
EVMParametersTest,
EthereumTxTest,
}, WithCustomFixture(EVMRuntimeFixture))

// SimpleContractsRuntime is the simple-contracts runtime test.
Expand Down

0 comments on commit 5bec25f

Please sign in to comment.