Skip to content

Commit 1239834

Browse files
committed
feat(node/p2p): refactor block validity checks
1 parent 4126e08 commit 1239834

File tree

5 files changed

+353
-81
lines changed

5 files changed

+353
-81
lines changed

Cargo.lock

+4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/node/p2p/Cargo.toml

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ kona-rpc = { workspace = true, features = ["jsonrpsee", "reqwest", "std"] }
2020
# Alloy
2121
alloy-rlp.workspace = true
2222
alloy-primitives = { workspace = true, features = ["k256", "getrandom"] }
23+
alloy-rpc-types-engine.workspace = true
2324

2425
# Op Alloy
2526
op-alloy-rpc-types-engine = { workspace = true, features = ["std"] }
@@ -58,6 +59,11 @@ arbtest.workspace = true
5859
arbitrary = { workspace = true, features = ["derive"] }
5960
alloy-primitives = { workspace = true, features = ["arbitrary"] }
6061
alloy-rpc-types-engine = { workspace = true, features = ["std"] }
62+
alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] }
63+
alloy-eips.workspace = true
64+
65+
op-alloy-consensus = { workspace = true, features = ["arbitrary", "k256"] }
66+
rand = { workspace = true, features = ["thread_rng"] }
6167

6268
[features]
6369
default = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
use std::time::SystemTime;
2+
3+
use alloy_primitives::Address;
4+
use libp2p::gossipsub::MessageAcceptance;
5+
use op_alloy_rpc_types_engine::OpNetworkPayloadEnvelope;
6+
7+
use super::BlockHandler;
8+
9+
/// These errors are returned by the [`BlockHandler::block_valid`] method.
10+
/// They specify the reason for which a block has been rejected/ignored.
11+
#[derive(Debug, thiserror::Error)]
12+
pub enum BlockInvalidError {
13+
#[error("Invalid timestamp. Current: {current}, Received: {received}")]
14+
Timestamp { current: u64, received: u64 },
15+
#[error("Invalid signature.")]
16+
Signature,
17+
#[error("Invalid signer, expected: {expected}, received: {received}")]
18+
Signer { expected: Address, received: Address },
19+
// TODO: add the rest of the errors variants in follow-up PRs
20+
}
21+
22+
impl From<BlockInvalidError> for MessageAcceptance {
23+
fn from(_value: BlockInvalidError) -> Self {
24+
MessageAcceptance::Reject
25+
}
26+
}
27+
28+
impl BlockHandler {
29+
/// Determines if a block is valid.
30+
///
31+
/// We validate the block according to the rules defined here:
32+
/// <https://specs.optimism.io/protocol/rollup-node-p2p.html#block-validation>
33+
///
34+
/// The block encoding/compression are assumed to be valid at this point (they are first checked
35+
/// in the handle).
36+
pub fn block_valid(
37+
&mut self,
38+
envelope: &OpNetworkPayloadEnvelope,
39+
) -> Result<(), BlockInvalidError> {
40+
let current_timestamp =
41+
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
42+
43+
// The timestamp is at most 5 seconds in the future.
44+
let is_future = envelope.payload.timestamp() > current_timestamp + 5;
45+
// The timestamp is at most 60 seconds in the past.
46+
let is_past = envelope.payload.timestamp() < current_timestamp - 60;
47+
48+
// CHECK: The timestamp is not too far in the future or past.
49+
if is_future || is_past {
50+
return Err(BlockInvalidError::Timestamp {
51+
current: current_timestamp,
52+
received: envelope.payload.timestamp(),
53+
});
54+
}
55+
56+
// CHECK: The signature is valid.
57+
let msg = envelope.payload_hash.signature_message(self.chain_id);
58+
let block_signer = *self.signer_recv.borrow();
59+
60+
// The block has a valid signature.
61+
let Ok(msg_signer) = envelope.signature.recover_address_from_prehash(&msg) else {
62+
return Err(BlockInvalidError::Signature);
63+
};
64+
65+
// The block is signed by the expected signer (the unsafe block signer).
66+
if msg_signer != block_signer {
67+
return Err(BlockInvalidError::Signer { expected: msg_signer, received: block_signer });
68+
}
69+
70+
Ok(())
71+
}
72+
}
73+
74+
#[cfg(test)]
75+
pub mod tests {
76+
77+
use super::*;
78+
use alloy_consensus::{Block, EMPTY_OMMER_ROOT_HASH};
79+
use alloy_eips::eip2718::Encodable2718;
80+
use alloy_primitives::{B256, Bytes, Signature};
81+
use alloy_rlp::BufMut;
82+
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
83+
use arbitrary::{Arbitrary, Unstructured};
84+
use op_alloy_consensus::OpTxEnvelope;
85+
use op_alloy_rpc_types_engine::{OpExecutionPayload, OpExecutionPayloadV4, PayloadHash};
86+
87+
fn valid_block() -> Block<OpTxEnvelope> {
88+
// Simulate some random data
89+
let mut data = vec![0; 1024 * 1024];
90+
let mut rng = rand::rng();
91+
rand::Rng::fill(&mut rng, &mut data[..]);
92+
93+
// Create unstructured data with the random bytes
94+
let u = Unstructured::new(&data);
95+
96+
// Generate a random instance of MyStruct
97+
let mut block: Block<OpTxEnvelope> = Block::arbitrary_take_rest(u).unwrap();
98+
99+
let transactions: Vec<Bytes> =
100+
block.body.transactions().map(|tx| tx.encoded_2718().into()).collect();
101+
102+
let transactions_root =
103+
alloy_consensus::proofs::ordered_trie_root_with_encoder(&transactions, |item, buf| {
104+
buf.put_slice(item)
105+
});
106+
107+
block.header.transactions_root = transactions_root;
108+
109+
// That is an edge case: if the base fee per gas is not set, the block will be rejected
110+
// because we don't know if the field is `None` or `Some(0)`. By default, we assume
111+
// it is `Some(0)` See the conversion here: `<https://github.com/alloy-rs/alloy/blob/033878d34177450028d1d37afd0fd20e08c99244/crates/rpc-types-engine/src/payload.rs#L345>`
112+
block.header.base_fee_per_gas = Some(block.header.base_fee_per_gas.unwrap_or_default());
113+
114+
let current_timestamp =
115+
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
116+
block.header.timestamp = current_timestamp;
117+
118+
block
119+
}
120+
121+
/// Make the block v1 compatible
122+
fn v1_valid_block() -> Block<OpTxEnvelope> {
123+
let mut block = valid_block();
124+
block.header.withdrawals_root = None;
125+
block.header.blob_gas_used = None;
126+
block.header.excess_blob_gas = None;
127+
block.header.parent_beacon_block_root = None;
128+
block.header.requests_hash = None;
129+
block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH;
130+
block.header.difficulty = Default::default();
131+
block.header.nonce = Default::default();
132+
133+
block
134+
}
135+
136+
/// Make the block v2 compatible
137+
pub fn v2_valid_block() -> Block<OpTxEnvelope> {
138+
let mut block = v1_valid_block();
139+
140+
block.body.withdrawals = Some(vec![].into());
141+
let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(
142+
&block.body.withdrawals.clone().unwrap_or_default(),
143+
);
144+
145+
block.header.withdrawals_root = Some(withdrawals_root);
146+
147+
block
148+
}
149+
150+
/// Make the block v3 compatible
151+
pub fn v3_valid_block() -> Block<OpTxEnvelope> {
152+
let mut block = valid_block();
153+
154+
block.body.withdrawals = Some(vec![].into());
155+
let withdrawals_root = alloy_consensus::proofs::calculate_withdrawals_root(
156+
&block.body.withdrawals.clone().unwrap_or_default(),
157+
);
158+
block.header.withdrawals_root = Some(withdrawals_root);
159+
160+
block.header.blob_gas_used = Some(0);
161+
block.header.excess_blob_gas = Some(0);
162+
block.header.parent_beacon_block_root =
163+
Some(block.header.parent_beacon_block_root.unwrap_or_default());
164+
165+
block.header.requests_hash = None;
166+
block.header.ommers_hash = EMPTY_OMMER_ROOT_HASH;
167+
block.header.difficulty = Default::default();
168+
block.header.nonce = Default::default();
169+
170+
block
171+
}
172+
173+
/// Make the block v4 compatible
174+
pub fn v4_valid_block() -> Block<OpTxEnvelope> {
175+
v3_valid_block()
176+
}
177+
178+
/// Generates a random valid block and ensure it is v1 compatible
179+
#[test]
180+
fn test_block_valid() {
181+
let block = v1_valid_block();
182+
183+
let v1 = ExecutionPayloadV1::from_block_slow(&block);
184+
185+
let payload = OpExecutionPayload::V1(v1);
186+
let envelope = OpNetworkPayloadEnvelope {
187+
payload,
188+
signature: Signature::test_signature(),
189+
payload_hash: PayloadHash(B256::ZERO),
190+
parent_beacon_block_root: None,
191+
};
192+
193+
let msg = envelope.payload_hash.signature_message(10);
194+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
195+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
196+
let mut handler = BlockHandler::new(10, unsafe_signer);
197+
198+
assert!(handler.block_valid(&envelope).is_ok());
199+
}
200+
201+
/// Generates a random block with an invalid timestamp and ensure it is rejected
202+
#[test]
203+
fn test_block_invalid_timestamp_early() {
204+
let mut block = v1_valid_block();
205+
206+
block.header.timestamp -= 61;
207+
208+
let v1 = ExecutionPayloadV1::from_block_slow(&block);
209+
210+
let payload = OpExecutionPayload::V1(v1);
211+
let envelope = OpNetworkPayloadEnvelope {
212+
payload,
213+
signature: Signature::test_signature(),
214+
payload_hash: PayloadHash(B256::ZERO),
215+
parent_beacon_block_root: None,
216+
};
217+
218+
let msg = envelope.payload_hash.signature_message(10);
219+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
220+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
221+
let mut handler = BlockHandler::new(10, unsafe_signer);
222+
223+
assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Timestamp { .. })));
224+
}
225+
226+
/// Generates a random block with an invalid timestamp and ensure it is rejected
227+
#[test]
228+
fn test_block_invalid_timestamp_too_far() {
229+
let mut block = v1_valid_block();
230+
231+
block.header.timestamp += 6;
232+
233+
let v1 = ExecutionPayloadV1::from_block_slow(&block);
234+
235+
let payload = OpExecutionPayload::V1(v1);
236+
let envelope = OpNetworkPayloadEnvelope {
237+
payload,
238+
signature: Signature::test_signature(),
239+
payload_hash: PayloadHash(B256::ZERO),
240+
parent_beacon_block_root: None,
241+
};
242+
243+
let msg = envelope.payload_hash.signature_message(10);
244+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
245+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
246+
let mut handler = BlockHandler::new(10, unsafe_signer);
247+
248+
assert!(matches!(handler.block_valid(&envelope), Err(BlockInvalidError::Timestamp { .. })));
249+
}
250+
251+
#[test]
252+
fn test_v2_block() {
253+
let block = v2_valid_block();
254+
255+
let v2 = ExecutionPayloadV2::from_block_slow(&block);
256+
257+
let payload = OpExecutionPayload::V2(v2);
258+
let envelope = OpNetworkPayloadEnvelope {
259+
payload,
260+
signature: Signature::test_signature(),
261+
payload_hash: PayloadHash(B256::ZERO),
262+
parent_beacon_block_root: None,
263+
};
264+
265+
let msg = envelope.payload_hash.signature_message(10);
266+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
267+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
268+
let mut handler = BlockHandler::new(10, unsafe_signer);
269+
270+
assert!(handler.block_valid(&envelope).is_ok());
271+
}
272+
273+
#[test]
274+
fn test_v3_block() {
275+
let block = v3_valid_block();
276+
277+
let v3 = ExecutionPayloadV3::from_block_slow(&block);
278+
279+
let payload = OpExecutionPayload::V3(v3);
280+
let envelope = OpNetworkPayloadEnvelope {
281+
payload,
282+
signature: Signature::test_signature(),
283+
payload_hash: PayloadHash(B256::ZERO),
284+
parent_beacon_block_root: Some(
285+
block.header.parent_beacon_block_root.unwrap_or_default(),
286+
),
287+
};
288+
289+
let msg = envelope.payload_hash.signature_message(10);
290+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
291+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
292+
let mut handler = BlockHandler::new(10, unsafe_signer);
293+
294+
assert!(handler.block_valid(&envelope).is_ok());
295+
}
296+
297+
#[test]
298+
fn test_v4_block() {
299+
let block = v4_valid_block();
300+
301+
let v3 = ExecutionPayloadV3::from_block_slow(&block);
302+
let v4 = OpExecutionPayloadV4::from_v3_with_withdrawals_root(
303+
v3,
304+
block.withdrawals_root.unwrap(),
305+
);
306+
307+
let payload = OpExecutionPayload::V4(v4);
308+
let envelope = OpNetworkPayloadEnvelope {
309+
payload,
310+
signature: Signature::test_signature(),
311+
payload_hash: PayloadHash(B256::ZERO),
312+
parent_beacon_block_root: Some(
313+
block.header.parent_beacon_block_root.unwrap_or_default(),
314+
),
315+
};
316+
317+
let msg = envelope.payload_hash.signature_message(10);
318+
let signer = envelope.signature.recover_address_from_prehash(&msg).unwrap();
319+
let (_, unsafe_signer) = tokio::sync::watch::channel(signer);
320+
let mut handler = BlockHandler::new(10, unsafe_signer);
321+
322+
assert!(handler.block_valid(&envelope).is_ok());
323+
}
324+
}

0 commit comments

Comments
 (0)