Skip to content

Commit

Permalink
feat(advance-runner)!: validate snapshot hash
Browse files Browse the repository at this point in the history
The snapshot's template hash gets compared with its counterpart
in the blockchain, causing the advance-runner to stop if it does not
match.

BREAKING CHANGE: adds the PROVIDER_HTTP_ENDPOINT parameter, which is
required to validate snapshots. Validation is enabled by default, but
can be disabled by setting SNAPSHOT_VALIDATION_ENABLED to false
  • Loading branch information
torives committed Aug 23, 2023
1 parent 2c552ae commit 6359eaa
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 67 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added snapshot validation. The node will now check if the snapshot's template hash matches the one stored in the blockchain

## [0.9.1] 2023-06-14

### Changed
Expand Down
3 changes: 3 additions & 0 deletions offchain/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions offchain/advance-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ rollups-events = { path = "../rollups-events" }
async-trait.workspace = true
backoff = { workspace = true, features = ["tokio"] }
clap = { workspace = true, features = ["derive", "env"] }
contracts = { path = "../contracts" }
hex.workspace = true
sha3 = { workspace = true, features = ["std"] }
snafu.workspace = true
state-fold-types = { workspace = true, features = ["ethers"] }
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] }
tonic.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
uuid = { workspace = true, features = ["v4"] }
url.workspace = true

[dev-dependencies]
test-fixtures = { path = "../test-fixtures" }
Expand Down
11 changes: 7 additions & 4 deletions offchain/advance-runner/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ impl AdvanceRunnerConfig {
pub fn parse() -> Result<Self, ConfigError> {
let cli_config = CLIConfig::parse();
let broker_config = cli_config.broker_cli_config.into();
let dapp_metadata = cli_config.dapp_metadata_cli_config.into();
let dapp_metadata: DAppMetadata =
cli_config.dapp_metadata_cli_config.into();
let server_manager_config =
ServerManagerConfig::parse_from_cli(cli_config.sm_cli_config);
let snapshot_config =
SnapshotConfig::parse_from_cli(cli_config.snapshot_cli_config)
.context(SnapshotConfigSnafu)?;
let snapshot_config = SnapshotConfig::new(
cli_config.snapshot_cli_config,
dapp_metadata.dapp_address.clone(),
)
.context(SnapshotConfigSnafu)?;
let backoff_max_elapsed_duration =
Duration::from_millis(cli_config.backoff_max_elapsed_duration);
let healthcheck_port = cli_config.healthcheck_port;
Expand Down
46 changes: 46 additions & 0 deletions offchain/advance-runner/src/dapp_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

use contracts::cartesi_dapp::CartesiDApp;
use rollups_events::{Address, Hash};
use snafu::{ResultExt, Snafu};
use state_fold_types::ethers::{
prelude::ContractError,
providers::{Http, HttpRateLimitRetryPolicy, Provider, RetryClient},
};
use std::sync::Arc;
use url::Url;

const MAX_RETRIES: u32 = 10;
const INITIAL_BACKOFF: u64 = 1000;

#[derive(Debug, Snafu)]
pub enum DappContractError {
#[snafu(display("failed to obtain hash from dapp contract"))]
ContractError {
source: ContractError<Provider<RetryClient<Http>>>,
},
}

pub async fn get_template_hash(
dapp_address: Address,
provider_http_endpoint: Url,
) -> Result<Hash, DappContractError> {
let provider = Provider::new(RetryClient::new(
Http::new(provider_http_endpoint),
Box::new(HttpRateLimitRetryPolicy),
MAX_RETRIES,
INITIAL_BACKOFF,
));

let cartesi_dapp =
CartesiDApp::new(dapp_address.inner(), Arc::new(provider));

let template_hash = cartesi_dapp
.get_template_hash()
.call()
.await
.context(ContractSnafu)?;

Ok(Hash::new(template_hash))
}
1 change: 1 addition & 0 deletions offchain/advance-runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub use error::AdvanceRunnerError;

mod broker;
pub mod config;
mod dapp_contract;
mod error;
pub mod runner;
mod server_manager;
Expand Down
18 changes: 8 additions & 10 deletions offchain/advance-runner/src/runner.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

use rollups_events::{Event, InputMetadata, RollupsData, RollupsInput};
use snafu::{ResultExt, Snafu};

use crate::broker::{BrokerFacade, BrokerFacadeError};
use crate::server_manager::{ServerManagerError, ServerManagerFacade};
use crate::snapshot::SnapshotManager;
use rollups_events::{Event, InputMetadata, RollupsData, RollupsInput};
use snafu::{ResultExt, Snafu};

#[derive(Debug, Snafu)]
pub enum RunnerError<SnapError: snafu::Error + 'static> {
Expand Down Expand Up @@ -53,8 +52,8 @@ pub enum RunnerError<SnapError: snafu::Error + 'static> {
))]
ParentIdMismatchError { expected: String, got: String },

#[snafu(display("failed to get hash from snapshot "))]
GetSnapshotHashError { source: SnapError },
#[snafu(display("failed to validate snapshot"))]
ValidateSnapshotError { source: SnapError },
}

type Result<T, SnapError> = std::result::Result<T, RunnerError<SnapError>>;
Expand Down Expand Up @@ -121,12 +120,11 @@ impl<Snap: SnapshotManager + std::fmt::Debug + 'static> Runner<Snap> {
.context(GetLatestSnapshotSnafu)?;
tracing::info!(?snapshot, "got latest snapshot");

