Skip to content

Commit

Permalink
core: Basic validation of EIP-4844 transactions (#992)
Browse files Browse the repository at this point in the history
* Add excess_data_gas to BlockHeader

* Start updating interfaces

* Fix compilation

* Update 3.21.4 interfaces

* Simplify generate_grpc.cmake

* Update proto again

* Excess data gas in remote client

* Update 3.21.4 again

* post-merge fix

* Add EIP-4844 transaction type

* Basic transaction validation

* Add max_fee_per_data_gas to Transaction

* calc_excess_data_gas

* Turn some EngineBase methods into free-standing functions

* Gas accounting part 1

* Clearer constants

* Gas accounting part 2

* Extract up_front_gas_cost()

* Stricter assert

* Gas accounting part 3

* up_front_gas_cost -> maximum_gas_cost
  • Loading branch information
yperbasis authored Apr 19, 2023
1 parent 6092aba commit 9589977
Show file tree
Hide file tree
Showing 22 changed files with 405 additions and 274 deletions.
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

0 comments on commit 9589977

Please sign in to comment.