From 02fa90d017a14baa07d9a27efb126c41870f5652 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:42:12 +0800 Subject: [PATCH] test(prt-rollups): add blockchain-reader test --- .../node/blockchain-reader/Cargo.toml | 10 + .../node/blockchain-reader/src/lib.rs | 386 ++++++++++++++++-- .../node/compute-runner/src/lib.rs | 2 +- cartesi-rollups/node/dave-rollups/Cargo.toml | 2 +- cartesi-rollups/node/epoch-manager/src/lib.rs | 2 +- prt/Makefile | 2 +- prt/client-rs/src/arena/arena.rs | 2 +- prt/client-rs/src/strategy/gc.rs | 2 +- prt/client-rs/src/strategy/player.rs | 2 +- prt/contracts/deploy_anvil.sh | 2 +- prt/tests/compute-rs/src/lib.rs | 2 +- 11 files changed, 376 insertions(+), 38 deletions(-) diff --git a/cartesi-rollups/node/blockchain-reader/Cargo.toml b/cartesi-rollups/node/blockchain-reader/Cargo.toml index 041c7c7e..c10ccc41 100644 --- a/cartesi-rollups/node/blockchain-reader/Cargo.toml +++ b/cartesi-rollups/node/blockchain-reader/Cargo.toml @@ -24,3 +24,13 @@ log = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } num-traits = { workspace = true } + +[dev-dependencies] +cartesi-dave-merkle = { workspace = true } +cartesi-prt-core = { workspace = true } +cartesi-prt-contracts = { path = "../../../prt/contract-bindings" } + +clap = { workspace = true } +clap_derive = { workspace = true } +rusqlite = { version = "0.31.0", features = ["bundled"] } +rusqlite_migration = "1.2.0" diff --git a/cartesi-rollups/node/blockchain-reader/src/lib.rs b/cartesi-rollups/node/blockchain-reader/src/lib.rs index 049dc681..f1225bf3 100644 --- a/cartesi-rollups/node/blockchain-reader/src/lib.rs +++ b/cartesi-rollups/node/blockchain-reader/src/lib.rs @@ -8,10 +8,11 @@ use alloy::{ contract::{Error, Event}, eips::BlockNumberOrTag::Finalized, hex::ToHexExt, + primitives::Address, providers::{ network::primitives::BlockTransactionsKind, Provider, ProviderBuilder, RootProvider, }, - sol_types::{private::Address, SolEvent}, + sol_types::SolEvent, transports::http::{reqwest::Url, Client, Http}, }; use alloy_rpc_types_eth::Topic; @@ -206,7 +207,7 @@ where .last_input() .map_err(|e| BlockchainReaderError::StateManagerError(e))?; - let (mut next_input_index_in_epoch, mut last_epoch_number) = { + let (mut next_input_index_in_epoch, mut last_input_epoch_number) = { match last_input { // continue inserting inputs from where it was left Some(input) => (input.input_index_in_epoch + 1, input.epoch_number), @@ -218,7 +219,7 @@ where let mut inputs = vec![]; let mut input_events_peekable = input_events.iter().peekable(); for epoch in sealed_epochs_iter { - if last_epoch_number > epoch.epoch_number { + if last_input_epoch_number > epoch.epoch_number { continue; } // iterate through newly sealed epochs, fill in the inputs accordingly @@ -230,12 +231,12 @@ where ); inputs.extend(inputs_of_epoch); - last_epoch_number = epoch.epoch_number + 1; + last_input_epoch_number = epoch.epoch_number + 1; } // all remaining inputs belong to an epoch that's not sealed yet let inputs_of_epoch = self.construct_input_ids( - last_epoch_number, + last_input_epoch_number, u64::MAX, &mut next_input_index_in_epoch, &mut input_events_peekable, @@ -438,32 +439,359 @@ impl PartitionProvider { } } -#[tokio::test] - -async fn test_input_reader() -> std::result::Result<(), Box> { - let genesis = 17784733; - let input_box = Address::from_str("0x59b22D57D4f067708AB0c00552767405926dc768")?; - let app = Address::from_str("0x0974cc873df893b302f6be7ecf4f9d4b1a15c366")? - .into_word() - .into(); - let infura_key = std::env::var("INFURA_KEY").expect("INFURA_KEY is not set"); - - let partition_provider = - PartitionProvider::new(format!("https://mainnet.infura.io/v3/{}", infura_key).as_ref())?; - let reader = EventReader::::new(); - - let res = reader - .next( - Some(&app), - &input_box, - genesis, - partition_provider.latest_finalized_block().await?, - &partition_provider, +#[cfg(test)] +mod blockchiain_reader_tests { + use crate::*; + use alloy::{ + primitives::Address, + sol_types::{SolCall, SolValue}, + transports::http::{Client, Http}, + }; + use cartesi_dave_contracts::daveconsensus::DaveConsensus::{self, EpochSealed}; + use cartesi_dave_merkle::Digest; + use cartesi_prt_contracts::{ + bottomtournamentfactory::BottomTournamentFactory, + middletournamentfactory::MiddleTournamentFactory, + multileveltournamentfactory::MultiLevelTournamentFactory::{ + self, CommitmentStructure, DisputeParameters, MultiLevelTournamentFactoryInstance, + TimeConstants, + }, + toptournamentfactory::TopTournamentFactory, + }; + use cartesi_prt_core::arena::{BlockchainConfig, EthArenaSender, SenderFiller}; + use cartesi_rollups_contracts::{ + inputbox::InputBox::{self, InputAdded}, + inputs::Inputs::EvmAdvanceCall, + }; + use rollups_state_manager::persistent_state_access::PersistentStateAccess; + + use clap::Parser; + use rusqlite::Connection; + use std::{ + process::{Child, Command, Stdio}, + sync::Arc, + }; + use tokio::{ + task::spawn, + time::{sleep, Duration}, + }; + + type Result = std::result::Result>; + const APP_ADDRESS: Address = Address::ZERO; + const URL: &str = "http://127.0.0.1:8545"; + const INITIAL_STATE: Digest = Digest::ZERO; + + async fn spawn_anvil() -> Result { + // Start the Anvil node + let anvil = Command::new("anvil") + .arg("-a") + .arg("40") + .arg("--block-time") + .arg("1") + .arg("--slots-in-an-epoch") + .arg("1") + .stdout(Stdio::piped()) + .spawn()?; + // Allow some time for the Anvil node to start + sleep(Duration::from_secs(1)).await; + + Ok(anvil) + } + + fn kill_child(mut child: Child) -> Result<()> { + Ok(child.kill()?) + } + + fn create_partition_rovider(url: &'static str) -> Result { + let partition_provider = PartitionProvider::new(url)?; + Ok(partition_provider) + } + + fn create_epoch_reader() -> EventReader { + EventReader::::new() + } + + fn create_input_reader() -> EventReader { + EventReader::::new() + } + + async fn create_provider() -> Result> { + let blockchain_config = BlockchainConfig::parse(); + let arena_sender = EthArenaSender::new(&blockchain_config)?; + Ok(arena_sender.client()) + } + + async fn deploy_inputbox<'a>( + provider: &'a Arc, + ) -> Result, &'a Arc>>> { + let inputbox = InputBox::deploy(provider).await?; + Ok(Arc::new(inputbox)) + } + + async fn deploy_daveconsensus<'a>( + provider: &'a Arc, + inputbox: &Address, + tournament_factory: &Address, + ) -> Result, &'a Arc>>> + { + let daveconsensus = DaveConsensus::deploy( + provider, + *inputbox, + APP_ADDRESS, + *tournament_factory, + INITIAL_STATE.into(), + ) + .await?; + Ok(Arc::new(daveconsensus)) + } + + async fn deploy_tournamentfactories<'a>( + provider: &'a Arc, + ) -> Result, &'a Arc>>> { + let dispute_parameters = DisputeParameters { + timeConstants: TimeConstants { + matchEffort: 60 * 2, + maxAllowance: 60 * (60 + 5 + 2), + }, + commitmentStructures: vec![ + CommitmentStructure { + log2step: 44, + height: 48, + }, + CommitmentStructure { + log2step: 28, + height: 16, + }, + CommitmentStructure { + log2step: 0, + height: 28, + }, + ], + }; + + let top_tournamentfactory = TopTournamentFactory::deploy(provider).await?; + let mid_tournamentfactory = MiddleTournamentFactory::deploy(provider).await?; + let bottom_tournamentfactory = BottomTournamentFactory::deploy(provider).await?; + let multi_tournamentfactory = MultiLevelTournamentFactory::deploy( + provider, + *top_tournamentfactory.address(), + *mid_tournamentfactory.address(), + *bottom_tournamentfactory.address(), + dispute_parameters, ) .await?; + Ok(Arc::new(multi_tournamentfactory)) + } + + async fn add_input<'a>( + inputbox: &'a Arc, &'a Arc>>, + input_payload: &'static str, + count: usize, + ) -> Result<()> { + for _ in 0..count { + inputbox + .addInput(APP_ADDRESS, input_payload.as_bytes().into()) + .send() + .await? + .watch() + .await?; + } + Ok(()) + } - // input box from mainnet shouldn't be empty - assert!(!res.is_empty(), "input box shouldn't be empty"); + async fn read_epochs_until_count( + consensus_address: &Address, + epoch_reader: &EventReader, + count: usize, + ) -> Result> { + let partition_provider = create_partition_rovider(URL)?; + let mut read_epochs = Vec::new(); + while read_epochs.len() != count { + // latest finalized block must be greater than 0 + let latest_finalized_block = + std::cmp::max(1, partition_provider.latest_finalized_block().await?); + + read_epochs = epoch_reader + .next( + None, + consensus_address, + 0, + latest_finalized_block, + &partition_provider, + ) + .await?; + // wait a few seconds for the input added block to be finalized + sleep(Duration::from_secs(1)).await; + } - Ok(()) + Ok(read_epochs) + } + + async fn read_inputs_until_count( + inputbox_address: &Address, + input_reader: &EventReader, + count: usize, + ) -> Result> { + let partition_provider = create_partition_rovider(URL)?; + let mut read_inputs = Vec::new(); + while read_inputs.len() != count { + // latest finalized block must be greater than 0 + let latest_finalized_block = + std::cmp::max(1, partition_provider.latest_finalized_block().await?); + + read_inputs = input_reader + .next( + Some(&APP_ADDRESS.into_word().into()), + inputbox_address, + 0, + latest_finalized_block, + &partition_provider, + ) + .await?; + // wait a few seconds for the input added block to be finalized + sleep(Duration::from_secs(1)).await; + } + + Ok(read_inputs) + } + + async fn read_inputs_from_db_until_count( + state_manager: &Arc, + epoch_number: u64, + count: usize, + ) -> Result>> + where + ::Error: Send + Sync + 'static, + { + let mut read_inputs = Vec::new(); + while read_inputs.len() != count { + read_inputs = state_manager.inputs(epoch_number)?; + // wait a few seconds for the db to be updated + sleep(Duration::from_secs(1)).await; + } + + Ok(read_inputs) + } + + async fn test_input_reader() -> Result<()> { + let anvil = spawn_anvil().await?; + let input_payload = "Hello!"; + let input_payload2 = "Hello Two!"; + + let provider = create_provider().await?; + let inputbox = deploy_inputbox(&provider).await?; + + let mut input_count = 2; + add_input(&inputbox, &input_payload, input_count).await?; + + let input_reader = create_input_reader(); + let mut read_inputs = + read_inputs_until_count(inputbox.address(), &input_reader, input_count).await?; + assert_eq!(read_inputs.len(), input_count); + + let received_payload = + EvmAdvanceCall::abi_decode(read_inputs[input_count - 1].input.as_ref(), true)?; + assert_eq!(received_payload.payload.as_ref(), input_payload.as_bytes()); + + input_count = 3; + add_input(&inputbox, &input_payload2, input_count).await?; + read_inputs = + read_inputs_until_count(inputbox.address(), &input_reader, input_count).await?; + assert_eq!(read_inputs.len(), input_count); + + let received_payload = + EvmAdvanceCall::abi_decode(read_inputs[input_count - 1].input.as_ref(), true)?; + assert_eq!(received_payload.payload.as_ref(), input_payload2.as_bytes()); + + kill_child(anvil)?; + + Ok(()) + } + + async fn test_epoch_reader() -> Result<()> { + let anvil = spawn_anvil().await?; + + let provider = create_provider().await?; + let inputbox = deploy_inputbox(&provider).await?; + + let multi_tournamentfactory = deploy_tournamentfactories(&provider).await?; + let daveconsensus = deploy_daveconsensus( + &provider, + inputbox.address(), + multi_tournamentfactory.address(), + ) + .await?; + + let epoch_reader = create_epoch_reader(); + let read_epochs = + read_epochs_until_count(daveconsensus.address(), &epoch_reader, 1).await?; + assert_eq!(read_epochs.len(), 1); + assert_eq!( + &read_epochs[0].initialMachineStateHash.abi_encode(), + INITIAL_STATE.slice() + ); + + kill_child(anvil)?; + + Ok(()) + } + + async fn test_blockchain_reader() -> Result<()> { + let anvil = spawn_anvil().await?; + let input_payload = "Hello!"; + let input_payload2 = "Hello Two!"; + + let provider = create_provider().await?; + let inputbox = deploy_inputbox(&provider).await?; + let state_manager = Arc::new(PersistentStateAccess::new( + Connection::open_in_memory().unwrap(), + )?); + + // add input for epoch 0 + let mut input_count = 2; + add_input(&inputbox, &input_payload, input_count).await?; + + let multi_tournamentfactory = deploy_tournamentfactories(&provider).await?; + let daveconsensus = deploy_daveconsensus( + &provider, + inputbox.address(), + multi_tournamentfactory.address(), + ) + .await?; + let mut blockchain_reader = BlockchainReader::new( + state_manager.clone(), + AddressBook { + app: APP_ADDRESS, + consensus: *daveconsensus.address(), + input_box: *inputbox.address(), + }, + URL, + 1, + )?; + + let _ = spawn(async move { + blockchain_reader.start().await.unwrap(); + }); + + read_inputs_from_db_until_count(&state_manager, 0, input_count).await?; + // add input for epoch 1 + input_count = 3; + add_input(&inputbox, &input_payload2, input_count).await?; + read_inputs_from_db_until_count(&state_manager, 1, input_count).await?; + + // kill anvil should cause the blockchain_reader to crash, thus end the test + kill_child(anvil).unwrap(); + + Ok(()) + } + + #[tokio::test] + // note: the tests involve interacting with anvil blockchain and a sqlite3 database, can be a bit time consuming (30+ seconds) + async fn test_readers_sequentially() -> Result<()> { + test_input_reader().await?; + test_epoch_reader().await?; + test_blockchain_reader().await?; + + Ok(()) + } } diff --git a/cartesi-rollups/node/compute-runner/src/lib.rs b/cartesi-rollups/node/compute-runner/src/lib.rs index bb7060b9..39fcb2b8 100644 --- a/cartesi-rollups/node/compute-runner/src/lib.rs +++ b/cartesi-rollups/node/compute-runner/src/lib.rs @@ -1,4 +1,4 @@ -use alloy::sol_types::private::Address; +use alloy::primitives::Address; use log::error; use std::result::Result; use std::{str::FromStr, sync::Arc, time::Duration}; diff --git a/cartesi-rollups/node/dave-rollups/Cargo.toml b/cartesi-rollups/node/dave-rollups/Cargo.toml index 33c8d0f6..5b7a745d 100644 --- a/cartesi-rollups/node/dave-rollups/Cargo.toml +++ b/cartesi-rollups/node/dave-rollups/Cargo.toml @@ -23,9 +23,9 @@ cartesi-prt-core = { workspace = true } alloy = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } +clap_derive = { workspace = true } futures = { workspace = true } tokio = { workspace = true } log = { workspace = true } -clap_derive = { workspace = true } rusqlite = { version = "0.31.0", features = ["bundled"] } env_logger = "0.11.5" diff --git a/cartesi-rollups/node/epoch-manager/src/lib.rs b/cartesi-rollups/node/epoch-manager/src/lib.rs index d2f39f75..ef817c28 100644 --- a/cartesi-rollups/node/epoch-manager/src/lib.rs +++ b/cartesi-rollups/node/epoch-manager/src/lib.rs @@ -1,4 +1,4 @@ -use alloy::{hex::ToHexExt, sol_types::private::Address}; +use alloy::{hex::ToHexExt, primitives::Address}; use anyhow::Result; use log::{error, info}; use num_traits::cast::ToPrimitive; diff --git a/prt/Makefile b/prt/Makefile index 5d0f2cf7..5dc11306 100644 --- a/prt/Makefile +++ b/prt/Makefile @@ -1,6 +1,6 @@ BINDINGS_DIR := ./contract-bindings/src/contract SRC_DIR := ./contracts -BINDINGS_FILTER := 'LeafTournament|RootTournament|^Tournament$$' +BINDINGS_FILTER := '^[^I].+TournamentFactory|LeafTournament|RootTournament|^Tournament$$' help: @echo ' clean - clean the generated bindings' diff --git a/prt/client-rs/src/arena/arena.rs b/prt/client-rs/src/arena/arena.rs index e83072b5..84b87852 100644 --- a/prt/client-rs/src/arena/arena.rs +++ b/prt/client-rs/src/arena/arena.rs @@ -2,7 +2,7 @@ use crate::machine::MachineCommitment; -use alloy::sol_types::private::Address; +use alloy::primitives::Address; use cartesi_dave_merkle::Digest; use ruint::aliases::U256; use std::collections::HashMap; diff --git a/prt/client-rs/src/strategy/gc.rs b/prt/client-rs/src/strategy/gc.rs index 6a8d8f4c..74e9762a 100644 --- a/prt/client-rs/src/strategy/gc.rs +++ b/prt/client-rs/src/strategy/gc.rs @@ -1,5 +1,5 @@ use ::log::info; -use alloy::sol_types::private::Address; +use alloy::primitives::Address; use anyhow::Result; use async_recursion::async_recursion; diff --git a/prt/client-rs/src/strategy/player.rs b/prt/client-rs/src/strategy/player.rs index c474566b..0a5bda66 100644 --- a/prt/client-rs/src/strategy/player.rs +++ b/prt/client-rs/src/strategy/player.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use ::log::{debug, error, info}; -use alloy::sol_types::private::Address; +use alloy::primitives::Address; use anyhow::Result; use async_recursion::async_recursion; use num_traits::{cast::ToPrimitive, One}; diff --git a/prt/contracts/deploy_anvil.sh b/prt/contracts/deploy_anvil.sh index cdcb4d06..5e876ba3 100755 --- a/prt/contracts/deploy_anvil.sh +++ b/prt/contracts/deploy_anvil.sh @@ -18,4 +18,4 @@ output=$(eval $forge_script) top_tournament_addresses=$(echo $output | grep -oP 'new TopTournament@(0x[a-fA-F0-9]{40})' | grep -oP '0x[a-fA-F0-9]{40}') top_tournament_address=$(echo $top_tournament_addresses | cut -d ' ' -f 1) -echo $top_tournament_address \ No newline at end of file +echo $top_tournament_address diff --git a/prt/tests/compute-rs/src/lib.rs b/prt/tests/compute-rs/src/lib.rs index 949eaba6..2886adc4 100644 --- a/prt/tests/compute-rs/src/lib.rs +++ b/prt/tests/compute-rs/src/lib.rs @@ -1,4 +1,4 @@ -use alloy::sol_types::private::Address; +use alloy::primitives::Address; use cartesi_prt_core::arena::BlockchainConfig; use clap::Parser;