Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions test/integration/evmone-cli/t8n/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ set_tests_properties(
add_test(
NAME ${PREFIX}/${TEST_CASE}/trace-0.jsonl
COMMAND ${CMAKE_COMMAND} -E cat
${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/trace-0-0x03141f8608bc78c8b8fb23275febeb6bf40bd348bd68b703a92bf2ce8ac8bdc0.jsonl
${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/trace-0-0x2ea18c1067829310d4eae2d89bbd2200d2f525d529484d46f53d5b42a53260f8.jsonl
)
set_tests_properties(
${PREFIX}/${TEST_CASE}/trace-0.jsonl PROPERTIES
Expand Down Expand Up @@ -319,7 +319,7 @@ set_tests_properties(
# The whole encoding: an empty transaction list is hex too, and is what a run which never read
# --input.txs writes.
PASS_REGULAR_EXPRESSION
"^0xf84ef84c8032830186a08080801ba0468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665aea05cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"^0xf84ef84c8032830186a08080801ba0118e6dc7beddd8c4fe1c2daa896c030f7cf7983b244113bf358d239f615ae084a02eee90a3198ef1d59e390db04dd385bd1cd27bddcacbf843c17a7dd46bc9700b"
)


Expand Down
4 changes: 2 additions & 2 deletions test/integration/evmone-cli/t8n/cancun_create_tx/txs.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x5b3bfaf21a50f2d82a624d2220b89f38631e0aa30be189a5aa0e4c63a3b9fa3d",
"s": "0x7001087f3f261f606995bea6483b8aec67b8c654213443904750b446723fa35d"
},
{
"input": "0x00",
Expand Down
4 changes: 2 additions & 2 deletions test/integration/evmone-cli/t8n/osaka_blob_schedule/txs.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x118e6dc7beddd8c4fe1c2daa896c030f7cf7983b244113bf358d239f615ae084",
"s": "0x2eee90a3198ef1d59e390db04dd385bd1cd27bddcacbf843c17a7dd46bc9700b"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x118e6dc7beddd8c4fe1c2daa896c030f7cf7983b244113bf358d239f615ae084",
"s": "0x2eee90a3198ef1d59e390db04dd385bd1cd27bddcacbf843c17a7dd46bc9700b"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x118e6dc7beddd8c4fe1c2daa896c030f7cf7983b244113bf358d239f615ae084",
"s": "0x2eee90a3198ef1d59e390db04dd385bd1cd27bddcacbf843c17a7dd46bc9700b"
}
]
5 changes: 5 additions & 0 deletions test/state/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ std::optional<address> recover_sender(const Transaction& tx, bytes_view txbytes)
{
// The signing preimage is the transaction's encoding without the trailing (v, r, s).
const auto typed = tx.type != Transaction::Type::legacy;

// The decoder bounds v, but a transaction built from JSON carries whatever v the input names.
if (typed ? tx.v > 1 : (tx.v != 27 && tx.v != 28 && tx.v < 35))
return std::nullopt;

auto envelope = txbytes.substr(typed ? 1 : 0); // Skip the EIP-2718 type byte.
bytes_view payload;
[[maybe_unused]] const auto is_list = rlp::take_list_payload(envelope, payload);
Expand Down
7 changes: 4 additions & 3 deletions test/state/transaction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ struct Transaction
/// Handles the legacy RLP list and the EIP-2718 typed envelope (type byte followed by an RLP list).
[[nodiscard]] std::optional<Transaction> decode_transaction(bytes_view data) noexcept;

/// Recovers the sender (the signer) of the transaction @p tx decoded from @p txbytes,
/// Recovers the sender (the signer) of the transaction @p tx serialized as @p txbytes,
/// or std::nullopt if the signature is invalid.
///
/// The serialization is needed as well because the signing preimage is a slice of it; @p tx must
/// be what decode_transaction(@p txbytes) returned.
/// be what decode_transaction(@p txbytes) would return.
///
/// The recovery is strict at every revision: r, s in [1, secp256k1n) and low s (EIP-2).
/// The recovery is strict at every revision: v valid for its type, r, s in [1, secp256k1n)
/// and low s (EIP-2).
[[nodiscard]] std::optional<address> recover_sender(
const Transaction& tx, bytes_view txbytes) noexcept;

Expand Down
135 changes: 111 additions & 24 deletions test/unittests/tooling_t8n_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ constexpr auto ALLOC_JSON = R"({

// Single legacy CREATE transaction; init code is `PUSH1 0x01 PUSH0 RETURN`,
// which deploys a one-byte runtime `0x00`. Three opcodes => three trace lines.
// Matches test/integration/evmone-cli/t8n/cancun_create_tx/txs.json[0]; the tx hash is
// well-known and used below.
// Matches test/integration/evmone-cli/t8n/cancun_create_tx/txs.json[0]. Signed by the account
// ALLOC_JSON funds; t8n ignores `sender`.
constexpr auto TX_JSON = R"([{
"to": null,
"input": "0x60015ff3",
Expand All @@ -51,8 +51,8 @@ constexpr auto TX_JSON = R"([{
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x5b3bfaf21a50f2d82a624d2220b89f38631e0aa30be189a5aa0e4c63a3b9fa3d",
"s": "0x7001087f3f261f606995bea6483b8aec67b8c654213443904750b446723fa35d"
}])";

