diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 655eeeaf1..2892c538f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -56,16 +56,20 @@ dependencies = [ name = "abacus-core" version = "0.1.0" dependencies = [ + "abacus-base", "async-trait", "bytes", "color-eyre", + "config", "ethers", "ethers-providers", "ethers-signers", "eyre", "hex", "lazy_static", + "maplit", "num", + "num-traits", "rocksdb", "serde", "serde_json", @@ -74,6 +78,7 @@ dependencies = [ "tokio", "tracing", "tracing-futures", + "walkdir", ] [[package]] diff --git a/rust/abacus-core/Cargo.toml b/rust/abacus-core/Cargo.toml index 52553470e..dfdb8107f 100644 --- a/rust/abacus-core/Cargo.toml +++ b/rust/abacus-core/Cargo.toml @@ -10,11 +10,14 @@ edition = "2021" ethers = { git = "https://github.com/gakonst/ethers-rs", branch = "master", default-features = false, features = ['legacy'] } ethers-signers = { git = "https://github.com/gakonst/ethers-rs", branch = "master", features=["aws"] } ethers-providers = { git = "https://github.com/gakonst/ethers-rs", branch = "master", features=["ws", "rustls"] } +config = "0.13" hex = "0.4.3" sha3 = "0.9.1" lazy_static = "*" thiserror = "*" async-trait = { version = "0.1", default-features = false } +num-traits = "0.2" +maplit = "1.0" tokio = { version = "1", features = ["rt", "macros"] } tracing = "0.1" tracing-futures = "0.2" @@ -26,8 +29,10 @@ bytes = { version = "1", features = ["serde"]} num = {version="0", features=["serde"]} [dev-dependencies] -tokio = {version = "1", features = ["rt", "time"]} +abacus-base = { path = "../abacus-base" } color-eyre = "0.6" +tokio = {version = "1", features = ["rt", "time"]} +walkdir = { version = "2" } [features] output = [] diff --git a/rust/abacus-core/src/chain.rs b/rust/abacus-core/src/chain.rs index 63c9837e0..fa75029af 100644 --- a/rust/abacus-core/src/chain.rs +++ b/rust/abacus-core/src/chain.rs @@ -71,7 +71,9 @@ macro_rules! domain_and_chain { } } -// Copied from https://github.com/abacus-network/abacus-monorepo/blob/54a41d5a4bbb86a3b08d02d7ff6662478c41e221/typescript/sdk/src/chain-metadata.ts +// The unit test in this file `tests::json_mappings_match_code_map` +// tries to ensure some stability between the {chain} X {domain} +// mapping below with the agent configuration file. domain_and_chain! { 0x63656c6f <=> "celo", 0x657468 <=> "ethereum", @@ -82,6 +84,9 @@ domain_and_chain! { 5 <=> "goerli", 3000 <=> "kovan", 80001 <=> "mumbai", + 6386274 <=> "arbitrum", + 6452067 <=> "bsc", + 28528 <=> "optimism", 13371 <=> "test1", 13372 <=> "test2", 13373 <=> "test3", @@ -90,3 +95,166 @@ domain_and_chain! { 0x6f702d6b <=> "optimismkovan", 0x61752d74 <=> "auroratestnet", } + +#[cfg(test)] +mod tests { + use abacus_base::Settings; + use config::{Config, File, FileFormat}; + use num_traits::identities::Zero; + use std::collections::BTreeSet; + use std::fs::read_to_string; + use std::path::Path; + use walkdir::WalkDir; + + /// Relative path to the `abacus-monorepo/rust/config/` + /// directory, which is where the agent's config files + /// currently live. + const AGENT_CONFIG_PATH_ROOT: &str = "../config"; + + /// We will not include any file paths of config/settings files + /// in the test suite if *any* substring of the file path matches + /// against one of the strings included in the blacklist below. + /// This is to ensure that e.g. when a backwards-incompatible + /// change is made in config file format, and agents can't parse + /// them anymore, we don't fail the test. (E.g. agents cannot + /// currently parse the older files in `config/dev/` or + /// `config/testnet`. + const BLACKLISTED_DIRS: [&str; 5] = [ + // Old envs which do not set now-required field + // "finality_blocks", which causes parsing to fail. + "config/dev/", + "config/testnet/", + // Ignore only-local names of fake chains used by + // e.g. test suites. + "test/test1_config.json", + "test/test2_config.json", + "test/test3_config.json", + ]; + + fn is_blacklisted(path: &Path) -> bool { + BLACKLISTED_DIRS + .iter() + .any(|x| path.to_str().unwrap().contains(x)) + } + + #[derive(Clone, Debug, Ord, PartialEq, PartialOrd, Eq, Hash)] + struct ChainCoordinate { + name: String, + domain: u32, + } + + fn config_paths(root: &Path) -> Vec { + WalkDir::new(root) + .min_depth(2) + .into_iter() + .filter_map(|x| x.ok()) + .map(|x| x.into_path()) + .filter(|x| !is_blacklisted(x)) + .map(|x| x.into_os_string()) + .filter_map(|x| x.into_string().ok()) + .collect() + } + + /// Provides a vector of parsed `abacus_base::Settings` objects + /// built from all of the version-controlled agent configuration files. + /// This is purely a utility to allow us to test a handful of critical + /// properties related to those configs and shouldn't be used outside + /// of a test env. This test simply tries to do some sanity checks + /// against the integrity of that data. + fn abacus_settings() -> Vec { + let root = Path::new(AGENT_CONFIG_PATH_ROOT); + let paths = config_paths(root); + let files: Vec = paths + .iter() + .filter_map(|x| read_to_string(x).ok()) + .collect(); + paths + .iter() + .zip(files.iter()) + .map(|(p, f)| { + Config::builder() + .add_source(File::from_str(f.as_str(), FileFormat::Json)) + .build() + .unwrap() + .try_deserialize() + .unwrap_or_else(|e| { + panic!("!cfg({}): {:?}: {}", p, e, f); + }) + }) + .collect() + } + + fn outbox_chain_names() -> BTreeSet { + abacus_settings() + .iter() + .map(|x| x.outbox.name.clone()) + .collect() + } + + fn inbox_chain_names() -> BTreeSet { + abacus_settings() + .iter() + .flat_map(|x: &Settings| x.inboxes.iter().map(|(k, _)| String::from(k))) + .collect() + } + + fn outbox_name_domain_coords() -> BTreeSet { + abacus_settings() + .iter() + .map(|x| ChainCoordinate { + name: x.outbox.name.clone(), + domain: x.outbox.domain.parse().unwrap(), + }) + .collect() + } + + fn inbox_name_domain_records() -> BTreeSet { + abacus_settings() + .iter() + .flat_map(|x: &Settings| { + x.inboxes.iter().map(|(_, v)| ChainCoordinate { + name: v.name.clone(), + domain: v.domain.parse().unwrap(), + }) + }) + .collect() + } + + #[test] + fn agent_json_config_consistency_checks() { + // Inbox/outbox and chain-presence equality + // (sanity checks that we have a complete list of + // relevant chains). + let inbox_chains = inbox_chain_names(); + let outbox_chains = outbox_chain_names(); + assert!(inbox_chains.symmetric_difference(&outbox_chains).count() == usize::zero()); + assert_eq!(&inbox_chains.len(), &outbox_chains.len()); + + // Verify that the the outbox-associative chain-name + // and domain-number records agree with the + // inbox-associative chain-name and domain-number + // records, since our configuration data is /not/ + // normalized and could drift out of sync. + let inbox_coords = inbox_name_domain_records(); + let outbox_coords = outbox_name_domain_coords(); + assert!(inbox_coords.symmetric_difference(&outbox_coords).count() == usize::zero()); + assert_eq!(&inbox_coords.len(), &outbox_coords.len()); + + // TODO(webbhorn): Also verify with this functionality + // we have entries for all of the Gelato contract + // addresses we need hardcoded in the binary for now. + + // Verify that the hard-coded, macro-maintained + // mapping in `abacus-core/src/chain.rs` named + // by the macro `domain_and_chain` is complete + // and in agreement with our on-disk json-based + // configuration data. + for ChainCoordinate { name, domain } in inbox_coords.iter().chain(outbox_coords.iter()) { + assert_eq!( + super::chain_from_domain(domain.to_owned()).unwrap(), + name.to_owned() + ); + assert_eq!(super::domain_from_chain(name).unwrap(), domain.to_owned()); + } + } +}