diff --git a/Cargo.lock b/Cargo.lock index 9e0fa1a..b89944d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -566,7 +566,7 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "durin-fault" -version = "0.0.1" +version = "0.1.0" dependencies = [ "alloy-primitives", "alloy-rpc-client", @@ -586,7 +586,7 @@ dependencies = [ [[package]] name = "durin-primitives" -version = "0.0.1" +version = "0.1.0" dependencies = [ "alloy-primitives", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index c274f79..6d6581b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*"] [workspace.package] edition = "2021" -version = "0.0.1" +version = "0.1.0" authors = ["clabby"] [workspace.dependencies] diff --git a/crates/fault/src/lib.rs b/crates/fault/src/lib.rs index df46077..3098f1d 100644 --- a/crates/fault/src/lib.rs +++ b/crates/fault/src/lib.rs @@ -1,6 +1,6 @@ //! The fault module contains types and traits related to the FaultDisputeGame. -#![allow(unused, dead_code)] +#![allow(dead_code, unused_imports)] #[cfg(test)] extern crate proptest; diff --git a/crates/fault/src/providers/cannon.rs b/crates/fault/src/providers/cannon.rs index 8de58c9..6b6e6d0 100644 --- a/crates/fault/src/providers/cannon.rs +++ b/crates/fault/src/providers/cannon.rs @@ -1,11 +1,10 @@ //! This module contains the implementation of the [crate::TraceProvider] trait for calling out to `cannon` to fetch //! state witnesses and proof values. -use crate::{Gindex, Position, TraceProvider}; -use alloy_primitives::keccak256; +use crate::{Position, TraceProvider}; use anyhow::Result; use durin_primitives::Claim; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; /// The [CannonTraceProvider] is a [TraceProvider] that runs `cannon` to retrieve state witnesses and proof values. pub struct CannonTraceProvider { @@ -13,20 +12,20 @@ pub struct CannonTraceProvider { } #[async_trait::async_trait] -impl TraceProvider<[u8; 32]> for CannonTraceProvider { - async fn absolute_prestate(&self) -> Result> { +impl TraceProvider for CannonTraceProvider { + async fn absolute_prestate(&self, _: Position) -> Result> { todo!() } - async fn absolute_prestate_hash(&self) -> Result { + async fn absolute_prestate_hash(&self, _: Position) -> Result { todo!() } - async fn state_at(&self, position: Position) -> Result> { + async fn state_at(&self, _: Position) -> Result> { todo!() } - async fn state_hash(&self, position: Position) -> Result { + async fn state_hash(&self, _: Position) -> Result { todo!() } diff --git a/crates/fault/src/providers/alphabet.rs b/crates/fault/src/providers/mocks/alphabet.rs similarity index 84% rename from crates/fault/src/providers/alphabet.rs rename to crates/fault/src/providers/mocks/alphabet.rs index e851458..1a560d5 100644 --- a/crates/fault/src/providers/alphabet.rs +++ b/crates/fault/src/providers/mocks/alphabet.rs @@ -28,31 +28,31 @@ impl AlphabetTraceProvider { } #[async_trait::async_trait] -impl TraceProvider<[u8; 32]> for AlphabetTraceProvider { - async fn absolute_prestate(&self) -> Result> { +impl TraceProvider for AlphabetTraceProvider { + async fn absolute_prestate(&self, _: Position) -> Result> { Ok(Arc::new(self.absolute_prestate)) } - async fn absolute_prestate_hash(&self) -> Result { + async fn absolute_prestate_hash(&self, _: Position) -> Result { let prestate = U256::from_be_bytes(self.absolute_prestate); let mut prestate_hash = keccak256(::abi_encode(&prestate)); prestate_hash[0] = VMStatus::Unfinished as u8; Ok(prestate_hash) } - async fn state_at(&self, position: Position) -> Result> { + async fn state_at(&self, position: Position) -> Result> { let absolute_prestate = u64::from_be_bytes(self.absolute_prestate[24..32].try_into()?); let trace_index = position.trace_index(self.max_depth); - // TODO: Handle wrapping. let state = absolute_prestate + trace_index + 1; - Ok(Arc::new(U256::from(state).to_be_bytes())) + Ok(Arc::<[u8; 32]>::new(U256::from(state).to_be_bytes())) } async fn state_hash(&self, position: Position) -> Result { + let state: [u8; 32] = (*self.state_at(position).await?).try_into()?; let state_sol = ( U256::from(position.trace_index(self.max_depth)), - U256::from_be_bytes(*self.state_at(position).await?), + U256::from_be_bytes(state), ); let mut state_hash = keccak256(AlphabetClaimConstruction::abi_encode(&state_sol)); state_hash[0] = VMStatus::Invalid as u8; @@ -77,14 +77,17 @@ mod test { max_depth: 4, }; - let prestate_sol = U256::from_be_bytes(*provider.absolute_prestate().await.unwrap()); + let prestate_bytes: [u8; 32] = (*provider.absolute_prestate(0).await.unwrap()) + .try_into() + .unwrap(); + let prestate_sol = U256::from_be_bytes(prestate_bytes); let prestate = ::abi_encode(&prestate_sol); assert_eq!( hex!("0000000000000000000000000000000000000000000000000000000000000061"), prestate.as_slice() ); - let mut prestate_hash = provider.absolute_prestate_hash().await.unwrap(); + let mut prestate_hash = provider.absolute_prestate_hash(0).await.unwrap(); prestate_hash[0] = VMStatus::Unfinished as u8; assert_eq!( hex!("03ecb75dd1820844c57b6762233d4e26853b3a7b8157bbd9f41f280a0f1cee9b"), @@ -110,7 +113,7 @@ mod test { assert_eq!( provider.state_at(position).await.unwrap(), - U256::from(expected).to_be_bytes().into() + U256::from(expected).to_be_bytes::<32>().into() ); assert_eq!(provider.state_hash(position).await.unwrap(), expected_hash); } diff --git a/crates/fault/src/providers/mocks/mock_output.rs b/crates/fault/src/providers/mocks/mock_output.rs new file mode 100644 index 0000000..4fcd0c4 --- /dev/null +++ b/crates/fault/src/providers/mocks/mock_output.rs @@ -0,0 +1,51 @@ +//! This module contains the implementation of the [crate::TraceProvider] trait for serving mock output commitments. + +use crate::{Gindex, Position, TraceProvider}; +use alloy_primitives::U256; +use anyhow::Result; +use durin_primitives::Claim; +use std::sync::Arc; + +/// The [MockOutputTraceProvider] is a [TraceProvider] that provides mock L2 output commitments for a [Position]. +pub struct MockOutputTraceProvider { + pub starting_block_number: u64, + pub leaf_depth: u8, +} + +impl MockOutputTraceProvider { + pub fn new(starting_block_number: u64, leaf_depth: u8) -> Self { + Self { + starting_block_number, + leaf_depth, + } + } +} + +#[async_trait::async_trait] +impl TraceProvider for MockOutputTraceProvider { + async fn absolute_prestate(&self, _: Position) -> Result> { + Ok(Arc::<[u8; 32]>::new( + U256::from(self.starting_block_number).to_be_bytes(), + )) + } + + async fn absolute_prestate_hash(&self, position: Position) -> Result { + // The raw state is equivalent to the state hash in the output trace provider. It must be 32 bytes in size. + Ok((*self.absolute_prestate(position).await?).try_into()?) + } + + async fn state_at(&self, position: Position) -> Result> { + let state = + U256::from(position.trace_index(self.leaf_depth) + self.starting_block_number + 1); + Ok(Arc::<[u8; 32]>::new(state.to_be_bytes())) + } + + async fn state_hash(&self, position: Position) -> Result { + // The raw state is equivalent to the state hash in the output trace provider. It must be 32 bytes in size. + Ok((*self.state_at(position).await?).try_into()?) + } + + async fn proof_at(&self, _: Position) -> Result> { + unimplemented!("Proofs are not supported for the OutputTraceProvider") + } +} diff --git a/crates/fault/src/providers/mocks/mod.rs b/crates/fault/src/providers/mocks/mod.rs new file mode 100644 index 0000000..713c7ed --- /dev/null +++ b/crates/fault/src/providers/mocks/mod.rs @@ -0,0 +1,7 @@ +//! Mock implementations of the [crate::TraceProvider] trait for testing. + +mod alphabet; +pub use self::alphabet::AlphabetTraceProvider; + +mod mock_output; +pub use self::mock_output::MockOutputTraceProvider; diff --git a/crates/fault/src/providers/mod.rs b/crates/fault/src/providers/mod.rs index e39ad66..c8dab92 100644 --- a/crates/fault/src/providers/mod.rs +++ b/crates/fault/src/providers/mod.rs @@ -1,13 +1,13 @@ //! This modules contains trace providers for the variants of the [crate::FaultDisputeGame]. -mod alphabet; -pub use self::alphabet::AlphabetTraceProvider; +mod split; +pub use self::split::SplitTraceProvider; mod output; pub use self::output::OutputTraceProvider; -mod split; -pub use self::split::SplitTraceProvider; - mod cannon; pub use self::cannon::CannonTraceProvider; + +mod mocks; +pub use self::mocks::{AlphabetTraceProvider, MockOutputTraceProvider}; diff --git a/crates/fault/src/providers/output.rs b/crates/fault/src/providers/output.rs index f3657d3..f9dbcae 100644 --- a/crates/fault/src/providers/output.rs +++ b/crates/fault/src/providers/output.rs @@ -2,7 +2,7 @@ //! rollup node. use crate::{Gindex, Position, TraceProvider}; -use alloy_primitives::{keccak256, B256}; +use alloy_primitives::B256; use alloy_rpc_client::RpcClient; use alloy_transport::TransportResult; use alloy_transport_http::Http; @@ -19,13 +19,21 @@ pub struct OutputTraceProvider { pub leaf_depth: u8, } +/// A minified response of the `optimism_outputAtBlock` RPC method from the rollup node, containing only the output root +/// requested. +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OutputAtBlockResponse { + pub output_root: B256, +} + impl OutputTraceProvider { pub fn try_new( - l2_archive_url: String, + l2_archive_url: impl AsRef, starting_block_number: u64, leaf_depth: u8, ) -> Result { - let rpc_client = RpcClient::builder().reqwest_http(Url::parse(&l2_archive_url)?); + let rpc_client = RpcClient::builder().reqwest_http(Url::parse(l2_archive_url.as_ref())?); Ok(Self { rpc_client, starting_block_number, @@ -34,39 +42,35 @@ impl OutputTraceProvider { } } -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct OutputAtBlockResponse { - pub output_root: B256, -} - #[async_trait::async_trait] -impl TraceProvider<[u8; 32]> for OutputTraceProvider { - async fn absolute_prestate(&self) -> Result> { +impl TraceProvider for OutputTraceProvider { + async fn absolute_prestate(&self, _: Position) -> Result> { let result: TransportResult = self .rpc_client - .prepare("optimism_outputAtBlock", (self.starting_block_number)) + .prepare("optimism_outputAtBlock", self.starting_block_number) .await; Ok(Arc::new(*result?.output_root)) } - async fn absolute_prestate_hash(&self) -> Result { - Ok(keccak256(self.absolute_prestate().await?.as_slice())) + async fn absolute_prestate_hash(&self, position: Position) -> Result { + // The raw state is equivalent to the state hash in the output trace provider. It must be 32 bytes in size. + Ok((*self.absolute_prestate(position).await?).try_into()?) } - async fn state_at(&self, position: Position) -> Result> { + async fn state_at(&self, position: Position) -> Result> { let result: TransportResult = self .rpc_client .prepare( "optimism_outputAtBlock", - (self.starting_block_number + position.trace_index(self.leaf_depth)), + self.starting_block_number + position.trace_index(self.leaf_depth) + 1, ) .await; Ok(Arc::new(*result?.output_root)) } async fn state_hash(&self, position: Position) -> Result { - Ok(keccak256(self.state_at(position).await?.as_slice())) + // The raw state is equivalent to the state hash in the output trace provider. It must be 32 bytes in size. + Ok((*self.state_at(position).await?).try_into()?) } async fn proof_at(&self, _: Position) -> Result> { diff --git a/crates/fault/src/providers/split.rs b/crates/fault/src/providers/split.rs index 1ae1912..554c7fd 100644 --- a/crates/fault/src/providers/split.rs +++ b/crates/fault/src/providers/split.rs @@ -2,53 +2,78 @@ //! based off of the input depth. This implementation can be used to compose several layers of bisection. use crate::{Gindex, Position, TraceProvider}; -use alloy_primitives::keccak256; use anyhow::Result; use durin_primitives::Claim; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; /// The [SplitTraceProvider] is a [TraceProvider] that composes two trace providers together based off of the input depth. -pub struct SplitTraceProvider +pub struct SplitTraceProvider where - T: AsRef<[u8]> + Send + Sync, - TOP: TraceProvider, - BOTTOM: TraceProvider, + Top: TraceProvider, + Bottom: TraceProvider, { - pub top: TOP, - pub bottom: BOTTOM, + pub top: Top, + pub bottom: Bottom, pub split_depth: u8, - pub _phantom: PhantomData, +} + +impl SplitTraceProvider +where + Top: TraceProvider, + Bottom: TraceProvider, +{ + pub fn new(top: Top, bottom: Bottom, split_depth: u8) -> Self { + Self { + top, + bottom, + split_depth, + } + } } #[async_trait::async_trait] -impl TraceProvider for SplitTraceProvider +impl TraceProvider for SplitTraceProvider where - T: AsRef<[u8]> + Send + Sync, - TOP: TraceProvider + Sync, - BOTTOM: TraceProvider + Sync, + Top: TraceProvider + Sync, + Bottom: TraceProvider + Sync, { - async fn absolute_prestate(&self) -> Result> { - todo!() + async fn absolute_prestate(&self, position: Position) -> Result> { + if position.depth() <= self.split_depth { + self.top.absolute_prestate(position).await + } else { + self.bottom.absolute_prestate(position).await + } } - async fn absolute_prestate_hash(&self) -> Result { - todo!() + async fn absolute_prestate_hash(&self, position: Position) -> Result { + if position.depth() <= self.split_depth { + self.top.absolute_prestate_hash(position).await + } else { + self.bottom.absolute_prestate_hash(position).await + } } - async fn state_at(&self, position: Position) -> Result> { + async fn state_at(&self, position: Position) -> Result> { if position.depth() <= self.split_depth { self.top.state_at(position).await } else { - // TODO: Pass relative position based on split depth? self.bottom.state_at(position).await } } async fn state_hash(&self, position: Position) -> Result { - Ok(keccak256(self.state_at(position).await?.as_ref())) + if position.depth() <= self.split_depth { + self.top.state_hash(position).await + } else { + self.bottom.state_hash(position).await + } } - async fn proof_at(&self, _: Position) -> Result> { - todo!() + async fn proof_at(&self, position: Position) -> Result> { + if position.depth() <= self.split_depth { + self.top.proof_at(position).await + } else { + self.bottom.proof_at(position).await + } } } diff --git a/crates/fault/src/solver.rs b/crates/fault/src/solver.rs index 6af3556..413797f 100644 --- a/crates/fault/src/solver.rs +++ b/crates/fault/src/solver.rs @@ -12,40 +12,35 @@ use tokio::sync::Mutex; /// A [FaultDisputeSolver] is a [DisputeSolver] that is played over a fault proof VM backend. The solver is responsible /// for honestly responding to any given [ClaimData] in a given [FaultDisputeState]. It uses a [TraceProvider] to fetch /// the absolute prestate of the VM as well as the state at any given [Position] within the tree. -pub struct FaultDisputeSolver +pub struct FaultDisputeSolver where - T: AsRef<[u8]>, - P: TraceProvider, - S: FaultClaimSolver, + S: FaultClaimSolver

, + P: TraceProvider, { pub inner: S, - _phantom_t: PhantomData, - _phantom_p: PhantomData

, + _phantom: PhantomData

, } -impl FaultDisputeSolver +impl FaultDisputeSolver where - T: AsRef<[u8]>, - P: TraceProvider, - S: FaultClaimSolver, + S: FaultClaimSolver

, + P: TraceProvider, { - pub fn provider(&self) -> &P { + pub fn provider(&self) -> &impl TraceProvider { self.inner.provider() } } #[async_trait::async_trait] -impl DisputeSolver> - for FaultDisputeSolver +impl DisputeSolver for FaultDisputeSolver where - T: AsRef<[u8]> + Sync + Send, - P: TraceProvider + Sync, - S: FaultClaimSolver + Sync, + S: FaultClaimSolver

+ Sync, + P: TraceProvider + Sync, { async fn available_moves( &self, game: Arc>, - ) -> Result]>> { + ) -> Result> { let game_lock = game.lock().await; // Fetch the local opinion on the root claim. @@ -79,19 +74,17 @@ where } } -impl FaultDisputeSolver +impl FaultDisputeSolver where - T: AsRef<[u8]>, - P: TraceProvider, - S: FaultClaimSolver, + S: FaultClaimSolver

, + P: TraceProvider, { const ROOT_CLAIM_POSITION: Position = 1; pub fn new(claim_solver: S) -> Self { Self { inner: claim_solver, - _phantom_t: PhantomData, - _phantom_p: PhantomData, + _phantom: PhantomData, } } } diff --git a/crates/fault/src/solvers/alpha.rs b/crates/fault/src/solvers/alpha.rs index bebef08..fa51647 100644 --- a/crates/fault/src/solvers/alpha.rs +++ b/crates/fault/src/solvers/alpha.rs @@ -1,33 +1,22 @@ //! Implementation of the [FaultClaimSolver] trait on the [FaultDisputeSolver]. -#![allow(dead_code, unused_variables)] - use crate::{ ClaimData, FaultClaimSolver, FaultDisputeGame, FaultDisputeState, FaultSolverResponse, Gindex, Position, TraceProvider, }; use anyhow::{anyhow, Result}; use durin_primitives::Claim; -use std::{marker::PhantomData, sync::Arc}; +use std::sync::Arc; use tokio::sync::Mutex; /// The alpha claim solver is the first iteration of the Fault dispute game solver used /// in the alpha release of the Fault proof system on Optimism. -struct AlphaClaimSolver -where - T: AsRef<[u8]>, - P: TraceProvider, -{ +struct AlphaClaimSolver { provider: P, - _phantom: PhantomData, } #[async_trait::async_trait] -impl FaultClaimSolver for AlphaClaimSolver -where - T: AsRef<[u8]> + Send + Sync, - P: TraceProvider + Sync, -{ +impl FaultClaimSolver

for AlphaClaimSolver

{ /// Finds the best move against a [crate::ClaimData] in a given [FaultDisputeState]. /// /// ### Takes @@ -42,7 +31,7 @@ where world: Arc>, claim_index: usize, attacking_root: bool, - ) -> Result> { + ) -> Result { let mut world_lock = world.lock().await; // Fetch the maximum depth of the game's position tree. @@ -94,7 +83,7 @@ where // the prestate position based off of `is_attack` and the incorrect claim's // position. let (pre_state, proof) = if claim.position.index_at_depth() == 0 && is_attack { - let pre_state = self.provider.absolute_prestate().await?; + let pre_state = self.provider.absolute_prestate(claim.position).await?; // TODO(clabby): There may be a proof for the absolute prestate in Cannon. let proof: Arc<[u8]> = Arc::new([]); @@ -142,16 +131,9 @@ where } } -impl AlphaClaimSolver -where - T: AsRef<[u8]>, - P: TraceProvider, -{ +impl AlphaClaimSolver

{ fn new(provider: P) -> Self { - Self { - provider, - _phantom: PhantomData, - } + Self { provider } } /// Fetches the state hash at a given position from a [TraceProvider]. @@ -174,7 +156,7 @@ where provider: &P, position: Position, observed_claim: &mut ClaimData, - ) -> Result> { + ) -> Result> { let state_at = provider.state_at(position).await.map_err(|e| { observed_claim.visited = false; e @@ -196,327 +178,303 @@ where } } -/// The rules module contains implementations of the [Rule] type for the -/// alpha solver. -/// -/// These rules define the conditions of the game state that must be met before -/// and after state transitions and are used to test the validity of the solving -/// algorithm with various resolution methods. -pub mod rules { - use crate::FaultDisputeState; - use durin_primitives::rule::Rule; - use std::sync::Arc; - - fn pre_move_rules() -> &'static [Rule>] { - &[] - } - - fn post_move_rules() -> &'static [Rule>] { - &[] - } -} - -// TODO: prop tests for solving claims. -#[cfg(test)] -mod test { - use super::*; - use crate::{providers::AlphabetTraceProvider, ClaimData, FaultDisputeSolver}; - use alloy_primitives::{hex, Address, U128, U256}; - use durin_primitives::{Claim, DisputeSolver, GameStatus}; - use tokio::sync::Mutex; - - fn mocks() -> ( - FaultDisputeSolver< - [u8; 32], - AlphabetTraceProvider, - AlphaClaimSolver<[u8; 32], AlphabetTraceProvider>, - >, - Claim, - ) { - let provider = AlphabetTraceProvider::new(b'a' as u64, 4); - let claim_solver = AlphaClaimSolver::new(provider); - let solver = FaultDisputeSolver::new(claim_solver); - let root_claim = Claim::from_slice(&hex!( - "c0ffee00c0de0000000000000000000000000000000000000000000000000000" - )); - (solver, root_claim) - } - - #[tokio::test] - async fn available_moves_root_only() { - let (solver, root_claim) = mocks(); - let moves = [ - ( - solver.provider().state_hash(1).await.unwrap(), - FaultSolverResponse::Skip(0), - ), - ( - root_claim, - FaultSolverResponse::Move(true, 0, solver.provider().state_hash(2).await.unwrap()), - ), - ]; - - for (claim, expected_move) in moves { - let mut state = FaultDisputeState::new( - vec![ClaimData { - parent_index: u32::MAX, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: claim, - position: 1, - clock: 0, - }], - claim, - GameStatus::InProgress, - 4, - ); - - let moves = solver - .available_moves(Arc::new(Mutex::new(state))) - .await - .unwrap(); - assert_eq!(&[expected_move], moves.as_ref()); - } - } - - #[tokio::test] - async fn available_moves_static() { - let (solver, root_claim) = mocks(); - let moves = [ - ( - solver.provider().state_hash(4).await.unwrap(), - FaultSolverResponse::Move( - false, - 2, - solver.provider().state_hash(10).await.unwrap(), - ), - ), - ( - root_claim, - FaultSolverResponse::Move(true, 2, solver.provider().state_hash(8).await.unwrap()), - ), - ]; - - for (claim, expected_move) in moves { - let mut state = FaultDisputeState::new( - vec![ - ClaimData { - parent_index: u32::MAX, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: root_claim, - position: 1, - clock: 0, - }, - ClaimData { - parent_index: 0, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: solver.provider().state_hash(2).await.unwrap(), - position: 2, - clock: 0, - }, - ClaimData { - parent_index: 1, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: claim, - position: 4, - clock: 0, - }, - ], - root_claim, - GameStatus::InProgress, - 4, - ); - - let moves = solver - .available_moves(Arc::new(Mutex::new(state))) - .await - .unwrap(); - assert_eq!(&[expected_move], moves.as_ref()); - } - } - - #[tokio::test] - async fn available_moves_static_many() { - let (solver, root_claim) = mocks(); - let mut state = FaultDisputeState::new( - vec![ - // Invalid root claim - ATTACK - ClaimData { - parent_index: u32::MAX, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: root_claim, - position: 1, - clock: 0, - }, - // Right level; Wrong claim - SKIP - ClaimData { - parent_index: 0, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: root_claim, - position: 2, - clock: 0, - }, - // Wrong level; Right claim - DEFEND - ClaimData { - parent_index: 1, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: solver.provider().state_hash(4).await.unwrap(), - position: 4, - clock: 0, - }, - // Right level; Wrong claim - SKIP - ClaimData { - parent_index: 3, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: root_claim, - position: 8, - clock: 0, - }, - ], - root_claim, - GameStatus::InProgress, - 4, - ); - - let moves = solver - .available_moves(Arc::new(Mutex::new(state))) - .await - .unwrap(); - assert_eq!( - &[ - FaultSolverResponse::Move(true, 0, solver.provider().state_hash(2).await.unwrap()), - FaultSolverResponse::Skip(1), - FaultSolverResponse::Move( - false, - 2, - solver.provider().state_hash(10).await.unwrap() - ), - FaultSolverResponse::Skip(3) - ], - moves.as_ref() - ); - } - - #[tokio::test] - async fn available_moves_static_step() { - let (solver, root_claim) = mocks(); - let cases = [ - ( - FaultSolverResponse::Step( - true, - 4, - Arc::new(U256::from(b'a').to_be_bytes()), - Arc::new([]), - ), - true, - ), - ( - FaultSolverResponse::Step( - false, - 4, - Arc::new(U256::from(b'b').to_be_bytes()), - Arc::new([]), - ), - false, - ), - ]; - - for (expected_response, wrong_leaf) in cases { - let mut state = FaultDisputeState::new( - vec![ - // Invalid root claim - ATTACK - ClaimData { - parent_index: u32::MAX, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: root_claim, - position: 1, - clock: 0, - }, - // Honest Attack - ClaimData { - parent_index: 0, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: solver.provider().state_hash(2).await.unwrap(), - position: 2, - clock: 0, - }, - // Wrong level; Wrong claim - ATTACK - ClaimData { - parent_index: 1, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: root_claim, - position: 4, - clock: 0, - }, - // Honest Attack - ClaimData { - parent_index: 2, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: true, - value: solver.provider().state_hash(8).await.unwrap(), - position: 8, - clock: 0, - }, - // Wrong level; Wrong claim - ATTACK STEP - ClaimData { - parent_index: 3, - countered_by: Address::ZERO, - claimant: Address::ZERO, - bond: U128::ZERO, - visited: false, - value: if wrong_leaf { - root_claim - } else { - solver.provider().state_hash(16).await.unwrap() - }, - position: 16, - clock: 0, - }, - ], - root_claim, - GameStatus::InProgress, - 4, - ); - - let moves = solver - .available_moves(Arc::new(Mutex::new(state))) - .await - .unwrap(); - assert_eq!(&[expected_response], moves.as_ref()); - } - } -} +// // TODO: prop tests for solving claims. +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::{providers::AlphabetTraceProvider, ClaimData, FaultDisputeSolver}; +// use alloy_primitives::{hex, Address, U128, U256}; +// use durin_primitives::{Claim, DisputeSolver, GameStatus}; +// use tokio::sync::Mutex; +// +// fn mocks() -> ( +// FaultDisputeSolver, AlphabetTraceProvider>, +// Claim, +// ) { +// let provider = AlphabetTraceProvider::new(b'a' as u64, 4); +// let claim_solver = AlphaClaimSolver::new(provider); +// let solver = FaultDisputeSolver::new(claim_solver); +// let root_claim = Claim::from_slice(&hex!( +// "c0ffee00c0de0000000000000000000000000000000000000000000000000000" +// )); +// (solver, root_claim) +// } +// +// #[tokio::test] +// async fn available_moves_root_only() { +// let (solver, root_claim) = mocks(); +// let moves = [ +// ( +// solver.provider().state_hash(1).await.unwrap(), +// FaultSolverResponse::Skip(0), +// ), +// ( +// root_claim, +// FaultSolverResponse::Move(true, 0, solver.provider().state_hash(2).await.unwrap()), +// ), +// ]; +// +// for (claim, expected_move) in moves { +// let mut state = FaultDisputeState::new( +// vec![ClaimData { +// parent_index: u32::MAX, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: claim, +// position: 1, +// clock: 0, +// }], +// claim, +// GameStatus::InProgress, +// 4, +// ); +// +// let moves = solver +// .available_moves(Arc::new(Mutex::new(state))) +// .await +// .unwrap(); +// assert_eq!(&[expected_move], moves.as_ref()); +// } +// } +// +// #[tokio::test] +// async fn available_moves_static() { +// let (solver, root_claim) = mocks(); +// let moves = [ +// ( +// solver.provider().state_hash(4).await.unwrap(), +// FaultSolverResponse::Move( +// false, +// 2, +// solver.provider().state_hash(10).await.unwrap(), +// ), +// ), +// ( +// root_claim, +// FaultSolverResponse::Move(true, 2, solver.provider().state_hash(8).await.unwrap()), +// ), +// ]; +// +// for (claim, expected_move) in moves { +// let mut state = FaultDisputeState::new( +// vec![ +// ClaimData { +// parent_index: u32::MAX, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: root_claim, +// position: 1, +// clock: 0, +// }, +// ClaimData { +// parent_index: 0, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: solver.provider().state_hash(2).await.unwrap(), +// position: 2, +// clock: 0, +// }, +// ClaimData { +// parent_index: 1, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: claim, +// position: 4, +// clock: 0, +// }, +// ], +// root_claim, +// GameStatus::InProgress, +// 4, +// ); +// +// let moves = solver +// .available_moves(Arc::new(Mutex::new(state))) +// .await +// .unwrap(); +// assert_eq!(&[expected_move], moves.as_ref()); +// } +// } +// +// #[tokio::test] +// async fn available_moves_static_many() { +// let (solver, root_claim) = mocks(); +// let mut state = FaultDisputeState::new( +// vec![ +// // Invalid root claim - ATTACK +// ClaimData { +// parent_index: u32::MAX, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: root_claim, +// position: 1, +// clock: 0, +// }, +// // Right level; Wrong claim - SKIP +// ClaimData { +// parent_index: 0, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: root_claim, +// position: 2, +// clock: 0, +// }, +// // Wrong level; Right claim - DEFEND +// ClaimData { +// parent_index: 1, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: solver.provider().state_hash(4).await.unwrap(), +// position: 4, +// clock: 0, +// }, +// // Right level; Wrong claim - SKIP +// ClaimData { +// parent_index: 3, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: root_claim, +// position: 8, +// clock: 0, +// }, +// ], +// root_claim, +// GameStatus::InProgress, +// 4, +// ); +// +// let moves = solver +// .available_moves(Arc::new(Mutex::new(state))) +// .await +// .unwrap(); +// assert_eq!( +// &[ +// FaultSolverResponse::Move(true, 0, solver.provider().state_hash(2).await.unwrap()), +// FaultSolverResponse::Skip(1), +// FaultSolverResponse::Move( +// false, +// 2, +// solver.provider().state_hash(10).await.unwrap() +// ), +// FaultSolverResponse::Skip(3) +// ], +// moves.as_ref() +// ); +// } +// +// #[tokio::test] +// async fn available_moves_static_step() { +// let (solver, root_claim) = mocks(); +// let cases = [ +// ( +// FaultSolverResponse::Step( +// true, +// 4, +// Arc::<[u8; 32]>::new(U256::from(b'a').to_be_bytes()), +// Arc::new([]), +// ), +// true, +// ), +// ( +// FaultSolverResponse::Step( +// false, +// 4, +// Arc::<[u8; 32]>::new(U256::from(b'b').to_be_bytes()), +// Arc::new([]), +// ), +// false, +// ), +// ]; +// +// for (expected_response, wrong_leaf) in cases { +// let mut state = FaultDisputeState::new( +// vec![ +// // Invalid root claim - ATTACK +// ClaimData { +// parent_index: u32::MAX, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: root_claim, +// position: 1, +// clock: 0, +// }, +// // Honest Attack +// ClaimData { +// parent_index: 0, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: solver.provider().state_hash(2).await.unwrap(), +// position: 2, +// clock: 0, +// }, +// // Wrong level; Wrong claim - ATTACK +// ClaimData { +// parent_index: 1, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: root_claim, +// position: 4, +// clock: 0, +// }, +// // Honest Attack +// ClaimData { +// parent_index: 2, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: true, +// value: solver.provider().state_hash(8).await.unwrap(), +// position: 8, +// clock: 0, +// }, +// // Wrong level; Wrong claim - ATTACK STEP +// ClaimData { +// parent_index: 3, +// countered_by: Address::ZERO, +// claimant: Address::ZERO, +// bond: U128::ZERO, +// visited: false, +// value: if wrong_leaf { +// root_claim +// } else { +// solver.provider().state_hash(16).await.unwrap() +// }, +// position: 16, +// clock: 0, +// }, +// ], +// root_claim, +// GameStatus::InProgress, +// 4, +// ); +// +// let moves = solver +// .available_moves(Arc::new(Mutex::new(state))) +// .await +// .unwrap(); +// assert_eq!(&[expected_response], moves.as_ref()); +// } +// } +// } diff --git a/crates/fault/src/solvers/alpha_chad.rs b/crates/fault/src/solvers/alpha_chad.rs new file mode 100644 index 0000000..0dc52de --- /dev/null +++ b/crates/fault/src/solvers/alpha_chad.rs @@ -0,0 +1,276 @@ +//! Implementation of the [FaultClaimSolver] trait on the [FaultDisputeSolver]. + +use crate::{ + providers::SplitTraceProvider, ClaimData, FaultClaimSolver, FaultDisputeGame, + FaultDisputeState, FaultSolverResponse, Gindex, Position, TraceProvider, +}; +use anyhow::{anyhow, Result}; +use durin_primitives::Claim; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// The alpha chad claim solver is the second iteration of the fault claim solver. It contains logic for handling +/// multiple bisection layers and acting on preimage hints. +struct ChadClaimSolver { + provider: SplitTraceProvider, +} + +#[async_trait::async_trait] +impl FaultClaimSolver> for ChadClaimSolver +where + Top: TraceProvider + Sync, + Bottom: TraceProvider + Sync, +{ + /// Finds the best move against a [crate::ClaimData] in a given [FaultDisputeState]. + /// + /// ### Takes + /// - `world`: The [FaultDisputeState] to solve against. + /// - `claim_index`: The index of the claim within the state DAG. + /// - `attacking_root`: A boolean indicating whether or not the solver is attacking the root. + /// + /// ### Returns + /// - [FaultSolverResponse] or [Err]: The best move against the claim. + async fn solve_claim( + &self, + world: Arc>, + claim_index: usize, + attacking_root: bool, + ) -> Result { + let mut world_lock = world.lock().await; + + // Fetch the split & maximum depth of the game's position tree. + let (split_depth, max_depth) = (world_lock.split_depth, world_lock.max_depth); + + // Fetch the ClaimData and its position's depth from the world state DAG. + let claim = world_lock + .state_mut() + .get_mut(claim_index) + .ok_or(anyhow!("Failed to fetch claim from passed state"))?; + let claim_depth = claim.position.depth(); + + // Mark the claim as visited. This mutates the passed state and must be reverted if an error is thrown below. + claim.visited = true; + + let local_claim = Self::fetch_state_hash(self.provider(), claim.position, claim).await?; + let local_agree = local_claim == claim.value; + let right_level = attacking_root != (claim_depth % 2 == 0); + + // Check if the observed claim is the root claim. + if claim.parent_index == u32::MAX { + // If we agree with the root claim and it is on a level that we are defending, we ignore it. + if local_agree && right_level { + return Ok(FaultSolverResponse::Skip(claim_index)); + } + + // The parent claim is the root claim, so if we disagree with it, by definition we must begin the game with + // an attack move. + let claimed_hash = + Self::fetch_state_hash(self.provider(), claim.position.make_move(true), claim) + .await?; + return Ok(FaultSolverResponse::Move(true, claim_index, claimed_hash)); + } else { + // Never attempt to defend an execution trace subgame root. We only attack if we disagree with it, otherwise + // we want to do nothing. + // TODO: This isn't entirely right. See `op-challenger` semantics. + if claim_depth == split_depth + 1 && local_agree { + return Ok(FaultSolverResponse::Skip(claim_index)); + } + + // Never counter a claim that is on a level we agree with, even if it is wrong. If it is uncountered, it + // furthers the goal of the honest challenger, and even if it is countered, the step will prove that it is + // also wrong. + // TODO: This isn't entirely right. See `op-challenger` semantics. + if right_level { + return Ok(FaultSolverResponse::Skip(claim_index)); + } + + // Compute the position of the next move. If we agree with the claim, we bisect right, otherwise we bisect + // left. + let move_pos = claim.position.make_move(!local_agree); + + // If the move position's depth is less than the max depth, it is a bisection move. If it is 1 greater than + // the max depth, it is a step move. + if move_pos.depth() <= max_depth { + let move_claim = Self::fetch_state_hash(self.provider(), move_pos, claim).await?; + Ok(FaultSolverResponse::Move( + !local_agree, + claim_index, + move_claim, + )) + } else { + // If the move is an attack against the first leaf, the prestate is the absolute prestate. Otherwise, + // the prestate is present in the branch taken during bisection. + let prestate = if move_pos.index_at_depth() + % 2u64.pow((max_depth - split_depth) as u32) + != 0 + { + // If the move is an attack, the prestate commits to `claim.position - 1`. + // If the move is a defense, the prestate commits to `claim.position`. + if local_agree { + Self::fetch_state_at(self.provider(), claim.position, claim).await? + } else { + Self::fetch_state_at(self.provider(), claim.position - 1, claim).await? + } + } else { + Self::fetch_absolute_prestate(self.provider(), move_pos, claim).await? + }; + + let proof = Self::fetch_proof_at(self.provider(), move_pos, claim).await?; + Ok(FaultSolverResponse::Step( + !local_agree, + claim_index, + prestate, + proof, + )) + } + } + } + + fn provider(&self) -> &SplitTraceProvider { + &self.provider + } +} + +impl ChadClaimSolver +where + Top: TraceProvider + Sync, + Bottom: TraceProvider + Sync, +{ + fn new(provider: SplitTraceProvider) -> Self { + Self { provider } + } + + /// Fetches the state hash at a given position from a [TraceProvider]. + /// If the fetch fails, the claim is marked as unvisited and the error is returned. + #[inline] + pub(crate) async fn fetch_absolute_prestate( + provider: &SplitTraceProvider, + position: Position, + observed_claim: &mut ClaimData, + ) -> Result> { + let absolute_prestate = provider.absolute_prestate(position).await.map_err(|e| { + observed_claim.visited = false; + e + })?; + Ok(absolute_prestate) + } + + /// Fetches the state hash at a given position from a [TraceProvider]. + /// If the fetch fails, the claim is marked as unvisited and the error is returned. + #[inline] + pub(crate) async fn fetch_state_hash( + provider: &SplitTraceProvider, + position: Position, + observed_claim: &mut ClaimData, + ) -> Result { + let state_hash = provider.state_hash(position).await.map_err(|e| { + observed_claim.visited = false; + e + })?; + Ok(state_hash) + } + + #[inline] + pub(crate) async fn fetch_state_at( + provider: &SplitTraceProvider, + position: Position, + observed_claim: &mut ClaimData, + ) -> Result> { + let state_at = provider.state_at(position).await.map_err(|e| { + observed_claim.visited = false; + e + })?; + Ok(state_at) + } + + #[inline] + pub(crate) async fn fetch_proof_at( + provider: &SplitTraceProvider, + position: Position, + observed_claim: &mut ClaimData, + ) -> Result> { + let proof_at = provider.proof_at(position).await.map_err(|e| { + observed_claim.visited = false; + e + })?; + Ok(proof_at) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + providers::{AlphabetTraceProvider, MockOutputTraceProvider}, + ClaimData, FaultDisputeSolver, + }; + use alloy_primitives::{hex, Address, U128}; + use durin_primitives::{Claim, DisputeSolver, GameStatus}; + use tokio::sync::Mutex; + + // Test tree configurations. + const MAX_DEPTH: u8 = 8; + const SPLIT_DEPTH: u8 = 4; + + fn mocks() -> ( + FaultDisputeSolver< + ChadClaimSolver, + SplitTraceProvider, + >, + Claim, + ) { + let output_provider = MockOutputTraceProvider::new(0, SPLIT_DEPTH); + let trace_provder = AlphabetTraceProvider::new(0, MAX_DEPTH); + let claim_solver = ChadClaimSolver::new(SplitTraceProvider::new( + output_provider, + trace_provder, + SPLIT_DEPTH, + )); + + let state_solver = FaultDisputeSolver::new(claim_solver); + let root_claim = Claim::from_slice(&hex!( + "c0ffee00c0de0000000000000000000000000000000000000000000000000000" + )); + (state_solver, root_claim) + } + + #[tokio::test] + async fn available_moves_root_only() { + let (solver, root_claim) = mocks(); + let moves = [ + ( + solver.provider().state_hash(1).await.unwrap(), + FaultSolverResponse::Skip(0), + ), + ( + root_claim, + FaultSolverResponse::Move(true, 0, solver.provider().state_hash(2).await.unwrap()), + ), + ]; + + for (claim, expected_move) in moves { + let state = FaultDisputeState::new( + vec![ClaimData { + parent_index: u32::MAX, + countered_by: Address::ZERO, + claimant: Address::ZERO, + bond: U128::ZERO, + visited: false, + value: claim, + position: 1, + clock: 0, + }], + claim, + GameStatus::InProgress, + SPLIT_DEPTH, + MAX_DEPTH, + ); + + let moves = solver + .available_moves(Arc::new(Mutex::new(state))) + .await + .unwrap(); + assert_eq!(&[expected_move], moves.as_ref()); + } + } +} diff --git a/crates/fault/src/solvers/mod.rs b/crates/fault/src/solvers/mod.rs index 2ae860d..86864fe 100644 --- a/crates/fault/src/solvers/mod.rs +++ b/crates/fault/src/solvers/mod.rs @@ -1,5 +1,7 @@ -//! This module contains implementations of the [crate::FaultClaimSolver] trait for various -//! solving methods and resolvers. +//! This module contains implementations of the [crate::FaultClaimSolver] trait for various solving methods +//! and resolvers. -mod alpha; -pub use self::alpha::*; +#[deprecated(since = "0.1.0")] +pub mod alpha; + +pub mod alpha_chad; diff --git a/crates/fault/src/state.rs b/crates/fault/src/state.rs index deb2f1a..d0f08a6 100644 --- a/crates/fault/src/state.rs +++ b/crates/fault/src/state.rs @@ -32,6 +32,8 @@ pub struct FaultDisputeState { /// The status of the dispute game. status: GameStatus, /// The max depth of the position tree. + pub split_depth: u8, + /// The max depth of the position tree. pub max_depth: u8, } @@ -40,12 +42,14 @@ impl FaultDisputeState { state: Vec, root_claim: Claim, status: GameStatus, + split_depth: u8, max_depth: u8, ) -> Self { Self { state, root_claim, status, + split_depth, max_depth, } } diff --git a/crates/fault/src/traits.rs b/crates/fault/src/traits.rs index c26d05d..3d83a47 100644 --- a/crates/fault/src/traits.rs +++ b/crates/fault/src/traits.rs @@ -20,7 +20,7 @@ pub trait FaultDisputeGame: DisputeGame { /// A [FaultClaimSolver] is a solver that finds the correct response to a given [durin_primitives::Claim] /// within a [FaultDisputeGame]. #[async_trait::async_trait] -pub trait FaultClaimSolver, P: TraceProvider> { +pub trait FaultClaimSolver { /// Finds the best move against a [crate::ClaimData] in a given [FaultDisputeState]. /// /// ### Takes @@ -35,7 +35,7 @@ pub trait FaultClaimSolver, P: TraceProvider> { world: Arc>, claim_index: usize, attacking_root: bool, - ) -> Result>; + ) -> Result; /// Returns a shared reference to the [TraceProvider] that the solver uses to fetch the state of the VM and /// commitments to it. @@ -45,15 +45,15 @@ pub trait FaultClaimSolver, P: TraceProvider> { /// A [TraceProvider] is a type that can provide the raw state (in bytes) at a given [Position] within /// a [FaultDisputeGame]. #[async_trait::async_trait] -pub trait TraceProvider> { +pub trait TraceProvider { /// Returns the raw absolute prestate (in bytes). - async fn absolute_prestate(&self) -> Result>; + async fn absolute_prestate(&self, position: Position) -> Result>; /// Returns the absolute prestate hash. - async fn absolute_prestate_hash(&self) -> Result; + async fn absolute_prestate_hash(&self, position: Position) -> Result; /// Returns the raw state (in bytes) at the given position. - async fn state_at(&self, position: Position) -> Result>; + async fn state_at(&self, position: Position) -> Result>; /// Returns the state hash at the given position. async fn state_hash(&self, position: Position) -> Result; diff --git a/crates/fault/src/types.rs b/crates/fault/src/types.rs index 6396084..91a5a84 100644 --- a/crates/fault/src/types.rs +++ b/crates/fault/src/types.rs @@ -10,14 +10,21 @@ pub type Clock = u128; /// The [FaultSolverResponse] enum describes the response that a solver should return when asked to make a move. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FaultSolverResponse> { +pub enum FaultSolverResponse { /// A response indicating that the proper move is to attack or defend the given claim. Move(bool, usize, Claim), /// A response indicating that the proper move is to skip the given claim. Skip(usize), - /// A response indicating that the proper move is to perform a VM step against - /// the given claim. - Step(bool, usize, Arc, Arc<[u8]>), + /// A response indicating that the proper move is to perform a VM step against the given claim. + Step(bool, usize, Arc<[u8]>, Arc<[u8]>), + /// A response indicating that the actor should request the FDG to post a local preimage to the `PreimageOracle`. + LocalPreimage(usize, usize, usize), + /// A response indicating that the actor should post a preimage directly to the `PreimageOracle`. + GlobalPreimage(usize, Arc<[u8]>), + /// A response indicating that the actor should resolve an individual claim. + ResolveClaim(usize), + /// A response indicating that the actor should resolve the entire dispute game. + ResolveGlobal, } /// The [VMStatus] enum describes the status of a VM at a given position.