mirror of
https://github.com/paradigmxyz/reth.git
synced 2026-04-30 03:01:58 -04:00
Compare commits
27 Commits
main
...
bal-devnet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa03691b6a | ||
|
|
0bd97e5b63 | ||
|
|
fda7636c92 | ||
|
|
82f36c066d | ||
|
|
768977e4cf | ||
|
|
4a8a14e91a | ||
|
|
2fce3e701d | ||
|
|
938313028d | ||
|
|
0790359003 | ||
|
|
089c0e2629 | ||
|
|
bc80e2a66b | ||
|
|
9af8265047 | ||
|
|
f3e30a3111 | ||
|
|
6715a093f1 | ||
|
|
d5bff1d478 | ||
|
|
20141a2ea0 | ||
|
|
57962a1b95 | ||
|
|
60468fe256 | ||
|
|
ce3a171ce0 | ||
|
|
ce7e80ad33 | ||
|
|
0c6a10d3fa | ||
|
|
d41a9a4078 | ||
|
|
b984ddd275 | ||
|
|
b9c330e1a9 | ||
|
|
7b2c458302 | ||
|
|
0722202930 | ||
|
|
8ec6e614f9 |
11
.github/workflows/hive.yml
vendored
11
.github/workflows/hive.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -28,9 +31,9 @@ jobs:
|
||||
secrets: inherit
|
||||
|
||||
prepare-hive:
|
||||
if: github.repository == 'paradigmxyz/reth'
|
||||
if: github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth'
|
||||
timeout-minutes: 45
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && 'depot-ubuntu-latest-16' || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
@@ -201,7 +204,7 @@ jobs:
|
||||
- prepare-hive
|
||||
name: Hive-Amsterdam / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
@@ -381,7 +384,7 @@ jobs:
|
||||
- prepare-hive
|
||||
name: Hive-Osaka / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
|
||||
# Use larger runners for eels tests to avoid OOM runner crashes
|
||||
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
runs-on: ${{ (github.repository == 'paradigmxyz/reth-oss' || github.repository == 'paradigmxyz/reth') && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
362
Cargo.lock
generated
362
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -433,14 +433,14 @@ reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
|
||||
reth-zstd-compressors = { version = "0.3.1", default-features = false }
|
||||
|
||||
# revm
|
||||
revm = { version = "38.0.0", default-features = false }
|
||||
revm-bytecode = { version = "10.0.0", default-features = false }
|
||||
revm-database = { version = "13.0.0", default-features = false }
|
||||
revm-state = { version = "11.0.0", default-features = false }
|
||||
revm-primitives = { version = "23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "35.0.0", default-features = false }
|
||||
revm-database-interface = { version = "11.0.0", default-features = false }
|
||||
revm-inspectors = "0.39.0"
|
||||
revm = { version = "=38.0.0", default-features = false }
|
||||
revm-bytecode = { version = "=10.0.0", default-features = false }
|
||||
revm-database = { version = "=13.0.1", default-features = false }
|
||||
revm-state = { version = "=11.0.1", default-features = false }
|
||||
revm-primitives = { version = "=23.0.0", default-features = false }
|
||||
revm-interpreter = { version = "=35.0.1", default-features = false }
|
||||
revm-database-interface = { version = "=11.0.1", default-features = false }
|
||||
revm-inspectors = "=0.39.0"
|
||||
|
||||
# eth
|
||||
alloy-dyn-abi = "1.5.6"
|
||||
@@ -700,3 +700,24 @@ vergen-git2 = "9.1.0"
|
||||
|
||||
# networking
|
||||
ipnet = "2.11"
|
||||
|
||||
[patch.crates-io]
|
||||
revm = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-context = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-database = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-handler = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-state = { git = "https://github.com/bluealloy/revm", rev = "3ed3bdfed9ad6e5ba37f4e1f015436ab89ca98be" }
|
||||
revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "5eebb56819ee6bec5bfbc69a415276ee1a784fec" }
|
||||
alloy-evm = { git = "https://github.com/alloy-rs/evm", branch = "bal-devnet-4" }
|
||||
reth-codecs = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
|
||||
reth-codecs-derive = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
|
||||
reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
|
||||
reth-rpc-traits = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
|
||||
reth-zstd-compressors = { git = "https://github.com/paradigmxyz/reth-core", rev = "8612239c4f3dda83cc389f577b9eb04f10ebf81d" }
|
||||
|
||||
@@ -191,8 +191,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
|
||||
}
|
||||
};
|
||||
|
||||
let bal= executor.take_bal();
|
||||
|
||||
if let Err(err) = consensus
|
||||
.validate_block_post_execution(&block, &result, None)
|
||||
.validate_block_post_execution(&block, &result, None,bal)
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to validate block {} {}",
|
||||
|
||||
@@ -18,6 +18,7 @@ reth-primitives-traits.workspace = true
|
||||
# ethereum
|
||||
alloy-primitives.workspace = true
|
||||
alloy-consensus.workspace = true
|
||||
alloy-eip7928.workspace = true
|
||||
|
||||
# misc
|
||||
auto_impl.workspace = true
|
||||
@@ -29,10 +30,9 @@ std = [
|
||||
"reth-primitives-traits/std",
|
||||
"alloy-primitives/std",
|
||||
"alloy-consensus/std",
|
||||
"reth-primitives-traits/std",
|
||||
"alloy-eip7928/std",
|
||||
"reth-execution-types/std",
|
||||
"thiserror/std",
|
||||
"alloy-eip7928/std",
|
||||
]
|
||||
test-utils = [
|
||||
"reth-primitives-traits/test-utils",
|
||||
]
|
||||
test-utils = ["reth-primitives-traits/test-utils"]
|
||||
|
||||
@@ -38,6 +38,7 @@ use alloc::{
|
||||
vec::Vec,
|
||||
};
|
||||
use alloy_consensus::Header;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
|
||||
use core::{error::Error, fmt::Display};
|
||||
|
||||
@@ -85,6 +86,7 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
|
||||
block: &RecoveredBlock<N::Block>,
|
||||
result: &BlockExecutionResult<N::Receipt>,
|
||||
receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
block_access_list: Option<BlockAccessList>,
|
||||
) -> Result<(), ConsensusError>;
|
||||
}
|
||||
|
||||
@@ -474,6 +476,12 @@ pub enum ConsensusError {
|
||||
/// EIP-7825: Transaction gas limit exceeds maximum allowed
|
||||
#[error(transparent)]
|
||||
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
|
||||
/// Error when an unexpected block access list cost is encountered.
|
||||
#[error("block access list exceeds gas limit")]
|
||||
BlockAccessListExceedsGasLimit,
|
||||
/// Error when the block access list hash doesn't match the expected value.
|
||||
#[error("block access list hash mismatch: {0}")]
|
||||
BlockAccessListHashMismatch(GotExpectedBoxed<B256>),
|
||||
/// Any additional consensus error, for example L2-specific errors.
|
||||
#[error(transparent)]
|
||||
Other(#[from] Arc<dyn Error + Send + Sync>),
|
||||
@@ -519,6 +527,23 @@ impl ConsensusError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the block access list against the gas limit.
|
||||
///
|
||||
/// EIP-7925 specifies that the total cost of the block access list items must not exceed
|
||||
/// the gas limit. Each item costs `ITEM_COST` gas.
|
||||
pub fn validate_block_access_list_gas(
|
||||
block_access_list: Option<&alloy_eip7928::BlockAccessList>,
|
||||
gas_limit: u64,
|
||||
) -> Result<(), ConsensusError> {
|
||||
if let Some(bal) = block_access_list {
|
||||
let bal_items = alloy_eip7928::total_bal_items(bal);
|
||||
if bal_items > gas_limit / alloy_eip7928::ITEM_COST as u64 {
|
||||
return Err(ConsensusError::BlockAccessListExceedsGasLimit)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl From<InvalidTransactionError> for ConsensusError {
|
||||
fn from(value: InvalidTransactionError) -> Self {
|
||||
Self::InvalidTransaction(value)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
|
||||
use alloc::sync::Arc;
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
|
||||
|
||||
@@ -77,6 +78,7 @@ impl<N: NodePrimitives> FullConsensus<N> for NoopConsensus {
|
||||
_block: &RecoveredBlock<N::Block>,
|
||||
_result: &BlockExecutionResult<N::Receipt>,
|
||||
_receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
_block_access_list: Option<BlockAccessList>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
|
||||
use alloy_eip7928::BlockAccessList;
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use reth_execution_types::BlockExecutionResult;
|
||||
use reth_primitives_traits::{Block, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader};
|
||||
@@ -52,6 +53,7 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
|
||||
_block: &RecoveredBlock<N::Block>,
|
||||
_result: &BlockExecutionResult<N::Receipt>,
|
||||
_receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
_block_access_list: Option<BlockAccessList>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
if self.fail_validation() {
|
||||
Err(ConsensusError::BaseFeeMissing)
|
||||
|
||||
@@ -123,6 +123,7 @@ where
|
||||
/// Whether sparse trie cache pruning is fully disabled.
|
||||
disable_sparse_trie_cache_pruning: bool,
|
||||
/// Whether to disable BAL-based parallel execution (falls back to tx-based prewarming).
|
||||
#[allow(unused)]
|
||||
disable_bal_parallel_execution: bool,
|
||||
/// Whether to disable BAL-driven parallel state root computation.
|
||||
disable_bal_parallel_state_root: bool,
|
||||
@@ -272,7 +273,9 @@ where
|
||||
halve_workers,
|
||||
config,
|
||||
);
|
||||
let install_state_hook = env.decoded_bal.is_none();
|
||||
// If no BALs are present or we have them explicitly disabled, we use sparse trie task and
|
||||
// need to send the updates to it via state hook
|
||||
let install_state_hook = env.decoded_bal.is_none() || self.disable_bal_parallel_state_root;
|
||||
let prewarm_handle = self.spawn_caching_with(
|
||||
env,
|
||||
prewarm_rx,
|
||||
@@ -505,14 +508,14 @@ where
|
||||
);
|
||||
{
|
||||
let to_prewarm_task = to_prewarm_task.clone();
|
||||
let disable_bal_parallel_execution = self.disable_bal_parallel_execution;
|
||||
let disable_bal_parallel_state_root = self.disable_bal_parallel_state_root;
|
||||
self.executor.spawn_blocking_named("prewarm", move || {
|
||||
let mode = if skip_prewarm {
|
||||
PrewarmMode::Skipped
|
||||
} else if let Some(decoded_bal) =
|
||||
maybe_decoded_bal.filter(|_| !disable_bal_parallel_execution)
|
||||
let mode = if let Some(decoded_bal) =
|
||||
maybe_decoded_bal.filter(|_| !disable_bal_parallel_state_root)
|
||||
{
|
||||
PrewarmMode::BlockAccessList(decoded_bal)
|
||||
} else if skip_prewarm {
|
||||
PrewarmMode::Skipped
|
||||
} else {
|
||||
PrewarmMode::Transactions(transactions)
|
||||
};
|
||||
@@ -798,7 +801,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
|
||||
|
||||
/// Returns a state hook to stream execution state updates to the sparse trie cache task.
|
||||
///
|
||||
/// Returns `None` when execution should not send state updates, such as BAL-driven execution.
|
||||
/// Returns `None` when BAL-driven hashed state streaming feeds the sparse trie task.
|
||||
pub fn state_hook(&self) -> Option<impl OnStateHook> {
|
||||
self.install_state_hook
|
||||
.then(|| self.state_root_handle.as_ref().map(|handle| handle.state_hook()))
|
||||
@@ -1159,19 +1162,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
let account = revm_state::Account {
|
||||
info: AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
},
|
||||
original_info: Box::new(AccountInfo::default()),
|
||||
storage,
|
||||
status: AccountStatus::Touched,
|
||||
transaction_id: 0,
|
||||
let mut account = revm_state::Account::default();
|
||||
account.info = AccountInfo {
|
||||
balance: U256::from(rng.random::<u64>()),
|
||||
nonce: rng.random::<u64>(),
|
||||
code_hash: KECCAK_EMPTY,
|
||||
code: Some(Default::default()),
|
||||
account_id: None,
|
||||
};
|
||||
account.storage = storage;
|
||||
account.status = AccountStatus::Touched;
|
||||
|
||||
state_update.insert(address, account);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,9 @@ use crate::tree::payload_processor::receipt_root_task::{IndexedReceipt, ReceiptR
|
||||
use reth_chain_state::{
|
||||
CanonicalInMemoryState, DeferredTrieData, ExecutedBlock, ExecutionTimingStats, LazyOverlay,
|
||||
};
|
||||
use reth_consensus::{ConsensusError, FullConsensus, ReceiptRootBloom};
|
||||
use reth_consensus::{
|
||||
validate_block_access_list_gas, ConsensusError, FullConsensus, ReceiptRootBloom,
|
||||
};
|
||||
use reth_engine_primitives::{
|
||||
ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator,
|
||||
};
|
||||
@@ -568,7 +570,7 @@ where
|
||||
// The receipt root task is spawned before execution and receives receipts incrementally
|
||||
// as transactions complete, allowing parallel computation during execution.
|
||||
let execute_block_start = Instant::now();
|
||||
let (output, senders, receipt_root_rx) =
|
||||
let (output, senders, receipt_root_rx, built_bal) =
|
||||
match self.execute_block(state_provider, env, &input, &mut handle) {
|
||||
Ok(output) => output,
|
||||
Err(err) => return self.handle_execution_error(input, err, &parent_block),
|
||||
@@ -650,6 +652,7 @@ where
|
||||
transaction_root,
|
||||
receipt_root_bloom,
|
||||
hashed_state,
|
||||
built_bal
|
||||
),
|
||||
block
|
||||
);
|
||||
@@ -906,6 +909,7 @@ where
|
||||
BlockExecutionOutput<N::Receipt>,
|
||||
Vec<Address>,
|
||||
tokio::sync::oneshot::Receiver<(B256, alloy_primitives::Bloom)>,
|
||||
Option<BlockAccessList>,
|
||||
),
|
||||
InsertBlockErrorKind,
|
||||
>
|
||||
@@ -913,15 +917,29 @@ where
|
||||
S: StateProvider + Send,
|
||||
Err: core::error::Error + Send + Sync + 'static,
|
||||
V: PayloadValidator<T, Block = N::Block>,
|
||||
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
|
||||
T: PayloadTypes<
|
||||
BuiltPayload: BuiltPayload<Primitives = N>,
|
||||
ExecutionData: ExecutionPayload,
|
||||
>,
|
||||
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
|
||||
{
|
||||
debug!(target: "engine::tree::payload_validator", "Executing block");
|
||||
|
||||
if let Some(bal_opt) = input.block_access_list() {
|
||||
let bal = bal_opt.map_err(BlockExecutionError::other)?;
|
||||
validate_block_access_list_gas(Some(&bal), input.gas_limit())
|
||||
.map_err(|e| {
|
||||
debug!(target: "engine::tree::payload_validator", "BAL is invalid since it contains more items than the gas limit allows");
|
||||
InsertBlockErrorKind::Consensus(e)
|
||||
})?
|
||||
}
|
||||
|
||||
let has_bal = input.block_access_list().is_some();
|
||||
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
|
||||
State::builder()
|
||||
.with_database(StateProviderDatabase::new(state_provider))
|
||||
.with_bundle_update()
|
||||
.with_bal_builder_if(has_bal)
|
||||
.build()
|
||||
});
|
||||
|
||||
@@ -980,6 +998,7 @@ where
|
||||
handle.iter_transactions(),
|
||||
&receipt_tx,
|
||||
&executed_tx_index,
|
||||
has_bal,
|
||||
)?;
|
||||
drop(receipt_tx);
|
||||
|
||||
@@ -994,6 +1013,11 @@ where
|
||||
debug_span!(target: "engine::tree", "merge_transitions")
|
||||
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
|
||||
|
||||
// Extract the built bal if payload has bal
|
||||
let built_bal = if has_bal { db.take_built_alloy_bal() } else { None };
|
||||
|
||||
tracing::debug!(has_bal = built_bal.is_some(), "Built BAL");
|
||||
|
||||
let output = BlockExecutionOutput { result, state: db.take_bundle() };
|
||||
|
||||
let execution_duration = execution_start.elapsed();
|
||||
@@ -1001,7 +1025,7 @@ where
|
||||
self.metrics.record_block_execution_gas_bucket(output.result.gas_used, execution_duration);
|
||||
debug!(target: "engine::tree::payload_validator", elapsed = ?execution_duration, "Executed block");
|
||||
|
||||
Ok((output, senders, result_rx))
|
||||
Ok((output, senders, result_rx, built_bal))
|
||||
}
|
||||
|
||||
/// Executes transactions and collects senders, streaming receipts to a background task.
|
||||
@@ -1013,18 +1037,20 @@ where
|
||||
/// - Collecting transaction senders for later use
|
||||
///
|
||||
/// Returns the executor (for finalization) and the collected senders.
|
||||
fn execute_transactions<E, Tx, InnerTx, Err>(
|
||||
fn execute_transactions<'a, E, Tx, InnerTx, Err, DB>(
|
||||
&self,
|
||||
mut executor: E,
|
||||
transaction_count: usize,
|
||||
transactions: impl Iterator<Item = Result<Tx, Err>>,
|
||||
receipt_tx: &crossbeam_channel::Sender<IndexedReceipt<N::Receipt>>,
|
||||
executed_tx_index: &AtomicUsize,
|
||||
has_bal: bool,
|
||||
) -> Result<(E, Vec<Address>), BlockExecutionError>
|
||||
where
|
||||
E: BlockExecutor<Receipt = N::Receipt>,
|
||||
E: BlockExecutor<Receipt = N::Receipt, Evm: alloy_evm::Evm<DB = &'a mut State<DB>>>,
|
||||
Tx: alloy_evm::block::ExecutableTx<E> + alloy_evm::RecoveredTx<InnerTx>,
|
||||
InnerTx: TxHashRef,
|
||||
DB: revm::Database + 'a,
|
||||
Err: core::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut senders = Vec::with_capacity(transaction_count);
|
||||
@@ -1035,6 +1061,11 @@ where
|
||||
.in_scope(|| executor.apply_pre_execution_changes())?;
|
||||
self.metrics.record_pre_execution(pre_exec_start.elapsed());
|
||||
|
||||
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
|
||||
if has_bal {
|
||||
executor.evm_mut().db_mut().bump_bal_index();
|
||||
}
|
||||
|
||||
// Execute transactions
|
||||
let exec_span = debug_span!(target: "engine::tree", "execution").entered();
|
||||
let mut transactions = transactions.into_iter();
|
||||
@@ -1079,6 +1110,10 @@ where
|
||||
let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
|
||||
}
|
||||
}
|
||||
// Bump BAL index after each transaction (EIP-7928)
|
||||
if has_bal {
|
||||
executor.evm_mut().db_mut().bump_bal_index();
|
||||
}
|
||||
}
|
||||
drop(exec_span);
|
||||
|
||||
@@ -1362,6 +1397,7 @@ where
|
||||
transaction_root: Option<B256>,
|
||||
receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
hashed_state: LazyHashedPostState,
|
||||
built_bal: Option<BlockAccessList>,
|
||||
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
|
||||
where
|
||||
V: PayloadValidator<T, Block = N::Block>,
|
||||
@@ -1388,9 +1424,13 @@ where
|
||||
let _enter =
|
||||
debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution")
|
||||
.entered();
|
||||
if let Err(err) =
|
||||
self.consensus.validate_block_post_execution(block, output, receipt_root_bloom)
|
||||
{
|
||||
|
||||
if let Err(err) = self.consensus.validate_block_post_execution(
|
||||
block,
|
||||
output,
|
||||
receipt_root_bloom,
|
||||
built_bal,
|
||||
) {
|
||||
// call post-block hook
|
||||
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
|
||||
return Err(err.into())
|
||||
|
||||
@@ -266,6 +266,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: Bytes::default(),
|
||||
})
|
||||
})
|
||||
@@ -280,6 +281,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
|
||||
};
|
||||
|
||||
@@ -314,6 +316,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
|
||||
})
|
||||
}
|
||||
@@ -331,6 +334,7 @@ mod tests {
|
||||
state_gas_used: 0,
|
||||
reservoir: 0,
|
||||
gas_refunded: 0,
|
||||
refill_amount: 0,
|
||||
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extern crate alloc;
|
||||
|
||||
use alloc::{fmt::Debug, sync::Arc};
|
||||
use alloy_consensus::{constants::MAXIMUM_EXTRA_DATA_SIZE, EMPTY_OMMER_ROOT_HASH};
|
||||
use alloy_eips::eip7840::BlobParams;
|
||||
use alloy_eips::{eip7840::BlobParams, eip7928::BlockAccessList};
|
||||
use reth_chainspec::{EthChainSpec, EthereumHardforks};
|
||||
use reth_consensus::{
|
||||
Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom, TransactionRoot,
|
||||
@@ -108,9 +108,15 @@ where
|
||||
block: &RecoveredBlock<N::Block>,
|
||||
result: &BlockExecutionResult<N::Receipt>,
|
||||
receipt_root_bloom: Option<ReceiptRootBloom>,
|
||||
block_access_list: Option<BlockAccessList>,
|
||||
) -> Result<(), ConsensusError> {
|
||||
let res =
|
||||
validate_block_post_execution(block, &self.chain_spec, result, receipt_root_bloom);
|
||||
let res = validate_block_post_execution(
|
||||
block,
|
||||
&self.chain_spec,
|
||||
result,
|
||||
receipt_root_bloom,
|
||||
block_access_list,
|
||||
);
|
||||
|
||||
if self.skip_requests_hash_check &&
|
||||
let Err(ConsensusError::BodyRequestsHashDiff(_)) = &res
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use alloc::vec::Vec;
|
||||
use alloy_consensus::{proofs::calculate_receipt_root, BlockHeader, TxReceipt};
|
||||
use alloy_eips::Encodable2718;
|
||||
use alloy_eips::{
|
||||
eip7928::{compute_block_access_list_hash, BlockAccessList},
|
||||
Encodable2718,
|
||||
};
|
||||
use alloy_primitives::{Bloom, Bytes, B256};
|
||||
use reth_chainspec::EthereumHardforks;
|
||||
use reth_consensus::ConsensusError;
|
||||
@@ -21,6 +24,7 @@ pub fn validate_block_post_execution<B, R, ChainSpec>(
|
||||
chain_spec: &ChainSpec,
|
||||
result: &BlockExecutionResult<R>,
|
||||
receipt_root_bloom: Option<(B256, Bloom)>,
|
||||
block_access_list: Option<BlockAccessList>,
|
||||
) -> Result<(), ConsensusError>
|
||||
where
|
||||
B: Block,
|
||||
@@ -79,6 +83,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that the block access list hash matches the calculated block access list hash
|
||||
if chain_spec.is_amsterdam_active_at_timestamp(block.header().timestamp()) &&
|
||||
block_access_list.is_some()
|
||||
{
|
||||
let block_bal_hash = block.header().block_access_list_hash().unwrap_or_default();
|
||||
let default_bal = BlockAccessList::default();
|
||||
let block_access_list_hash =
|
||||
compute_block_access_list_hash(block_access_list.as_ref().unwrap_or(&default_bal));
|
||||
if block_access_list_hash != block_bal_hash {
|
||||
return Err(ConsensusError::BlockAccessListHashMismatch(
|
||||
(block_access_list_hash, block_bal_hash).into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ where
|
||||
transactions,
|
||||
output: BlockExecutionResult { receipts, requests, gas_used, blob_gas_used },
|
||||
state_root,
|
||||
block_access_list_hash,
|
||||
..
|
||||
} = input;
|
||||
|
||||
@@ -90,6 +91,12 @@ where
|
||||
};
|
||||
}
|
||||
|
||||
let bal_hash = if self.chain_spec.is_amsterdam_active_at_timestamp(timestamp) {
|
||||
block_access_list_hash
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let header = Header {
|
||||
parent_hash: ctx.parent_hash,
|
||||
ommers_hash: EMPTY_OMMER_ROOT_HASH,
|
||||
@@ -112,8 +119,8 @@ where
|
||||
blob_gas_used: block_blob_gas_used,
|
||||
excess_blob_gas,
|
||||
requests_hash,
|
||||
block_access_list_hash: None,
|
||||
slot_number: None,
|
||||
block_access_list_hash: bal_hash,
|
||||
slot_number: ctx.slot_number,
|
||||
};
|
||||
|
||||
Ok(Block {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
use alloy_consensus::Transaction;
|
||||
use alloy_primitives::U256;
|
||||
use alloy_primitives::{Bytes, U256};
|
||||
use alloy_rlp::Encodable;
|
||||
use alloy_rpc_types_engine::PayloadAttributes as EthPayloadAttributes;
|
||||
use reth_basic_payload_builder::{
|
||||
@@ -446,7 +446,9 @@ where
|
||||
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
|
||||
}
|
||||
|
||||
let BlockBuilderOutcome { execution_result, block, .. } = if let Some(mut handle) = trie_handle
|
||||
let BlockBuilderOutcome { execution_result, block, block_access_list, .. } = if let Some(
|
||||
mut handle,
|
||||
) = trie_handle
|
||||
{
|
||||
// Drop the state hook, which drops the StateHookSender and triggers
|
||||
// FinishedStateUpdates via its Drop impl, signaling the trie task to finalize.
|
||||
@@ -485,8 +487,10 @@ where
|
||||
max_rlp_length: MAX_RLP_BLOCK_SIZE,
|
||||
}));
|
||||
}
|
||||
let block_access_list: Option<Bytes> =
|
||||
block_access_list.map(|block_access_list| alloy_rlp::encode(&block_access_list).into());
|
||||
|
||||
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, None)
|
||||
let payload = EthBuiltPayload::new(sealed_block, total_fees, requests, block_access_list)
|
||||
// add blob sidecars from the executed txs
|
||||
.with_sidecars(blob_sidecars);
|
||||
|
||||
|
||||
@@ -79,4 +79,11 @@ where
|
||||
Self::Right(b) => b.size_hint(),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_bal(&mut self) -> Option<alloy_eips::eip7928::BlockAccessList> {
|
||||
match self {
|
||||
Self::Left(a) => a.take_bal(),
|
||||
Self::Right(b) => b.take_bal(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
use crate::{ConfigureEvm, Database, OnStateHook, TxEnvFor};
|
||||
use alloc::{boxed::Box, sync::Arc, vec::Vec};
|
||||
use alloy_consensus::{BlockHeader, Header};
|
||||
use alloy_eips::eip2718::WithEncoded;
|
||||
use alloy_eips::{
|
||||
eip2718::WithEncoded,
|
||||
eip7928::{compute_block_access_list_hash, BlockAccessList},
|
||||
};
|
||||
pub use alloy_evm::block::{BlockExecutor, BlockExecutorFactory, GasOutput};
|
||||
use alloy_evm::{
|
||||
block::{CommitChanges, ExecutableTxParts},
|
||||
@@ -21,7 +24,10 @@ use reth_primitives_traits::{
|
||||
use reth_storage_api::StateProvider;
|
||||
pub use reth_storage_errors::provider::ProviderError;
|
||||
use reth_trie_common::{updates::TrieUpdates, HashedPostState};
|
||||
use revm::database::{states::bundle_state::BundleRetention, BundleState, State};
|
||||
use revm::{
|
||||
database::{states::bundle_state::BundleRetention, BundleState, State},
|
||||
state::bal::Bal,
|
||||
};
|
||||
|
||||
/// A type that knows how to execute a block. It is assumed to operate on a
|
||||
/// [`crate::Evm`] internally and use [`State`] as database.
|
||||
@@ -145,6 +151,9 @@ pub trait Executor<DB: Database>: Sized {
|
||||
///
|
||||
/// This is used to optimize DB commits depending on the size of the state.
|
||||
fn size_hint(&self) -> usize;
|
||||
|
||||
/// Take built [`BlockAccessList`] from executor
|
||||
fn take_bal(&mut self) -> Option<BlockAccessList>;
|
||||
}
|
||||
|
||||
/// Input for block building. Consumed by [`BlockAssembler`].
|
||||
@@ -162,6 +171,7 @@ pub trait Executor<DB: Database>: Sized {
|
||||
/// - `bundle_state`: Accumulated state changes from all transactions
|
||||
/// - `state_provider`: Access to the current state for additional lookups
|
||||
/// - `state_root`: The calculated state root after all changes
|
||||
/// - `block_access_list_hash`: Block access list hash (EIP-7928, Amsterdam)
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
@@ -178,6 +188,7 @@ pub trait Executor<DB: Database>: Sized {
|
||||
/// bundle_state: &state_changes,
|
||||
/// state_provider: &state,
|
||||
/// state_root: calculated_root,
|
||||
/// block_access_list_hash: Some(calculated_bal_hash),
|
||||
/// };
|
||||
///
|
||||
/// let block = assembler.assemble_block(input)?;
|
||||
@@ -205,6 +216,8 @@ pub struct BlockAssemblerInput<'a, 'b, F: BlockExecutorFactory, H = Header> {
|
||||
pub state_provider: &'b dyn StateProvider,
|
||||
/// State root for this block.
|
||||
pub state_root: B256,
|
||||
/// Block access list hash (EIP-7928, Amsterdam).
|
||||
pub block_access_list_hash: Option<B256>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
|
||||
@@ -222,6 +235,7 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
|
||||
bundle_state: &'a BundleState,
|
||||
state_provider: &'b dyn StateProvider,
|
||||
state_root: B256,
|
||||
block_access_list_hash: Option<B256>,
|
||||
) -> Self {
|
||||
Self {
|
||||
evm_env,
|
||||
@@ -232,6 +246,7 @@ impl<'a, 'b, F: BlockExecutorFactory, H> BlockAssemblerInput<'a, 'b, F, H> {
|
||||
bundle_state,
|
||||
state_provider,
|
||||
state_root,
|
||||
block_access_list_hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,6 +316,8 @@ pub struct BlockBuilderOutcome<N: NodePrimitives> {
|
||||
pub trie_updates: TrieUpdates,
|
||||
/// The built block.
|
||||
pub block: RecoveredBlock<N::Block>,
|
||||
/// Block access list built during execution (EIP-7928, Amsterdam).
|
||||
pub block_access_list: Option<BlockAccessList>,
|
||||
}
|
||||
|
||||
/// A type that knows how to execute and build a block.
|
||||
@@ -453,7 +470,11 @@ where
|
||||
type Executor = Executor;
|
||||
|
||||
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
|
||||
self.executor.apply_pre_execution_changes()
|
||||
self.executor.apply_pre_execution_changes()?;
|
||||
// Bump BAL index after pre-execution changes (EIP-7928: index 0 is pre-execution)
|
||||
self.executor.evm_mut().db_mut().bump_bal_index();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_transaction_with_commit_condition(
|
||||
@@ -466,6 +487,8 @@ where
|
||||
self.executor.execute_transaction_with_commit_condition((tx_env, &tx), f)?
|
||||
{
|
||||
self.transactions.push(tx);
|
||||
// Bump BAL index after each committed transaction (EIP-7928)
|
||||
self.executor.evm_mut().db_mut().bump_bal_index();
|
||||
Ok(Some(gas_used))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -483,6 +506,11 @@ where
|
||||
// merge all transitions into bundle state
|
||||
db.merge_transitions(BundleRetention::Reverts);
|
||||
|
||||
// extract the built block access list (EIP-7928, Amsterdam) and compute its hash
|
||||
let block_access_list = db.take_built_alloy_bal();
|
||||
let block_access_list_hash =
|
||||
block_access_list.as_ref().map(|bal| compute_block_access_list_hash(bal));
|
||||
|
||||
let hashed_state = state.hashed_post_state(&db.bundle_state);
|
||||
let (state_root, trie_updates) = match state_root_precomputed {
|
||||
Some(precomputed) => precomputed,
|
||||
@@ -503,11 +531,18 @@ where
|
||||
bundle_state: &db.bundle_state,
|
||||
state_provider: &state,
|
||||
state_root,
|
||||
block_access_list_hash,
|
||||
})?;
|
||||
|
||||
let block = RecoveredBlock::new_unhashed(block, senders);
|
||||
|
||||
Ok(BlockBuilderOutcome { execution_result: result, hashed_state, trie_updates, block })
|
||||
Ok(BlockBuilderOutcome {
|
||||
execution_result: result,
|
||||
hashed_state,
|
||||
trie_updates,
|
||||
block,
|
||||
block_access_list,
|
||||
})
|
||||
}
|
||||
|
||||
fn executor_mut(&mut self) -> &mut Self::Executor {
|
||||
@@ -554,11 +589,33 @@ where
|
||||
block: &RecoveredBlock<<Self::Primitives as NodePrimitives>::Block>,
|
||||
) -> Result<BlockExecutionResult<<Self::Primitives as NodePrimitives>::Receipt>, Self::Error>
|
||||
{
|
||||
let result = self
|
||||
let mut executor = self
|
||||
.strategy_factory
|
||||
.executor_for_block(&mut self.db, block)
|
||||
.map_err(BlockExecutionError::other)?
|
||||
.execute_block(block.transactions_recovered())?;
|
||||
.map_err(BlockExecutionError::other)?;
|
||||
|
||||
let has_bal = block.header().block_access_list_hash().is_some();
|
||||
|
||||
if has_bal {
|
||||
executor.evm_mut().db_mut().bal_state.bal_builder = Some(Bal::new());
|
||||
} else {
|
||||
executor.evm_mut().db_mut().bal_state.bal_builder = None;
|
||||
}
|
||||
|
||||
executor.apply_pre_execution_changes()?;
|
||||
|
||||
if has_bal {
|
||||
executor.evm_mut().db_mut().bump_bal_index();
|
||||
}
|
||||
|
||||
for tx in block.transactions_recovered() {
|
||||
executor.execute_transaction(tx)?;
|
||||
if has_bal {
|
||||
executor.evm_mut().db_mut().bump_bal_index();
|
||||
}
|
||||
}
|
||||
|
||||
let result = executor.apply_post_execution_changes()?;
|
||||
|
||||
self.db.merge_transitions(BundleRetention::Reverts);
|
||||
|
||||
@@ -592,6 +649,10 @@ where
|
||||
fn size_hint(&self) -> usize {
|
||||
self.db.bundle_state.size_hint()
|
||||
}
|
||||
|
||||
fn take_bal(&mut self) -> Option<BlockAccessList> {
|
||||
self.db.take_built_alloy_bal()
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper trait marking a 'static type that can be converted into an [`ExecutableTxParts`] for
|
||||
@@ -697,6 +758,10 @@ mod tests {
|
||||
fn size_hint(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn take_bal(&mut self) -> Option<BlockAccessList> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -35,10 +35,11 @@ pub enum EthVersion {
|
||||
|
||||
impl EthVersion {
|
||||
/// The latest known eth version
|
||||
pub const LATEST: Self = Self::Eth69;
|
||||
pub const LATEST: Self = Self::Eth70;
|
||||
|
||||
/// All known eth versions
|
||||
pub const ALL_VERSIONS: &'static [Self] = &[Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
|
||||
pub const ALL_VERSIONS: &'static [Self] =
|
||||
&[Self::Eth70, Self::Eth69, Self::Eth68, Self::Eth67, Self::Eth66];
|
||||
|
||||
/// Returns true if the version is eth/66
|
||||
pub const fn is_eth66(&self) -> bool {
|
||||
|
||||
@@ -202,7 +202,7 @@ where
|
||||
// update the cached reads
|
||||
self.update_cached_reads(parent_header_hash, request_cache).await;
|
||||
|
||||
self.consensus.validate_block_post_execution(&block, &output, None)?;
|
||||
self.consensus.validate_block_post_execution(&block, &output, None, None)?;
|
||||
|
||||
self.ensure_payment(&block, &output, &message)?;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use alloy_primitives::BlockNumber;
|
||||
use num_traits::Zero;
|
||||
use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
|
||||
use reth_config::config::ExecutionConfig;
|
||||
use reth_consensus::FullConsensus;
|
||||
use reth_consensus::{validate_block_access_list_gas, FullConsensus};
|
||||
use reth_db::{static_file::HeaderMask, tables};
|
||||
use reth_evm::{execute::Executor, metrics::ExecutorMetrics, ConfigureEvm};
|
||||
use reth_execution_types::Chain;
|
||||
@@ -357,7 +357,19 @@ where
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Err(err) = self.consensus.validate_block_post_execution(&block, &result, None) {
|
||||
let bal = executor.take_bal().unwrap_or_default();
|
||||
if block.header().block_access_list_hash().is_some() &&
|
||||
let Err(err) = validate_block_access_list_gas(Some(&bal), block.gas_limit())
|
||||
{
|
||||
return Err(StageError::Block {
|
||||
block: Box::new(block.block_with_parent()),
|
||||
error: BlockErrorKind::Validation(err),
|
||||
})
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
self.consensus.validate_block_post_execution(&block, &result, None, Some(bal))
|
||||
{
|
||||
return Err(StageError::Block {
|
||||
block: Box::new(block.block_with_parent()),
|
||||
error: BlockErrorKind::Validation(err),
|
||||
|
||||
@@ -1424,6 +1424,7 @@ pub fn ensure_intrinsic_gas<T: EthPoolTransaction>(
|
||||
.map(|l| l.iter().map(|i| i.storage_keys.len()).sum::<usize>())
|
||||
.unwrap_or_default() as u64,
|
||||
transaction.authorization_list().map(|l| l.len()).unwrap_or_default() as u64,
|
||||
revm_primitives::eip8037::CPSB_GLAMSTERDAM,
|
||||
);
|
||||
|
||||
let gas_limit = transaction.gas_limit();
|
||||
|
||||
@@ -252,7 +252,7 @@ fn run_case(case: &BlockchainTest) -> Result<(), Error> {
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Consensus checks after block execution
|
||||
validate_block_post_execution(block, &chain_spec, &output, None)
|
||||
validate_block_post_execution(block, &chain_spec, &output, None, None)
|
||||
.map_err(|err| Error::block_failed(block_number, err))?;
|
||||
|
||||
// Compute and check the post state root
|
||||
|
||||
Reference in New Issue
Block a user