/// Runs t8n over the given pre-state and transactions, and returns the result JSON.
Expand All @@ -77,8 +77,7 @@ std::string run_t8n(std::string_view alloc_json, std::string_view txs_json, evmc
return out_result.str();
}

/// Legacy transaction calling CALLEE. t8n takes `sender` from the JSON and only checks `hash`
/// when present, so the signature is never recovered.
/// Legacy transaction calling CALLEE, signed like TX_JSON.
constexpr auto TX_TO_CALLEE = R"([{
"to": "0x000000000000000000000000000000000000c0de",
"input": "0x",
Expand All @@ -88,11 +87,22 @@ constexpr auto TX_TO_CALLEE = R"([{
"gasPrice": "0x32",
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"v": "0x1c",
"r": "0x1059a4656fdf5558d07e5e1e0734e9002f4946f727b45aa52907f76f220735ce",
"s": "0x7bf197eb7303fde325d7c24dda50315f155814df4716e1e611ae0e3460231045"
}])";

/// Runs t8n over ALLOC_JSON and returns the error of the one transaction it must reject.
std::string rejection(std::string_view txs_json, evmc_revision rev)
{
const auto result = json::parse(run_t8n(ALLOC_JSON, txs_json, rev));
EXPECT_EQ(result.at("receipts"), json::array());
EXPECT_EQ(result.at("rejected").size(), 1u);
if (result.at("rejected").empty())
return {};
return result.at("rejected")[0].at("error");
}

/// Runs TX_TO_CALLEE against a callee deployed with the given code.
std::string run_call_to(std::string_view callee_code, evmc_revision rev)
{
Expand Down Expand Up @@ -196,8 +206,8 @@ TEST(tooling_t8n, blob_transaction_creating_a_contract_is_rejected)
"nonce": "0x0",
"value": "0x0",
"v": "0x0",
"r": "0xfc12b67159a3567f8bdbc49e0be369a2e20e09d57a51c41310543a4128409464",
"s": "0x2de0cfe5495c4f58ff60645ceda0afd67a4c90a70bc89fe207269435b35e5b67",
"r": "0x78ce8eb2b34bb6cce4cedfb5249c29467270c41450fa4a2439797804292cfba6",
"s": "0x5437b757e8094f62ca2a3e3a8696cfbd531620bc3c337368db36d0829e073004",
"maxFeePerGas": "0x32",
"maxPriorityFeePerGas": "0x2",
"maxFeePerBlobGas": "0xa",
Expand All @@ -207,11 +217,86 @@ TEST(tooling_t8n, blob_transaction_creating_a_contract_is_rejected)
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b"
}])";

const auto result = json::parse(run_t8n(ALLOC_JSON, BLOB_CREATE_TX, EVMC_CANCUN));
EXPECT_EQ(result.at("receipts"), json::array());
ASSERT_EQ(result.at("rejected").size(), 1u);
EXPECT_EQ(
result.at("rejected")[0].at("error"), "TransactionException.TYPE_3_TX_CONTRACT_CREATION");
rejection(BLOB_CREATE_TX, EVMC_CANCUN), "TransactionException.TYPE_3_TX_CONTRACT_CREATION");
}

