diff --git a/.vscode/settings.json b/.vscode/settings.json index ca332eb..f5d84be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,9 @@ "rust-analyzer.linkedProjects": [ "./light-client-guest/guest/Cargo.toml", "./batch-guest/guest/Cargo.toml", - "./host/Cargo.toml" + "./host/Cargo.toml", + "./core/Cargo.toml", + "./service/Cargo.toml" ], "rust-analyzer.check.extraEnv": { "RISC0_SKIP_BUILD": "1", diff --git a/Cargo.lock b/Cargo.lock index db7e8e7..533eb4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2580,6 +2580,7 @@ dependencies = [ "tendermint-light-client-verifier", "tendermint-rpc", "tokio", + "tracing", "tracing-subscriber 0.3.18", ] @@ -4877,6 +4878,23 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "service" +version = "0.1.0" +dependencies = [ + "alloy", + "alloy-sol-types", + "anyhow", + "bincode", + "clap", + "host", + "risc0-tm-core", + "tendermint-rpc", + "tokio", + "tracing", + "tracing-subscriber 0.3.18", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index f959075..d97223d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["core", "host"] +members = ["core", "host", "service"] default-members = ["host"] [workspace.dependencies] diff --git a/README.md b/README.md index b85de16..b47453e 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,5 @@ cargo run -p host -- --help ``` > Note: This CLI as well as other APIs will change in the short term. If you need anything specific from this, [open an issue](https://github.com/risc0/blobstream0/issues/new)! + +For more docs on running the Blobstream service, see [usage-guide.md](./usage-guide.md). diff --git a/host/Cargo.toml b/host/Cargo.toml index 6da5e7c..553cbb5 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] alloy = { version = "0.1", features = ["node-bindings", "network", "providers", "transports", "signer-local"] } +alloy-contract = "0.1" alloy-sol-types = "0.7" anyhow = "1.0" batch-guest = { path = "../batch-guest" } @@ -21,10 +22,10 @@ tendermint = { workspace = true } tendermint-light-client-verifier = { workspace = true } tendermint-rpc = { workspace = true, features = ["http-client"] } tokio = { version = "1.38.0", features = ["rt", "macros", "fs"] } +tracing = "0.1.40" tracing-subscriber = { version = "0.3", features = ["env-filter"] } [dev-dependencies] -alloy-contract = "0.1" serde_json = "1.0" serde_with = { version = "3.8", features = ["base64"] } tokio = { version = "1.38.0", features = ["rt", "macros", "fs"] } diff --git a/host/src/lib.rs b/host/src/lib.rs index bdcfc10..f798258 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -21,11 +21,14 @@ use risc0_tm_core::{ IBlobstream::{IBlobstreamInstance, RangeCommitment}, LightClientCommit, }; -use risc0_zkvm::{default_prover, is_dev_mode, sha::Digestible, ExecutorEnv, Prover, Receipt}; +use risc0_zkvm::{ + default_prover, is_dev_mode, sha::Digestible, ExecutorEnv, Prover, ProverOpts, Receipt, +}; use std::ops::Range; use tendermint::{block::Height, node::Id, validator::Set}; use tendermint_light_client_verifier::types::LightBlock; use tendermint_rpc::{Client, HttpClient, Paging}; +use tracing::{instrument, Level}; async fn fetch_light_block( client: &HttpClient, @@ -60,6 +63,7 @@ pub struct LightBlockProof { } /// Prove a single block with the trusted light client block and the height to fetch and prove. +#[instrument(skip(prover, client, previous_block), err, level = Level::TRACE)] pub async fn prove_block( prover: &dyn Prover, client: &HttpClient, @@ -102,6 +106,7 @@ pub async fn prove_block( } /// Fetches and proves a range of light client blocks. +#[instrument(skip(client), err, level = Level::TRACE)] pub async fn prove_block_range(client: &HttpClient, range: Range) -> anyhow::Result { let prover = default_prover(); @@ -127,12 +132,16 @@ pub async fn prove_block_range(client: &HttpClient, range: Range) -> anyhow let env = batch_env_builder.write(&batch_receipts)?.build()?; // Note: must block in place to not have issues with Bonsai blocking client when selected - let prove_info = tokio::task::block_in_place(move || prover.prove(env, BATCH_GUEST_ELF))?; + tracing::debug!("Proving batch of blocks"); + let prove_info = tokio::task::block_in_place(move || { + prover.prove_with_opts(env, BATCH_GUEST_ELF, &ProverOpts::groth16()) + })?; Ok(prove_info.receipt) } /// Post batch proof to Eth based chain. +#[instrument(skip(contract, receipt), err, level = Level::TRACE)] pub async fn post_batch( contract: &IBlobstreamInstance, receipt: &Receipt, @@ -142,6 +151,7 @@ where P: Provider, N: Network, { + tracing::info!("Posting batch (dev mode={})", is_dev_mode()); let seal = match is_dev_mode() { true => [&[0u8; 4], receipt.claim()?.digest().as_bytes()].concat(), false => groth16::encode(receipt.inner.groth16()?.seal.clone())?, diff --git a/host/src/main.rs b/host/src/main.rs index 08a58b5..48fb392 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -16,15 +16,53 @@ use std::path::PathBuf; +use alloy::{ + hex::FromHex, + network::EthereumWallet, + primitives::{hex, Address, FixedBytes}, + providers::ProviderBuilder, + signers::local::PrivateKeySigner, +}; +use alloy_sol_types::sol; use clap::Parser; use host::prove_block_range; +use risc0_tm_core::IBlobstream; use tendermint_rpc::HttpClient; use tokio::fs; -use tracing_subscriber::EnvFilter; +use tracing_subscriber::fmt::format; +use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + +// TODO elsewhere if keeping dev mode deploy through CLI +sol!( + #[sol(rpc)] + MockVerifier, + // TODO probably not ideal to reference build directory, fine for now. + "../contracts/out/RiscZeroMockVerifier.sol/RiscZeroMockVerifier.json" +); +sol!( + #[sol(rpc)] + RiscZeroGroth16Verifier, + // TODO probably not ideal to reference build directory, fine for now. + "../contracts/out/RiscZeroGroth16Verifier.sol/RiscZeroGroth16Verifier.json" +); + +// Pulled from https://github.com/risc0/risc0-ethereum/blob/ebec385cc526adb9279c1af55d699c645ca6d694/contracts/src/groth16/ControlID.sol +const CONTROL_ID: [u8; 32] = + hex!("a516a057c9fbf5629106300934d48e0e775d4230e41e503347cad96fcbde7e2e"); +const BN254_CONTROL_ID: [u8; 32] = + hex!("0eb6febcf06c5df079111be116f79bd8c7e85dc9448776ef9a59aaf2624ab551"); + +#[derive(Parser, Debug)] +#[command(name = "blobstream0-cli")] +#[command(bin_name = "blobstream0-cli")] +enum BlobstreamCli { + ProveRange(ProveRangeArgs), + Deploy(DeployArgs), +} #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] -struct Args { +struct ProveRangeArgs { /// The start height #[clap(long)] start: u64, @@ -42,24 +80,101 @@ struct Args { out: PathBuf, } +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct DeployArgs { + /// The Ethereum RPC URL + #[clap(long)] + eth_rpc: String, + + /// Hex encoded private key to use for submitting proofs to Ethereum + #[clap(long)] + private_key_hex: String, + + /// Address of risc0 verifier to use (either mock or groth16) + #[clap(long)] + verifier_address: Option, + + /// Trusted height for contract + #[clap(long)] + tm_height: u64, + + /// Trusted block hash for contract + #[clap(long)] + tm_block_hash: String, + + /// If deploying verifier, will it deploy the mock verifier + #[clap(long)] + dev: bool, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() + .event_format(format().compact()) + .with_span_events(FmtSpan::CLOSE) .with_env_filter(EnvFilter::from_default_env()) .init(); - let Args { - start, - end, - tendermint_rpc, - out, - } = Args::parse(); + match BlobstreamCli::parse() { + BlobstreamCli::ProveRange(range) => { + let ProveRangeArgs { + start, + end, + tendermint_rpc, + out, + } = range; + + let client = HttpClient::new(tendermint_rpc.as_str())?; + + let receipt = prove_block_range(&client, start..end).await?; + + fs::write(out, bincode::serialize(&receipt)?).await?; + } + BlobstreamCli::Deploy(deploy) => { + let signer: PrivateKeySigner = deploy.private_key_hex.parse()?; + let wallet = EthereumWallet::from(signer); - let client = HttpClient::new(tendermint_rpc.as_str())?; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_http(deploy.eth_rpc.parse()?); + let verifier_address: Address = if let Some(address) = deploy.verifier_address { + address.parse()? + } else { + let deployed_address = if deploy.dev { + tracing::debug!("Deploying mock verifier"); + MockVerifier::deploy(&provider, [0, 0, 0, 0].into()) + .await? + .address() + .clone() + } else { + tracing::debug!("Deploying groth16 verifier"); + RiscZeroGroth16Verifier::deploy( + &provider, + CONTROL_ID.into(), + BN254_CONTROL_ID.into(), + ) + .await? + .address() + .clone() + }; + println!("deployed verifier to address: {}", deployed_address); + deployed_address + }; - let receipt = prove_block_range(&client, start..end).await?; + // Deploy the contract. + let contract = IBlobstream::deploy( + &provider, + verifier_address, + FixedBytes::<32>::from_hex(deploy.tm_block_hash)?, + deploy.tm_height, + ) + .await?; - fs::write(out, bincode::serialize(&receipt)?).await?; + println!("deployed contract to address: {}", contract.address()); + } + } Ok(()) } diff --git a/service/Cargo.toml b/service/Cargo.toml new file mode 100644 index 0000000..f3875cc --- /dev/null +++ b/service/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "service" +version = "0.1.0" +edition = "2021" + +[dependencies] +alloy = { version = "0.1", features = ["node-bindings", "network", "providers", "transports", "signer-local"] } +alloy-sol-types = "0.7" +anyhow = "1.0" +# batch-guest = { path = "../batch-guest" } +bincode = "1.3.3" +# ciborium = { workspace = true } +clap = { version = "4.5", features = ["derive"] } +host = { path = "../host" } +# light-client-guest = { path = "../light-client-guest" } +# reqwest = "0.12.4" +# risc0-ethereum-contracts = { git = "https://github.com/risc0/risc0-ethereum", tag = "v1.0.0" } +risc0-tm-core = { path = "../core" } +# risc0-zkvm = { version = "1.0.1" } +# serde = { workspace = true } +# tendermint = { workspace = true } +# tendermint-light-client-verifier = { workspace = true } +tendermint-rpc = { workspace = true, features = ["http-client"] } +tokio = { version = "1.38.0", features = ["rt", "macros", "fs"] } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/service/src/blobstream.rs b/service/src/blobstream.rs new file mode 100644 index 0000000..79d6ed4 --- /dev/null +++ b/service/src/blobstream.rs @@ -0,0 +1,142 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{sync::Arc, time::Duration}; + +use alloy::{network::Network, primitives::FixedBytes, providers::Provider, transports::Transport}; +use host::{post_batch, prove_block_range}; +use risc0_tm_core::IBlobstream::IBlobstreamInstance; +use tendermint_rpc::{Client, HttpClient}; +use tokio::task::JoinError; + +macro_rules! handle_temporal_result { + ($res:expr, $consecutive_failures:expr) => { + match $res { + Ok(r) => r, + Err(e) => { + tracing::warn!("failed to request current state: {}", e); + $consecutive_failures += 1; + tokio::time::sleep(std::time::Duration::from_secs(15)).await; + continue; + } + } + }; +} + +pub(crate) struct BlobstreamService { + contract: Arc>, + tm_client: Arc, + batch_size: u64, +} + +impl BlobstreamService { + pub fn new( + contract: IBlobstreamInstance, + tm_client: HttpClient, + batch_size: u64, + ) -> Self { + Self { + contract: Arc::new(contract), + tm_client: Arc::new(tm_client), + batch_size, + } + } +} + +impl BlobstreamService +where + T: Transport + Clone, + P: Provider + 'static, + N: Network, +{ + async fn fetch_current_state(&self) -> Result, JoinError> { + let contract = Arc::clone(&self.contract); + let height_task = tokio::spawn(async move { contract.latestHeight().call().await }); + let contract = Arc::clone(&self.contract); + let hash_task = tokio::spawn(async move { contract.latestBlockHash().call().await }); + let tm_client = Arc::clone(&self.tm_client); + let tm_height_task = tokio::spawn(async move { + tm_client + .status() + .await + .map(|status| status.sync_info.latest_block_height) + }); + + let (height, hash, tm_height) = tokio::try_join!(height_task, hash_task, tm_height_task)?; + + let result = || { + let height = height?._0; + let eth_verified_hash = hash?._0; + let tm_height = tm_height?.value(); + Ok(BlobstreamState { + eth_verified_height: height, + eth_verified_hash, + tm_height, + }) + }; + + Ok(result()) + } + + /// Spawn blobstream service, which will run indefinitely until a fatal error when awaited. + pub async fn spawn(&self) -> anyhow::Result<()> { + let mut consecutive_failures = 0; + while consecutive_failures < 5 { + let BlobstreamState { + eth_verified_height, + tm_height, + // TODO check this hash against tm node as sanity check + eth_verified_hash: _, + } = handle_temporal_result!(self.fetch_current_state().await?, consecutive_failures); + tracing::info!( + "Contract height: {eth_verified_height}, tendermint height: {tm_height}" + ); + + // TODO can prove proactively, this is very basic impl + let block_target = eth_verified_height + self.batch_size; + if block_target > tm_height { + let wait_time = 15 * (block_target - tm_height); + tracing::info!( + "Not enough tendermint blocks to create batch, waiting {} seconds", + wait_time + ); + // Cannot create a batch yet, wait until ready + tokio::time::sleep(Duration::from_secs(wait_time)).await; + continue; + } + + let receipt = handle_temporal_result!( + prove_block_range(&self.tm_client, eth_verified_height..block_target).await, + consecutive_failures + ); + handle_temporal_result!( + post_batch(&self.contract, &receipt).await, + consecutive_failures + ); + + consecutive_failures = 0; + + // TODO ensure height is updated + } + + anyhow::bail!("Reached limit of consecutive errors"); + } +} + +struct BlobstreamState { + eth_verified_height: u64, + #[allow(dead_code)] + eth_verified_hash: FixedBytes<32>, + tm_height: u64, +} diff --git a/service/src/main.rs b/service/src/main.rs new file mode 100644 index 0000000..1f9d9ac --- /dev/null +++ b/service/src/main.rs @@ -0,0 +1,87 @@ +// Copyright 2024 RISC Zero, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use self::blobstream::BlobstreamService; +use alloy::{ + network::EthereumWallet, primitives::Address, providers::ProviderBuilder, + signers::local::PrivateKeySigner, +}; +use clap::Parser; +use risc0_tm_core::IBlobstream; +use tendermint_rpc::HttpClient; +use tracing_subscriber::fmt::format; +use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter}; + +mod blobstream; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// The Tendermint RPC URL + #[clap(long)] + tendermint_rpc: String, + + /// The Ethereum RPC URL + #[clap(long)] + eth_rpc: String, + + /// The deployed contract on Ethereum to reference + #[clap(long)] + eth_address: Address, + + /// Hex encoded private key to use for submitting proofs to Ethereum + #[clap(long)] + private_key_hex: String, + + /// Number of blocks proved in each batch of block headers + #[clap(long)] + batch_size: u64, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .event_format(format().compact()) + .with_span_events(FmtSpan::CLOSE) + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + let Args { + tendermint_rpc, + eth_rpc, + eth_address, + private_key_hex, + batch_size, + } = Args::parse(); + + let tm_client = HttpClient::new(tendermint_rpc.as_str())?; + + let signer: PrivateKeySigner = private_key_hex.parse().expect("should parse private key"); + let wallet = EthereumWallet::from(signer); + + // Create a provider with the wallet. + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_http(eth_rpc.parse()?); + + let contract = IBlobstream::new(eth_address, provider); + + tracing::info!("Starting service"); + BlobstreamService::new(contract, tm_client, batch_size) + .spawn() + .await?; + + Ok(()) +} diff --git a/usage-guide.md b/usage-guide.md new file mode 100644 index 0000000..c59e0ad --- /dev/null +++ b/usage-guide.md @@ -0,0 +1,56 @@ +## Running the Blobstream Zero Service + +This service will watch the Ethereum contract for the current head of the verified chain, request light client blocks from a Tendermint node, generate a batch proof, and then post that proof back to the Ethereum contract. + +### Local testing + +Start up a local Eth dev node: +```console +anvil +``` + +Deploy verifier and blobstream contract + +```console +RUST_LOG=info cargo run -- deploy --eth-rpc http://127.0.0.1:8545 --private-key-hex 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --tm-height 10 --tm-block-hash 5D3BDD6B58620A0B6C5A9122863D11DA68EB18935D12A9F4E4CF1A27EB39F1AC --dev +``` + +> The `--tm-height` and `--tm-block-hash` options are pulled from the network that is being synced. Make sure these match the network from `--tendermint-rpc` in the following command. + +Start the service: + +``` +RISC0_DEV_MODE=true RUST_LOG=host=trace,info cargo run -p service -- --tendermint-rpc https://rpc.celestia-mocha.com --eth-rpc http://127.0.0.1:8545/ --eth-address 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 --private-key-hex 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --batch-size 4 +``` + +Where the `--tendermint-rpc` param can be configured to be any other network endpoint, and the `--batch-size` can be configured. + +> Note: The `--eth-address` here is hard coded to be the first deployment when running the first deployment. Either restart the anvil node or update the `--eth-address` parameter to the output from the deploy if making changes to the contract. + + +#### Local with snark proofs from Bonsai + +The flow is similar to above, except that the `--dev` flag is removed from the deployment, to deploy the groth16 verifier in its place for that step: + +``` +RUST_LOG=info cargo run -- deploy --eth-rpc http://127.0.0.1:8545 --private-key-hex 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --tm-height 10 --tm-block-hash 5D3BDD6B58620A0B6C5A9122863D11DA68EB18935D12A9F4E4CF1A27EB39F1AC +``` + +### Sepolia + +Currently there are already groth16 and mock verifiers deployed to Sepolia. + +- Sepolia groth16 verifier: `0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187` +- Sepolia mock verifier: `0x6e5D3e69934814470CEE91C63afb7033056682F3` + +Deploy the blobstream contract with the `--verifier-address` from above: + +``` +RUST_LOG=info cargo run -- deploy --eth-rpc https://ethereum-sepolia-rpc.publicnode.com --private-key-hex --tm-height 1802142 --tm-block-hash 6D8FD8ADC8FBD5E7765EC557D9DF86041F63F9109202A888D8D246B3BCC3B46A --verifier-address 0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187 +``` + +Run the service with `RISC0_DEV_MODE=true` if you chose the mock verifier. + +``` +RUST_LOG=host=trace,info cargo run -p service --release -- --tendermint-rpc https://rpc.celestia-mocha.com --eth-rpc https://ethereum-sepolia-rpc.publicnode.com --eth-address --private-key-hex --batch-size 16 +```