1use alloy_consensus::{
2 BlobTransactionValidationError, BlockHeader, EnvKzgSettings, Transaction, TxReceipt,
3};
4use alloy_eips::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::Runtime;
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: Runtime,
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, 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 blobs_bundle: BlobsBundleV1,
351 ) -> Result<Vec<B256>, ValidationApiError> {
352 let versioned_hashes = blobs_bundle.versioned_hashes();
353 let sidecar =
354 blobs_bundle.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
355
356 sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
357 Ok(versioned_hashes)
358 }
359
360 pub fn validate_blobs_bundle_v2(
362 &self,
363 blobs_bundle: BlobsBundleV2,
364 ) -> Result<Vec<B256>, ValidationApiError> {
365 let versioned_hashes = blobs_bundle.versioned_hashes();
366 let sidecar =
367 blobs_bundle.try_into_sidecar().map_err(|_| ValidationApiError::InvalidBlobsBundle)?;
368
369 sidecar.validate(&versioned_hashes, EnvKzgSettings::default().get())?;
370 Ok(versioned_hashes)
371 }
372
373 async fn validate_builder_submission_v3(
375 &self,
376 request: BuilderBlockValidationRequestV3,
377 ) -> Result<(), ValidationApiError> {
378 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
379 payload: ExecutionPayload::V3(request.request.execution_payload),
380 sidecar: ExecutionPayloadSidecar::v3(CancunPayloadFields {
381 parent_beacon_block_root: request.parent_beacon_block_root,
382 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
383 }),
384 })?;
385
386 self.validate_message_against_block(
387 block,
388 request.request.message,
389 request.registered_gas_limit,
390 )
391 .await
392 }
393
394 async fn validate_builder_submission_v4(
396 &self,
397 request: BuilderBlockValidationRequestV4,
398 ) -> Result<(), ValidationApiError> {
399 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
400 payload: ExecutionPayload::V3(request.request.execution_payload),
401 sidecar: ExecutionPayloadSidecar::v4(
402 CancunPayloadFields {
403 parent_beacon_block_root: request.parent_beacon_block_root,
404 versioned_hashes: self.validate_blobs_bundle(request.request.blobs_bundle)?,
405 },
406 PraguePayloadFields {
407 requests: RequestsOrHash::Requests(
408 request.request.execution_requests.to_requests(),
409 ),
410 },
411 ),
412 })?;
413
414 self.validate_message_against_block(
415 block,
416 request.request.message,
417 request.registered_gas_limit,
418 )
419 .await
420 }
421
422 async fn validate_builder_submission_v5(
424 &self,
425 request: BuilderBlockValidationRequestV5,
426 ) -> Result<(), ValidationApiError> {
427 let block = self.payload_validator.ensure_well_formed_payload(ExecutionData {
428 payload: ExecutionPayload::V3(request.request.execution_payload),
429 sidecar: ExecutionPayloadSidecar::v4(
430 CancunPayloadFields {
431 parent_beacon_block_root: request.parent_beacon_block_root,
432 versioned_hashes: self
433 .validate_blobs_bundle_v2(request.request.blobs_bundle)?,
434 },
435 PraguePayloadFields {
436 requests: RequestsOrHash::Requests(
437 request.request.execution_requests.to_requests(),
438 ),
439 },
440 ),
441 })?;
442
443 let chain_spec = self.provider.chain_spec();
445 if chain_spec.is_osaka_active_at_timestamp(block.timestamp()) &&
446 block.rlp_length() > MAX_RLP_BLOCK_SIZE
447 {
448 return Err(ValidationApiError::Consensus(ConsensusError::BlockTooLarge {
449 rlp_length: block.rlp_length(),
450 max_rlp_length: MAX_RLP_BLOCK_SIZE,
451 }));
452 }
453
454 self.validate_message_against_block(
455 block,
456 request.request.message,
457 request.registered_gas_limit,
458 )
459 .await
460 }
461}
462
463#[async_trait]
464impl<Provider, E, T> BlockSubmissionValidationApiServer for ValidationApi<Provider, E, T>
465where
466 Provider: BlockReaderIdExt<Header = <E::Primitives as NodePrimitives>::BlockHeader>
467 + ChainSpecProvider<ChainSpec: EthereumHardforks>
468 + StateProviderFactory
469 + Clone
470 + 'static,
471 E: ConfigureEvm + 'static,
472 T: PayloadTypes<ExecutionData = ExecutionData>,
473{
474 async fn validate_builder_submission_v1(
475 &self,
476 _request: BuilderBlockValidationRequest,
477 ) -> RpcResult<()> {
478 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV1` is not supported");
479 Err(internal_rpc_err("unimplemented"))
480 }
481
482 async fn validate_builder_submission_v2(
483 &self,
484 _request: BuilderBlockValidationRequestV2,
485 ) -> RpcResult<()> {
486 warn!(target: "rpc::flashbots", "Method `flashbots_validateBuilderSubmissionV2` is not supported");
487 Err(internal_rpc_err("unimplemented"))
488 }
489
490 async fn validate_builder_submission_v3(
492 &self,
493 request: BuilderBlockValidationRequestV3,
494 ) -> RpcResult<()> {
495 let this = self.clone();
496 let (tx, rx) = oneshot::channel();
497
498 self.task_spawner.spawn_blocking_task(async move {
499 let result = Self::validate_builder_submission_v3(&this, request)
500 .await
501 .map_err(ErrorObject::from);
502 let _ = tx.send(result);
503 });
504
505 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
506 }
507
508 async fn validate_builder_submission_v4(
510 &self,
511 request: BuilderBlockValidationRequestV4,
512 ) -> RpcResult<()> {
513 let this = self.clone();
514 let (tx, rx) = oneshot::channel();
515
516 self.task_spawner.spawn_blocking_task(async move {
517 let result = Self::validate_builder_submission_v4(&this, request)
518 .await
519 .map_err(ErrorObject::from);
520 let _ = tx.send(result);
521 });
522
523 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
524 }
525
526 async fn validate_builder_submission_v5(
528 &self,
529 request: BuilderBlockValidationRequestV5,
530 ) -> RpcResult<()> {
531 let this = self.clone();
532 let (tx, rx) = oneshot::channel();
533
534 self.task_spawner.spawn_blocking_task(async move {
535 let result = Self::validate_builder_submission_v5(&this, request)
536 .await
537 .map_err(ErrorObject::from);
538 let _ = tx.send(result);
539 });
540
541 rx.await.map_err(|_| internal_rpc_err("Internal blocking task error"))?
542 }
543}
544
545pub struct ValidationApiInner<Provider, E: ConfigureEvm, T: PayloadTypes> {
546 provider: Provider,
548 consensus: Arc<dyn FullConsensus<E::Primitives>>,
550 payload_validator:
552 Arc<dyn PayloadValidator<T, Block = <E::Primitives as NodePrimitives>::Block>>,
553 evm_config: E,
555 disallow: AddressSet,
557 validation_window: u64,
559 cached_state: RwLock<(B256, CachedReads)>,
564 task_spawner: Runtime,
566 metrics: ValidationMetrics,
568}
569
570fn hash_disallow_list(disallow: &AddressSet) -> String {
575 let mut sorted: Vec<_> = disallow.iter().collect();
576 sorted.sort_unstable(); let mut hasher = Sha256::new();
579 for addr in sorted {
580 hasher.update(addr.as_slice());
581 }
582
583 format!("{:x}", hasher.finalize())
584}
585
586impl<Provider, E: ConfigureEvm, T: PayloadTypes> fmt::Debug for ValidationApiInner<Provider, E, T> {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 f.debug_struct("ValidationApiInner").finish_non_exhaustive()
589 }
590}
591
592#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
594pub struct ValidationApiConfig {
595 pub disallow: AddressSet,
597 pub validation_window: u64,
599}
600
601impl ValidationApiConfig {
602 pub const DEFAULT_VALIDATION_WINDOW: u64 = 3;
604}
605
606impl Default for ValidationApiConfig {
607 fn default() -> Self {
608 Self { disallow: Default::default(), validation_window: Self::DEFAULT_VALIDATION_WINDOW }
609 }
610}
611
612#[derive(Debug, thiserror::Error)]
614pub enum ValidationApiError {
615 #[error("block gas limit mismatch: {_0}")]
616 GasLimitMismatch(GotExpected<u64>),
617 #[error("block gas used mismatch: {_0}")]
618 GasUsedMismatch(GotExpected<u64>),
619 #[error("block parent hash mismatch: {_0}")]
620 ParentHashMismatch(GotExpected<B256>),
621 #[error("block hash mismatch: {_0}")]
622 BlockHashMismatch(GotExpected<B256>),
623 #[error("missing latest block in database")]
624 MissingLatestBlock,
625 #[error("parent block not found")]
626 MissingParentBlock,
627 #[error("block is too old, outside validation window")]
628 BlockTooOld,
629 #[error("could not verify proposer payment")]
630 ProposerPayment,
631 #[error("invalid blobs bundle")]
632 InvalidBlobsBundle,
633 #[error("block accesses blacklisted address: {_0}")]
634 Blacklist(Address),
635 #[error(transparent)]
636 Blob(#[from] BlobTransactionValidationError),
637 #[error(transparent)]
638 Consensus(#[from] ConsensusError),
639 #[error(transparent)]
640 Provider(#[from] ProviderError),
641 #[error(transparent)]
642 Execution(#[from] BlockExecutionError),
643 #[error(transparent)]
644 Payload(#[from] NewPayloadError),
645}
646
647impl From<ValidationApiError> for ErrorObject<'static> {
648 fn from(error: ValidationApiError) -> Self {
649 match error {
650 ValidationApiError::GasLimitMismatch(_) |
651 ValidationApiError::GasUsedMismatch(_) |
652 ValidationApiError::ParentHashMismatch(_) |
653 ValidationApiError::BlockHashMismatch(_) |
654 ValidationApiError::Blacklist(_) |
655 ValidationApiError::ProposerPayment |
656 ValidationApiError::InvalidBlobsBundle |
657 ValidationApiError::Blob(_) => invalid_params_rpc_err(error.to_string()),
658
659 ValidationApiError::MissingLatestBlock |
660 ValidationApiError::MissingParentBlock |
661 ValidationApiError::BlockTooOld |
662 ValidationApiError::Consensus(_) |
663 ValidationApiError::Provider(_) => internal_rpc_err(error.to_string()),
664 ValidationApiError::Execution(err) => match err {
665 error @ BlockExecutionError::Validation(_) => {
666 invalid_params_rpc_err(error.to_string())
667 }
668 error @ BlockExecutionError::Internal(_) => internal_rpc_err(error.to_string()),
669 },
670 ValidationApiError::Payload(err) => match err {
671 error @ NewPayloadError::Eth(_) => invalid_params_rpc_err(error.to_string()),
672 error @ NewPayloadError::Other(_) => internal_rpc_err(error.to_string()),
673 },
674 }
675 }
676}
677
678#[derive(Metrics)]
680#[metrics(scope = "builder.validation")]
681pub(crate) struct ValidationMetrics {
682 pub(crate) disallow_size: Gauge,
684}
685
686#[cfg(test)]
687mod tests {
688 use super::{hash_disallow_list, AddressSet};
689 use revm_primitives::Address;
690
691 #[test]
692 fn test_hash_disallow_list_deterministic() {
693 let mut addresses = AddressSet::default();
694 addresses.insert(Address::from([1u8; 20]));
695 addresses.insert(Address::from([2u8; 20]));
696
697 let hash1 = hash_disallow_list(&addresses);
698 let hash2 = hash_disallow_list(&addresses);
699
700 assert_eq!(hash1, hash2);
701 }
702
703 #[test]
704 fn test_hash_disallow_list_different_content() {
705 let mut addresses1 = AddressSet::default();
706 addresses1.insert(Address::from([1u8; 20]));
707
708 let mut addresses2 = AddressSet::default();
709 addresses2.insert(Address::from([2u8; 20]));
710
711 let hash1 = hash_disallow_list(&addresses1);
712 let hash2 = hash_disallow_list(&addresses2);
713
714 assert_ne!(hash1, hash2);
715 }
716
717 #[test]
718 fn test_hash_disallow_list_order_independent() {
719 let mut addresses1 = AddressSet::default();
720 addresses1.insert(Address::from([1u8; 20]));
721 addresses1.insert(Address::from([2u8; 20]));
722
723 let mut addresses2 = AddressSet::default();
724 addresses2.insert(Address::from([2u8; 20])); addresses2.insert(Address::from([1u8; 20]));
726
727 let hash1 = hash_disallow_list(&addresses1);
728 let hash2 = hash_disallow_list(&addresses2);
729
730 assert_eq!(hash1, hash2);
731 }
732
733 #[test]
734 fn test_disallow_list_hash_rbuilder_parity() {
736 let json = r#"["0x05E0b5B40B7b66098C2161A5EE11C5740A3A7C45","0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1","0x03893a7c7463AE47D46bc7f091665f1893656003","0x04DBA1194ee10112fE6C3207C0687DEf0e78baCf"]"#;
737 let blocklist: Vec<Address> = serde_json::from_str(json).unwrap();
738 let blocklist: AddressSet = blocklist.into_iter().collect();
739 let expected_hash = "ee14e9d115e182f61871a5a385ab2f32ecf434f3b17bdbacc71044810d89e608";
740 let hash = hash_disallow_list(&blocklist);
741 assert_eq!(expected_hash, hash);
742 }
743}