TEST(tooling_t8n, unrecoverable_signature_is_rejected)
{
// r = 5 is in range yet no curve point has x = 5 (5**3 + 7 is a quadratic non-residue), so
// only the recovery fails. `sender` names the funded account, so taking it instead of the
// signature would execute the transaction.
static constexpr auto UNRECOVERABLE_TX = R"([{
"to": "0x000000000000000000000000000000000000c0de",
"input": "0x",
"gas": "0x186a0",
"nonce": "0x0",
"value": "0x1",
"gasPrice": "0x32",
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x5",
"s": "0x1"
}])";

EXPECT_EQ(
rejection(UNRECOVERABLE_TX, EVMC_SHANGHAI), "TransactionException.INVALID_SIGNATURE_VRS");
}

TEST(tooling_t8n, transaction_with_an_impossible_v_is_rejected)
{
// 34 is neither 27, 28 nor EIP-155's 35+; 2 is not a y parity.
static constexpr auto LEGACY_BAD_V = R"([{
"to": "0x000000000000000000000000000000000000c0de",
"input": "0x",
"gas": "0x186a0",
"nonce": "0x0",
"value": "0x0",
"gasPrice": "0x32",
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x22",
"r": "0x1",
"s": "0x1"
}])";
static constexpr auto TYPED_BAD_V = R"([{
"to": "0x000000000000000000000000000000000000c0de",
"input": "0x",
"gas": "0x186a0",
"nonce": "0x0",
"value": "0x0",
"maxFeePerGas": "0x32",
"maxPriorityFeePerGas": "0x2",
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x2",
"r": "0x1",
"s": "0x1"
}])";

EXPECT_EQ(rejection(LEGACY_BAD_V, EVMC_SHANGHAI), "TransactionException.INVALID_SIGNATURE_VRS");
EXPECT_EQ(rejection(TYPED_BAD_V, EVMC_SHANGHAI), "TransactionException.INVALID_SIGNATURE_VRS");
}

TEST(tooling_t8n, transaction_signed_for_another_chain_is_rejected)
{
// Signed for chain 2 (v = 35 + 2 * 2 + 0), run on chain 1.
static constexpr auto TX_FOR_CHAIN_2 = R"([{
"to": null,
"input": "0x60015ff3",
"gas": "0x186a0",
"nonce": "0x0",
"value": "0x0",
"gasPrice": "0x32",
"chainId": "0x2",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x27",
"r": "0xf38f7d563c747b90705401b348a87bc0fbd94def6b31675f09ef74539504050a",
"s": "0x4c6b5cce6e4a4cd2c70e5d41dffab7a9c067f292b4c9b827487ed04ba7d175c6"
}])";

EXPECT_EQ(rejection(TX_FOR_CHAIN_2, EVMC_SHANGHAI), "TransactionException.INVALID_CHAINID");
}

TEST(tooling_t8n, a_block_requesting_nothing_reports_the_empty_requests_hash)
Expand Down Expand Up @@ -372,8 +457,8 @@ TEST(tooling_t8n, mismatched_tx_hash_throws)
"chainId": "0x1",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc",
"r": "0x5b3bfaf21a50f2d82a624d2220b89f38631e0aa30be189a5aa0e4c63a3b9fa3d",
"s": "0x7001087f3f261f606995bea6483b8aec67b8c654213443904750b446723fa35d",
"hash": "0xdeadbeef00000000000000000000000000000000000000000000000000000000"
}])";

Expand Down Expand Up @@ -408,8 +493,8 @@ TEST(tooling_t8n, max_chain_id)
"chainId": "0xffffffffffffffff",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0x1b",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x5b3bfaf21a50f2d82a624d2220b89f38631e0aa30be189a5aa0e4c63a3b9fa3d",
"s": "0x7001087f3f261f606995bea6483b8aec67b8c654213443904750b446723fa35d"
}])";

std::istringstream env{ENV_JSON};
Expand Down Expand Up @@ -437,18 +522,20 @@ TEST(tooling_t8n, max_v)
// The maximum `v` (uint64 max = 0xffffffffffffffff) must be parsed and executed without
// overflow; regression test for `v` being loaded as `uint8_t`, which threw
// `from_json<uint8_t>: value > 0xFF`.
// Only chain id 0x7fffffffffffffee with y parity 0 produces such a `v`, so the transaction
// is signed for that chain; its gas is 0x186a1 because that preimage signs with parity 0.
static constexpr auto TX_MAX_V = R"([{
"to": null,
"input": "0x60015ff3",
"gas": "0x186a0",
"gas": "0x186a1",
"nonce": "0x0",
"value": "0x0",
"gasPrice": "0x32",
"chainId": "0x1",
"chainId": "0x7fffffffffffffee",
"sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"v": "0xffffffffffffffff",
"r": "0x468a915f087692bb9be503831a3dfef2cf9c8dee26deb40ff2ec99e8d22665ae",
"s": "0x5cedae0810c3851ecd1004bfdbfe6ddc7753c2d665993bb01ce75af7857b13dc"
"r": "0x463a1ed30b21771a94e83a0ded54cce75850152b18dcd13a1a068d31a5599270",
"s": "0x4f1a132cab35ed35cebe2512004572b1e2bede650ae75a3923be03327ae600c2"
}])";

