Skip to content

Commit ed1ec0e

Browse files
azteca1998edg-l
andauthored
feat(l1): implement EIP-7843 (Amsterdam) (#5973)
**Motivation** This PR implement [EIP-7843](https://eips.ethereum.org/EIPS/eip-7843). **Description** This PR implements the `SLOTNUM` opcode. <!-- Link to issues: Resolves #111, Resolves #222 --> **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. --------- Co-authored-by: Edgar <git@edgl.dev>
1 parent 668ec50 commit ed1ec0e

17 files changed

Lines changed: 228 additions & 7 deletions

File tree

benches/benches/build_block_benchmark.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ fn create_payload_block(genesis_block: &Block, store: &Store) -> (Block, u64) {
146146
random: genesis_block.header.prev_randao,
147147
withdrawals: None,
148148
beacon_root: genesis_block.header.parent_beacon_block_root,
149+
slot_number: None,
149150
version: 3,
150151
elasticity_multiplier: 1,
151152
gas_ceil: DEFAULT_BUILDER_GAS_CEIL,

crates/blockchain/payload.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pub struct BuildPayloadArgs {
8484
pub random: H256,
8585
pub withdrawals: Option<Vec<Withdrawal>>,
8686
pub beacon_root: Option<H256>,
87+
pub slot_number: Option<u64>,
8788
pub version: u8,
8889
pub elasticity_multiplier: u64,
8990
pub gas_ceil: u64,
@@ -170,6 +171,7 @@ pub fn create_payload(
170171
requests_hash: chain_config
171172
.is_prague_activated(args.timestamp)
172173
.then_some(*DEFAULT_REQUESTS_HASH),
174+
slot_number: args.slot_number,
173175
..Default::default()
174176
};
175177

crates/l2/sequencer/block_producer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ impl BlockProducer {
183183
random: H256::zero(),
184184
withdrawals: Default::default(),
185185
beacon_root: Some(head_beacon_block_root),
186+
slot_number: None,
186187
version,
187188
elasticity_multiplier: self.elasticity_multiplier,
188189
gas_ceil: self.block_gas_limit,

crates/networking/rpc/engine/fork_choice.rs

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ use tracing::{debug, info, warn};
1111
use crate::{
1212
rpc::{RpcApiContext, RpcHandler},
1313
types::{
14-
fork_choice::{ForkChoiceResponse, ForkChoiceState, PayloadAttributesV3},
14+
fork_choice::{
15+
ForkChoiceResponse, ForkChoiceState, PayloadAttributesV3, PayloadAttributesV4,
16+
},
1517
payload::PayloadStatus,
1618
},
1719
utils::RpcErr,
@@ -128,6 +130,46 @@ impl RpcHandler for ForkChoiceUpdatedV3 {
128130
}
129131
}
130132

133+
#[derive(Debug)]
134+
pub struct ForkChoiceUpdatedV4 {
135+
pub fork_choice_state: ForkChoiceState,
136+
pub payload_attributes: Option<PayloadAttributesV4>,
137+
}
138+
139+
impl From<ForkChoiceUpdatedV4> for RpcRequest {
140+
fn from(val: ForkChoiceUpdatedV4) -> Self {
141+
RpcRequest {
142+
method: "engine_forkchoiceUpdatedV4".to_string(),
143+
params: Some(vec![
144+
serde_json::json!(val.fork_choice_state),
145+
serde_json::json!(val.payload_attributes),
146+
]),
147+
..Default::default()
148+
}
149+
}
150+
}
151+
152+
impl RpcHandler for ForkChoiceUpdatedV4 {
153+
fn parse(params: &Option<Vec<Value>>) -> Result<Self, RpcErr> {
154+
let (fork_choice_state, payload_attributes) = parse_v4(params)?;
155+
Ok(ForkChoiceUpdatedV4 {
156+
fork_choice_state,
157+
payload_attributes,
158+
})
159+
}
160+
161+
async fn handle(&self, context: RpcApiContext) -> Result<Value, RpcErr> {
162+
let (head_block_opt, mut response) =
163+
handle_forkchoice(&self.fork_choice_state, context.clone(), 4).await?;
164+
if let (Some(head_block), Some(attributes)) = (head_block_opt, &self.payload_attributes) {
165+
validate_attributes_v4(attributes, &head_block, &context)?;
166+
let payload_id = build_payload_v4(attributes, context, &self.fork_choice_state).await?;
167+
response.set_id(payload_id);
168+
}
169+
serde_json::to_value(response).map_err(|error| RpcErr::Internal(error.to_string()))
170+
}
171+
}
172+
131173
fn parse(
132174
params: &Option<Vec<Value>>,
133175
is_v3: bool,
@@ -386,6 +428,7 @@ async fn build_payload(
386428
random: attributes.prev_randao,
387429
withdrawals: attributes.withdrawals.clone(),
388430
beacon_root: attributes.parent_beacon_block_root,
431+
slot_number: None,
389432
version,
390433
elasticity_multiplier: ELASTICITY_MULTIPLIER,
391434
gas_ceil: context.gas_ceil,
@@ -411,3 +454,104 @@ async fn build_payload(
411454
.await;
412455
Ok(payload_id)
413456
}
457+
458+
fn parse_v4(
459+
params: &Option<Vec<Value>>,
460+
) -> Result<(ForkChoiceState, Option<PayloadAttributesV4>), RpcErr> {
461+
let params = params
462+
.as_ref()
463+
.ok_or(RpcErr::BadParams("No params provided".to_owned()))?;
464+
465+
if params.len() != 2 && params.len() != 1 {
466+
return Err(RpcErr::BadParams("Expected 2 or 1 params".to_owned()));
467+
}
468+
469+
let forkchoice_state: ForkChoiceState = serde_json::from_value(params[0].clone())?;
470+
let mut payload_attributes: Option<PayloadAttributesV4> = None;
471+
if params.len() == 2 {
472+
payload_attributes =
473+
match serde_json::from_value::<Option<PayloadAttributesV4>>(params[1].clone()) {
474+
Ok(attributes) => attributes,
475+
Err(error) => {
476+
warn!("Could not parse payload attributes {}", error);
477+
None
478+
}
479+
};
480+
}
481+
Ok((forkchoice_state, payload_attributes))
482+
}
483+
484+
fn validate_attributes_v4(
485+
attributes: &PayloadAttributesV4,
486+
head_block: &BlockHeader,
487+
context: &RpcApiContext,
488+
) -> Result<(), RpcErr> {
489+
// Similar validation to V3
490+
let chain_config = context.storage.get_chain_config();
491+
if !chain_config.is_amsterdam_activated(attributes.timestamp) {
492+
return Err(RpcErr::InvalidPayloadAttributes(
493+
"V4 payload attributes used for pre-Amsterdam timestamp".to_string(),
494+
));
495+
}
496+
if attributes.withdrawals.is_none() {
497+
return Err(RpcErr::InvalidPayloadAttributes(
498+
"V4 payload attributes missing withdrawals".to_string(),
499+
));
500+
}
501+
if attributes.parent_beacon_block_root.is_none() {
502+
return Err(RpcErr::InvalidPayloadAttributes(
503+
"V4 payload attributes missing parent_beacon_block_root".to_string(),
504+
));
505+
}
506+
validate_timestamp_v4(attributes, head_block)
507+
}
508+
509+
fn validate_timestamp_v4(
510+
attributes: &PayloadAttributesV4,
511+
head_block: &BlockHeader,
512+
) -> Result<(), RpcErr> {
513+
if attributes.timestamp <= head_block.timestamp {
514+
return Err(RpcErr::InvalidPayloadAttributes(
515+
"invalid timestamp".to_string(),
516+
));
517+
}
518+
Ok(())
519+
}
520+
521+
async fn build_payload_v4(
522+
attributes: &PayloadAttributesV4,
523+
context: RpcApiContext,
524+
fork_choice_state: &ForkChoiceState,
525+
) -> Result<u64, RpcErr> {
526+
let args = BuildPayloadArgs {
527+
parent: fork_choice_state.head_block_hash,
528+
timestamp: attributes.timestamp,
529+
fee_recipient: attributes.suggested_fee_recipient,
530+
random: attributes.prev_randao,
531+
withdrawals: attributes.withdrawals.clone(),
532+
beacon_root: attributes.parent_beacon_block_root,
533+
slot_number: Some(attributes.slot_number),
534+
version: 4,
535+
elasticity_multiplier: ELASTICITY_MULTIPLIER,
536+
gas_ceil: context.gas_ceil,
537+
};
538+
let payload_id = args
539+
.id()
540+
.map_err(|error| RpcErr::Internal(error.to_string()))?;
541+
542+
info!(
543+
id = payload_id,
544+
slot = attributes.slot_number,
545+
"Fork choice updated V4 includes payload attributes. Creating a new payload"
546+
);
547+
let payload = match create_payload(&args, &context.storage, context.node_data.extra_data) {
548+
Ok(payload) => payload,
549+
Err(ChainError::EvmError(error)) => return Err(error.into()),
550+
Err(error) => return Err(RpcErr::Internal(error.to_string())),
551+
};
552+
context
553+
.blockchain
554+
.initiate_payload_build(payload, payload_id)
555+
.await;
556+
Ok(payload_id)
557+
}

crates/networking/rpc/engine/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ pub type ExchangeCapabilitiesRequest = Vec<String>;
1515

1616
/// List of capabilities that the execution layer client supports. Add new capabilities here.
1717
/// More info: https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#engine_exchangecapabilities
18-
pub const CAPABILITIES: [&str; 19] = [
18+
pub const CAPABILITIES: [&str; 20] = [
1919
"engine_forkchoiceUpdatedV1",
2020
"engine_forkchoiceUpdatedV2",
2121
"engine_forkchoiceUpdatedV3",
22+
"engine_forkchoiceUpdatedV4",
2223
"engine_newPayloadV1",
2324
"engine_newPayloadV2",
2425
"engine_newPayloadV3",

crates/networking/rpc/rpc.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use crate::engine::{
77
ExchangeCapabilitiesRequest,
88
blobs::BlobsV1Request,
99
exchange_transition_config::ExchangeTransitionConfigV1Req,
10-
fork_choice::{ForkChoiceUpdatedV1, ForkChoiceUpdatedV2, ForkChoiceUpdatedV3},
10+
fork_choice::{
11+
ForkChoiceUpdatedV1, ForkChoiceUpdatedV2, ForkChoiceUpdatedV3, ForkChoiceUpdatedV4,
12+
},
1113
payload::{
1214
GetPayloadBodiesByHashV1Request, GetPayloadBodiesByRangeV1Request, GetPayloadV1Request,
1315
GetPayloadV2Request, GetPayloadV3Request, GetPayloadV4Request, NewPayloadV1Request,
@@ -797,6 +799,7 @@ pub async fn map_engine_requests(
797799
"engine_forkchoiceUpdatedV1" => ForkChoiceUpdatedV1::call(req, context).await,
798800
"engine_forkchoiceUpdatedV2" => ForkChoiceUpdatedV2::call(req, context).await,
799801
"engine_forkchoiceUpdatedV3" => ForkChoiceUpdatedV3::call(req, context).await,
802+
"engine_forkchoiceUpdatedV4" => ForkChoiceUpdatedV4::call(req, context).await,
800803
"engine_newPayloadV5" => NewPayloadV5Request::call(req, context).await,
801804
"engine_newPayloadV4" => NewPayloadV4Request::call(req, context).await,
802805
"engine_newPayloadV3" => NewPayloadV3Request::call(req, context).await,

crates/networking/rpc/types/fork_choice.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ pub struct PayloadAttributesV3 {
2323
pub parent_beacon_block_root: Option<H256>,
2424
}
2525

26+
#[derive(Debug, Deserialize, Default, Serialize, Clone)]
27+
#[serde(rename_all = "camelCase")]
28+
#[allow(unused)]
29+
pub struct PayloadAttributesV4 {
30+
#[serde(with = "serde_utils::u64::hex_str")]
31+
pub timestamp: u64,
32+
pub prev_randao: H256,
33+
pub suggested_fee_recipient: Address,
34+
pub withdrawals: Option<Vec<Withdrawal>>,
35+
pub parent_beacon_block_root: Option<H256>,
36+
#[serde(with = "serde_utils::u64::hex_str")]
37+
pub slot_number: u64,
38+
}
39+
2640
#[derive(Debug, Serialize, Deserialize)]
2741
#[serde(rename_all = "camelCase")]
2842
pub struct ForkChoiceResponse {

crates/networking/rpc/types/payload.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ pub struct ExecutionPayload {
5151
default
5252
)]
5353
pub excess_blob_gas: Option<u64>,
54+
// ExecutionPayloadV4 fields (EIP-7843)
55+
#[serde(
56+
skip_serializing_if = "Option::is_none",
57+
with = "serde_utils::u64::hex_str_opt",
58+
default
59+
)]
60+
pub slot_number: Option<u64>,
5461
// ExecutionPayloadV4 fields. Optional since we support previous versions.
5562
#[serde(
5663
skip_serializing_if = "Option::is_none",
@@ -141,6 +148,7 @@ impl ExecutionPayload {
141148
parent_beacon_block_root,
142149
// TODO: set the value properly
143150
requests_hash,
151+
slot_number: self.slot_number,
144152
block_access_list_hash,
145153
..Default::default()
146154
};
@@ -172,6 +180,7 @@ impl ExecutionPayload {
172180
withdrawals: block.body.withdrawals,
173181
blob_gas_used: block.header.blob_gas_used,
174182
excess_blob_gas: block.header.excess_blob_gas,
183+
slot_number: block.header.slot_number,
175184
// TODO: need to finish this after we are able to get BAL from blocks
176185
block_access_list: None,
177186
}

crates/vm/backends/levm/mod.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ impl LEVM {
334334
coinbase: block_header.coinbase,
335335
timestamp: block_header.timestamp.into(),
336336
prev_randao: Some(block_header.prev_randao),
337+
slot_number: block_header
338+
.slot_number
339+
.map(U256::from)
340+
.unwrap_or(U256::zero()),
337341
chain_id: chain_config.chain_id.into(),
338342
base_fee_per_gas: block_header.base_fee_per_gas.unwrap_or_default().into(),
339343
base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
@@ -404,7 +408,7 @@ impl LEVM {
404408
db: &mut GeneralizedDatabase,
405409
vm_type: VMType,
406410
) -> Result<ExecutionResult, EvmError> {
407-
let mut env = env_from_generic(tx, block_header, db)?;
411+
let mut env = env_from_generic(tx, block_header, db, vm_type)?;
408412

409413
env.block_gas_limit = i64::MAX as u64; // disable block gas limit
410414

@@ -560,7 +564,7 @@ impl LEVM {
560564
db: &mut GeneralizedDatabase,
561565
vm_type: VMType,
562566
) -> Result<(ExecutionResult, AccessList), VMError> {
563-
let mut env = env_from_generic(&tx, header, db)?;
567+
let mut env = env_from_generic(&tx, header, db, vm_type)?;
564568

565569
adjust_disabled_base_fee(&mut env);
566570

@@ -802,12 +806,31 @@ fn env_from_generic(
802806
tx: &GenericTransaction,
803807
header: &BlockHeader,
804808
db: &GeneralizedDatabase,
809+
vm_type: VMType,
805810
) -> Result<Environment, VMError> {
806811
let chain_config = db.store.get_chain_config()?;
807812
let gas_price =
808813
calculate_gas_price_for_generic(tx, header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE));
809814
let block_excess_blob_gas = header.excess_blob_gas.map(U256::from);
810815
let config = EVMConfig::new_from_chain_config(&chain_config, header);
816+
817+
// Validate slot_number for Amsterdam+ blocks
818+
// For L2 chains, slot_number is always 0
819+
let slot_number = if let VMType::L2(_) = vm_type {
820+
U256::zero()
821+
} else if config.fork >= Fork::Amsterdam {
822+
header
823+
.slot_number
824+
.map(U256::from)
825+
.ok_or(VMError::Internal(InternalError::Custom(
826+
"slot_number must be present in Amsterdam+ blocks".to_string(),
827+
)))?
828+
} else {
829+
// Pre-Amsterdam: slot_number should be None, default to zero
830+
// This value should never be used since SLOTNUM opcode doesn't exist pre-Amsterdam
831+
header.slot_number.map(U256::from).unwrap_or(U256::zero())
832+
};
833+
811834
Ok(Environment {
812835
origin: tx.from.0.into(),
813836
gas_limit: tx
@@ -818,6 +841,7 @@ fn env_from_generic(
818841
coinbase: header.coinbase,
819842
timestamp: header.timestamp.into(),
820843
prev_randao: Some(header.prev_randao),
844+
slot_number,
821845
chain_id: chain_config.chain_id.into(),
822846
base_fee_per_gas: header.base_fee_per_gas.unwrap_or_default().into(),
823847
base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,

crates/vm/levm/src/environment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub struct Environment {
2626
pub timestamp: U256,
2727
pub prev_randao: Option<H256>,
2828
pub difficulty: U256,
29+
pub slot_number: U256,
2930
pub chain_id: U256,
3031
pub base_fee_per_gas: U256,
3132
pub base_blob_fee_per_gas: U256,

0 commit comments

Comments
 (0)