diff --git a/configuration/CHANGELOG.md b/configuration/CHANGELOG.md index 205c35ad..6d3ad9fa 100644 --- a/configuration/CHANGELOG.md +++ b/configuration/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- feature: `CoreContracts` and related types derive `PartialEq` - feature: add more explicit Processor Config TS declaration - refactor: make Processor config keys optional, and prevent trivial ser. - fix: update TS AgentConfig to match rust diff --git a/configuration/src/bridge.rs b/configuration/src/bridge.rs index a9f3decd..c2c613cd 100644 --- a/configuration/src/bridge.rs +++ b/configuration/src/bridge.rs @@ -1,6 +1,7 @@ //! Nomad-bridge related configuration structs use std::collections::HashSet; +use std::hash::Hash; use nomad_types::deser_nomad_u64; use nomad_types::{NomadIdentifier, NomadLocator, Proxy}; @@ -8,7 +9,7 @@ use nomad_types::{NomadIdentifier, NomadLocator, Proxy}; use crate::network::CustomTokenSpecifier; /// Deploy-time custom tokens -#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)] +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "camelCase")] pub struct DeployedCustomToken { /// Token domain and ID diff --git a/configuration/src/contracts.rs b/configuration/src/contracts.rs index 81189feb..e4b77c5d 100644 --- a/configuration/src/contracts.rs +++ b/configuration/src/contracts.rs @@ -6,7 +6,7 @@ use nomad_types::deser_nomad_u32; use nomad_types::{NomadIdentifier, Proxy}; /// Evm Core Contracts -#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct EvmCoreContracts { /// Contract Deploy Height @@ -28,7 +28,7 @@ pub struct EvmCoreContracts { } /// Core Contract abstract -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(untagged)] pub enum CoreContracts { /// EVM Core diff --git a/nomad-base/CHANGELOG.md b/nomad-base/CHANGELOG.md index d03d0623..95a8329d 100644 --- a/nomad-base/CHANGELOG.md +++ b/nomad-base/CHANGELOG.md @@ -2,6 +2,9 @@ ### Unreleased +- feature: add core integrity check to agent bootup process +- feature: add core integrity check store/retrieve to DB +- feature: add `integrity_check` to `NomadAgent` trait - un-nest, simplify & add event to setup code for determining which replicas to run - un-nest, simplify & add event to setup code for config source discovery diff --git a/nomad-base/src/nomad_db.rs b/nomad-base/src/nomad_db.rs index 3ae0f5ff..62af64c6 100644 --- a/nomad-base/src/nomad_db.rs +++ b/nomad-base/src/nomad_db.rs @@ -1,10 +1,15 @@ -use color_eyre::Result; +use color_eyre::{ + eyre::{ensure, WrapErr}, + Result, +}; use ethers::core::types::H256; use nomad_core::db::{DbError, TypedDB, DB}; use nomad_core::{ accumulator::NomadProof, utils, CommittedMessage, Decode, NomadMessage, RawCommittedMessage, SignedUpdate, SignedUpdateWithMeta, UpdateMeta, }; +use nomad_xyz_configuration::contracts::CoreContracts; +use nomad_xyz_configuration::NomadConfig; use tokio::time::sleep; use tracing::{debug, info}; @@ -26,12 +31,20 @@ const UPDATER_PRODUCED_UPDATE: &str = "updater_produced_update_"; const PROVER_LATEST_COMMITTED: &str = "prover_latest_committed_"; const PROCESSOR_ATTEMPTED: &str = "processor_attempted_"; +const CORE_INTEGRITY: &str = "core_ingerity_"; + /// DB handle for storing data tied to a specific home. /// /// Key structure: ```__``` #[derive(Debug, Clone)] pub struct NomadDB(TypedDB); +impl From for NomadDB { + fn from(db: TypedDB) -> Self { + NomadDB(db) + } +} + impl std::ops::Deref for NomadDB { type Target = TypedDB; @@ -389,6 +402,43 @@ impl NomadDB { None => Ok(false), } } + + /// Stores a core in the DB for later integrity checks + pub fn store_core(&self, name: &str, core: &CoreContracts) -> Result<()> { + let serialized = serde_json::to_string(core)?; + Ok(self.store_keyed_encodable(CORE_INTEGRITY, &name.to_owned(), &serialized)?) + } + + /// Retrieves a core from the DB + pub fn retrieve_core(&self, name: &str) -> Result> { + if let Some(core_json) = + self.retrieve_keyed_decodable::<_, _, String>(CORE_INTEGRITY, &name.to_owned())? + { + return Ok(serde_json::from_str(&core_json)?); + } + Ok(None) + } + + /// Check a core's integrity against the DB. If there is no persisted + /// object for that core, store it for later integrity checks + pub fn check_core_integrity(&self, name: &str, core: &CoreContracts) -> Result<()> { + if let Some(integrity) = self.retrieve_core(name)? { + ensure!(integrity == *core, "integrity check failed"); + } else { + self.store_core(name, core)?; + } + Ok(()) + } + + /// Checks the integrity of core contract addresses. Error if the DB + /// contains differing addresses + pub fn check_integrity(&self, config: &NomadConfig) -> Result<()> { + for (name, core) in config.core().iter() { + self.check_core_integrity(name, core) + .wrap_err_with(|| format!("Error checking core for {}", name))?; + } + Ok(()) + } } #[cfg(test)] @@ -458,4 +508,102 @@ mod test { }) .await; } + + #[tokio::test] + async fn db_integrity_check() { + let core: CoreContracts = serde_json::from_str( + r#"{ + "deployHeight": 12098988, + "governanceRouter": { + "beacon": "0x1631d12da55cbfb540d46e0dd9bbfb1d3f293dc8", + "implementation": "0x2e588e0cff16cb8dd343551b435f5fee94f35230", + "proxy": "0x6cc740e1e17b7b72e1d6c46afea4d44d86657102" + }, + "home": { + "beacon": "0x4b162c5c62a67e8a1772c0f04715ed2606b51421", + "implementation": "0xe015da2b3cfdefb210ad5125744b552e80905468", + "proxy": "0x884dad9316c61ed353b1d6931ba46663e1c3aacf" + }, + "replicas": { + "evmostestnet": { + "beacon": "0x0c09e151720e0bcf4e2db42a3b5608b3de78e8d7", + "implementation": "0x7c8cc92daa7d9172dfe5d8319cc74a6166d05c2c", + "proxy": "0xb372d6b312f678494cf4e1bf5d149e733640e968" + }, + "goerli": { + "beacon": "0x0c09e151720e0bcf4e2db42a3b5608b3de78e8d7", + "implementation": "0x7c8cc92daa7d9172dfe5d8319cc74a6166d05c2c", + "proxy": "0x5f4d75de162b4c050f27ce2f2374d50e3d7fbbb6" + }, + "neontestnet": { + "beacon": "0x0c09e151720e0bcf4e2db42a3b5608b3de78e8d7", + "implementation": "0x7c8cc92daa7d9172dfe5d8319cc74a6166d05c2c", + "proxy": "0x495ef7cfee3850ba2afb5fea4c7c06ee0d1d0d6e" + }, + "rinkeby": { + "beacon": "0x0c09e151720e0bcf4e2db42a3b5608b3de78e8d7", + "implementation": "0x7c8cc92daa7d9172dfe5d8319cc74a6166d05c2c", + "proxy": "0x921dbedc12ba3299deaf8dd9fff0f435d8839edf" + } + }, + "updaterManager": "0x7f1b402a570f3221e03e41ef2408b5a215bb0448", + "upgradeBeaconController": "0x87c44484add9020e7d6c98132311e1cd118ac236", + "xAppConnectionManager": "0x42e8c0f7981add4c8081be20c813d49571f446f4" + }"#, + ) + .unwrap(); + + let wrong: CoreContracts = serde_json::from_str( + r#"{ + "deployHeight": 12098988, + "governanceRouter": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + }, + "home": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + }, + "replicas": { + "evmostestnet": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + }, + "goerli": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + }, + "neontestnet": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + }, + "rinkeby": { + "beacon": "0x0000000000000000000000000000000000000000", + "implementation": "0x0000000000000000000000000000000000000000", + "proxy": "0x0000000000000000000000000000000000000000" + } + }, + "updaterManager": "0x0000000000000000000000000000000000000000", + "upgradeBeaconController": "0x0000000000000000000000000000000000000000", + "xAppConnectionManager": "0x0000000000000000000000000000000000000000" + }"#, + ) + .unwrap(); + + run_test_db(|db| async move { + let db = NomadDB::new("bootup integrity test", db); + db.check_core_integrity("toast", &core).unwrap(); + assert!( + db.check_core_integrity("toast", &wrong).is_err(), + "should have caught changed addrs" + ); + db.check_core_integrity("toast", &core).unwrap(); + }) + .await; + } } diff --git a/nomad-base/src/settings/macros.rs b/nomad-base/src/settings/macros.rs index 6544134c..97aa630b 100644 --- a/nomad-base/src/settings/macros.rs +++ b/nomad-base/src/settings/macros.rs @@ -110,11 +110,21 @@ macro_rules! decl_settings { let base = nomad_base::Settings::from_config_and_secrets(&agent, &home, &remote_networks, &config, &secrets); base.validate_against_config_and_secrets(&agent, &home, &remote_networks, &config, &secrets)?; + + // perform integrity checks + let db: nomad_base::NomadDB = + nomad_core::db::TypedDB::new( + "integrity_check".into(), + nomad_core::db::DB::from_path(&base.db)? + ).into(); + db.check_integrity(&config)?; + let mut agent = config.agent().get(&home).expect("agent config").[<$name:lower>].clone(); // Override with environment vars, if present agent.load_env_overrides(); + Ok(Self { base, agent, diff --git a/nomad-core/CHANGELOG.md b/nomad-core/CHANGELOG.md index bd4f3c3d..77b8d0ef 100644 --- a/nomad-core/CHANGELOG.md +++ b/nomad-core/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- refactor: Change DB methods to use explicit generics instead of `impl Trait` - require `Common: std::fmt::Display` - refactor: Add IRSA credentials to client instantiation - implement `Encode` and `Decode` for `bool` diff --git a/nomad-core/src/db/typed_db.rs b/nomad-core/src/db/typed_db.rs index f24d5db6..bfd2572b 100644 --- a/nomad-core/src/db/typed_db.rs +++ b/nomad-core/src/db/typed_db.rs @@ -54,22 +54,32 @@ impl TypedDB { } /// Store encodable kv pair - pub fn store_keyed_encodable( + pub fn store_keyed_encodable( &self, - prefix: impl AsRef<[u8]>, + prefix: P, key: &K, value: &V, - ) -> Result<(), DbError> { + ) -> Result<(), DbError> + where + P: AsRef<[u8]>, + K: Encode, + V: Encode, + { self.db .store_keyed_encodable(self.full_prefix(prefix), key, value) } /// Retrieve decodable value given encodable key - pub fn retrieve_keyed_decodable( + pub fn retrieve_keyed_decodable( &self, - prefix: impl AsRef<[u8]>, + prefix: P, key: &K, - ) -> Result, DbError> { + ) -> Result, DbError> + where + P: AsRef<[u8]>, + K: Encode, + V: Decode, + { self.db .retrieve_keyed_decodable(self.full_prefix(prefix), key) } diff --git a/nomad-core/src/lib.rs b/nomad-core/src/lib.rs index f9b54802..57e16462 100644 --- a/nomad-core/src/lib.rs +++ b/nomad-core/src/lib.rs @@ -8,6 +8,8 @@ #![forbid(unsafe_code)] #![forbid(where_clauses_object_safety)] +use std::string::FromUtf8Error; + pub use accumulator; /// AWS global state and init @@ -87,4 +89,7 @@ pub enum NomadError { /// IO error from Read/Write usage #[error(transparent)] IoError(#[from] std::io::Error), + /// decoding error + #[error(transparent)] + DecodingError(#[from] FromUtf8Error), } diff --git a/nomad-core/src/traits/encode.rs b/nomad-core/src/traits/encode.rs index 127aa998..a92eed77 100644 --- a/nomad-core/src/traits/encode.rs +++ b/nomad-core/src/traits/encode.rs @@ -178,3 +178,31 @@ impl Decode for bool { } } } + +impl Encode for String { + fn write_to(&self, writer: &mut W) -> std::io::Result + where + W: std::io::Write, + { + let buf = self.as_bytes(); + let len = buf.len() as u32; + len.write_to(writer)?; + writer.write_all(buf)?; + Ok(buf.len() + 4) + } +} + +impl Decode for String { + fn read_from(reader: &mut R) -> Result + where + R: std::io::Read, + Self: Sized, + { + let length = u32::read_from(reader)? as usize; + let mut buf = vec![0u8; length]; + + reader.read_exact(buf.as_mut())?; + + Ok(String::from_utf8(buf)?) + } +} diff --git a/nomad-types/src/lib.rs b/nomad-types/src/lib.rs index 4e8186da..c61a0ab4 100644 --- a/nomad-types/src/lib.rs +++ b/nomad-types/src/lib.rs @@ -9,7 +9,7 @@ pub use macros::*; use color_eyre::{eyre::bail, Report, Result}; use ethers::prelude::{Address, H160, H256}; use serde::{de, Deserializer}; -use std::{fmt, ops::DerefMut, str::FromStr}; +use std::{fmt, hash::Hash, ops::DerefMut, str::FromStr}; /// A Hex String of length `N` representing bytes of length `N / 2` #[derive(Debug, Clone, PartialEq)] @@ -223,9 +223,11 @@ pub struct NomadLocator { } /// An EVM beacon proxy -#[derive( - Default, Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash, -)] +/// +/// NOTE: the proxy does NOT include the implementation in its `Hash`, +/// `PartialEq` or `Eq` implementations. This is done so that a proxy will be +/// equal to itself, regardless of the current implementation +#[derive(Default, Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq)] #[serde(rename_all = "camelCase")] pub struct Proxy { /// Implementation address @@ -236,6 +238,18 @@ pub struct Proxy { pub beacon: NomadIdentifier, } +impl PartialEq for Proxy { + fn eq(&self, other: &Self) -> bool { + self.proxy == other.proxy && self.beacon == other.beacon + } +} +impl Hash for Proxy { + fn hash(&self, state: &mut H) { + self.proxy.hash(state); + self.beacon.hash(state); + } +} + #[cfg(test)] mod test { use serde_json::json;