std::istringstream env{ENV_JSON};
Expand All @@ -458,7 +545,7 @@ TEST(tooling_t8n, max_v)

tooling::T8NArgs args;
args.rev = EVMC_SHANGHAI;
args.chain_id = 1;
args.chain_id = 0x7fffffffffffffee;
args.alloc = &alloc;
args.env = &env;
args.txs = &txs;
Expand Down
17 changes: 15 additions & 2 deletions test/utils/block_transition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,21 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state::

for (size_t i = 0; i < txs.size(); ++i)
{
const auto& tx = txs[i];
const auto computed_tx_hash = keccak256(rlp::encode(tx));
auto tx = txs[i];
const auto txbytes = rlp::encode(tx);
const auto computed_tx_hash = keccak256(txbytes);

if (opts.recover_senders)
{
const auto sender = state::recover_sender(tx, txbytes);
if (!sender.has_value())
{
rejected_txs.push_back(
{computed_tx_hash, i, make_error_code(state::INVALID_SIGNATURE)});
continue;
}
tx.sender = *sender;
}

std::optional<StreamRedirect> trace_guard;
if (trace_enabled)
Expand Down
4 changes: 4 additions & 0 deletions test/utils/block_transition.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ struct BlockTransitionOptions
/// (t8n pre-state-only mode). The transaction loop and finalization still run.
bool skip_system_calls = false;

/// Recover each transaction's sender from its signature instead of taking the one the
/// input names; a signature recovering nothing rejects the transaction.
bool recover_senders = true;

/// Called once per transaction (just before execution) to obtain a per-tx
/// trace sink; std::clog is redirected to the returned stream for the
/// duration of that transaction. Unset = tracing disabled.
Expand Down
7 changes: 4 additions & 3 deletions test/utils/blockchaintest_runner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& r
const auto& pre_state = parent_data_it->second.post_state;

auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev,
blob_gas_limit, {.block_reward = mining_reward(rev)});
blob_gas_limit, {.block_reward = mining_reward(rev), .recover_senders = false});

if (res.requests_error)
{
Expand Down Expand Up @@ -379,8 +379,9 @@ void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& r
const auto sender_not_recovered = contains_any(
test_block.expected_exception, "TransactionException.INVALID_SIGNATURE_VRS");

const auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions,
rev, blob_gas_limit, {.block_reward = mining_reward(rev)});
const auto res =
apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev,
blob_gas_limit, {.block_reward = mining_reward(rev), .recover_senders = false});
if (!res.rejected.empty())
{
// A transaction was rejected: the fixture must name that reason, not merely
Expand Down
11 changes: 8 additions & 3 deletions test/utils/t8n.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,21 @@ void t8n(evmc::VM& vm, const T8NArgs& args)
j_result["receipts"] = JSON::array();
j_result["rejected"] = JSON::array();

// Parse the transactions, assign the chain ID and validate any provided hash. A non-array
// `txs` value yields zero transactions but still produces a full, finalized block result.
// Parse the transactions and validate any provided hash. A non-array `txs` value yields
// zero transactions but still produces a full, finalized block result.
std::vector<state::Transaction> txs;
if (j_txs.is_array())
{
txs.reserve(j_txs.size());
for (const auto& j_tx : j_txs)
{
auto tx = from_json<state::Transaction>(j_tx);
tx.chain_id = args.chain_id;

// A legacy transaction's chain id is the one its v encodes (EIP-155), as
// decode_transaction() reads it; taking --state.chainid made the chain id
// check vacuous.
if (tx.type == state::Transaction::Type::legacy)
tx.chain_id = tx.v >= 35 ? (tx.v - 35) / 2 : 0;

if (const auto loaded_tx_hash = load_optional<hash256>(j_tx, "hash"))
{
Expand Down