diff --git a/rust/agents/relayer/src/msg/blacklist.rs b/rust/agents/relayer/src/msg/blacklist.rs new file mode 100644 index 000000000..1e9202086 --- /dev/null +++ b/rust/agents/relayer/src/msg/blacklist.rs @@ -0,0 +1,123 @@ +use hyperlane_core::HyperlaneMessage; + +#[derive(Debug, Clone, Default)] +pub struct AddressBlacklist { + // A list of addresses that are blocked from being relayed. + // Addresses are any length to support different address types. + pub blacklist: Vec>, +} + +impl AddressBlacklist { + pub fn new(blacklist: Vec>) -> Self { + Self { blacklist } + } + + /// Returns true if the message is blocked by the blacklist. + /// At the moment, this only checks if the sender, recipient, or body of the + /// message contains any of the blocked addresses. + pub fn find_blacklisted_address(&self, message: &HyperlaneMessage) -> Option> { + self.blacklist.iter().find_map(|address| { + if is_subsequence(message.sender.as_bytes(), address) + || is_subsequence(message.recipient.as_bytes(), address) + || is_subsequence(&message.body, address) + { + // Return the blocked address that was found. + Some(address.clone()) + } else { + None + } + }) + } +} + +/// Returns true if `needle` is a subsequence of `haystack`. +fn is_subsequence(mut haystack: &[T], needle: &[T]) -> bool { + if needle.is_empty() { + return true; + } + + while !haystack.is_empty() { + if needle.len() > haystack.len() { + return false; + } + if haystack.starts_with(needle) { + return true; + } + haystack = &haystack[1..]; + } + false +} + +#[cfg(test)] +mod test { + use hyperlane_core::H256; + + use super::*; + + #[test] + fn test_is_subsequence() { + assert!(is_subsequence(b"hello", b"hello")); + assert!(is_subsequence(b"hello", b"he")); + assert!(is_subsequence(b"hello", b"lo")); + assert!(is_subsequence(b"hello", b"")); + assert!(is_subsequence(b"hello", b"o")); + + assert!(!is_subsequence(b"hello", b"hello world")); + assert!(!is_subsequence(b"hello", b"world")); + assert!(!is_subsequence(b"hello", b"world hello")); + } + + #[test] + fn test_is_blocked() { + let blocked = b"blocked"; + let blocklist = AddressBlacklist::new(vec![blocked.to_vec()]); + + let bytes_with_subsequence = |subsequence: &[u8], index: usize, len: usize| { + let mut bytes = vec![0; len]; + bytes[index..index + subsequence.len()].copy_from_slice(subsequence); + bytes + }; + + let h256_with_subsequence = |subsequence: &[u8], index: usize| { + let bytes = bytes_with_subsequence(subsequence, index, H256::len_bytes()); + H256::from_slice(&bytes) + }; + + // Blocked - sender includes the blocked address + let message = HyperlaneMessage { + sender: h256_with_subsequence(blocked, 0), + ..Default::default() + }; + assert_eq!( + blocklist.find_blacklisted_address(&message), + Some(blocked.to_vec()) + ); + + // Blocked - recipient includes the blocked address + let message = HyperlaneMessage { + recipient: h256_with_subsequence(blocked, 20), + ..Default::default() + }; + assert_eq!( + blocklist.find_blacklisted_address(&message), + Some(blocked.to_vec()) + ); + + // Blocked - body includes the blocked address + let message = HyperlaneMessage { + body: bytes_with_subsequence(blocked, 100 - blocked.len(), 100), + ..Default::default() + }; + assert_eq!( + blocklist.find_blacklisted_address(&message), + Some(blocked.to_vec()) + ); + + // Not blocked - sender, recipient, and body do not include the blocked address + let message = HyperlaneMessage { + body: vec![1; 100], + ..Default::default() + }; + assert!(blocklist.find_blacklisted_address(&message).is_none()); + } +} diff --git a/rust/agents/relayer/src/msg/mod.rs b/rust/agents/relayer/src/msg/mod.rs index dd7bac22b..abf00ec05 100644 --- a/rust/agents/relayer/src/msg/mod.rs +++ b/rust/agents/relayer/src/msg/mod.rs @@ -25,6 +25,7 @@ //! - FallbackProviderSubmitter (Serialized, but if some RPC provider sucks, //! switch everyone to new one) +pub(crate) mod blacklist; pub(crate) mod gas_payment; pub(crate) mod metadata; pub(crate) mod op_queue; diff --git a/rust/agents/relayer/src/msg/processor.rs b/rust/agents/relayer/src/msg/processor.rs index 664c1dc64..40808ad3d 100644 --- a/rust/agents/relayer/src/msg/processor.rs +++ b/rust/agents/relayer/src/msg/processor.rs @@ -8,6 +8,7 @@ use std::{ use async_trait::async_trait; use derive_new::new; +use ethers::utils::hex; use eyre::Result; use hyperlane_base::{ db::{HyperlaneRocksDB, ProcessMessage}, @@ -18,15 +19,19 @@ use prometheus::IntGauge; use tokio::sync::mpsc::UnboundedSender; use tracing::{debug, instrument, trace}; -use super::{metadata::AppContextClassifier, pending_message::*}; +use super::{blacklist::AddressBlacklist, metadata::AppContextClassifier, pending_message::*}; use crate::{processor::ProcessorExt, settings::matching_list::MatchingList}; /// Finds unprocessed messages from an origin and submits then through a channel /// for to the appropriate destination. #[allow(clippy::too_many_arguments)] pub struct MessageProcessor { - whitelist: Arc, - blacklist: Arc, + /// A matching list of messages that should be whitelisted. + message_whitelist: Arc, + /// A matching list of messages that should be blacklisted. + message_blacklist: Arc, + /// Addresses that messages may not interact with. + address_blacklist: Arc, metrics: MessageProcessorMetrics, /// channel for each destination chain to send operations (i.e. message /// submissions) to @@ -217,8 +222,8 @@ impl Debug for MessageProcessor { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "MessageProcessor {{ whitelist: {:?}, blacklist: {:?}, nonce_iterator: {:?}}}", - self.whitelist, self.blacklist, self.nonce_iterator + "MessageProcessor {{ message_whitelist: {:?}, message_blacklist: {:?}, address_blacklist: {:?}, nonce_iterator: {:?}}}", + self.message_whitelist, self.message_blacklist, self.address_blacklist, self.nonce_iterator ) } } @@ -247,14 +252,25 @@ impl ProcessorExt for MessageProcessor { let destination = msg.destination; // Skip if not whitelisted. - if !self.whitelist.msg_matches(&msg, true) { - debug!(?msg, whitelist=?self.whitelist, "Message not whitelisted, skipping"); + if !self.message_whitelist.msg_matches(&msg, true) { + debug!(?msg, whitelist=?self.message_whitelist, "Message not whitelisted, skipping"); return Ok(()); } // Skip if the message is blacklisted - if self.blacklist.msg_matches(&msg, false) { - debug!(?msg, blacklist=?self.blacklist, "Message blacklisted, skipping"); + if self.message_whitelist.msg_matches(&msg, false) { + debug!(?msg, blacklist=?self.message_whitelist, "Message blacklisted, skipping"); + return Ok(()); + } + + // Skip if the message involves a blacklisted address + if let Some(blacklisted_address) = self.address_blacklist.find_blacklisted_address(&msg) + { + debug!( + ?msg, + blacklisted_address = hex::encode(blacklisted_address), + "Message involves blacklisted address, skipping" + ); return Ok(()); } @@ -291,18 +307,21 @@ impl ProcessorExt for MessageProcessor { } impl MessageProcessor { + #[allow(clippy::too_many_arguments)] pub fn new( db: HyperlaneRocksDB, - whitelist: Arc, - blacklist: Arc, + message_whitelist: Arc, + message_blacklist: Arc, + address_blacklist: Arc, metrics: MessageProcessorMetrics, send_channels: HashMap>, destination_ctxs: HashMap>, metric_app_contexts: Vec<(MatchingList, String)>, ) -> Self { Self { - whitelist, - blacklist, + message_whitelist, + message_blacklist, + address_blacklist, metrics, send_channels, destination_ctxs, @@ -477,6 +496,7 @@ mod test { db.clone(), Default::default(), Default::default(), + Default::default(), dummy_processor_metrics(origin_domain.id()), HashMap::from([(destination_domain.id(), send_channel)]), HashMap::from([(destination_domain.id(), message_context)]), diff --git a/rust/agents/relayer/src/relayer.rs b/rust/agents/relayer/src/relayer.rs index 3be0f1e59..e8b903153 100644 --- a/rust/agents/relayer/src/relayer.rs +++ b/rust/agents/relayer/src/relayer.rs @@ -33,6 +33,7 @@ use tracing::{error, info, info_span, instrument::Instrumented, warn, Instrument use crate::{ merkle_tree::builder::MerkleTreeBuilder, msg::{ + blacklist::AddressBlacklist, gas_payment::GasPaymentEnforcer, metadata::{BaseMetadataBuilder, IsmAwareAppContextClassifier}, op_submitter::{SerialSubmitter, SerialSubmitterMetrics}, @@ -70,8 +71,9 @@ pub struct Relayer { prover_syncs: HashMap>>, merkle_tree_hook_syncs: HashMap>>, dbs: HashMap, - whitelist: Arc, - blacklist: Arc, + message_whitelist: Arc, + message_blacklist: Arc, + address_blacklist: Arc, transaction_gas_limit: Option, skip_transaction_gas_limit_for: HashSet, allow_local_checkpoint_syncers: bool, @@ -89,11 +91,12 @@ impl Debug for Relayer { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "Relayer {{ origin_chains: {:?}, destination_chains: {:?}, whitelist: {:?}, blacklist: {:?}, transaction_gas_limit: {:?}, skip_transaction_gas_limit_for: {:?}, allow_local_checkpoint_syncers: {:?} }}", + "Relayer {{ origin_chains: {:?}, destination_chains: {:?}, message_whitelist: {:?}, message_blacklist: {:?}, address_blacklist: {:?}, transaction_gas_limit: {:?}, skip_transaction_gas_limit_for: {:?}, allow_local_checkpoint_syncers: {:?} }}", self.origin_chains, self.destination_chains, - self.whitelist, - self.blacklist, + self.message_whitelist, + self.message_blacklist, + self.address_blacklist, self.transaction_gas_limit, self.skip_transaction_gas_limit_for, self.allow_local_checkpoint_syncers @@ -177,14 +180,16 @@ impl BaseAgent for Relayer { .map(|(k, v)| (k, v as _)) .collect(); - let whitelist = Arc::new(settings.whitelist); - let blacklist = Arc::new(settings.blacklist); + let message_whitelist = Arc::new(settings.whitelist); + let message_blacklist = Arc::new(settings.blacklist); + let address_blacklist = Arc::new(AddressBlacklist::new(settings.address_blacklist)); let skip_transaction_gas_limit_for = settings.skip_transaction_gas_limit_for; let transaction_gas_limit = settings.transaction_gas_limit; info!( - %whitelist, - %blacklist, + %message_whitelist, + %message_blacklist, + ?address_blacklist, ?transaction_gas_limit, ?skip_transaction_gas_limit_for, "Whitelist configuration" @@ -275,8 +280,9 @@ impl BaseAgent for Relayer { interchain_gas_payment_syncs, prover_syncs, merkle_tree_hook_syncs, - whitelist, - blacklist, + message_whitelist, + message_blacklist, + address_blacklist, transaction_gas_limit, skip_transaction_gas_limit_for, allow_local_checkpoint_syncers: settings.allow_local_checkpoint_syncers, @@ -487,8 +493,9 @@ impl Relayer { let message_processor = MessageProcessor::new( self.dbs.get(origin).unwrap().clone(), - self.whitelist.clone(), - self.blacklist.clone(), + self.message_whitelist.clone(), + self.message_blacklist.clone(), + self.address_blacklist.clone(), metrics, send_channels, destination_ctxs, diff --git a/rust/agents/relayer/src/settings/mod.rs b/rust/agents/relayer/src/settings/mod.rs index 2e6b00573..dc4c1e543 100644 --- a/rust/agents/relayer/src/settings/mod.rs +++ b/rust/agents/relayer/src/settings/mod.rs @@ -8,6 +8,7 @@ use std::{collections::HashSet, path::PathBuf}; use convert_case::Case; use derive_more::{AsMut, AsRef, Deref, DerefMut}; +use ethers::utils::hex; use eyre::{eyre, Context}; use hyperlane_base::{ impl_loadable_from_settings, @@ -46,6 +47,10 @@ pub struct RelayerSettings { pub whitelist: MatchingList, /// Filter for what messages to block. pub blacklist: MatchingList, + /// Filter for what addresses to block interactions with. + /// This is intentionally not an H256 to allow for addresses of any length without + /// adding any padding. + pub address_blacklist: Vec>, /// This is optional. If not specified, any amount of gas will be valid, otherwise this /// is the max allowed gas in wei to relay a transaction. pub transaction_gas_limit: Option, @@ -191,6 +196,14 @@ impl FromRawConf for RelayerSettings { .and_then(parse_matching_list) .unwrap_or_default(); + let address_blacklist = p + .chain(&mut err) + .get_opt_key("addressBlacklist") + .parse_string() + .end() + .map(|str| parse_address_list(str, &mut err, || &p.cwp + "address_blacklist")) + .unwrap_or_default(); + let transaction_gas_limit = p .chain(&mut err) .get_opt_key("transactionGasLimit") @@ -268,6 +281,7 @@ impl FromRawConf for RelayerSettings { gas_payment_enforcement, whitelist, blacklist, + address_blacklist, transaction_gas_limit, skip_transaction_gas_limit_for, allow_local_checkpoint_syncers, @@ -311,3 +325,53 @@ fn parse_matching_list(p: ValueParser) -> ConfigResult { err.into_result(ml) } + +fn parse_address_list( + str: &str, + err: &mut ConfigParsingError, + err_path: impl Fn() -> ConfigPath, +) -> Vec> { + str.split(',') + .filter_map(|s| { + let mut s = s.trim().to_owned(); + if let Some(stripped) = s.strip_prefix("0x") { + s = stripped.to_owned(); + } + hex::decode(s).take_err(err, &err_path) + }) + .collect_vec() +} + +#[cfg(test)] +mod test { + use super::*; + use hyperlane_core::H160; + + #[test] + fn test_parse_address_blacklist() { + let valid_address1 = b"valid".to_vec(); + let valid_address2 = H160::random().as_bytes().to_vec(); + + // Successful parsing + let input = format!( + "0x{}, {}", + hex::encode(&valid_address1), + hex::encode(&valid_address2) + ); + let mut err = ConfigParsingError::default(); + let res = parse_address_list(&input, &mut err, ConfigPath::default); + assert_eq!(res, vec![valid_address1.clone(), valid_address2.clone()]); + assert!(err.is_ok()); + + // An error in the final address provided + let input = format!( + "0x{}, {}, 0xaazz", + hex::encode(&valid_address1), + hex::encode(&valid_address2) + ); + let mut err = ConfigParsingError::default(); + let res = parse_address_list(&input, &mut err, ConfigPath::default); + assert_eq!(res, vec![valid_address1, valid_address2]); + assert!(!err.is_ok()); + } +} diff --git a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs index 7314e2a00..ae6629e95 100644 --- a/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs +++ b/rust/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs @@ -546,10 +546,10 @@ pub(crate) mod test { pub sequence: u32, } - impl Into> for MockSequencedData { - fn into(self) -> Indexed { - let sequence = self.sequence; - Indexed::new(self).with_sequence(sequence) + impl From for Indexed { + fn from(val: MockSequencedData) -> Self { + let sequence = val.sequence; + Indexed::new(val).with_sequence(sequence) } } diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 566986588..32c5831e4 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -223,7 +223,7 @@ const hyperlane: RootAgentConfig = { rpcConsensusType: RpcConsensusType.Fallback, docker: { repo, - tag: '3bb9d0a-20240619-130157', + tag: '3fddaeb-20240619-163111', }, gasPaymentEnforcement: gasPaymentEnforcement, metricAppContexts, diff --git a/typescript/infra/src/config/agent/relayer.ts b/typescript/infra/src/config/agent/relayer.ts index 3d88c16ff..f9d4bac73 100644 --- a/typescript/infra/src/config/agent/relayer.ts +++ b/typescript/infra/src/config/agent/relayer.ts @@ -1,4 +1,6 @@ import { BigNumberish } from 'ethers'; +import { Logger } from 'pino'; +import { z } from 'zod'; import { AgentConfig, @@ -10,7 +12,13 @@ import { MatchingList, RelayerConfig as RelayerAgentConfig, } from '@hyperlane-xyz/sdk'; -import { Address, ProtocolType, addressToBytes32 } from '@hyperlane-xyz/utils'; +import { + Address, + ProtocolType, + addressToBytes32, + isValidAddressEvm, + rootLogger, +} from '@hyperlane-xyz/utils'; import { getChain, getDomainId } from '../../../config/registry.js'; import { AgentAwsUser } from '../../agents/aws/user.js'; @@ -34,6 +42,7 @@ export interface BaseRelayerConfig { gasPaymentEnforcement: GasPaymentEnforcement[]; whitelist?: MatchingList; blacklist?: MatchingList; + addressBlacklist?: string; transactionGasLimit?: BigNumberish; skipTransactionGasLimitFor?: string[]; metricAppContexts?: MetricAppContext[]; @@ -58,12 +67,15 @@ export interface HelmRelayerChainValues { export class RelayerConfigHelper extends AgentConfigHelper { readonly #relayerConfig: BaseRelayerConfig; + readonly logger: Logger; constructor(agentConfig: RootAgentConfig) { if (!agentConfig.relayer) throw Error('Relayer is not defined for this context'); super(agentConfig, agentConfig.relayer); + this.#relayerConfig = agentConfig.relayer; + this.logger = rootLogger.child({ module: 'RelayerConfigHelper' }); } async buildConfig(): Promise { @@ -80,6 +92,11 @@ export class RelayerConfigHelper extends AgentConfigHelper { if (baseConfig.blacklist) { relayerConfig.blacklist = JSON.stringify(baseConfig.blacklist); } + + relayerConfig.addressBlacklist = (await this.getSanctionedAddresses()).join( + ',', + ); + if (baseConfig.transactionGasLimit) { relayerConfig.transactionGasLimit = baseConfig.transactionGasLimit.toString(); @@ -130,6 +147,40 @@ export class RelayerConfigHelper extends AgentConfigHelper { return chainSigners; } + async getSanctionedAddresses() { + // All Ethereum-style addresses from https://github.com/0xB10C/ofac-sanctioned-digital-currency-addresses/tree/lists + const currencies = ['ARB', 'ETC', 'ETH', 'USDC', 'USDT']; + + const schema = z.array(z.string()); + + const allSanctionedAddresses = await Promise.all( + currencies.map(async (currency) => { + const rawUrl = `https://raw.githubusercontent.com/0xB10C/ofac-sanctioned-digital-currency-addresses/lists/sanctioned_addresses_${currency}.json`; + this.logger.debug( + { + currency, + rawUrl, + }, + 'Fetching sanctioned addresses', + ); + const json = await fetch(rawUrl); + const sanctionedAddresses = schema.parse(await json.json()); + return sanctionedAddresses; + }), + ); + + return allSanctionedAddresses.flat().filter((address) => { + if (!isValidAddressEvm(address)) { + this.logger.debug( + { address }, + 'Invalid sanctioned address, throwing out', + ); + return false; + } + return true; + }); + } + // Returns whether the relayer requires AWS credentials get requiresAwsCredentials(): boolean { // If AWS is present on the agentConfig, we are using AWS keys and need credentials regardless. diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index 49f05e883..ae20d551b 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -314,6 +314,10 @@ export const RelayerAgentConfigSchema = AgentConfigSchema.extend({ .describe( 'If no blacklist is provided ALL will be considered to not be on the blacklist.', ), + addressBlacklist: z + .string() + .optional() + .describe('Comma separated list of addresses to blacklist.'), transactionGasLimit: ZUWei.optional().describe( 'This is optional. If not specified, any amount of gas will be valid, otherwise this is the max allowed gas in wei to relay a transaction.', ),