Skip to content

feature: bootup db integrity check #228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configuration/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion configuration/src/bridge.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! 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};

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
Expand Down
4 changes: 2 additions & 2 deletions configuration/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions nomad-base/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
150 changes: 149 additions & 1 deletion nomad-base/src/nomad_db.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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: ```<entity>_<additional_prefix(es)>_<key>```
#[derive(Debug, Clone)]
pub struct NomadDB(TypedDB);

impl From<TypedDB> for NomadDB {
fn from(db: TypedDB) -> Self {
NomadDB(db)
}
}

impl std::ops::Deref for NomadDB {
type Target = TypedDB;

Expand Down Expand Up @@ -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<Option<CoreContracts>> {
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)]
Expand Down Expand Up @@ -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;
}
}
10 changes: 10 additions & 0 deletions nomad-base/src/settings/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions nomad-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
22 changes: 16 additions & 6 deletions nomad-core/src/db/typed_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,32 @@ impl TypedDB {
}

/// Store encodable kv pair
pub fn store_keyed_encodable<K: Encode, V: Encode>(
pub fn store_keyed_encodable<P, K, V>(
&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<K: Encode, V: Decode>(
pub fn retrieve_keyed_decodable<P, K, V>(
&self,
prefix: impl AsRef<[u8]>,
prefix: P,
key: &K,
) -> Result<Option<V>, DbError> {
) -> Result<Option<V>, DbError>
where
P: AsRef<[u8]>,
K: Encode,
V: Decode,
{
self.db
.retrieve_keyed_decodable(self.full_prefix(prefix), key)
}
Expand Down
5 changes: 5 additions & 0 deletions nomad-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#![forbid(unsafe_code)]
#![forbid(where_clauses_object_safety)]

use std::string::FromUtf8Error;

pub use accumulator;

/// AWS global state and init
Expand Down Expand Up @@ -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),
}
28 changes: 28 additions & 0 deletions nomad-core/src/traits/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,31 @@ impl Decode for bool {
}
}
}

impl Encode for String {
fn write_to<W>(&self, writer: &mut W) -> std::io::Result<usize>
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<R>(reader: &mut R) -> Result<Self, NomadError>
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)?)
}
}
22 changes: 18 additions & 4 deletions nomad-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand All @@ -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<H: std::hash::Hasher>(&self, state: &mut H) {
self.proxy.hash(state);
self.beacon.hash(state);
}
}

#[cfg(test)]
mod test {
use serde_json::json;
Expand Down