Skip to content

Implement zeroShadow Hermod support#4426

Open
jmg-duarte wants to merge 1 commit into
mainfrom
jmgd/hermod
Open

Implement zeroShadow Hermod support#4426
jmg-duarte wants to merge 1 commit into
mainfrom
jmgd/hermod

Conversation

@jmg-duarte
Copy link
Copy Markdown
Contributor

Description

Adds support for Hermod, zeroShadow's self-hosted sanctioned-address agent, as an additional banned-user backend alongside the existing Chainalysis on-chain oracle.

Hermod is queried over HTTP with an HMAC-SHA256 obfuscated form of the user address (per-customer key), so we never send a raw address off-host. A hit returns 200, a miss returns 404. Both backends can run together — an address is banned if either reports it as sanctioned.

Changes

  • New HermodConfig in crates/configs/src/banned_users.rs (url, hmac_key, optional api_key), wired into both autopilot and orderbook startup.
  • HMAC key and API key support the %ENV_VAR indirection.
    • Added deserialize_optional_string_from_env helper for the optional API key.
    • Debug impl redacts both secrets.
  • New crates/order-validation/src/banned/hermod.rs implementing the agent client: signs {address:#x} with HMAC-SHA256, hits GET <base>/addresses/<hex-sig>, optional Bearer auth, base-URL trailing-slash normalisation.
  • Refactored crates/order-validation/src/banned.rs to introduce a Backend trait shared by the Chainalysis Onchain checker and the new Hermod checker.
    • Same 1-hour LRU cache and 60s background refresh behaviour for both. Users::banned now fans out to whichever subset of backends is configured.
  • Unit tests for config deserialisation, secret redaction, env-var indirection, HMAC determinism, and base-URL normalisation.

How to test

  1. cargo nextest run -p configs -p order-validation — covers the new config parsing, redaction, env-var indirection, HMAC signing, and URL normalisation tests.

@jmg-duarte jmg-duarte marked this pull request as ready for review May 21, 2026 09:50
@jmg-duarte jmg-duarte requested a review from a team as a code owner May 21, 2026 09:50
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the Hermod (zeroShadow) sanctioned-address checker and refactors the banned-user detection logic into a trait-based system. The implementation includes HMAC-SHA256 address obfuscation, HTTP-based lookups, and a background cache maintenance task. Critical feedback was provided regarding the use of unbounded join_all for network requests; it is recommended to use concurrency-limited streams like buffer_unordered in both the lookup and maintenance flows to ensure system stability and avoid triggering rate limits.

Comment on lines +54 to +73
async fn check(&self, addresses: &HashSet<Address>, banned: &mut HashSet<Address>) {
let mut need_lookup = Vec::new();
for address in addresses {
if banned.contains(address) {
continue;
}
match self.cache().get(address) {
Some(metadata) => {
metadata.is_banned.then(|| banned.insert(*address));
}
None => need_lookup.push(*address),
}
}

let to_cache = join_all(
need_lookup
.into_iter()
.map(|address| async move { (address, self.fetch(address).await) }),
)
.await;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using join_all without a concurrency limit can lead to a burst of HTTP requests if need_lookup contains many addresses. This can cause stability issues or trigger rate limits on the backend. Consider using a concurrency-limited stream instead. Additionally, ensure that individual fetch errors (like timeouts) are handled explicitly and not cached, as per repository guidelines on batch operations and caching.

    async fn check(&self, addresses: &HashSet<Address>, banned: &mut HashSet<Address>) {
        use futures::StreamExt;

        let mut need_lookup = Vec::new();
        for address in addresses {
            if banned.contains(address) {
                continue;
            }
            match self.cache().get(address) {
                Some(metadata) => {
                    metadata.is_banned.then(|| banned.insert(*address));
                }
                None => need_lookup.push(*address),
            }
        }

        let to_cache = futures::stream::iter(need_lookup)
            .map(|address| async move { (address, self.fetch(address).await) })
            .buffer_unordered(10)
            .collect::<Vec<_>>()
            .await;
    }
References
  1. When fetching a batch of items where individual fetches can fail, do not silently ignore errors.
  2. Do not cache timeouts if the timeout is moved deeper into the code, as this might lead to incorrect caching of failures.

Comment on lines +138 to +145
let results = join_all(
expired_data
.into_iter()
.map(|(address, metadata)| detector.determine_status(*address, metadata)),
)
.await
.into_iter()
.flatten();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The background maintenance task uses join_all on all expired entries. If a large portion of the cache expires simultaneously, this will attempt to fire thousands of concurrent HTTP requests. Use buffer_unordered to limit concurrency. Additionally, ensure errors are not silently ignored (e.g., by using .flatten() on Results); in background tasks, unrecoverable errors should trigger a fail-fast response like panicking, as per repository guidelines.

                let results = futures::stream::iter(expired_data)
                    .map(|(address, metadata)| detector.determine_status(*address, metadata))
                    .buffer_unordered(10)
                    .collect::<Vec<_>>()
                    .await
                    .into_iter()
                    .flatten();
References
  1. In critical background tasks, panicking on unrecoverable errors is an acceptable strategy to ensure a fail-fast behavior.
  2. When fetching a batch of items where individual fetches can fail, do not silently ignore errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant