From c928b009bda814d3db325466ae3b6202ee498257 Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Thu, 4 Dec 2025 12:35:43 -0500 Subject: [PATCH 1/4] feat(anchor-evm): add CLI options for EVM self-anchoring Add command-line options to ceramic-one daemon for configuring EVM-based self-anchoring, eliminating the need for a centralized anchor service. CLI Options: --evm-rpc-url RPC endpoint for EVM chain --evm-private-key Private key for signing (hex, no 0x prefix) --evm-chain-id EVM chain ID (e.g., 100 for Gnosis) --evm-contract-address Anchor contract address --evm-confirmations Block confirmations to wait (default: 4) All four required options must be provided together. Example: ceramic-one daemon \ --evm-rpc-url "https://gnosis-mainnet.g.alchemy.com/v2/YOUR_KEY" \ --evm-private-key "your_private_key_hex_without_0x" \ --evm-chain-id 100 \ --evm-contract-address "0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC" \ --anchor-interval 3600 Environment variables are also supported (recommended for production): CERAMIC_ONE_EVM_RPC_URL, CERAMIC_ONE_EVM_PRIVATE_KEY, etc. Additional changes: - Add wait_for_pending_transactions() to handle tx from previous runs - Deprecate --remote-anchor-service-url in favor of EVM options - Use test_log::test(tokio::test) instead of manual tracing setup - Add comprehensive CLI documentation to anchor-evm/README.md - Add self-anchoring section to main README.md --- Cargo.lock | 2 +- README.md | 4 + anchor-evm/Cargo.toml | 1 - anchor-evm/README.md | 67 ++++++++- anchor-evm/src/evm_transaction_manager.rs | 132 ++++++++++++----- anchor-evm/src/integration_test.rs | 90 ++++++------ one/Cargo.toml | 1 + one/src/daemon.rs | 168 ++++++++++++++++------ 8 files changed, 333 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1dd402ad..60c4f4589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2184,7 +2184,6 @@ dependencies = [ "test-log", "tokio", "tracing", - "tracing-subscriber", "url", ] @@ -2597,6 +2596,7 @@ dependencies = [ "arrow-flight", "async-stream", "async-trait", + "ceramic-anchor-evm", "ceramic-anchor-remote", "ceramic-anchor-service", "ceramic-api", diff --git a/README.md b/README.md index b731b6fc4..19b01c576 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ $ ceramic-one daemon --store-dir ./custom-store-dir The process honors RUST_LOG env variable for controlling its logging output. For example, to enable debug logging for code from this repo but error logging for all other code use: +## Self-Anchoring + +Ceramic One supports self-anchoring directly to EVM-compatible blockchains (Gnosis, Ethereum, Polygon, etc.), eliminating the need for a centralized anchor service. See [anchor-evm/README.md](./anchor-evm/README.md) for setup instructions. + ## License Fully open source and dual-licensed under MIT and Apache 2. diff --git a/anchor-evm/Cargo.toml b/anchor-evm/Cargo.toml index 18f31b40e..e301b83e9 100644 --- a/anchor-evm/Cargo.toml +++ b/anchor-evm/Cargo.toml @@ -26,4 +26,3 @@ ceramic-sql.workspace = true cid.workspace = true expect-test.workspace = true test-log.workspace = true -tracing-subscriber.workspace = true diff --git a/anchor-evm/README.md b/anchor-evm/README.md index 527b0382b..f9f2e6bd0 100644 --- a/anchor-evm/README.md +++ b/anchor-evm/README.md @@ -133,7 +133,72 @@ let config = EvmConfig { }; ``` -## Integration with Ceramic +## Usage with Ceramic One + +The simplest way to enable EVM self-anchoring is via CLI options when running ceramic-one: + +### CLI Options + +```bash +ceramic-one daemon \ + --evm-rpc-url "https://gnosis-mainnet.g.alchemy.com/v2/YOUR_KEY" \ + --evm-private-key "your_private_key_hex_without_0x" \ + --evm-chain-id 100 \ + --evm-contract-address "0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC" \ + --evm-confirmations 4 \ + --anchor-interval 3600 +``` + +### Environment Variables + +For production deployments, use environment variables to avoid exposing secrets: + +```bash +# Required EVM options +export CERAMIC_ONE_EVM_RPC_URL="https://gnosis-mainnet.g.alchemy.com/v2/YOUR_KEY" +export CERAMIC_ONE_EVM_PRIVATE_KEY="your_private_key_hex_without_0x" +export CERAMIC_ONE_EVM_CHAIN_ID="100" +export CERAMIC_ONE_EVM_CONTRACT_ADDRESS="0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC" + +# Optional tuning +export CERAMIC_ONE_EVM_CONFIRMATIONS="4" # Block confirmations (default: 4) +export CERAMIC_ONE_ANCHOR_INTERVAL="3600" # Seconds between anchors (default: 3600) +export CERAMIC_ONE_ANCHOR_BATCH_SIZE="1000000" # Max events per batch + +# Run daemon +ceramic-one daemon +``` + +### Available CLI Options + +| Option | Environment Variable | Description | Default | +|--------|---------------------|-------------|---------| +| `--evm-rpc-url` | `CERAMIC_ONE_EVM_RPC_URL` | RPC endpoint for EVM chain | Required | +| `--evm-private-key` | `CERAMIC_ONE_EVM_PRIVATE_KEY` | Private key for signing (hex, no 0x) | Required | +| `--evm-chain-id` | `CERAMIC_ONE_EVM_CHAIN_ID` | EVM chain ID (e.g., 100 for Gnosis) | Required | +| `--evm-contract-address` | `CERAMIC_ONE_EVM_CONTRACT_ADDRESS` | Anchor contract address | Required | +| `--evm-confirmations` | `CERAMIC_ONE_EVM_CONFIRMATIONS` | Block confirmations to wait | 4 | +| `--anchor-interval` | `CERAMIC_ONE_ANCHOR_INTERVAL` | Seconds between anchor batches | 3600 | + +All four EVM options must be provided together. + +### Example: Gnosis Chain Setup + +```bash +# 1. Fund a wallet with xDAI (even 0.1 xDAI is sufficient for years of anchoring) +# 2. Export your private key (hex format, no 0x prefix) +# 3. Run ceramic-one: + +ceramic-one daemon \ + --network mainnet \ + --evm-rpc-url "https://gnosis-mainnet.g.alchemy.com/v2/YOUR_KEY" \ + --evm-private-key "55e16063c21943ad9d70fa10b0b9713c7dc42d4119ca6b83f36056d6188f4c70" \ + --evm-chain-id 100 \ + --evm-contract-address "0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC" \ + --anchor-interval 3600 +``` + +## Programmatic Integration The `EvmTransactionManager` implements the `TransactionManager` trait and can be used as a drop-in replacement for the remote CAS: diff --git a/anchor-evm/src/evm_transaction_manager.rs b/anchor-evm/src/evm_transaction_manager.rs index bf301db79..f449b29e6 100644 --- a/anchor-evm/src/evm_transaction_manager.rs +++ b/anchor-evm/src/evm_transaction_manager.rs @@ -94,19 +94,19 @@ impl EvmTransactionManager { /// Validate the configuration pub fn validate_config(config: &EvmConfig) -> Result<()> { if config.private_key.is_empty() { - return Err(anyhow!("Private key cannot be empty")); + anyhow::bail!("Private key cannot be empty"); } if config.contract_address.is_empty() { - return Err(anyhow!("Contract address cannot be empty")); + anyhow::bail!("Contract address cannot be empty"); } if config.confirmations == 0 { - return Err(anyhow!("Confirmations must be greater than 0")); + anyhow::bail!("Confirmations must be greater than 0"); } if config.retry_config.max_retries == 0 { - return Err(anyhow!("Max retries must be greater than 0")); + anyhow::bail!("Max retries must be greater than 0"); } Ok(()) @@ -121,9 +121,9 @@ impl EvmTransactionManager { // This matches: uint8arrays.toString(rootCid.bytes.slice(4), 'base16') if cid_bytes.len() < 36 { // 4 prefix + 32 hash bytes - return Err(anyhow!( + anyhow::bail!( "CID too short: need at least 36 bytes (4 prefix + 32 hash)" - )); + ); } let hash_bytes = &cid_bytes[4..]; // Skip multicodec prefix @@ -171,11 +171,11 @@ impl EvmTransactionManager { .map_err(|e| anyhow!("Failed to connect to EVM node: {}", e))?; if actual_chain_id != self.config.chain_id { - return Err(anyhow!( + anyhow::bail!( "Chain ID mismatch: configured {} but connected to {}", self.config.chain_id, actual_chain_id - )); + ); } info!("Connected to EVM chain with ID: {}", actual_chain_id); @@ -187,6 +187,9 @@ impl EvmTransactionManager { .map_err(|e| anyhow!("Failed to get wallet balance: {}", e))?; info!("Starting wallet balance: {} wei", starting_balance); + // Wait for any pending transactions from previous runs to clear + Self::wait_for_pending_transactions(&provider, wallet_address).await; + // Create contract instance let contract = AnchorContract::new(contract_address, provider.clone()); @@ -229,10 +232,10 @@ impl EvmTransactionManager { // Check if transaction reverted if !receipt.status() { - return Err(anyhow!( + anyhow::bail!( "Transaction {} reverted - anchor contract rejected the call", tx_hash - )); + ); } // Get block number from receipt - a mined transaction should always have this @@ -272,38 +275,32 @@ impl EvmTransactionManager { .get_balance(wallet_address) .await .unwrap_or(U256::ZERO); - return Err(anyhow!( + anyhow::bail!( "Insufficient funds for transaction. Current balance: {} wei. Error: {}", current_balance, e - )); + ); } - if error_str.contains("nonce") && error_str.contains("expired") - || error_str.contains("nonce too low") - || error_str.contains("replacement transaction underpriced") + // Check if a previous transaction from THIS run was mined + if self.config.retry_config.check_previous_success + && (error_str.contains("nonce too low") + || error_str.contains("replacement transaction underpriced")) { - // Nonce error - check if a previous transaction was mined - if self.config.retry_config.check_previous_success - && !previous_tx_hashes.is_empty() - { - info!("Nonce error detected, checking if previous transaction was mined..."); - for prev_tx in previous_tx_hashes.iter().rev() { - if let Ok(Some(_)) = provider - .get_transaction_receipt(prev_tx.parse().unwrap_or_default()) - .await + for prev_tx in previous_tx_hashes.iter().rev() { + if let Ok(Some(_)) = provider + .get_transaction_receipt(prev_tx.parse().unwrap_or_default()) + .await + { + info!( + "Previous transaction {} was mined successfully", + prev_tx + ); + if let Ok(ending_balance) = + provider.get_balance(wallet_address).await { - info!( - "Previous transaction {} was mined successfully", - prev_tx - ); - // Log ending wallet balance - if let Ok(ending_balance) = - provider.get_balance(wallet_address).await - { - info!("Ending wallet balance: {} wei", ending_balance); - } - return Ok(prev_tx.clone()); + info!("Ending wallet balance: {} wei", ending_balance); } + return Ok(prev_tx.clone()); } } } @@ -325,6 +322,67 @@ impl EvmTransactionManager { })) } + /// Wait for any pending transactions from this wallet to be mined. + /// This prevents "could not replace existing tx" errors from previous runs. + async fn wait_for_pending_transactions>>( + provider: &P, + wallet_address: Address, + ) { + const MAX_WAIT: Duration = Duration::from_secs(120); + const POLL_INTERVAL: Duration = Duration::from_secs(5); + + let pending_nonce = match provider + .get_transaction_count(wallet_address) + .pending() + .await + { + Ok(n) => n, + Err(_) => return, // Can't check, proceed anyway + }; + + let confirmed_nonce = match provider.get_transaction_count(wallet_address).await { + Ok(n) => n, + Err(_) => return, + }; + + if pending_nonce == confirmed_nonce { + return; // No pending transactions + } + + info!( + pending_nonce, + confirmed_nonce, "Waiting for pending transactions to be mined" + ); + + let start = std::time::Instant::now(); + while start.elapsed() < MAX_WAIT { + sleep(POLL_INTERVAL).await; + + let current_pending = provider + .get_transaction_count(wallet_address) + .pending() + .await + .unwrap_or(pending_nonce); + let current_confirmed = provider + .get_transaction_count(wallet_address) + .await + .unwrap_or(confirmed_nonce); + + if current_pending == current_confirmed { + info!("Pending transactions cleared, proceeding"); + return; + } + + debug!( + pending = current_pending, + confirmed = current_confirmed, + "Still waiting for pending transactions" + ); + } + + warn!("Timed out waiting for pending transactions, proceeding anyway"); + } + /// Wait for the required number of confirmations async fn wait_for_confirmations>>( &self, @@ -337,10 +395,10 @@ impl EvmTransactionManager { loop { if start_time.elapsed() > self.config.confirmation_timeout { - return Err(anyhow!( + anyhow::bail!( "Timeout waiting for confirmations for tx: {}", tx_hash - )); + ); } interval.tick().await; diff --git a/anchor-evm/src/integration_test.rs b/anchor-evm/src/integration_test.rs index f526c1460..90c38b9fe 100644 --- a/anchor-evm/src/integration_test.rs +++ b/anchor-evm/src/integration_test.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::Result; use ceramic_anchor_service::TransactionManager; use ceramic_core::Cid; +use tracing::{error, info}; use crate::{EvmConfig, EvmTransactionManager, RetryConfig}; @@ -19,14 +20,9 @@ use crate::{EvmConfig, EvmTransactionManager, RetryConfig}; /// - TEST_RPC_URL: RPC endpoint (defaults to public Gnosis RPC) /// - TEST_CONTRACT_ADDRESS: Contract address (defaults to deployed test contract) /// - TEST_CHAIN_ID: Chain ID (defaults to 100 for Gnosis) -#[tokio::test] +#[test_log::test(tokio::test)] #[ignore] async fn test_evm_anchoring() -> Result<()> { - tracing_subscriber::fmt() - .with_env_filter("debug") - .try_init() - .ok(); - let private_key = std::env::var("TEST_PRIVATE_KEY").expect("TEST_PRIVATE_KEY environment variable required"); @@ -56,14 +52,12 @@ async fn test_evm_anchoring() -> Result<()> { poll_interval: Duration::from_secs(5), }; - println!("RPC: {}", rpc_url); - println!("Chain ID: {}", chain_id); - println!("Contract: {}", contract_address); + info!(rpc = %rpc_url, chain_id, contract = %contract_address, "Test configuration"); let tx_manager = EvmTransactionManager::new(config).await?; let test_root = Cid::from_str("bafyreia776z4jdg5zgycivcpr3q6lcu6llfowkrljkmq3bex2k5hkzat54")?; - println!("Test root CID: {}", test_root); + info!(root_cid = %test_root, "Starting anchor test"); let start_time = std::time::Instant::now(); @@ -71,11 +65,14 @@ async fn test_evm_anchoring() -> Result<()> { Ok(root_time_event) => { let duration = start_time.elapsed(); - println!("Anchoring successful in {:?}", duration); - println!("Chain ID: {}", root_time_event.proof.chain_id()); - println!("Transaction Type: {}", root_time_event.proof.tx_type()); - println!("Transaction Hash: {}", root_time_event.proof.tx_hash()); - println!("Proof CID: {}", root_time_event.detached_time_event.proof); + info!( + duration = ?duration, + chain_id = %root_time_event.proof.chain_id(), + tx_type = %root_time_event.proof.tx_type(), + tx_hash = %root_time_event.proof.tx_hash(), + proof_cid = %root_time_event.detached_time_event.proof, + "Anchoring successful" + ); assert_eq!( root_time_event.proof.chain_id(), @@ -88,7 +85,7 @@ async fn test_evm_anchoring() -> Result<()> { Ok(()) } Err(e) => { - println!("Anchoring failed: {}", e); + error!(error = %e, "Anchoring failed"); Err(e) } } @@ -192,7 +189,7 @@ fn test_network_configurations() { /// TEST_PRIVATE_KEY="your_private_key_hex" \ /// cargo test -p ceramic-anchor-evm test_anchor_service_with_evm -- --ignored --nocapture /// ``` -#[tokio::test] +#[test_log::test(tokio::test)] #[ignore] async fn test_anchor_service_with_evm() -> Result<()> { use ceramic_anchor_service::{AnchorService, MockAnchorEventService, Store}; @@ -200,11 +197,6 @@ async fn test_anchor_service_with_evm() -> Result<()> { use ceramic_sql::sqlite::SqlitePool; use std::sync::Arc; - tracing_subscriber::fmt() - .with_env_filter("info") - .try_init() - .ok(); - let private_key = std::env::var("TEST_PRIVATE_KEY").expect("TEST_PRIVATE_KEY environment variable required"); @@ -219,10 +211,12 @@ async fn test_anchor_service_with_evm() -> Result<()> { .and_then(|s| s.parse().ok()) .unwrap_or(100); - println!("=== Full AnchorService Integration Test ==="); - println!("RPC: {}", rpc_url); - println!("Chain ID: {}", chain_id); - println!("Contract: {}", contract_address); + info!( + rpc = %rpc_url, + chain_id, + contract = %contract_address, + "Full AnchorService Integration Test" + ); // Create EVM transaction manager let config = EvmConfig { @@ -264,35 +258,32 @@ async fn test_anchor_service_with_evm() -> Result<()> { .events_since_high_water_mark(node_id, 0, 1_000_000) .await?; - println!("Anchor requests: {}", anchor_requests.len()); + info!(count = anchor_requests.len(), "Fetched anchor requests"); for (i, req) in anchor_requests.iter().enumerate() { - println!(" Request {}: id={}, prev={}", i, req.id, req.prev); + info!(index = i, id = %req.id, prev = %req.prev, "Anchor request"); } // Anchor the batch let start_time = std::time::Instant::now(); - println!("\nAnchoring batch..."); + info!("Anchoring batch..."); let time_event_batch = anchor_service .anchor_batch(anchor_requests.as_slice()) .await?; let duration = start_time.elapsed(); - println!("Anchoring completed in {:?}", duration); + info!(duration = ?duration, "Anchoring completed"); // Verify results - println!("\n=== Results ==="); - println!( - "Time events created: {}", - time_event_batch.raw_time_events.events.len() - ); - - // Check proof let proof = &time_event_batch.proof; - println!("Proof chain ID: {}", proof.chain_id()); - println!("Proof tx_type: {}", proof.tx_type()); - println!("Proof tx_hash: {}", proof.tx_hash()); - println!("Proof root: {}", proof.root()); + info!( + time_events = time_event_batch.raw_time_events.events.len(), + chain_id = %proof.chain_id(), + tx_type = %proof.tx_type(), + tx_hash = %proof.tx_hash(), + root = %proof.root(), + "Results" + ); // Verify chain ID format assert_eq!( @@ -315,15 +306,15 @@ async fn test_anchor_service_with_evm() -> Result<()> { "Should have one time event per anchor request" ); - // Print time event details (events is Vec<(AnchorRequest, RawTimeEvent)>) + // Log time event details (events is Vec<(AnchorRequest, RawTimeEvent)>) for (i, (_anchor_req, time_event)) in time_event_batch.raw_time_events.events.iter().enumerate() { - println!( - "\nTime Event {}:\n prev: {}\n proof: {}\n path: {}", - i, - time_event.prev(), - time_event.proof(), - time_event.path() + info!( + index = i, + prev = %time_event.prev(), + proof = %time_event.proof(), + path = %time_event.path(), + "Time event" ); } @@ -348,8 +339,7 @@ async fn test_anchor_service_with_evm() -> Result<()> { ); } - println!("\n✅ All assertions passed!"); - println!("The full anchor flow with EVM anchoring works correctly."); + info!("All assertions passed! The full anchor flow with EVM anchoring works correctly."); Ok(()) } diff --git a/one/Cargo.toml b/one/Cargo.toml index 196a2c573..ed6407f8f 100644 --- a/one/Cargo.toml +++ b/one/Cargo.toml @@ -15,6 +15,7 @@ arrow-cast.workspace = true arrow-flight.workspace = true async-stream.workspace = true async-trait.workspace = true +ceramic-anchor-evm.workspace = true ceramic-anchor-remote.workspace = true ceramic-anchor-service.workspace = true ceramic-api-server = { workspace = true, default-features = false, features = [ diff --git a/one/src/daemon.rs b/one/src/daemon.rs index e6cbc0067..3091fbbd7 100644 --- a/one/src/daemon.rs +++ b/one/src/daemon.rs @@ -5,6 +5,7 @@ use crate::{ DBOptsExperimental, Info, LogOpts, Network, }; use anyhow::{anyhow, bail, Context as _, Result}; +use ceramic_anchor_evm::{EvmConfig, EvmTransactionManager}; use ceramic_anchor_remote::RemoteCas; use ceramic_anchor_service::AnchorService; use ceramic_core::NodeKey; @@ -183,34 +184,33 @@ pub struct DaemonOpts { )] flight_sql_bind_address: String, - /// Remote anchor service URL. Requires using the experimental-features flag + /// [DEPRECATED] Remote anchor service URL. Use EVM anchoring options instead. + /// + /// This option is deprecated and will be removed in a future release. + /// Use --evm-rpc-url, --evm-private-key, --evm-chain-id, and --evm-contract-address + /// for self-anchoring to an EVM blockchain. + /// Requires using the experimental-features flag. #[arg( long, env = "CERAMIC_ONE_REMOTE_ANCHOR_SERVICE_URL", - requires = "experimental_features" + requires = "experimental_features", + hide = true )] + #[deprecated(note = "Use EVM anchoring options instead")] remote_anchor_service_url: Option, - /// Ceramic One anchor interval in seconds + /// Anchor interval in seconds /// - /// The interval between building a tree for all unanchored events and sending to a CAS - /// Requires using the experimental-features flag - #[arg( - long, - default_value_t = 3600, - env = "CERAMIC_ONE_ANCHOR_INTERVAL", - requires = "experimental_features" - )] + /// The interval between building a tree for all unanchored events and anchoring to the blockchain. + #[arg(long, default_value_t = 3600, env = "CERAMIC_ONE_ANCHOR_INTERVAL")] anchor_interval: u64, - /// Ceramic One anchor batch size - /// Requires using the experimental-features flag + /// Anchor batch size (maximum events per batch) #[arg( long, default_value_t = 1_000_000, hide = true, - env = "CERAMIC_ONE_ANCHOR_BATCH_SIZE", - requires = "experimental_features" + env = "CERAMIC_ONE_ANCHOR_BATCH_SIZE" )] anchor_batch_size: u64, @@ -238,6 +238,31 @@ pub struct DaemonOpts { )] anchor_poll_retry_count: u64, + /// EVM RPC URL for self-anchoring (e.g., https://gnosis-mainnet.g.alchemy.com/v2/...) + /// + /// When provided along with other EVM options, enables self-anchoring directly + /// to an EVM blockchain instead of using a remote anchor service. + #[arg(long, env = "CERAMIC_ONE_EVM_RPC_URL")] + evm_rpc_url: Option, + + /// Private key for signing EVM anchor transactions (hex string without 0x prefix) + /// + /// WARNING: Handle this key securely. Use environment variables in production. + #[arg(long, env = "CERAMIC_ONE_EVM_PRIVATE_KEY")] + evm_private_key: Option, + + /// EVM chain ID for anchoring (e.g., 100 for Gnosis Chain) + #[arg(long, env = "CERAMIC_ONE_EVM_CHAIN_ID")] + evm_chain_id: Option, + + /// EVM anchor contract address (e.g., 0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC) + #[arg(long, env = "CERAMIC_ONE_EVM_CONTRACT_ADDRESS")] + evm_contract_address: Option, + + /// Number of block confirmations to wait for EVM anchoring + #[arg(long, default_value_t = 4, env = "CERAMIC_ONE_EVM_CONFIRMATIONS")] + evm_confirmations: u64, + /// Ethereum RPC URLs used for time events validation. Required when connecting to mainnet and uses fallback URLs if not specified for other networks. /// Note: only the first valid RPC URL for a particular chain will be used by the time event validator #[arg( @@ -600,35 +625,94 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { shutdown.wait_fut(), )); - // Start anchoring if remote anchor service URL is provided - let anchor_service_handle = - if let Some(remote_anchor_service_url) = opts.remote_anchor_service_url { - info!( - node_did = node_key.did_key(), - url = remote_anchor_service_url, - poll_interval = opts.anchor_poll_interval, - "starting remote cas anchor service" - ); - let remote_cas = RemoteCas::new( - node_key, - remote_anchor_service_url, - Duration::from_secs(opts.anchor_poll_interval), - opts.anchor_poll_retry_count, - ); - let anchor_service = AnchorService::new( - Arc::new(remote_cas), - event_svc.clone(), - sqlite_pool.clone(), - node_id, - Duration::from_secs(opts.anchor_interval), - opts.anchor_batch_size, - ); - - Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) - } else { - None + // Start anchoring service if EVM or remote CAS options are provided + // EVM anchoring takes precedence over deprecated remote CAS + let anchor_service_handle = if let (Some(rpc_url), Some(private_key), Some(chain_id), Some(contract_address)) = ( + opts.evm_rpc_url.as_ref(), + opts.evm_private_key.as_ref(), + opts.evm_chain_id, + opts.evm_contract_address.as_ref(), + ) { + info!( + node_did = node_key.did_key(), + chain_id = chain_id, + confirmations = opts.evm_confirmations, + "starting EVM self-anchoring service" + ); + + let evm_config = EvmConfig { + rpc_url: rpc_url.clone(), + private_key: private_key.clone(), + chain_id, + contract_address: contract_address.clone(), + confirmations: opts.evm_confirmations, + ..Default::default() }; + let evm_tx_manager = EvmTransactionManager::new(evm_config) + .await + .context("Failed to initialize EVM transaction manager")?; + + let anchor_service = AnchorService::new( + Arc::new(evm_tx_manager), + event_svc.clone(), + sqlite_pool.clone(), + node_id, + Duration::from_secs(opts.anchor_interval), + opts.anchor_batch_size, + ); + + Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) + } else if opts.evm_rpc_url.is_some() + || opts.evm_private_key.is_some() + || opts.evm_contract_address.is_some() + { + // Partial EVM options provided - this is an error + bail!( + "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ + --evm-chain-id, and --evm-contract-address must be provided together. \ + Got partial option: {}", + if opts.evm_rpc_url.is_some() { "--evm-rpc-url" } + else if opts.evm_private_key.is_some() { "--evm-private-key" } + else { "--evm-contract-address" } + ); + } else if opts.evm_chain_id.is_some() { + bail!( + "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ + --evm-chain-id, and --evm-contract-address must be provided together." + ); + } else if let Some(remote_anchor_service_url) = opts.remote_anchor_service_url { + // Deprecated remote CAS fallback + warn!( + "[DEPRECATED] Using remote anchor service URL. This option is deprecated and will be \ + removed in a future release. Use EVM anchoring options instead." + ); + info!( + node_did = node_key.did_key(), + url = remote_anchor_service_url, + poll_interval = opts.anchor_poll_interval, + "starting remote cas anchor service" + ); + let remote_cas = RemoteCas::new( + node_key, + remote_anchor_service_url, + Duration::from_secs(opts.anchor_poll_interval), + opts.anchor_poll_retry_count, + ); + let anchor_service = AnchorService::new( + Arc::new(remote_cas), + event_svc.clone(), + sqlite_pool.clone(), + node_id, + Duration::from_secs(opts.anchor_interval), + opts.anchor_batch_size, + ); + + Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) + } else { + None + }; + let (pipeline_handle, pipeline_waiter) = pipeline.into_parts(); // Build HTTP server let mut ceramic_server = ceramic_api::Server::new( From b75424ccf24a42c22a3d8d49643421cdb1a92b0c Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:58:08 -0500 Subject: [PATCH 2/4] fix: resolve CI failures with formatting and deprecated field warning - Run cargo fmt to fix formatting issues in daemon.rs and evm_transaction_manager.rs - Add #[allow(deprecated)] for intentional use of deprecated remote_anchor_service_url field --- anchor-evm/src/evm_transaction_manager.rs | 14 +- one/src/daemon.rs | 166 ++++++++++++---------- 2 files changed, 91 insertions(+), 89 deletions(-) diff --git a/anchor-evm/src/evm_transaction_manager.rs b/anchor-evm/src/evm_transaction_manager.rs index f449b29e6..0b01ae915 100644 --- a/anchor-evm/src/evm_transaction_manager.rs +++ b/anchor-evm/src/evm_transaction_manager.rs @@ -121,9 +121,7 @@ impl EvmTransactionManager { // This matches: uint8arrays.toString(rootCid.bytes.slice(4), 'base16') if cid_bytes.len() < 36 { // 4 prefix + 32 hash bytes - anyhow::bail!( - "CID too short: need at least 36 bytes (4 prefix + 32 hash)" - ); + anyhow::bail!("CID too short: need at least 36 bytes (4 prefix + 32 hash)"); } let hash_bytes = &cid_bytes[4..]; // Skip multicodec prefix @@ -291,10 +289,7 @@ impl EvmTransactionManager { .get_transaction_receipt(prev_tx.parse().unwrap_or_default()) .await { - info!( - "Previous transaction {} was mined successfully", - prev_tx - ); + info!("Previous transaction {} was mined successfully", prev_tx); if let Ok(ending_balance) = provider.get_balance(wallet_address).await { @@ -395,10 +390,7 @@ impl EvmTransactionManager { loop { if start_time.elapsed() > self.config.confirmation_timeout { - anyhow::bail!( - "Timeout waiting for confirmations for tx: {}", - tx_hash - ); + anyhow::bail!("Timeout waiting for confirmations for tx: {}", tx_hash); } interval.tick().await; diff --git a/one/src/daemon.rs b/one/src/daemon.rs index 3091fbbd7..e106e2b72 100644 --- a/one/src/daemon.rs +++ b/one/src/daemon.rs @@ -627,91 +627,101 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { // Start anchoring service if EVM or remote CAS options are provided // EVM anchoring takes precedence over deprecated remote CAS - let anchor_service_handle = if let (Some(rpc_url), Some(private_key), Some(chain_id), Some(contract_address)) = ( - opts.evm_rpc_url.as_ref(), - opts.evm_private_key.as_ref(), - opts.evm_chain_id, - opts.evm_contract_address.as_ref(), - ) { - info!( - node_did = node_key.did_key(), - chain_id = chain_id, - confirmations = opts.evm_confirmations, - "starting EVM self-anchoring service" - ); - - let evm_config = EvmConfig { - rpc_url: rpc_url.clone(), - private_key: private_key.clone(), - chain_id, - contract_address: contract_address.clone(), - confirmations: opts.evm_confirmations, - ..Default::default() - }; - - let evm_tx_manager = EvmTransactionManager::new(evm_config) - .await - .context("Failed to initialize EVM transaction manager")?; - - let anchor_service = AnchorService::new( - Arc::new(evm_tx_manager), - event_svc.clone(), - sqlite_pool.clone(), - node_id, - Duration::from_secs(opts.anchor_interval), - opts.anchor_batch_size, - ); + let anchor_service_handle = + if let (Some(rpc_url), Some(private_key), Some(chain_id), Some(contract_address)) = ( + opts.evm_rpc_url.as_ref(), + opts.evm_private_key.as_ref(), + opts.evm_chain_id, + opts.evm_contract_address.as_ref(), + ) { + info!( + node_did = node_key.did_key(), + chain_id = chain_id, + confirmations = opts.evm_confirmations, + "starting EVM self-anchoring service" + ); + + let evm_config = EvmConfig { + rpc_url: rpc_url.clone(), + private_key: private_key.clone(), + chain_id, + contract_address: contract_address.clone(), + confirmations: opts.evm_confirmations, + ..Default::default() + }; - Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) - } else if opts.evm_rpc_url.is_some() - || opts.evm_private_key.is_some() - || opts.evm_contract_address.is_some() - { - // Partial EVM options provided - this is an error - bail!( - "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ + let evm_tx_manager = EvmTransactionManager::new(evm_config) + .await + .context("Failed to initialize EVM transaction manager")?; + + let anchor_service = AnchorService::new( + Arc::new(evm_tx_manager), + event_svc.clone(), + sqlite_pool.clone(), + node_id, + Duration::from_secs(opts.anchor_interval), + opts.anchor_batch_size, + ); + + Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) + } else if opts.evm_rpc_url.is_some() + || opts.evm_private_key.is_some() + || opts.evm_contract_address.is_some() + { + // Partial EVM options provided - this is an error + bail!( + "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ --evm-chain-id, and --evm-contract-address must be provided together. \ Got partial option: {}", - if opts.evm_rpc_url.is_some() { "--evm-rpc-url" } - else if opts.evm_private_key.is_some() { "--evm-private-key" } - else { "--evm-contract-address" } - ); - } else if opts.evm_chain_id.is_some() { - bail!( - "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ + if opts.evm_rpc_url.is_some() { + "--evm-rpc-url" + } else if opts.evm_private_key.is_some() { + "--evm-private-key" + } else { + "--evm-contract-address" + } + ); + } else if opts.evm_chain_id.is_some() { + bail!( + "Incomplete EVM anchoring configuration. All of --evm-rpc-url, --evm-private-key, \ --evm-chain-id, and --evm-contract-address must be provided together." - ); - } else if let Some(remote_anchor_service_url) = opts.remote_anchor_service_url { - // Deprecated remote CAS fallback - warn!( + ); + } else if let Some(remote_anchor_service_url) = { + // Allow access to the deprecated field since we're providing a deprecation warning + #[allow(deprecated)] + let url = opts.remote_anchor_service_url; + url + } { + // Deprecated remote CAS fallback + warn!( "[DEPRECATED] Using remote anchor service URL. This option is deprecated and will be \ removed in a future release. Use EVM anchoring options instead." ); - info!( - node_did = node_key.did_key(), - url = remote_anchor_service_url, - poll_interval = opts.anchor_poll_interval, - "starting remote cas anchor service" - ); - let remote_cas = RemoteCas::new( - node_key, - remote_anchor_service_url, - Duration::from_secs(opts.anchor_poll_interval), - opts.anchor_poll_retry_count, - ); - let anchor_service = AnchorService::new( - Arc::new(remote_cas), - event_svc.clone(), - sqlite_pool.clone(), - node_id, - Duration::from_secs(opts.anchor_interval), - opts.anchor_batch_size, - ); - - Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) - } else { - None - }; + info!( + node_did = node_key.did_key(), + url = remote_anchor_service_url, + poll_interval = opts.anchor_poll_interval, + "starting remote cas anchor service" + ); + let remote_cas = RemoteCas::new( + node_key, + remote_anchor_service_url, + Duration::from_secs(opts.anchor_poll_interval), + opts.anchor_poll_retry_count, + ); + let anchor_service = AnchorService::new( + Arc::new(remote_cas), + event_svc.clone(), + sqlite_pool.clone(), + node_id, + Duration::from_secs(opts.anchor_interval), + opts.anchor_batch_size, + ); + + Some(tokio::spawn(anchor_service.run(shutdown.wait_fut()))) + } else { + None + }; let (pipeline_handle, pipeline_waiter) = pipeline.into_parts(); // Build HTTP server From 4aa8f30cdf7dcaa8bc52206af83ba166f2cd61d7 Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:12:16 -0500 Subject: [PATCH 3/4] refactor(anchor-evm): simplify EVM RPC configuration for self-anchoring Consolidate RPC URL configuration so that --evm-rpc-url is automatically used for both submitting anchors AND validating anchor proofs on that chain. Changes: - Add --additional-chain-rpc-urls for validating anchors from other chains - Deprecate --ethereum-rpc-urls (keep as hidden alias with warning) --- anchor-evm/README.md | 11 +++++-- one/src/daemon.rs | 54 +++++++++++++++++++++++++++++++--- one/src/migrations.rs | 42 ++++++++++++++++++++++++-- tests/networks/basic-rust.yaml | 2 +- 4 files changed, 99 insertions(+), 10 deletions(-) diff --git a/anchor-evm/README.md b/anchor-evm/README.md index f9f2e6bd0..baaf5aa70 100644 --- a/anchor-evm/README.md +++ b/anchor-evm/README.md @@ -165,22 +165,29 @@ export CERAMIC_ONE_EVM_CONFIRMATIONS="4" # Block confirmations (default: 4) export CERAMIC_ONE_ANCHOR_INTERVAL="3600" # Seconds between anchors (default: 3600) export CERAMIC_ONE_ANCHOR_BATCH_SIZE="1000000" # Max events per batch +# Optional: Additional RPC URLs for validating anchors from other chains +# (e.g., historical anchors or synced events anchored on different chains) +export CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS="https://ethereum-rpc.publicnode.com,https://sepolia-rpc.publicnode.com" + # Run daemon ceramic-one daemon ``` +**Note:** The `CERAMIC_ONE_EVM_RPC_URL` is automatically used for both submitting anchor transactions AND validating anchor proofs on that chain. Use `CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS` only if you need to validate anchors from other chains. + ### Available CLI Options | Option | Environment Variable | Description | Default | |--------|---------------------|-------------|---------| -| `--evm-rpc-url` | `CERAMIC_ONE_EVM_RPC_URL` | RPC endpoint for EVM chain | Required | +| `--evm-rpc-url` | `CERAMIC_ONE_EVM_RPC_URL` | RPC endpoint for EVM chain (used for both anchoring and validation) | Required | | `--evm-private-key` | `CERAMIC_ONE_EVM_PRIVATE_KEY` | Private key for signing (hex, no 0x) | Required | | `--evm-chain-id` | `CERAMIC_ONE_EVM_CHAIN_ID` | EVM chain ID (e.g., 100 for Gnosis) | Required | | `--evm-contract-address` | `CERAMIC_ONE_EVM_CONTRACT_ADDRESS` | Anchor contract address | Required | | `--evm-confirmations` | `CERAMIC_ONE_EVM_CONFIRMATIONS` | Block confirmations to wait | 4 | | `--anchor-interval` | `CERAMIC_ONE_ANCHOR_INTERVAL` | Seconds between anchor batches | 3600 | +| `--additional-chain-rpc-urls` | `CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS` | Additional RPC URLs for validating anchors from other chains | None | -All four EVM options must be provided together. +All four EVM options must be provided together for self-anchoring. ### Example: Gnosis Chain Setup diff --git a/one/src/daemon.rs b/one/src/daemon.rs index e106e2b72..f0eb5387a 100644 --- a/one/src/daemon.rs +++ b/one/src/daemon.rs @@ -263,16 +263,35 @@ pub struct DaemonOpts { #[arg(long, default_value_t = 4, env = "CERAMIC_ONE_EVM_CONFIRMATIONS")] evm_confirmations: u64, - /// Ethereum RPC URLs used for time events validation. Required when connecting to mainnet and uses fallback URLs if not specified for other networks. - /// Note: only the first valid RPC URL for a particular chain will be used by the time event validator + /// [DEPRECATED] Use --additional-chain-rpc-urls instead. + /// + /// Ethereum RPC URLs used for time events validation. + /// This option is deprecated and will be removed in a future release. #[arg( long, use_value_delimiter = true, value_delimiter = ',', - env = "CERAMIC_ONE_ETHEREUM_RPC_URLS" + env = "CERAMIC_ONE_ETHEREUM_RPC_URLS", + hide = true )] + #[deprecated(note = "Use --additional-chain-rpc-urls instead")] ethereum_rpc_urls: Vec, + /// Additional EVM RPC URLs for validating anchor proofs from other chains. + /// + /// Use this to add RPC endpoints for chains other than the one configured with --evm-rpc-url. + /// This is useful for validating historical anchors or synced events that were anchored on different chains. + /// + /// Note: only the first valid RPC URL for a particular chain will be used by the time event validator. + /// The --evm-rpc-url (if provided) is automatically included in the validation list. + #[arg( + long, + use_value_delimiter = true, + value_delimiter = ',', + env = "CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS" + )] + additional_chain_rpc_urls: Vec, + /// Location of the object store bucket, of the form: /// /// * s3:// @@ -410,9 +429,36 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { None }; + // Build the list of RPC URLs for chain inclusion validation + // Priority: evm_rpc_url (if provided) + additional_chain_rpc_urls + deprecated ethereum_rpc_urls + let validation_rpc_urls = { + let mut urls = Vec::new(); + + // Add the self-anchoring RPC URL first (if provided) - it should be used for validation too + if let Some(ref evm_url) = opts.evm_rpc_url { + urls.push(evm_url.clone()); + } + + // Add additional chain RPC URLs + urls.extend(opts.additional_chain_rpc_urls.clone()); + + // Handle deprecated ethereum_rpc_urls with warning + #[allow(deprecated)] + if !opts.ethereum_rpc_urls.is_empty() { + warn!( + "[DEPRECATED] --ethereum-rpc-urls / CERAMIC_ONE_ETHEREUM_RPC_URLS is deprecated. \ + Use --additional-chain-rpc-urls / CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS instead." + ); + #[allow(deprecated)] + urls.extend(opts.ethereum_rpc_urls.clone()); + } + + urls + }; + let rpc_providers = opts .network - .get_eth_rpc_providers(opts.ethereum_rpc_urls) + .get_eth_rpc_providers(validation_rpc_urls) .await?; // Construct services from pool diff --git a/one/src/migrations.rs b/one/src/migrations.rs index 2b971fc08..2b188c748 100644 --- a/one/src/migrations.rs +++ b/one/src/migrations.rs @@ -132,14 +132,31 @@ pub struct FromIpfsOpts { #[clap(long, env = "CERAMIC_ONE_VALIDATE_CHAIN")] validate_chain: bool, - /// Ethereum RPC URLs used for time events validation. Required when connecting to mainnet and uses fallback URLs if not specified for other networks. + /// [DEPRECATED] Use --additional-chain-rpc-urls instead. + /// + /// Ethereum RPC URLs used for time events validation. + /// This option is deprecated and will be removed in a future release. #[arg( long, use_value_delimiter = true, value_delimiter = ',', - env = "CERAMIC_ONE_ETHEREUM_RPC_URLS" + env = "CERAMIC_ONE_ETHEREUM_RPC_URLS", + hide = true )] + #[deprecated(note = "Use --additional-chain-rpc-urls instead")] ethereum_rpc_urls: Vec, + + /// Additional EVM RPC URLs for validating anchor proofs from other chains. + /// + /// Use this to add RPC endpoints for chains other than the default for the network. + /// Note: only the first valid RPC URL for a particular chain will be used. + #[arg( + long, + use_value_delimiter = true, + value_delimiter = ',', + env = "CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS" + )] + additional_chain_rpc_urls: Vec, } impl From<&FromIpfsOpts> for DBOpts { @@ -182,9 +199,28 @@ async fn from_ipfs(opts: FromIpfsOpts) -> Result<()> { let network = opts.network.to_network(&opts.local_network_id)?; let db_opts: DBOpts = (&opts).into(); let sqlite_pool = db_opts.get_sqlite_pool(SqliteOpts::default()).await?; + // Build the list of RPC URLs for chain inclusion validation + // Priority: additional_chain_rpc_urls + deprecated ethereum_rpc_urls + let validation_rpc_urls = { + let mut urls = opts.additional_chain_rpc_urls.clone(); + + // Handle deprecated ethereum_rpc_urls with warning + #[allow(deprecated)] + if !opts.ethereum_rpc_urls.is_empty() { + warn!( + "[DEPRECATED] --ethereum-rpc-urls / CERAMIC_ONE_ETHEREUM_RPC_URLS is deprecated. \ + Use --additional-chain-rpc-urls / CERAMIC_ONE_ADDITIONAL_CHAIN_RPC_URLS instead." + ); + #[allow(deprecated)] + urls.extend(opts.ethereum_rpc_urls.clone()); + } + + urls + }; + let rpc_providers = opts .network - .get_eth_rpc_providers(opts.ethereum_rpc_urls) + .get_eth_rpc_providers(validation_rpc_urls) .await?; // TODO: feature flags here? or just remove this entirely when enabling diff --git a/tests/networks/basic-rust.yaml b/tests/networks/basic-rust.yaml index c0809265a..1ddb5fd8f 100644 --- a/tests/networks/basic-rust.yaml +++ b/tests/networks/basic-rust.yaml @@ -13,7 +13,7 @@ spec: ipfs: rust: env: - CERAMIC_ONE_ETHEREUM_RPC_URLS: "http://ganache:8545" + CERAMIC_ONE_EVM_RPC_URL: "http://ganache:8545" CERAMIC_ONE_FLIGHT_SQL_BIND_ADDRESS: "0.0.0.0:5102" resourceLimits: cpu: "4" From a4ad0bd26f3ec461f3f9e32f93b6e8e12c14ae8d Mon Sep 17 00:00:00 2001 From: Mohsin Zaidi <2236875+smrz2001@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:28:30 -0500 Subject: [PATCH 4/4] test fixes --- tests/networks/basic-rust.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/networks/basic-rust.yaml b/tests/networks/basic-rust.yaml index 1ddb5fd8f..af91fb098 100644 --- a/tests/networks/basic-rust.yaml +++ b/tests/networks/basic-rust.yaml @@ -14,6 +14,9 @@ spec: rust: env: CERAMIC_ONE_EVM_RPC_URL: "http://ganache:8545" + CERAMIC_ONE_EVM_PRIVATE_KEY: "0000000000000000000000000000000000000000000000000000000000000001" + CERAMIC_ONE_EVM_CHAIN_ID: "1337" + CERAMIC_ONE_EVM_CONTRACT_ADDRESS: "0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC" CERAMIC_ONE_FLIGHT_SQL_BIND_ADDRESS: "0.0.0.0:5102" resourceLimits: cpu: "4"