1use alloy_consensus::{
2 BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
3};
4use alloy_eips::{eip4844::kzg_to_versioned_hash, eip7685::RequestsOrHash};
5use alloy_rpc_types_beacon::relay::{
6 BidTrace, BuilderBlockValidationRequest, BuilderBlockValidationRequestV2,
7 BuilderBlockValidationRequestV3, BuilderBlockValidationRequestV4,
8 BuilderBlockValidationRequestV5,
9};
10use alloy_rpc_types_engine::{
11 BlobsBundleV1, BlobsBundleV2, CancunPayloadFields, ExecutionData, ExecutionPayload,
12 ExecutionPayloadSidecar, PraguePayloadFields,
13};
14use async_trait::async_trait;
15use core::fmt;
16use jsonrpsee::core::RpcResult;
17use jsonrpsee_types::error::ErrorObject;
18use reth_chainspec::{ChainSpecProvider, EthereumHardforks};
19use reth_consensus::{Consensus, FullConsensus};
20use reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE;
21use reth_engine_primitives::PayloadValidator;
22use reth_errors::{BlockExecutionError, ConsensusError, ProviderError};
23use reth_evm::{execute::Executor, ConfigureEvm};
24use reth_execution_types::BlockExecutionOutput;
25use reth_metrics::{
26 metrics,
27 metrics::{gauge, Gauge},
28 Metrics,
29};
30use reth_node_api::{NewPayloadError, PayloadTypes};
31use reth_primitives_traits::{
32 constants::GAS_LIMIT_BOUND_DIVISOR, BlockBody, GotExpected, NodePrimitives, RecoveredBlock,
33 SealedBlock, SealedHeaderFor,
34};
35use reth_revm::{cached::CachedReads, database::StateProviderDatabase};
36use reth_rpc_api::BlockSubmissionValidationApiServer;
37use reth_rpc_server_types::result::{internal_rpc_err, invalid_params_rpc_err};
38use reth_storage_api::{BlockReaderIdExt, StateProviderFactory};
39use reth_tasks::TaskSpawner;
40use revm_primitives::{Address, B256, U256};
41use serde::{Deserialize, Serialize};
42use sha2::{Digest, Sha256};
43use std::{collections::HashSet, sync::Arc};
44use tokio::sync::{oneshot, RwLock};
45use tracing::warn;
46
47#[derive(Clone, Debug, derive_more::Deref)]
49pub struct ValidationApi<Provider, E: ConfigureEvm, T: PayloadTypes> {
50 #[deref]
51 inner: Arc<ValidationApiInner<Provider, E, T>>,
52}
53
54impl<Provider, E, T> ValidationApi<Provider, E, T>
55where
56 E: ConfigureEvm,
57 T: PayloadTypes,
58{
59 pub fn new(
61 provider: Provider,
62 consensus: Arc<dyn FullConsensus<E::Primitives, Error = ConsensusError>>,
63 evm_config: E,
64 config: ValidationApiConfig,
65 task_spawner: Box<dyn TaskSpawner>,
66 payload_validator: Arc<
67 dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>,
68 >,
69 ) -> Self {
70 let ValidationApiConfig { disallow, validation_window } = config;
71
72 let inner = Arc::new(ValidationApiInner {
73 provider,
74 consensus,
75 payload_validator,
76 evm_config,
77 disallow,
78 validation_window,
79 cached_state: Default::default(),
80 task_spawner,
81 metrics: Default::default(),
82 });
83
84 inner.metrics.disallow_size.set(inner.disallow.len() as f64);
85
86 let disallow_hash = hash_disallow_list(&inner.disallow);
87 let hash_gauge = gauge!("builder_validation_disallow_hash", "hash" => disallow_hash);
88 hash_gauge.set(1.0);
89
90 Self { inner }
91 }
92
93 async fn cached_reads(&self, head: B256) -> CachedReads {
95 let cache = self.inner.cached_state.read().await;
96 if cache.0 == head {
97 cache.1.clone()
98 } else {
99 Default::default()
100 }
101 }
102
103 async fn update_cached_reads(&self, head: B256, cached_state: CachedReads) {
105 let mut cache = self.inner.cached_state.write().await;
106 if cache.0 == head {
107 cache.1.extend(cached_state);
108 } else {
109 *cache = (head, cached_state)
110 }
111 }
112}
113
114impl<Provider, E, T> ValidationApi<Provider, E, T>
115where
116 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
117 + ChainSpecProvider<ChainSpec: EthereumHardforks>
118 + StateProviderFactory
119 + 'static,
120 E: ConfigureEvm + 'static,
121 T: PayloadTypes<ExecutionData = ExecutionData>,
122{
123 pub async fn validate_message_against_block(
125 &self,
126 block: RecoveredBlock<<E::Primitives as NodePrimitives>::Block>,
127 message: BidTrace,
128 registered_gas_limit: u64,
129 ) -> Result<(), ValidationApiError> {
130 self.validate_message_against_header(block.sealed_header(), &message)?;
131
132 self.consensus.validate_header(block.sealed_header())?;
133 self.consensus.validate_block_pre_execution(block.sealed_block())?;
134
135 if !self.disallow.is_empty() {
136 if self.disallow.contains(&block.beneficiary()) {
137 return Err(ValidationApiError::Blacklist(block.beneficiary()))
138 }
139 if self.disallow.contains(&message.proposer_fee_recipient) {
140 return Err(ValidationApiError::Blacklist(message.proposer_fee_recipient))
141 }
142 for (sender, tx) in block.senders_iter().zip(block.body().transactions()) {
143 if self.disallow.contains(sender) {
144 return Err(ValidationApiError::Blacklist(*sender))
145 }
146 if let Some(to) = tx.to() {
147 if self.disallow.contains(&to) {
148 return Err(ValidationApiError::Blacklist(to))
149 }
150 }
151 }
152 }
153
154 let latest_header =
155 self.provider.latest_header()?.ok_or_else(|| ValidationApiError::MissingLatestBlock)?;
156
157 let parent_header = if block.parent_hash() == latest_header.hash() {
158 latest_header
159 } else {
160 let parent_header = self
162 .provider
163 .sealed_header_by_hash(block.parent_hash())?
164 .ok_or_else(|| ValidationApiError::MissingParentBlock)?;
165
166 if latest_header.number().saturating_sub(parent_header.number()) >
167 self.validation_window
168 {
169 return Err(ValidationApiError::BlockTooOld)
170 }
171 parent_header
172 };
173
174 self.consensus.validate_header_against_parent(block.sealed_header(), &parent_header)?;
175 self.validate_gas_limit(registered_gas_limit, &parent_header, block.sealed_header())?;
176 let parent_header_hash = parent_header.hash();
177 let state_provider = self.provider.state_by_block_hash(parent_header_hash)?;
178
179 let mut request_cache = self.cached_reads(parent_header_hash).await;
180
181 let cached_db = request_cache.as_db_mut(StateProviderDatabase::new(&state_provider));
182 let executor = self.evm_config.batch_executor(cached_db);
183
184 let mut accessed_blacklisted = None;
185 let output = executor.execute_with_state_closure(&block, |state| {
186 if !self.disallow.is_empty() {
187 for account in state.cache.accounts.keys() {
190 if self.disallow.contains(account) {
191 accessed_blacklisted = Some(*account);
192 }
193 }
194 }
195 })?;
196
197 if let Some(account) = accessed_blacklisted {
198 return Err(ValidationApiError::Blacklist(account))
199 }
200
201 self.update_cached_reads(parent_header_hash, request_cache).await;
203
204 self.consensus.validate_block_post_execution(&block, &output)?;
205
206 self.ensure_payment(&block, &output, &message)?;
207
208 let state_root =
209 state_provider.state_root(state_provider.hashed_post_state(&output.state))?;
210
211 if state_root != block.header().state_root() {
212 return Err(ConsensusError::BodyStateRootDiff(
213 GotExpected { got: state_root, expected: block.header().state_root() }.into(),
214 )
215 .into())
216 }
217
218 Ok(())
219 }
220
221 fn validate_message_against_header(
223 &self,
224 header: &SealedHeaderFor<E::Primitives>,
225 message: &BidTrace,
226 ) -> Result<(), ValidationApiError> {
227 if header.hash() != message.block_hash {
228 Err(ValidationApiError::BlockHashMismatch(GotExpected {
229 got: message.block_hash,
230 expected: header.hash(),
231 }))
232 } else if header.parent_hash() != message.parent_hash {
233 Err(ValidationApiError::ParentHashMismatch(GotExpected {
234 got: message.parent_hash,
235 expected: header.parent_hash(),
236 }))
237 } else if header.gas_limit() != message.gas_limit {
238 Err(ValidationApiError::GasLimitMismatch(GotExpected {
239 got: message.gas_limit,
240 expected: header.gas_limit(),
241 }))
242 } else if header.gas_used() != message.gas_used {
243 Err(ValidationApiError::GasUsedMismatch(GotExpected {
244 got: message.gas_used,
245 expected: header.gas_used(),
246 }))
247 } else {
248 Ok(())
249 }
250 }
251
252 fn validate_gas_limit(
257 &self,
258 registered_gas_limit: u64,
259 parent_header: &SealedHeaderFor<E::Primitives>,
260 header: &SealedHeaderFor<E::Primitives>,
261 ) -> Result<(), ValidationApiError> {
262 let max_gas_limit =
263 parent_header.gas_limit() + parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR - 1;
264 let min_gas_limit =
265 parent_header.gas_limit() - parent_header.gas_limit() / GAS_LIMIT_BOUND_DIVISOR + 1;
266
267 let best_gas_limit =
268 std::cmp::max(min_gas_limit, std::cmp::min(max_gas_limit, registered_gas_limit));
269
270 if best_gas_limit != header.gas_limit() {
271 return Err(ValidationApiError::GasLimitMismatch(GotExpected {
272 got: header.gas_limit(),
273 expected: best_gas_limit,
274 }))
275 }
276
277 Ok(())
278 }
279
280 fn ensure_payment(
285 &self,
286 block: &SealedBlock<<E::Primitives as NodePrimitives>::Block>,
287 output: &BlockExecutionOutput<<E::Primitives as NodePrimitives>::Receipt>,
288 message: &BidTrace,
289 ) -> Result<(), ValidationApiError> {
290 let (mut balance_before, balance_after) = if let Some(acc) =
291 output.state.state.get(&message.proposer_fee_recipient)
292 {
293 let balance_before = acc.original_info.as_ref().map(|i| i.balance).unwrap_or_default();
294 let balance_after = acc.info.as_ref().map(|i| i.balance).unwrap_or_default();
295
296 (balance_before, balance_after)
297 } else {
298 (U256::ZERO, U256::ZERO)
301 };
302
303 if let Some(withdrawals) = block.body().withdrawals() {
304 for withdrawal in withdrawals {
305 if withdrawal.address == message.proposer_fee_recipient {
306 balance_before += withdrawal.amount_wei();
307 }
308 }
309 }
310
311 if balance_after >= balance_before.saturating_add(message.value) {
312 return Ok(())
313 }
314
315 let (receipt, tx) = output
316 .receipts
317 .last()
318 .zip(block.body().transactions().last())
319 .ok_or(ValidationApiError::ProposerPayment)?;
320
321 if !receipt.status() {
322 return Err(ValidationApiError::ProposerPayment)
323 }
324
325 if tx.to() != Some(message.proposer_fee_recipient) {
326 return Err(ValidationApiError::ProposerPayment)
327 }
328
329 if tx.value() != message.value {
330 return Err(ValidationApiError::ProposerPayment)
331 }
332
333 if !tx.input().is_empty() {
334 return Err(ValidationApiError::ProposerPayment)
335 }
336
337 if let Some(block_base_fee) = block.header().base_fee_per_gas() {
338 if tx.effective_tip_per_gas(block_base_fee).unwrap_or_default() != 0 {
339 return Err(ValidationApiError::ProposerPayment)
340 }
341 }
342
343 Ok(())
344 }
345
346 pub fn validate_blobs_bundle(
348 &self,
349 mut blobs_bundle: BlobsBundleV1,
350 ) -> Result<Vec<B256>, ValidationApiError> {
351 if blobs_bundle.commitments.len() != blobs_bundle.proofs.len() ||
352 blobs_bundle.commitments.len() != blobs_bundle.blobs.len()
353 {
354 return Err(ValidationApiError::InvalidBlobsBundle)
355 }
356
357 let versioned_hashes = blobs_bundle
358 .commitments
359 .iter()
360 .map(|c| kzg_to_versioned_hash(c.as_slice()))
361 .collect::<Vec<_>>();
362
363 let sidecar = blobs_bundle.pop_sidecar(blobs_bundle.blobs.len());
364
365 sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
366
367 Ok(versioned_hashes)
368 }
369 pub fn validate_blobs_bundle_v2(
371 &self,
372 blobs_bundle: BlobsBundleV2,
373 ) -> Result<Vec<B256>, ValidationApiError> {
374 let versioned_hashes = blobs_bundle
375 .commitments
376 .iter()
377 .map(|c| kzg_to_versioned_hash(c.as_slice()))
378 .collect::<Vec<_>>();
379
380 blobs_bundle
381 .try_into_sidecar()
382 .map_err(|_| ValidationApiError::InvalidBlobsBundle)?
383 .validate(&versioned_hashes, EnvKzgSettings::default().get())?;
384
385 Ok(versioned_hashes)
386 }
387
388 async fn validate_builder_submission_v3(
390 &self,
391 request: BuilderBlockValidationRequestV3,
392 ) -> Result<(), ValidationApiError> {
393 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
394 payload: ExecutionPayload::V3(request.request.execution_payload),
395 sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
396 parent_beacon_block_root: request.parent_beacon_block_root,
397 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
398 }),
399 })?;
400
401 self.validate_message_against_block(
402 block,
403 request.request.message,
404 request.registered_gas_limit,
405 )
406 .await
407 }
408
409 async fn validate_builder_submission_v4(
411 &self,
412 request: BuilderBlockValidationRequestV4,
413 ) -> Result<(), ValidationApiError> {
414 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
415 payload: ExecutionPayload::V3(request.request.execution_payload),
416 sidecar: ExecutionPayloadSidecar::v4(
417 CancunPayloadFields {
418 parent_beacon_block_root: request.parent_beacon_block_root,
419 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
420 },
421 PraguePayloadFields {
422 requests: RequestsOrHash::Requests(
423 request.request.execution_requests.to_requests(),
424 ),
425 },
426 ),
427 })?;
428
429 self.validate_message_against_block(
430 block,
431 request.request.message,
432 request.registered_gas_limit,
433 )
434 .await
435 }
436
437 async fn validate_builder_submission_v5(
439 &self,
440 request: BuilderBlockValidationRequestV5,
441 ) -> Result<(), ValidationApiError> {
442 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
443 payload: ExecutionPayload::V3(request.request.execution_payload),
444 sidecar: ExecutionPayloadSidecar::v4(
445 CancunPayloadFields {
446 parent_beacon_block_root: request.parent_beacon_block_root,
447 versioned_hashes: self
448 .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
449 },
450 PraguePayloadFields {
451 requests: RequestsOrHash::Requests(
452 request.request.execution_requests.to_requests(),
453 ),
454 },
455 ),
456 })?;
457
458 let chain_spec = self.provider.chain_spec();
460 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
461 block.rlp_length() > MAX_RLP_BLOCK_SIZE
462 {
463 return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
464 rlp_length: block.rlp_length(),
465 max_rlp_length: MAX_RLP_BLOCK_SIZE,
466 }));
467 }
468
469 self.validate_message_against_block(
470 block,
471 request.request.message,
472 request.registered_gas_limit,
473 )
474 .await
475 }
476}
477
478#[async_trait]
479impl<Provider, E, T> BlockSubmissionValidationApiServer for ValidationApi<Provider, E, T>
480where
481 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
482 + ChainSpecProvider<ChainSpec: EthereumHardforks>
483 + StateProviderFactory
484 + Clone
485 + 'static,
486 E: ConfigureEvm + 'static,
487 T: PayloadTypes<ExecutionData = ExecutionData>,
488{
489 async fn validate_builder_submission_v1(
490 &self,
491 _request: BuilderBlockValidationRequest,
492 ) -> RpcResult<()> {
493 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV1` is not supported");
494 Err(internal_rpc_err("unimplemented"))
495 }
496
497 async fn validate_builder_submission_v2(
498 &self,
499 _request: BuilderBlockValidationRequestV2,
500 ) -> RpcResult<()> {
501 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV2` is not supported");
502 Err(internal_rpc_err("unimplemented"))
503 }
504
505 async fn validate_builder_submission_v3(
507 &self,
508 request: BuilderBlockValidationRequestV3,
509 ) -> RpcResult<()> {
510 let this = self.clone();
511 let (tx, rx) = oneshot::channel();
512
513 self.task_spawner.spawn_blocking(Box::pin(async move {
514 let result = Self::validate_builder_submission_v3(&this, request)
515 .await
516 .map_err(ErrorObject::from);
517 let _ = tx.send(result);
518 }));
519
520 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
521 }
522
523 async fn validate_builder_submission_v4(
525 &self,
526 request: BuilderBlockValidationRequestV4,
527 ) -> RpcResult<()> {
528 let this = self.clone();
529 let (tx, rx) = oneshot::channel();
530
531 self.task_spawner.spawn_blocking(Box::pin(async move {
532 let result = Self::validate_builder_submission_v4(&this, request)
533 .await
534 .map_err(ErrorObject::from);
535 let _ = tx.send(result);
536 }));
537
538 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
539 }
540
541 async fn validate_builder_submission_v5(
543 &self,
544 request: BuilderBlockValidationRequestV5,
545 ) -> RpcResult<()> {
546 let this = self.clone();
547 let (tx, rx) = oneshot::channel();
548
549 self.task_spawner.spawn_blocking(Box::pin(async move {
550 let result = Self::validate_builder_submission_v5(&this, request)
551 .await
552 .map_err(ErrorObject::from);
553 let _ = tx.send(result);
554 }));
555
556 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
557 }
558}
559
560pub struct ValidationApiInner<Provider, E: ConfigureEvm, T: PayloadTypes> {
561 provider: Provider,
563 consensus: Arc<dyn FullConsensus<E::Primitives, Error = ConsensusError>>,
565 payload_validator:
567 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
568 evm_config: E,
570 disallow: HashSet<Address>,
572 validation_window: u64,
574 cached_state: RwLock<(B256, CachedReads)>,
579 task_spawner: Box<dyn TaskSpawner>,
581 metrics: ValidationMetrics,
583}
584
585fn hash_disallow_list(disallow: &HashSet<Address>) -> String {
590 let mut sorted: Vec<_> = disallow.iter().collect();
591 sorted.sort(); let mut hasher = Sha256::new();
594 for addr in sorted {
595 hasher.update(addr.as_slice());
596 }
597
598 format!("{:x}", hasher.finalize())
599}
600
601impl<Provider, E: ConfigureEvm, T: PayloadTypes> fmt::Debug for ValidationApiInner<Provider, E, T> {
602 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603 f.debug_struct("ValidationApiInner").finish_non_exhaustive()
604 }
605}
606
607#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
609pub struct ValidationApiConfig {
610 pub disallow: HashSet<Address>,
612 pub validation_window: u64,
614}
615
616impl ValidationApiConfig {
617 pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
619}
620
621impl Default for ValidationApiConfig {
622 fn default() -> Self {
623 Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
624 }
625}
626
627#[derive(Debug, thiserror::Error)]
629pub enum ValidationApiError {
630 #[error("block gas limit mismatch: {_0}")]
631 GasLimitMismatch(GotExpected<u64>),
632 #[error("block gas used mismatch: {_0}")]
633 GasUsedMismatch(GotExpected<u64>),
634 #[error("block parent hash mismatch: {_0}")]
635 ParentHashMismatch(GotExpected<B256>),
636 #[error("block hash mismatch: {_0}")]
637 BlockHashMismatch(GotExpected<B256>),
638 #[error("missing latest block in database")]
639 MissingLatestBlock,
640 #[error("parent block not found")]
641 MissingParentBlock,
642 #[error("block is too old, outside validation window")]
643 BlockTooOld,
644 #[error("could not verify proposer payment")]
645 ProposerPayment,
646 #[error("invalid blobs bundle")]
647 InvalidBlobsBundle,
648 #[error("block accesses blacklisted address: {_0}")]
649 Blacklist(Address),
650 #[error(transparent)]
651 Blob(#[from] BlobTransactionValidationError),
652 #[error(transparent)]
653 Consensus(#[from] ConsensusError),
654 #[error(transparent)]
655 Provider(#[from] ProviderError),
656 #[error(transparent)]
657 Execution(#[from] BlockExecutionError),
658 #[error(transparent)]
659 Payload(#[from] NewPayloadError),
660}
661
662impl From<ValidationApiError> for ErrorObject<'static> {
663 fn from(error: ValidationApiError) -> Self {
664 match error {
665 ValidationApiError::GasLimitMismatch(_) |
666 ValidationApiError::GasUsedMismatch(_) |
667 ValidationApiError::ParentHashMismatch(_) |
668 ValidationApiError::BlockHashMismatch(_) |
669 ValidationApiError::Blacklist(_) |
670 ValidationApiError::ProposerPayment |
671 ValidationApiError::InvalidBlobsBundle |
672 ValidationApiError::Blob(_) => invalid_params_rpc_err(error.to_string()),
673
674 ValidationApiError::MissingLatestBlock |
675 ValidationApiError::MissingParentBlock |
676 ValidationApiError::BlockTooOld |
677 ValidationApiError::Consensus(_) |
678 ValidationApiError::Provider(_) => internal_rpc_err(error.to_string()),
679 ValidationApiError::Execution(err) => match err {
680 error @ BlockExecutionError::Validation(_) => {
681 invalid_params_rpc_err(error.to_string())
682 }
683 error @ BlockExecutionError::Internal(_) => internal_rpc_err(error.to_string()),
684 },
685 ValidationApiError::Payload(err) => match err {
686 error @ NewPayloadError::Eth(_) => invalid_params_rpc_err(error.to_string()),
687 error @ NewPayloadError::Other(_) => internal_rpc_err(error.to_string()),
688 },
689 }
690 }
691}
692
693#[derive(Metrics)]
695#[metrics(scope = "builder.validation")]
696pub(crate) struct ValidationMetrics {
697 pub(crate) disallow_size: Gauge,
699}
700
701#[cfg(test)]
702mod tests {
703 use super::hash_disallow_list;
704 use revm_primitives::Address;
705 use std::collections::HashSet;
706
707 #[test]
708 fn test_hash_disallow_list_deterministic() {
709 let mut addresses = HashSet::new();
710 addresses.insert(Address::from([1u8; 20]));
711 addresses.insert(Address::from([2u8; 20]));
712
713 let hash1 = hash_disallow_list(&addresses);
714 let hash2 = hash_disallow_list(&addresses);
715
716 assert_eq!(hash1, hash2);
717 }
718
719 #[test]
720 fn test_hash_disallow_list_different_content() {
721 let mut addresses1 = HashSet::new();
722 addresses1.insert(Address::from([1u8; 20]));
723
724 let mut addresses2 = HashSet::new();
725 addresses2.insert(Address::from([2u8; 20]));
726
727 let hash1 = hash_disallow_list(&addresses1);
728 let hash2 = hash_disallow_list(&addresses2);
729
730 assert_ne!(hash1, hash2);
731 }
732
733 #[test]
734 fn test_hash_disallow_list_order_independent() {
735 let mut addresses1 = HashSet::new();
736 addresses1.insert(Address::from([1u8; 20]));
737 addresses1.insert(Address::from([2u8; 20]));
738
739 let mut addresses2 = HashSet::new();
740 addresses2.insert(Address::from([2u8; 20])); addresses2.insert(Address::from([1u8; 20]));
742
743 let hash1 = hash_disallow_list(&addresses1);
744 let hash2 = hash_disallow_list(&addresses2);
745
746 assert_eq!(hash1, hash2);
747 }
748
749 #[test]
750 fn test_disallow_list_hash_rbuilder_parity() {
752 let json = r#"["0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45","0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1","0x03893a7c7463AE47D46bc7f091665f1893656003","0x04DBA1194ee10112fE6C3207C0687DEf0e78baCf"]"#;
753 let blocklist: Vec<Address> = serde_json::from_str(json).unwrap();
754 let blocklist: HashSet<Address> = blocklist.into_iter().collect();
755 let expected_hash = "ee14e9d115e182f61871a5a385ab2f32ecf434f3b17bdbacc71044810d89e608";
756 let hash = hash_disallow_list(&blocklist);
757 assert_eq!(expected_hash, hash);
758 }
759}