diff --git a/rust/agents/relayer/src/msg/processor.rs b/rust/agents/relayer/src/msg/processor.rs index 11a26797f..831468083 100644 --- a/rust/agents/relayer/src/msg/processor.rs +++ b/rust/agents/relayer/src/msg/processor.rs @@ -14,7 +14,7 @@ use abacus_core::{ db::AbacusDB, AbacusCommon, AbacusContract, CommittedMessage, MultisigSignedCheckpoint, Outbox, }; -use crate::{merkle_tree_builder::MerkleTreeBuilder, settings::whitelist::Whitelist}; +use crate::{merkle_tree_builder::MerkleTreeBuilder, settings::matching_list::MatchingList}; use super::SubmitMessageArgs; @@ -23,7 +23,8 @@ pub(crate) struct MessageProcessor { outbox: Outboxes, db: AbacusDB, inbox_contracts: InboxContracts, - whitelist: Arc, + whitelist: Arc, + blacklist: Arc, metrics: MessageProcessorMetrics, tx_msg: mpsc::UnboundedSender, ckpt_rx: watch::Receiver>, @@ -37,7 +38,8 @@ impl MessageProcessor { outbox: Outboxes, db: AbacusDB, inbox_contracts: InboxContracts, - whitelist: Arc, + whitelist: Arc, + blacklist: Arc, metrics: MessageProcessorMetrics, tx_msg: mpsc::UnboundedSender, ckpt_rx: watch::Receiver>, @@ -47,6 +49,7 @@ impl MessageProcessor { db: db.clone(), inbox_contracts, whitelist, + blacklist, metrics, tx_msg, ckpt_rx, @@ -143,7 +146,7 @@ impl MessageProcessor { } // Skip if not whitelisted. - if !self.whitelist.msg_matches(&message.message) { + if !self.whitelist.msg_matches(&message.message, true) { debug!( inbox_name=?self.inbox_contracts.inbox.chain_name(), local_domain=?self.inbox_contracts.inbox.local_domain(), @@ -155,6 +158,19 @@ impl MessageProcessor { return Ok(()); } + // skip if the message is blacklisted + if self.blacklist.msg_matches(&message.message, false) { + debug!( + inbox_name=?self.inbox_contracts.inbox.chain_name(), + local_domain=?self.inbox_contracts.inbox.local_domain(), + dst=?message.message.destination, + blacklist=?self.blacklist, + msg=?message, + "Message blacklisted, skipping idx {}", self.message_leaf_index); + self.message_leaf_index += 1; + return Ok(()); + } + // If validator hasn't published checkpoint covering self.message_leaf_index yet, wait // until it has, before forwarding the message to the submitter channel. let mut ckpt; diff --git a/rust/agents/relayer/src/relayer.rs b/rust/agents/relayer/src/relayer.rs index 2e77460e6..4e2031960 100644 --- a/rust/agents/relayer/src/relayer.rs +++ b/rust/agents/relayer/src/relayer.rs @@ -18,7 +18,7 @@ use abacus_core::{AbacusContract, MultisigSignedCheckpoint}; use crate::msg::gelato_submitter::GelatoSubmitter; use crate::msg::processor::{MessageProcessor, MessageProcessorMetrics}; use crate::msg::serial_submitter::SerialSubmitter; -use crate::settings::whitelist::Whitelist; +use crate::settings::matching_list::MatchingList; use crate::settings::RelayerSettings; use crate::{checkpoint_fetcher::CheckpointFetcher, msg::serial_submitter::SerialSubmitterMetrics}; @@ -28,7 +28,8 @@ pub struct Relayer { signed_checkpoint_polling_interval: u64, multisig_checkpoint_syncer: MultisigCheckpointSyncer, core: AbacusAgentCore, - whitelist: Arc, + whitelist: Arc, + blacklist: Arc, } impl AsRef for Relayer { @@ -51,16 +52,10 @@ impl Agent for Relayer { let multisig_checkpoint_syncer: MultisigCheckpointSyncer = settings .multisigcheckpointsyncer .try_into_multisig_checkpoint_syncer()?; - let whitelist = Arc::new( - settings - .whitelist - .as_ref() - .map(|wl| serde_json::from_str(wl)) - .transpose() - .expect("Invalid whitelist received") - .unwrap_or_default(), - ); - info!(whitelist = %whitelist, "Whitelist configuration"); + + let whitelist = parse_matching_list(&settings.whitelist); + let blacklist = parse_matching_list(&settings.blacklist); + info!(whitelist = %whitelist, blacklist = %blacklist, "Whitelist configuration"); Ok(Self { signed_checkpoint_polling_interval: settings @@ -73,6 +68,7 @@ impl Agent for Relayer { .try_into_abacus_core(Self::AGENT_NAME, true) .await?, whitelist, + blacklist, }) } } @@ -152,6 +148,7 @@ impl Relayer { self.outbox().db(), inbox_contracts, self.whitelist.clone(), + self.blacklist.clone(), metrics, new_messages_send_channel, signed_checkpoint_receiver, @@ -200,5 +197,15 @@ impl Relayer { } } +fn parse_matching_list(list: &Option) -> Arc { + Arc::new( + list.as_deref() + .map(serde_json::from_str) + .transpose() + .expect("Invalid matching list received") + .unwrap_or_default(), + ) +} + #[cfg(test)] mod test {} diff --git a/rust/agents/relayer/src/settings/whitelist.rs b/rust/agents/relayer/src/settings/matching_list.rs similarity index 52% rename from rust/agents/relayer/src/settings/whitelist.rs rename to rust/agents/relayer/src/settings/matching_list.rs index 38c9294d9..fcd565867 100644 --- a/rust/agents/relayer/src/settings/whitelist.rs +++ b/rust/agents/relayer/src/settings/matching_list.rs @@ -9,17 +9,17 @@ use serde::{Deserialize, Deserializer}; use abacus_core::AbacusMessage; -/// Whitelist defining which messages should be relayed. If no wishlist is provided ALL -/// messages will be relayed. +/// Defines a set of patterns for determining if a message should or should not +/// be relayed. This is useful for determine if a message matches a given set or +/// rules. /// /// Valid options for each of the tuple elements are /// - wildcard "*" /// - single value in decimal or hex (must start with `0x`) format /// - list of values in decimal or hex format -/// - defaults to wildcards #[derive(Debug, Deserialize, Default, Clone)] #[serde(transparent)] -pub struct Whitelist(Option>); +pub struct MatchingList(Option>); #[derive(Debug, Clone, PartialEq)] enum Filter { @@ -177,51 +177,71 @@ impl<'de> Deserialize<'de> for Filter { } #[derive(Debug, Deserialize, Clone)] -#[serde(tag = "type", rename_all = "camelCase")] -struct WhitelistElement { - #[serde(default)] - source_domain: Filter, - #[serde(default)] - source_address: Filter, - #[serde(default)] - destination_domain: Filter, - #[serde(default)] - destination_address: Filter, +#[serde(tag = "type")] +struct ListElement { + #[serde(default, rename = "sourceDomain")] + src_domain: Filter, + #[serde(default, rename = "sourceAddress")] + src_address: Filter, + #[serde(default, rename = "destinationDomain")] + dst_domain: Filter, + #[serde(default, rename = "destinationAddress")] + dst_address: Filter, } -impl Display for WhitelistElement { +impl Display for ListElement { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{{sourceDomain: {}, sourceAddress: {}, destinationDomain: {}, destinationAddress: {}}}", self.source_domain, self.source_address, self.destination_domain, self.destination_address) + write!(f, "{{sourceDomain: {}, sourceAddress: {}, destinationDomain: {}, destinationAddress: {}}}", self.src_domain, self.src_address, self.dst_domain, self.dst_address) } } -impl Whitelist { - pub fn msg_matches(&self, msg: &AbacusMessage) -> bool { - self.matches(msg.origin, &msg.sender, msg.destination, &msg.recipient) +#[derive(Copy, Clone, Debug)] +struct MatchInfo<'a> { + src_domain: u32, + src_addr: &'a H256, + dst_domain: u32, + dst_addr: &'a H256, +} + +impl<'a> From<&'a AbacusMessage> for MatchInfo<'a> { + fn from(msg: &'a AbacusMessage) -> Self { + Self { + src_domain: msg.origin, + src_addr: &msg.sender, + dst_domain: msg.destination, + dst_addr: &msg.recipient, + } } +} - pub fn matches( - &self, - src_domain: u32, - src_addr: &H256, - dst_domain: u32, - dst_addr: &H256, - ) -> bool { +impl MatchingList { + /// Check if a message matches any of the rules. + /// - `default`: What to return if the the matching list is empty. + pub fn msg_matches(&self, msg: &AbacusMessage, default: bool) -> bool { + self.matches(msg.into(), default) + } + + /// Check if a message matches any of the rules. + /// - `default`: What to return if the the matching list is empty. + fn matches(&self, info: MatchInfo, default: bool) -> bool { if let Some(rules) = &self.0 { - rules.iter().any(|rule| { - rule.source_domain.matches(&src_domain) - && rule.source_address.matches(src_addr) - && rule.destination_domain.matches(&dst_domain) - && rule.destination_address.matches(dst_addr) - }) + matches_any_rule(rules.iter(), info) } else { - // by default if there is no whitelist, allow everything - true + default } } } -impl Display for Whitelist { +fn matches_any_rule<'a>(mut rules: impl Iterator, info: MatchInfo) -> bool { + rules.any(|rule| { + rule.src_domain.matches(&info.src_domain) + && rule.src_address.matches(info.src_addr) + && rule.dst_domain.matches(&info.dst_domain) + && rule.dst_address.matches(info.dst_addr) + }) +} + +impl Display for MatchingList { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if let Some(wl) = &self.0 { write!(f, "[")?; @@ -250,62 +270,129 @@ fn parse_addr(addr_str: &str) -> Result { #[cfg(test)] mod test { + use crate::settings::matching_list::MatchInfo; use ethers::prelude::*; - use super::{Filter::*, Whitelist}; + use super::{Filter::*, MatchingList}; #[test] fn basic_config() { - let whitelist: Whitelist = serde_json::from_str(r#"[{"sourceDomain": "*", "sourceAddress": "*", "destinationDomain": "*", "destinationAddress": "*"}, {}]"#).unwrap(); - assert!(whitelist.0.is_some()); - assert_eq!(whitelist.0.as_ref().unwrap().len(), 2); - let elem = &whitelist.0.as_ref().unwrap()[0]; - assert_eq!(elem.destination_domain, Wildcard); - assert_eq!(elem.destination_address, Wildcard); - assert_eq!(elem.source_domain, Wildcard); - assert_eq!(elem.source_address, Wildcard); - - let elem = &whitelist.0.as_ref().unwrap()[1]; - assert_eq!(elem.destination_domain, Wildcard); - assert_eq!(elem.destination_address, Wildcard); - assert_eq!(elem.source_domain, Wildcard); - assert_eq!(elem.source_address, Wildcard); + let list: MatchingList = serde_json::from_str(r#"[{"sourceDomain": "*", "sourceAddress": "*", "destinationDomain": "*", "destinationAddress": "*"}, {}]"#).unwrap(); + assert!(list.0.is_some()); + assert_eq!(list.0.as_ref().unwrap().len(), 2); + let elem = &list.0.as_ref().unwrap()[0]; + assert_eq!(elem.dst_domain, Wildcard); + assert_eq!(elem.dst_address, Wildcard); + assert_eq!(elem.src_domain, Wildcard); + assert_eq!(elem.src_address, Wildcard); + + let elem = &list.0.as_ref().unwrap()[1]; + assert_eq!(elem.dst_domain, Wildcard); + assert_eq!(elem.dst_address, Wildcard); + assert_eq!(elem.src_domain, Wildcard); + assert_eq!(elem.src_address, Wildcard); + + assert!(list.matches( + MatchInfo { + src_domain: 0, + src_addr: &H256::default(), + dst_domain: 0, + dst_addr: &H256::default() + }, + false + )); + + assert!(list.matches( + MatchInfo { + src_domain: 34, + src_addr: &"0x9d4454B023096f34B160D6B654540c56A1F81688" + .parse::() + .unwrap() + .into(), + dst_domain: 5456, + dst_addr: &H256::default() + }, + false + )) } #[test] fn config_with_address() { - let whitelist: Whitelist = serde_json::from_str(r#"[{"sourceAddress": "0x9d4454B023096f34B160D6B654540c56A1F81688", "destinationAddress": "9d4454B023096f34B160D6B654540c56A1F81688"}]"#).unwrap(); - assert!(whitelist.0.is_some()); - assert_eq!(whitelist.0.as_ref().unwrap().len(), 1); - let elem = &whitelist.0.as_ref().unwrap()[0]; - assert_eq!(elem.destination_domain, Wildcard); + let list: MatchingList = serde_json::from_str(r#"[{"sourceAddress": "0x9d4454B023096f34B160D6B654540c56A1F81688", "destinationAddress": "9d4454B023096f34B160D6B654540c56A1F81688"}]"#).unwrap(); + assert!(list.0.is_some()); + assert_eq!(list.0.as_ref().unwrap().len(), 1); + let elem = &list.0.as_ref().unwrap()[0]; + assert_eq!(elem.dst_domain, Wildcard); assert_eq!( - elem.destination_address, + elem.dst_address, Enumerated(vec!["0x9d4454B023096f34B160D6B654540c56A1F81688" .parse::() .unwrap() .into()]) ); - assert_eq!(elem.source_domain, Wildcard); + assert_eq!(elem.src_domain, Wildcard); assert_eq!( - elem.source_address, + elem.src_address, Enumerated(vec!["0x9d4454B023096f34B160D6B654540c56A1F81688" .parse::() .unwrap() .into()]) ); + + assert!(list.matches( + MatchInfo { + src_domain: 34, + src_addr: &"0x9d4454B023096f34B160D6B654540c56A1F81688" + .parse::() + .unwrap() + .into(), + dst_domain: 5456, + dst_addr: &"9d4454B023096f34B160D6B654540c56A1F81688" + .parse::() + .unwrap() + .into() + }, + false + )); + + assert!(!list.matches( + MatchInfo { + src_domain: 34, + src_addr: &"0x9d4454B023096f34B160D6B654540c56A1F81688" + .parse::() + .unwrap() + .into(), + dst_domain: 5456, + dst_addr: &H256::default() + }, + false + )); } #[test] fn config_with_multiple_domains() { - let whitelist: Whitelist = + let whitelist: MatchingList = serde_json::from_str(r#"[{"destinationDomain": ["13372", "13373"]}]"#).unwrap(); assert!(whitelist.0.is_some()); assert_eq!(whitelist.0.as_ref().unwrap().len(), 1); let elem = &whitelist.0.as_ref().unwrap()[0]; - assert_eq!(elem.destination_domain, Enumerated(vec![13372, 13373])); - assert_eq!(elem.destination_address, Wildcard); - assert_eq!(elem.source_domain, Wildcard); - assert_eq!(elem.source_address, Wildcard); + assert_eq!(elem.dst_domain, Enumerated(vec![13372, 13373])); + assert_eq!(elem.dst_address, Wildcard); + assert_eq!(elem.src_domain, Wildcard); + assert_eq!(elem.src_address, Wildcard); + } + + #[test] + fn matches_empty_list() { + let info = MatchInfo { + src_domain: 0, + src_addr: &H256::default(), + dst_domain: 0, + dst_addr: &H256::default(), + }; + // whitelist use + assert!(MatchingList(None).matches(info, true)); + // blacklist use + assert!(!MatchingList(None).matches(info, false)); } } diff --git a/rust/agents/relayer/src/settings/mod.rs b/rust/agents/relayer/src/settings/mod.rs index dd99e501c..7fa52d186 100644 --- a/rust/agents/relayer/src/settings/mod.rs +++ b/rust/agents/relayer/src/settings/mod.rs @@ -2,7 +2,7 @@ use abacus_base::decl_settings; -pub mod whitelist; +pub mod matching_list; decl_settings!(Relayer { /// The polling interval to check for new signed checkpoints in seconds @@ -11,6 +11,10 @@ decl_settings!(Relayer { maxprocessingretries: String, /// The multisig checkpoint syncer configuration multisigcheckpointsyncer: abacus_base::MultisigCheckpointSyncerConf, - /// This is optional. See `Whitelist` for more. + /// This is optional. If no whitelist is provided ALL messages will be considered on the + /// whitelist. whitelist: Option, + /// This is optional. If no blacklist is provided ALL will be considered to not be on + /// the blacklist. + blacklist: Option, });