-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathvalidation.rs
More file actions
418 lines (365 loc) · 15.1 KB
/
validation.rs
File metadata and controls
418 lines (365 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
use std::{fmt::Debug, sync::Arc};
use alloy_consensus::{
BlockHeader as AlloyBlockHeader, EMPTY_OMMER_ROOT_HASH, constants::MAXIMUM_EXTRA_DATA_SIZE,
};
use alloy_primitives::{Address, B256, U256};
use alloy_sol_types::{SolCall, sol};
use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator, ReceiptRootBloom};
use reth_consensus_common::validation::{
validate_against_parent_hash_number, validate_body_against_header, validate_header_base_fee,
validate_header_extra_data, validate_header_gas,
};
use reth_ethereum_consensus::validate_block_post_execution;
use reth_execution_types::BlockExecutionResult;
use reth_primitives::SealedBlock;
use reth_primitives_traits::{
Block, BlockBody, BlockHeader, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader,
SignedTransaction,
};
use crate::eip4396::{SHASTA_INITIAL_BASE_FEE, calculate_next_block_eip4396_base_fee};
use alethia_reth_chainspec::{hardfork::TaikoHardforks, spec::TaikoChainSpec};
use alethia_reth_evm::alloy::TAIKO_GOLDEN_TOUCH_ADDRESS;
sol! {
function anchor(bytes32, bytes32, uint64, uint32) external;
function anchorV2(uint64, bytes32, uint32, (uint8, uint8, uint32, uint64, uint32)) external;
function anchorV3(uint64, bytes32, uint32, (uint8, uint8, uint32, uint64, uint32), bytes32[]) external;
function anchorV4((uint48, bytes32, bytes32)) external;
}
/// Anchor / system transaction call selectors.
pub const ANCHOR_V1_SELECTOR: &[u8; 4] = &anchorCall::SELECTOR;
pub const ANCHOR_V2_SELECTOR: &[u8; 4] = &anchorV2Call::SELECTOR;
pub const ANCHOR_V3_SELECTOR: &[u8; 4] = &anchorV3Call::SELECTOR;
pub const ANCHOR_V4_SELECTOR: &[u8; 4] = &anchorV4Call::SELECTOR;
/// The gas limit for the anchor transactions before Pacaya hardfork.
pub const ANCHOR_V1_V2_GAS_LIMIT: u64 = 250_000;
/// The gas limit for the anchor transactions in Pacaya and Shasta hardfork blocks.
pub const ANCHOR_V3_V4_GAS_LIMIT: u64 = 1_000_000;
/// Minimal block reader interface used by Taiko consensus.
pub trait TaikoBlockReader: Send + Sync + Debug {
/// Returns the timestamp of the block referenced by the given hash, if present.
fn block_timestamp_by_hash(&self, hash: B256) -> Option<u64>;
}
/// Taiko consensus implementation.
///
/// Provides basic checks as outlined in the execution specs.
#[derive(Debug, Clone)]
pub struct TaikoBeaconConsensus {
chain_spec: Arc<TaikoChainSpec>,
block_reader: Arc<dyn TaikoBlockReader>,
}
impl TaikoBeaconConsensus {
/// Create a new instance of [`TaikoBeaconConsensus`]
pub fn new(chain_spec: Arc<TaikoChainSpec>, block_reader: Arc<dyn TaikoBlockReader>) -> Self {
Self { chain_spec, block_reader }
}
}
impl<N> FullConsensus<N> for TaikoBeaconConsensus
where
N: NodePrimitives,
{
/// Validate a block with regard to execution results:
///
/// - Compares the receipts root in the block header to the block body
/// - Compares the gas used in the block header to the actual gas usage after execution
fn validate_block_post_execution(
&self,
block: &RecoveredBlock<N::Block>,
result: &BlockExecutionResult<N::Receipt>,
receipt_root_bloom: Option<ReceiptRootBloom>,
) -> Result<(), ConsensusError> {
validate_block_post_execution(
block,
&self.chain_spec,
&result.receipts,
&result.requests,
receipt_root_bloom,
)?;
validate_anchor_transaction_in_block::<<N as NodePrimitives>::Block>(
block,
&self.chain_spec,
)
}
}
impl<B: Block> Consensus<B> for TaikoBeaconConsensus {
/// Ensures the block response data matches the header.
///
/// This ensures the body response items match the header's hashes:
/// - ommer hash
/// - transaction root
/// - withdrawals root
fn validate_body_against_header(
&self,
body: &B::Body,
header: &SealedHeader<B::Header>,
) -> Result<(), ConsensusError> {
validate_body_against_header(body, header.header())
}
/// Validate a block without regard for state:
///
/// - Compares the ommer hash in the block header to the block body
/// - Compares the transactions root in the block header to the block body
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
// In Taiko network, ommer hash is always empty.
if block.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
return Err(ConsensusError::BodyOmmersHashDiff(
GotExpected { got: block.ommers_hash(), expected: EMPTY_OMMER_ROOT_HASH }.into(),
));
}
Ok(())
}
}
impl<H> HeaderValidator<H> for TaikoBeaconConsensus
where
H: BlockHeader,
{
/// Validate if header is correct and follows consensus specification.
///
/// This is called on standalone header to check if all hashes are correct.
fn validate_header(&self, header: &SealedHeader<H>) -> Result<(), ConsensusError> {
let header = header.header();
if !header.difficulty().is_zero() {
return Err(ConsensusError::TheMergeDifficultyIsNotZero);
}
if !header.nonce().is_some_and(|nonce| nonce.is_zero()) {
return Err(ConsensusError::TheMergeNonceIsNotZero);
}
if header.ommers_hash() != EMPTY_OMMER_ROOT_HASH {
return Err(ConsensusError::TheMergeOmmerRootIsNotEmpty);
}
validate_header_extra_data(header, MAXIMUM_EXTRA_DATA_SIZE)?;
validate_header_gas(header)?;
validate_header_base_fee(header, &self.chain_spec)
}
/// Validate that the header information regarding parent are correct.
fn validate_header_against_parent(
&self,
header: &SealedHeader<H>,
parent: &SealedHeader<H>,
) -> Result<(), ConsensusError> {
validate_against_parent_hash_number(header.header(), parent)?;
let header_base_fee =
{ header.header().base_fee_per_gas().ok_or(ConsensusError::BaseFeeMissing)? };
if self.chain_spec.is_shasta_active(header.timestamp()) {
// Shasta hardfork introduces stricter timestamp validation:
// timestamps must strictly increase (no equal timestamps allowed)
if header.timestamp() <= parent.timestamp() {
return Err(ConsensusError::TimestampIsInPast {
parent_timestamp: parent.timestamp(),
timestamp: header.timestamp(),
});
}
// Calculate the expected base fee using EIP-4396 rules. For the first post-genesis
// block there is no grandparent timestamp, so reuse the default Shasta base fee.
let expected_base_fee = if parent.number() == 0 {
SHASTA_INITIAL_BASE_FEE
} else {
parent_block_time(self.block_reader.as_ref(), parent)
.map(|block_time| {
calculate_next_block_eip4396_base_fee(parent.header(), block_time)
})
// If we cannot retrieve the grandparent timestamp (e.g. when running without a
// fully wired block reader), fall back to the header's base fee to avoid
// rejecting the block outright.
.unwrap_or(header_base_fee)
};
// Verify the block's base fee matches the expected value.
if header_base_fee != expected_base_fee {
return Err(ConsensusError::BaseFeeDiff(GotExpected {
got: header_base_fee,
expected: expected_base_fee,
}));
}
} else {
// For blocks before Shasta, the timestamp must be greater than or equal to the parent's
// timestamp.
if header.timestamp() < parent.timestamp() {
return Err(ConsensusError::TimestampIsInPast {
parent_timestamp: parent.timestamp(),
timestamp: header.timestamp(),
});
}
}
Ok(())
}
}
/// Validates that the header has a base fee set (required after EIP-4396).
#[inline]
pub fn validate_against_parent_eip4396_base_fee<H: BlockHeader>(
header: &H,
) -> Result<(), ConsensusError> {
if header.base_fee_per_gas().is_none() {
return Err(ConsensusError::BaseFeeMissing);
}
Ok(())
}
/// Calculates the time difference between the parent and grandparent blocks.
fn parent_block_time<H>(
block_reader: &dyn TaikoBlockReader,
parent: &SealedHeader<H>,
) -> Result<u64, ConsensusError>
where
H: BlockHeader,
{
let grandparent_hash = parent.header().parent_hash();
let grandparent_timestamp = block_reader
.block_timestamp_by_hash(grandparent_hash)
.ok_or(ConsensusError::ParentUnknown { hash: grandparent_hash })?;
Ok(parent.header().timestamp() - grandparent_timestamp)
}
/// Context required to validate an anchor transaction.
pub struct AnchorValidationContext {
/// Timestamp for hardfork selection.
pub timestamp: u64,
/// Block number for hardfork selection and gas limit rules.
pub block_number: u64,
/// Base fee per gas for tip validation.
pub base_fee_per_gas: u64,
}
/// Validates a single anchor transaction against the current hardfork rules.
pub fn validate_anchor_transaction(
anchor_transaction: &impl SignedTransaction,
chain_spec: &TaikoChainSpec,
ctx: AnchorValidationContext,
) -> Result<(), ConsensusError> {
// Ensure the input data starts with one of the anchor selectors.
if chain_spec.is_shasta_active(ctx.timestamp) {
validate_input_selector(anchor_transaction.input(), ANCHOR_V4_SELECTOR)?;
} else if chain_spec.is_pacaya_active_at_block(ctx.block_number) {
validate_input_selector(anchor_transaction.input(), ANCHOR_V3_SELECTOR)?;
} else if chain_spec.is_ontake_active_at_block(ctx.block_number) {
validate_input_selector(anchor_transaction.input(), ANCHOR_V2_SELECTOR)?;
} else {
validate_input_selector(anchor_transaction.input(), ANCHOR_V1_SELECTOR)?;
}
// Ensure the value is zero.
if anchor_transaction.value() != U256::ZERO {
return Err(ConsensusError::Other("Anchor transaction value must be zero".into()));
}
// Ensure the gas limit is correct.
let gas_limit = if chain_spec.is_pacaya_active_at_block(ctx.block_number) {
ANCHOR_V3_V4_GAS_LIMIT
} else {
ANCHOR_V1_V2_GAS_LIMIT
};
if anchor_transaction.gas_limit() != gas_limit {
return Err(ConsensusError::Other(format!(
"Anchor transaction gas limit must be {gas_limit}, got {}",
anchor_transaction.gas_limit()
)));
}
// Ensure the tip is equal to zero.
let anchor_transaction_tip =
anchor_transaction.effective_tip_per_gas(ctx.base_fee_per_gas).ok_or_else(|| {
ConsensusError::Other("Anchor transaction tip must be set to zero".into())
})?;
if anchor_transaction_tip != 0 {
return Err(ConsensusError::Other(format!(
"Anchor transaction tip must be zero, got {anchor_transaction_tip}"
)));
}
// Ensure the sender is the treasury address.
let sender = anchor_transaction.try_recover().map_err(|err| {
ConsensusError::Other(format!("Anchor transaction sender must be recoverable: {err}"))
})?;
if sender != Address::from(TAIKO_GOLDEN_TOUCH_ADDRESS) {
return Err(ConsensusError::Other(format!(
"Anchor transaction sender must be the treasury address, got {sender}"
)));
}
Ok(())
}
/// Validates the anchor transaction in the block.
pub fn validate_anchor_transaction_in_block<B>(
block: &RecoveredBlock<B>,
chain_spec: &TaikoChainSpec,
) -> Result<(), ConsensusError>
where
B: Block,
{
let anchor_transaction = match block.body().transactions().first() {
Some(tx) => tx,
None => return Ok(()),
};
validate_anchor_transaction(
anchor_transaction,
chain_spec,
AnchorValidationContext {
timestamp: block.header().timestamp(),
block_number: block.number(),
base_fee_per_gas: block.header().base_fee_per_gas().ok_or_else(|| {
ConsensusError::Other("Block base fee per gas must be set".into())
})?,
},
)
}
// Validates the transaction input data against the expected selector.
fn validate_input_selector(
input: &[u8],
expected_selector: &[u8; 4],
) -> Result<(), ConsensusError> {
if !input.starts_with(expected_selector) {
return Err(ConsensusError::Other(format!(
"Anchor transaction input data does not match the expected selector: {expected_selector:?}"
)));
}
Ok(())
}
#[cfg(test)]
mod test {
use alloy_consensus::Header;
use alloy_primitives::U64;
use super::validate_input_selector;
use super::*;
#[test]
fn test_validate_against_parent_eip4396_base_fee() {
let mut header = Header::default();
assert!(validate_against_parent_eip4396_base_fee(&header).is_err());
header.base_fee_per_gas = Some(U64::random().to::<u64>());
assert!(validate_against_parent_eip4396_base_fee(&header).is_ok());
}
#[test]
fn test_validate_input_selector() {
// Valid selector
let input = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc];
let expected_selector = [0x12, 0x34, 0x56, 0x78];
assert!(validate_input_selector(&input, &expected_selector).is_ok());
// Invalid selector
let wrong_selector = [0x11, 0x22, 0x33, 0x44];
assert!(validate_input_selector(&input, &wrong_selector).is_err());
// Empty input
let empty_input = [];
assert!(validate_input_selector(&empty_input, &expected_selector).is_err());
}
#[test]
fn test_anchor_v4_selector_matches_protocol() {
assert_eq!(ANCHOR_V4_SELECTOR, &[0x52, 0x3e, 0x68, 0x54]);
}
#[test]
fn test_validate_header_against_parent() {
use crate::eip4396::{
BLOCK_TIME_TARGET, MAX_BASE_FEE, calculate_next_block_eip4396_base_fee,
};
// Test calculate_next_block_eip4396_base_fee function
let mut parent = Header {
gas_limit: 30_000_000,
base_fee_per_gas: Some(1_000_000_000),
number: 1,
..Default::default()
};
// Test 1: Gas used equals target (gas_limit / 2)
parent.gas_used = 15_000_000;
let base_fee = calculate_next_block_eip4396_base_fee(&parent, BLOCK_TIME_TARGET);
assert_eq!(base_fee, 1_000_000_000, "Base fee should remain the same when at target");
// Test 2: Gas used above target
parent.gas_used = 20_000_000;
let base_fee = calculate_next_block_eip4396_base_fee(&parent, BLOCK_TIME_TARGET);
assert_eq!(
base_fee, MAX_BASE_FEE,
"Base fee should stay clamped at MAX_BASE_FEE when the parent is already at the cap"
);
// Test 3: Gas used below target
parent.gas_used = 10_000_000;
let base_fee = calculate_next_block_eip4396_base_fee(&parent, BLOCK_TIME_TARGET);
assert!(base_fee < 1_000_000_000, "Base fee should decrease when below target");
}
}