Separate banned address backends from caches#4446
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the banned user detection logic by introducing a shared Cached layer that handles caching, batching, and background refresh for all backends, making the Hermod and Onchain backends pure fetchers. A critical security issue was identified in fetch_all where a single backend failure could cause the entire check to fail-open, potentially bypassing banned status checks.
| async fn fetch_all(&self, address: Address) -> Option<bool> { | ||
| join_all(self.backends.iter().map(|b| fetch_one(b.as_ref(), address))) | ||
| .await | ||
| .into_iter() | ||
| .collect::<Option<Vec<bool>>>() | ||
| .map(|results| results.into_iter().any(|banned| banned)) | ||
| } |
There was a problem hiding this comment.
If any backend fails (returns None), the current implementation of fetch_all returns None due to the behavior of collect::<Option<Vec<bool>>>(). This causes the entire check to fail-open and ignore any successful Some(true) (banned) results from other backends, creating a security bypass. Instead, if any backend successfully identifies an address as banned, fetch_all should return Some(true).
| async fn fetch_all(&self, address: Address) -> Option<bool> { | |
| join_all(self.backends.iter().map(|b| fetch_one(b.as_ref(), address))) | |
| .await | |
| .into_iter() | |
| .collect::<Option<Vec<bool>>>() | |
| .map(|results| results.into_iter().any(|banned| banned)) | |
| } | |
| async fn fetch_all(&self, address: Address) -> Option<bool> { | |
| let results = join_all(self.backends.iter().map(|b| fetch_one(b.as_ref(), address))).await; | |
| if results.iter().any(|r| *r == Some(true)) { | |
| Some(true) | |
| } else if results.iter().any(|r| r.is_none()) { | |
| None | |
| } else { | |
| Some(false) | |
| } | |
| } |
Description
Separates the cache layer from the remote backends in the banned-user validator. Chainalysis and Hermod previously each owned their own caching; this consolidates it into one shared layer with pure fetchers behind it.
Changes
banned.rsinto abanned/module:mod.rs(publicUsersAPI),cached.rs(shared cache + refresh),onchain.rs(Chainalysis),hermod.rs(zeroShadow).Backendtrait — backends only implementfetch(address)andname(); caching/batching/refresh is no longer their concern.BackendErrorenum with#[from]conversions.How to test
cargo nextest run -p order-validationcargo clippy --locked --workspace --all-features --all-targets -- -D warnings