let offchain_hash = self
.snapshot_manager
.get_template_hash(&snapshot)
self.snapshot_manager
.validate(&snapshot)
.await
.context(GetSnapshotHashSnafu)?;
tracing::trace!(?offchain_hash, "got snapshot hash");
.context(ValidateSnapshotSnafu)?;
tracing::trace!("snapshot is valid");

let event_id = self
.broker
Expand Down
36 changes: 34 additions & 2 deletions offchain/advance-runner/src/snapshot/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

use clap::Parser;
use rollups_events::Address;
use snafu::{ensure, Snafu};
use std::path::PathBuf;
use url::Url;

#[derive(Debug, Clone)]
pub struct FSManagerConfig {
pub snapshot_dir: PathBuf,
pub snapshot_latest: PathBuf,
pub validation_enabled: bool,
pub provider_http_endpoint: Option<Url>,
pub dapp_address: Address,
}

#[derive(Debug, Clone)]
Expand All @@ -18,8 +23,9 @@ pub enum SnapshotConfig {
}

impl SnapshotConfig {
pub fn parse_from_cli(
pub fn new(
cli_config: SnapshotCLIConfig,
dapp_address: Address,
) -> Result<Self, SnapshotConfigError> {
if cli_config.snapshot_enabled {
let snapshot_dir = PathBuf::from(cli_config.snapshot_dir);
Expand All @@ -28,9 +34,22 @@ impl SnapshotConfig {
let snapshot_latest = PathBuf::from(cli_config.snapshot_latest);
ensure!(snapshot_latest.is_symlink(), SymlinkSnafu);

let validation_enabled = cli_config.snapshot_validation_enabled;
if validation_enabled {
ensure!(
cli_config.provider_http_endpoint.is_some(),
NoProviderEndpointSnafu,
);
}

let provider_http_endpoint = cli_config.provider_http_endpoint;

Ok(SnapshotConfig::FileSystem(FSManagerConfig {
snapshot_dir,
snapshot_latest,
validation_enabled,
provider_http_endpoint,
dapp_address,
}))
} else {
Ok(SnapshotConfig::Disabled)
Expand All @@ -39,18 +58,22 @@ impl SnapshotConfig {
}

#[derive(Debug, Snafu)]
#[allow(clippy::enum_variant_names)]
pub enum SnapshotConfigError {
#[snafu(display("Snapshot dir isn't a directory"))]
DirError {},

#[snafu(display("Snapshot latest isn't a symlink"))]
SymlinkError {},

#[snafu(display("A provider http endpoint is required"))]
NoProviderEndpointError {},
}

#[derive(Parser, Debug)]
#[command(name = "snapshot")]
pub struct SnapshotCLIConfig {
/// If set to false, disable snapshots
/// If set to false, disables snapshots. Enabled by default
#[arg(long, env, default_value_t = true)]
snapshot_enabled: bool,

Expand All @@ -61,4 +84,13 @@ pub struct SnapshotCLIConfig {
/// Path to the symlink of the latest snapshot
#[arg(long, env)]
snapshot_latest: String,

/// If set to false, disables snapshot validation. Enabled by default
#[arg(long, env, default_value_t = true)]
snapshot_validation_enabled: bool,

/// The endpoint for a JSON-RPC provider.
/// Required if SNAPSHOT_VALIDATION_ENABLED is `true`
#[arg(long, env, value_parser = Url::parse)]
provider_http_endpoint: Option<Url>,
}
20 changes: 7 additions & 13 deletions offchain/advance-runner/src/snapshot/disabled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

use super::{Snapshot, SnapshotManager};
use rollups_events::Hash;

#[derive(Debug)]
pub struct SnapshotDisabled {}
Expand All @@ -17,7 +16,7 @@ impl SnapshotManager for SnapshotDisabled {

/// Get the most recent snapshot
#[tracing::instrument(level = "trace", skip_all)]
async fn get_latest(&self) -> Result<Snapshot, SnapshotDisabledError> {
async fn get_latest(&self) -> Result<Snapshot, Self::Error> {
tracing::trace!("snapshots disabled; returning default");
Ok(Default::default())
}
Expand All @@ -28,26 +27,21 @@ impl SnapshotManager for SnapshotDisabled {
&self,
_: u64,
_: u64,
) -> Result<Snapshot, SnapshotDisabledError> {
) -> Result<Snapshot, Self::Error> {
tracing::trace!("snapshots disabled; returning default");
Ok(Default::default())
}

/// Set the most recent snapshot
#[tracing::instrument(level = "trace", skip_all)]
async fn set_latest(
&self,
_: Snapshot,
) -> Result<(), SnapshotDisabledError> {
async fn set_latest(&self, _: Snapshot) -> Result<(), Self::Error> {
tracing::trace!("snapshots disabled; ignoring");
Ok(())
}

async fn get_template_hash(
&self,
_: &Snapshot,
) -> Result<Hash, SnapshotDisabledError> {
tracing::trace!("snapshots disabled; returning default");
Ok(Hash::default())
#[tracing::instrument(level = "trace", skip_all)]
async fn validate(&self, _: &Snapshot) -> Result<(), Self::Error> {
tracing::trace!("snapshots disabled; ignoring");
Ok(())
}
}
Loading

0 comments on commit 6359eaa

Please sign in to comment.