Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Basic validation of EIP-4844 transactions #992

Merged
merged 27 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
989e6b6
Add excess_data_gas to BlockHeader
yperbasis Apr 7, 2023
18a3715
Start updating interfaces
yperbasis Apr 7, 2023
238c5b5
Merge branch 'master' into eip-4844
yperbasis Apr 7, 2023
4c5514d
Fix compilation
yperbasis Apr 10, 2023
3c47f20
Merge branch 'master' into eip-4844
yperbasis Apr 10, 2023
7939e85
Update 3.21.4 interfaces
yperbasis Apr 10, 2023
85ba111
Simplify generate_grpc.cmake
yperbasis Apr 10, 2023
2d57589
Update proto again
yperbasis Apr 10, 2023
529f136
Excess data gas in remote client
yperbasis Apr 10, 2023
2e43e3b
Update 3.21.4 again
yperbasis Apr 10, 2023
3ea570d
Merge branch 'master' into eip-4844
yperbasis Apr 11, 2023
804ef74
Merge branch 'master' into eip-4844
yperbasis Apr 13, 2023
8d26902
post-merge fix
yperbasis Apr 13, 2023
b24454c
Add EIP-4844 transaction type
yperbasis Apr 16, 2023
032f21f
Merge branch 'master' into eip-4844
yperbasis Apr 16, 2023
492e53f
Basic transaction validation
yperbasis Apr 16, 2023
7d58cdb
Add max_fee_per_data_gas to Transaction
yperbasis Apr 16, 2023
bf7eec4
calc_excess_data_gas
yperbasis Apr 16, 2023
16b09e1
Turn some EngineBase methods into free-standing functions
yperbasis Apr 16, 2023
10883e6
Gas accounting part 1
yperbasis Apr 16, 2023
63eba31
Clearer constants
yperbasis Apr 17, 2023
bae8d7e
Gas accounting part 2
yperbasis Apr 17, 2023
d21a641
Merge branch 'master' into eip-4844
yperbasis Apr 18, 2023
579fcea
Extract up_front_gas_cost()
yperbasis Apr 18, 2023
e1aa500
Stricter assert
yperbasis Apr 18, 2023
f3af746
Gas accounting part 3
yperbasis Apr 18, 2023
1f020c5
up_front_gas_cost -> maximum_gas_cost
yperbasis Apr 18, 2023
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
15 changes: 2 additions & 13 deletions cmd/test/consensus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ RunResults transaction_test(const nlohmann::json& j) {
txn.from.reset();

if (ValidationResult err{
pre_validate_transaction(txn, rev, config.chain_id, /*base_fee_per_gas=*/std::nullopt)};
pre_validate_transaction(txn, rev, config.chain_id, /*base_fee_per_gas=*/std::nullopt,
/*data_gas_price=*/std::nullopt)};
err != ValidationResult::kOk) {
if (should_be_valid) {
std::cout << "Validation error " << magic_enum::enum_name<ValidationResult>(err) << std::endl;
Expand All @@ -609,18 +610,6 @@ RunResults transaction_test(const nlohmann::json& j) {
}
}

const intx::uint512 max_gas_cost{intx::umul(intx::uint256{txn.gas_limit}, txn.max_fee_per_gas)};
// A corollary check of the following assertion from EIP-1559:
// signer.balance >= transaction.gas_limit * transaction.max_fee_per_gas
if (intx::count_significant_bytes(max_gas_cost) > 32) {
if (should_be_valid) {
std::cout << "gas_limit * max_fee_per_gas overflow\n";
return Status::kFailed;
} else {
continue;
}
}

txn.recover_sender();
if (should_be_valid && !txn.from.has_value()) {
std::cout << "Failed to recover sender" << std::endl;
Expand Down
8 changes: 8 additions & 0 deletions silkworm/core/chain/protocol_param.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ namespace param {
inline constexpr uint64_t kBaseFeeMaxChangeDenominator{8};
inline constexpr uint64_t kElasticityMultiplier{2};

// EIP-4844: Shard Blob Transactions
inline constexpr uint8_t kBlobCommitmentVersionKzg{1};
inline constexpr uint64_t kMaxDataGasPerBlock{1u << 19};
inline constexpr uint64_t kTargetDataGasPerBlock{1u << 18};
inline constexpr uint64_t kDataGasPerBlob{1u << 17};
inline constexpr uint64_t kMinDataGasPrice{1};
inline constexpr uint64_t kDataGasPriceUpdateFraction{2225652};

} // namespace param

} // namespace silkworm
62 changes: 48 additions & 14 deletions silkworm/core/consensus/base/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace silkworm::consensus {

ValidationResult EngineBase::pre_validate_block_body(const Block& block, const BlockState& state) {
const BlockHeader& header{block.header};
const evmc_revision rev{chain_config_.revision(header.number, header.timestamp)};

const evmc::bytes32 txn_root{compute_transaction_root(block)};
if (txn_root != header.transactions_root) {
Expand All @@ -36,7 +37,6 @@ ValidationResult EngineBase::pre_validate_block_body(const Block& block, const B
return err;
}

const evmc_revision rev{chain_config_.revision(header.number, header.timestamp)};
if (rev < EVMC_SHANGHAI && block.withdrawals) {
return ValidationResult::kUnexpectedWithdrawals;
}
Expand All @@ -53,6 +53,23 @@ ValidationResult EngineBase::pre_validate_block_body(const Block& block, const B
}
}

size_t total_blobs{0};
for (const Transaction& tx : block.transactions) {
total_blobs += tx.blob_versioned_hashes.size();
}
if (total_blobs > param::kMaxDataGasPerBlock / param::kDataGasPerBlob) {
return ValidationResult::kTooManyBlobs;
}

const std::optional<BlockHeader> parent{get_parent_header(state, header)};
if (!parent) {
return ValidationResult::kUnknownParent;
}

if (header.excess_data_gas != calc_excess_data_gas(*parent, total_blobs, rev)) {
return ValidationResult::kWrongExcessDataGas;
}

if (block.ommers.empty()) {
return header.ommers_hash == kEmptyListHash ? ValidationResult::kOk : ValidationResult::kWrongOmmersHash;
} else if (prohibit_ommers_) {
Expand All @@ -71,9 +88,11 @@ ValidationResult EngineBase::pre_validate_transactions(const Block& block) {
const BlockHeader& header{block.header};

const evmc_revision rev{chain_config_.revision(header.number, header.timestamp)};
const std::optional<intx::uint256> data_gas_price{header.data_gas_price()};
for (const Transaction& txn : block.transactions) {
if (ValidationResult err{pre_validate_transaction(txn, rev, chain_config_.chain_id, header.base_fee_per_gas)};
err != ValidationResult::kOk) {
ValidationResult err{pre_validate_transaction(txn, rev, chain_config_.chain_id,
header.base_fee_per_gas, data_gas_price)};
if (err != ValidationResult::kOk) {
return err;
}
}
Expand Down Expand Up @@ -176,11 +195,12 @@ ValidationResult EngineBase::validate_block_header(const BlockHeader& header, co
}
}

if (header.base_fee_per_gas != expected_base_fee_per_gas(header, parent.value())) {
const evmc_revision rev{chain_config_.revision(header.number, header.timestamp)};

if (header.base_fee_per_gas != expected_base_fee_per_gas(*parent, rev)) {
return ValidationResult::kWrongBaseFee;
}

const evmc_revision rev{chain_config_.revision(header.number, header.timestamp)};
if (rev < EVMC_SHANGHAI && header.withdrawals_root) {
return ValidationResult::kUnexpectedWithdrawals;
}
Expand Down Expand Up @@ -227,20 +247,17 @@ bool EngineBase::is_kin(const BlockHeader& branch_header, const BlockHeader& mai

evmc::address EngineBase::get_beneficiary(const BlockHeader& header) { return header.beneficiary; }

std::optional<intx::uint256> EngineBase::expected_base_fee_per_gas(const BlockHeader& header,
const BlockHeader& parent) {
if (chain_config_.revision(header.number, header.timestamp) < EVMC_LONDON) {
std::optional<intx::uint256> expected_base_fee_per_gas(const BlockHeader& parent, const evmc_revision rev) {
if (rev < EVMC_LONDON) {
return std::nullopt;
}

if (header.number == chain_config_.london_block) {
if (!parent.base_fee_per_gas) {
return param::kInitialBaseFee;
}

const uint64_t parent_gas_target{parent.gas_limit / param::kElasticityMultiplier};

assert(parent.base_fee_per_gas.has_value());
const intx::uint256 parent_base_fee_per_gas{parent.base_fee_per_gas.value()};
const intx::uint256& parent_base_fee_per_gas{*parent.base_fee_per_gas};

if (parent.gas_used == parent_gas_target) {
return parent_base_fee_per_gas;
Expand All @@ -266,14 +283,31 @@ std::optional<intx::uint256> EngineBase::expected_base_fee_per_gas(const BlockHe
}
}

evmc::bytes32 EngineBase::compute_transaction_root(const BlockBody& body) {
std::optional<intx::uint256> calc_excess_data_gas(const BlockHeader& parent,
std::size_t num_blobs,
const evmc_revision rev) {
if (rev < EVMC_CANCUN) {
return std::nullopt;
}

const uint64_t consumed_data_gas{num_blobs * param::kDataGasPerBlob};
const intx::uint256 parent_excess_data_gas{parent.excess_data_gas.value_or(0)};

if (parent_excess_data_gas + consumed_data_gas < param::kTargetDataGasPerBlock) {
return 0;
} else {
return parent_excess_data_gas + consumed_data_gas - param::kTargetDataGasPerBlock;
}
}

evmc::bytes32 compute_transaction_root(const BlockBody& body) {
static constexpr auto kEncoder = [](Bytes& to, const Transaction& txn) {
rlp::encode(to, txn, /*for_signing=*/false, /*wrap_eip2718_into_string=*/false);
};
return trie::root_hash(body.transactions, kEncoder);
}

evmc::bytes32 EngineBase::compute_ommers_hash(const BlockBody& body) {
evmc::bytes32 compute_ommers_hash(const BlockBody& body) {
if (body.ommers.empty()) {
return kEmptyListHash;
}
Expand Down
22 changes: 13 additions & 9 deletions silkworm/core/consensus/base/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,9 @@ class EngineBase : public IEngine {
//! \brief Validates the difficulty of the header
virtual ValidationResult validate_difficulty(const BlockHeader& header, const BlockHeader& parent) = 0;

//! \brief See https://eips.ethereum.org/EIPS/eip-1559
std::optional<intx::uint256> expected_base_fee_per_gas(const BlockHeader& header, const BlockHeader& parent);

//! \brief Returns parent header (if any) of provided header
static std::optional<BlockHeader> get_parent_header(const BlockState& state, const BlockHeader& header);

//! \brief Calculate the transaction root of a block body
static evmc::bytes32 compute_transaction_root(const BlockBody& body);

//! \brief Calculate the hash of ommers of a block body
static evmc::bytes32 compute_ommers_hash(const BlockBody& body);

protected:
const ChainConfig& chain_config_;
bool prohibit_ommers_{false};
Expand All @@ -80,4 +71,17 @@ class EngineBase : public IEngine {
std::vector<BlockHeader>& old_ommers);
};

//! \see EIP-1559: Fee market change for ETH 1.0 chain
std::optional<intx::uint256> expected_base_fee_per_gas(const BlockHeader& parent, const evmc_revision);

//! \see EIP-4844: Shard Blob Transactions
std::optional<intx::uint256> calc_excess_data_gas(const BlockHeader& parent, std::size_t num_blobs,
const evmc_revision);

//! \brief Calculate the transaction root of a block body
evmc::bytes32 compute_transaction_root(const BlockBody& body);

//! \brief Calculate the hash of ommers of a block body
evmc::bytes32 compute_ommers_hash(const BlockBody& body);

} // namespace silkworm::consensus
48 changes: 38 additions & 10 deletions silkworm/core/consensus/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,27 @@

namespace silkworm::consensus {

bool transaction_type_is_supported(Transaction::Type type, evmc_revision rev) {
static constexpr evmc_revision kMinRevisionByType[]{
EVMC_FRONTIER, // kLegacy
EVMC_BERLIN, // kEip2930
EVMC_LONDON, // kEip1559
EVMC_CANCUN, // kEip4844
};
const auto n{static_cast<std::size_t>(type)};
return n < std::size(kMinRevisionByType) && rev >= kMinRevisionByType[n];
}

ValidationResult pre_validate_transaction(const Transaction& txn, const evmc_revision rev, const uint64_t chain_id,
const std::optional<intx::uint256>& base_fee_per_gas) {
const std::optional<intx::uint256>& base_fee_per_gas,
const std::optional<intx::uint256>& data_gas_price) {
if (txn.chain_id.has_value()) {
if (rev < EVMC_SPURIOUS_DRAGON || txn.chain_id.value() != chain_id) {
return ValidationResult::kWrongChainId;
}
}

if (txn.type == Transaction::Type::kEip2930) {
if (rev < EVMC_BERLIN) {
return ValidationResult::kUnsupportedTransactionType;
}
} else if (txn.type == Transaction::Type::kEip1559) {
if (rev < EVMC_LONDON) {
return ValidationResult::kUnsupportedTransactionType;
}
} else if (txn.type != Transaction::Type::kLegacy) {
if (!transaction_type_is_supported(txn.type, rev)) {
return ValidationResult::kUnsupportedTransactionType;
}

Expand All @@ -69,6 +73,10 @@ ValidationResult pre_validate_transaction(const Transaction& txn, const evmc_rev
return ValidationResult::kIntrinsicGas;
}

if (intx::count_significant_bytes(txn.maximum_gas_cost()) > 32) {
return ValidationResult::kInsufficientFunds;
}

// EIP-2681: Limit account nonce to 2^64-1
if (txn.nonce >= UINT64_MAX) {
return ValidationResult::kNonceTooHigh;
Expand All @@ -80,6 +88,26 @@ ValidationResult pre_validate_transaction(const Transaction& txn, const evmc_rev
return ValidationResult::kMaxInitCodeSizeExceeded;
}

// EIP-4844: Shard Blob Transactions
if (txn.type == Transaction::Type::kEip4844) {
if (txn.blob_versioned_hashes.empty()) {
return ValidationResult::kNoBlobs;
}
for (const Hash& h : txn.blob_versioned_hashes) {
if (h.bytes[0] != param::kBlobCommitmentVersionKzg) {
return ValidationResult::kWrongBlobCommitmentVersion;
}
}
SILKWORM_ASSERT(txn.max_fee_per_data_gas);
SILKWORM_ASSERT(data_gas_price);
if (txn.max_fee_per_data_gas < data_gas_price) {
return ValidationResult::kMaxFeePerDataGasTooLow;
}
// TODO(yperbasis): There is an equal amount of versioned hashes, kzg commitments and blobs.
// The KZG commitments hash to the versioned hashes, i.e. kzg_to_versioned_hash(kzg[i]) == versioned_hash[i]
// The KZG commitments match the blob contents.
}

return ValidationResult::kOk;
}

Expand Down
6 changes: 4 additions & 2 deletions silkworm/core/consensus/engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ class IEngine {
virtual evmc::address get_beneficiary(const BlockHeader& header) = 0;
};

bool transaction_type_is_supported(Transaction::Type, evmc_revision);

//! \brief Performs validation of a transaction that can be done prior to sender recovery and block execution.
//! \return Any of kIntrinsicGas, kInvalidSignature, kWrongChainId, kUnsupportedTransactionType, or kOk.
//! \remarks Should sender of transaction not yet recovered a check on signature's validity is performed
//! \remarks These function is agnostic to whole block validity
ValidationResult pre_validate_transaction(const Transaction& txn, evmc_revision revision, uint64_t chain_id,
const std::optional<intx::uint256>& base_fee_per_gas);
const std::optional<intx::uint256>& base_fee_per_gas,
const std::optional<intx::uint256>& data_gas_price);

//! \brief Creates an instance of proper Consensus Engine on behalf of chain configuration
std::unique_ptr<IEngine> engine_factory(const ChainConfig& chain_config);
Expand Down
34 changes: 18 additions & 16 deletions silkworm/core/consensus/engine_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,65 +48,67 @@ TEST_CASE("Consensus Engine Seal") {

TEST_CASE("Validate transaction types") {
const std::optional<intx::uint256> base_fee_per_gas{std::nullopt};
const std::optional<intx::uint256> data_gas_price{std::nullopt};

Transaction txn;
txn.type = Transaction::Type::kLegacy;
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);

txn.type = static_cast<Transaction::Type>(0x03); // unsupported transaction type
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);

txn.type = Transaction::Type::kEip2930;
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);

txn.type = Transaction::Type::kEip1559;
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_ISTANBUL, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_BERLIN, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kUnsupportedTransactionType);
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kUnsupportedTransactionType);
}

TEST_CASE("Validate max_fee_per_gas") {
const std::optional<intx::uint256> base_fee_per_gas{1'000'000'000};
const std::optional<intx::uint256> data_gas_price{std::nullopt};

Transaction txn;
txn.type = Transaction::Type::kEip1559;

txn.max_priority_fee_per_gas = 500'000'000;
txn.max_fee_per_gas = 700'000'000;
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kMaxFeeLessThanBase);

txn.max_priority_fee_per_gas = 3'000'000'000;
txn.max_fee_per_gas = 2'000'000'000;
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) ==
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) ==
ValidationResult::kMaxPriorityFeeGreaterThanMax);

txn.max_priority_fee_per_gas = 2'000'000'000;
txn.max_fee_per_gas = 2'000'000'000;
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kMaxPriorityFeeGreaterThanMax);

txn.max_priority_fee_per_gas = 1'000'000'000;
txn.max_fee_per_gas = 2'000'000'000;
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas) !=
CHECK(pre_validate_transaction(txn, EVMC_LONDON, 1, base_fee_per_gas, data_gas_price) !=
ValidationResult::kMaxPriorityFeeGreaterThanMax);
}

Expand Down
2 changes: 1 addition & 1 deletion silkworm/core/consensus/pos/engine_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ TEST_CASE("Proof-of-Stake consensus engine") {
EthashEngine ethash_engine{kMainnetConfig};
ProofOfStakeEngine pos_engine{kMainnetConfig};

header.base_fee_per_gas = pos_engine.expected_base_fee_per_gas(header, parent.header);
header.base_fee_per_gas = expected_base_fee_per_gas(parent.header, EVMC_LONDON);

InMemoryState state;
state.insert_block(parent, header.parent_hash);
Expand Down
Loading