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