From f7a14d1e5208659bb5297721201b160b0a54002a Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Wed, 18 Feb 2026 18:38:44 -0500 Subject: [PATCH 1/3] Rename stem crate to atom, update Solidity contracts Renames crates/stem -> crates/atom and src/Stem.sol -> src/Atom.sol to reflect the Atom contract naming convention. --- Cargo.lock | 52 +++++------ Cargo.toml | 2 +- README.md | 32 +++---- capnp/stem.capnp | 4 +- crates/{stem => atom}/Cargo.toml | 8 +- crates/{stem => atom}/build.rs | 0 .../examples/atom_indexer.rs} | 22 ++--- crates/{stem => atom}/examples/finalizer.rs | 10 +-- .../{stem => atom}/examples/membrane_poll.rs | 14 +-- crates/{stem => atom}/src/abi.rs | 2 +- crates/{stem => atom}/src/config.rs | 2 +- crates/{stem => atom}/src/cursor.rs | 0 crates/{stem => atom}/src/finalizer.rs | 4 +- crates/{stem => atom}/src/indexer.rs | 12 +-- crates/{stem => atom}/src/lib.rs | 8 +- crates/{stem => atom}/src/membrane.rs | 0 crates/{stem => atom}/tests/common/mod.rs | 14 +-- .../tests/finalizer_integration.rs | 16 ++-- crates/{stem => atom}/tests/integration.rs | 12 +-- .../tests/membrane_integration.rs | 14 +-- .../{stem => atom}/tests/reorg_finalizer.rs | 16 ++-- crates/{stem => atom}/tests/reorg_naive.rs | 10 +-- script/Deploy.s.sol | 6 +- src/{Stem.sol => Atom.sol} | 16 ++-- test/{Stem.t.sol => Atom.t.sol} | 88 +++++++++---------- 25 files changed, 182 insertions(+), 182 deletions(-) rename crates/{stem => atom}/Cargo.toml (83%) rename crates/{stem => atom}/build.rs (100%) rename crates/{stem/examples/stem_indexer.rs => atom/examples/atom_indexer.rs} (78%) rename crates/{stem => atom}/examples/finalizer.rs (93%) rename crates/{stem => atom}/examples/membrane_poll.rs (96%) rename crates/{stem => atom}/src/abi.rs (99%) rename crates/{stem => atom}/src/config.rs (96%) rename crates/{stem => atom}/src/cursor.rs (100%) rename crates/{stem => atom}/src/finalizer.rs (99%) rename crates/{stem => atom}/src/indexer.rs (98%) rename crates/{stem => atom}/src/lib.rs (79%) rename crates/{stem => atom}/src/membrane.rs (100%) rename crates/{stem => atom}/tests/common/mod.rs (96%) rename crates/{stem => atom}/tests/finalizer_integration.rs (90%) rename crates/{stem => atom}/tests/integration.rs (88%) rename crates/{stem => atom}/tests/membrane_integration.rs (94%) rename crates/{stem => atom}/tests/reorg_finalizer.rs (90%) rename crates/{stem => atom}/tests/reorg_naive.rs (95%) rename src/{Stem.sol => Atom.sol} (84%) rename test/{Stem.t.sol => Atom.t.sol} (52%) diff --git a/Cargo.lock b/Cargo.lock index a804d72..aea8d01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,6 +702,32 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "atom" +version = "0.1.0" +dependencies = [ + "alloy", + "anyhow", + "capnp", + "capnp-rpc", + "capnpc", + "futures-util", + "hex", + "k256", + "rand 0.8.5", + "reqwest", + "rlp", + "serde", + "serde_json", + "sha3", + "thiserror", + "tokio", + "tokio-test", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2890,32 +2916,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stem" -version = "0.1.0" -dependencies = [ - "alloy", - "anyhow", - "capnp", - "capnp-rpc", - "capnpc", - "futures-util", - "hex", - "k256", - "rand 0.8.5", - "reqwest", - "rlp", - "serde", - "serde_json", - "sha3", - "thiserror", - "tokio", - "tokio-test", - "tokio-tungstenite", - "tracing", - "tracing-subscriber", -] - [[package]] name = "strum" version = "0.27.2" diff --git a/Cargo.toml b/Cargo.toml index c346166..4dab1c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] resolver = "2" -members = ["crates/stem"] +members = ["crates/atom"] diff --git a/README.md b/README.md index 13c9e35..7c23f30 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Stem -Off-chain runtime for the [Stem smart contract](src/Stem.sol). +Off-chain runtime for the [Atom smart contract](src/Atom.sol). Stem indexes `HeadUpdated` events emitted by an on-chain anchor contract, finalizes them with reorg safety (configurable confirmation depth), and exposes @@ -24,7 +24,7 @@ HeadUpdated → Indexer → Finalizer → Epoch → Membrane The runtime is a four-stage pipeline: -### 1. Stem contract (`src/Stem.sol`) +### 1. Atom contract (`src/Atom.sol`) On-chain anchor. The owner calls `setHead(newCid)` to advance a monotonic `seq` and emit a `HeadUpdated` event. The `head()` view returns the canonical @@ -39,7 +39,7 @@ event HeadUpdated( ); ``` -### 2. Indexer (`StemIndexer`) +### 2. Indexer (`AtomIndexer`) Subscribes to `HeadUpdated` via WebSocket for live events and backfills missed blocks via HTTP `eth_getLogs` on startup and reconnect. Broadcasts @@ -57,7 +57,7 @@ Consumes observed events from the indexer and outputs only those that are - **Eligibility** is decided by a pluggable `Strategy` trait. The built-in `ConfirmationDepth(K)` strategy requires `tip >= event.block_number + K`. - **Canonical cross-check**: after eligibility, the finalizer calls - `Stem.head()` and only emits if the on-chain `(seq, cid)` matches the + `Atom.head()` and only emits if the on-chain `(seq, cid)` matches the candidate event. - **Deduplication** by `(tx_hash, log_index)` ensures exactly-once delivery across reconnects and backfills. @@ -91,14 +91,14 @@ call `graft()` again to obtain a fresh session under the new epoch. ```bash forge build -cargo build -p stem +cargo build -p atom ``` ### Test ```bash forge test -cargo test -p stem +cargo test -p atom ``` ## Deploy (local) @@ -119,24 +119,24 @@ forge script script/Deploy.s.sol \ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Note the deployed address from the output, then set the first head: -cast send "setHead(bytes)" "0x$(echo -n 'ipfs://first' | xxd -p)" \ +cast send "setHead(bytes)" "0x$(echo -n 'ipfs://first' | xxd -p)" \ --rpc-url http://127.0.0.1:8545 \ --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ``` ## Examples -All examples live in `crates/stem/examples/` and connect to a running node -with a deployed Stem contract. +All examples live in `crates/atom/examples/` and connect to a running node +with a deployed Atom contract. -### `stem_indexer` — raw observed events +### `atom_indexer` — raw observed events Prints every `HeadUpdated` event as it arrives (no finalization). ```bash -cargo run -p stem --example stem_indexer -- \ +cargo run -p atom --example atom_indexer -- \ --rpc-url http://127.0.0.1:8545 \ - --contract + --contract ``` ### `finalizer` — finalized events as JSON @@ -145,10 +145,10 @@ Runs the full indexer + finalizer pipeline and prints one JSON object per finalized event. ```bash -cargo run -p stem --example finalizer -- \ +cargo run -p atom --example finalizer -- \ --ws-url ws://127.0.0.1:8545 \ --http-url http://127.0.0.1:8545 \ - --contract \ + --contract \ --depth 2 ``` @@ -160,10 +160,10 @@ fails with a `staleEpoch` error; the example re-grafts and polls successfully under the new epoch. ```bash -cargo run -p stem --example membrane_poll -- \ +cargo run -p atom --example membrane_poll -- \ --ws-url ws://127.0.0.1:8545 \ --http-url http://127.0.0.1:8545 \ - --contract \ + --contract \ --depth 2 ``` diff --git a/capnp/stem.capnp b/capnp/stem.capnp index 54cb7ef..0d30db1 100644 --- a/capnp/stem.capnp +++ b/capnp/stem.capnp @@ -1,8 +1,8 @@ @0x9bce094a026970c4; struct Epoch { - seq @0 :UInt64; # Monotonic epoch sequence number (from Stem.seq). - head @1 :Data; # Opaque head bytes from the Stem contract. + seq @0 :UInt64; # Monotonic epoch sequence number (from Atom.seq). + head @1 :Data; # Opaque head bytes from the Atom contract. adoptedBlock @2 :UInt64;# Block number at which this epoch was adopted. } diff --git a/crates/stem/Cargo.toml b/crates/atom/Cargo.toml similarity index 83% rename from crates/stem/Cargo.toml rename to crates/atom/Cargo.toml index f84dccf..e42d642 100644 --- a/crates/stem/Cargo.toml +++ b/crates/atom/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "stem" +name = "atom" version = "0.1.0" edition = "2021" -description = "Off-chain Stem runtime: head-following, finalization, and caching for the Stem contract" +description = "Off-chain Atom runtime: head-following, finalization, and caching for the Atom contract" [build-dependencies] @@ -32,8 +32,8 @@ rlp = "0.5" tokio-test = "0.4" [[example]] -name = "stem_indexer" -path = "examples/stem_indexer.rs" +name = "atom_indexer" +path = "examples/atom_indexer.rs" [[example]] name = "finalizer" diff --git a/crates/stem/build.rs b/crates/atom/build.rs similarity index 100% rename from crates/stem/build.rs rename to crates/atom/build.rs diff --git a/crates/stem/examples/stem_indexer.rs b/crates/atom/examples/atom_indexer.rs similarity index 78% rename from crates/stem/examples/stem_indexer.rs rename to crates/atom/examples/atom_indexer.rs index 7d4682f..e381958 100644 --- a/crates/stem/examples/stem_indexer.rs +++ b/crates/atom/examples/atom_indexer.rs @@ -1,22 +1,22 @@ -//! Example: connect to an RPC endpoint and log Stem head updates. +//! Example: connect to an RPC endpoint and log Atom head updates. //! -//! Imports the stem lib, runs StemIndexer against a Stem contract, and prints each +//! Imports the atom lib, runs AtomIndexer against an Atom contract, and prints each //! HeadUpdated event (seq, block, writer, cid length). WebSocket URL is derived from //! the HTTP RPC URL (http -> ws, https -> wss). //! //! Usage: //! -//! cargo run -p stem --example stem_indexer -- --rpc-url --contract +//! cargo run -p atom --example atom_indexer -- --rpc-url --contract //! -//! Getting the contract address: deploy Stem with Foundry, then use the printed address: +//! Getting the contract address: deploy Atom with Foundry, then use the printed address: //! //! anvil //! forge script script/Deploy.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0xac0974... -//! # "Stem deployed at: 0x..." is the address to pass as --contract +//! # "Atom deployed at: 0x..." is the address to pass as --contract //! -//! cargo run -p stem --example stem_indexer -- --rpc-url http://127.0.0.1:8545 --contract 0x... +//! cargo run -p atom --example atom_indexer -- --rpc-url http://127.0.0.1:8545 --contract 0x... -use stem::{IndexerConfig, StemIndexer}; +use atom::{IndexerConfig, AtomIndexer}; use std::sync::Arc; fn main() -> Result<(), Box> { @@ -37,8 +37,8 @@ fn main() -> Result<(), Box> { } "--help" | "-h" => { eprintln!( - "Usage: stem_indexer --rpc-url --contract \n\ - Logs HeadUpdated events from the Stem contract. WS URL is derived from RPC URL." + "Usage: atom_indexer --rpc-url --contract \n\ + Logs HeadUpdated events from the Atom contract. WS URL is derived from RPC URL." ); std::process::exit(0); } @@ -47,7 +47,7 @@ fn main() -> Result<(), Box> { i += 1; } if rpc_url.is_empty() || contract.is_empty() { - eprintln!("Usage: stem_indexer --rpc-url --contract "); + eprintln!("Usage: atom_indexer --rpc-url --contract "); eprintln!(" (WebSocket URL is derived from the RPC URL)"); std::process::exit(1); } @@ -73,7 +73,7 @@ fn main() -> Result<(), Box> { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); std::thread::spawn(move || { diff --git a/crates/stem/examples/finalizer.rs b/crates/atom/examples/finalizer.rs similarity index 93% rename from crates/stem/examples/finalizer.rs rename to crates/atom/examples/finalizer.rs index e370bd7..33b0932 100644 --- a/crates/stem/examples/finalizer.rs +++ b/crates/atom/examples/finalizer.rs @@ -5,13 +5,13 @@ //! //! Usage: //! -//! cargo run -p stem --example finalizer -- --ws-url --http-url --contract +//! cargo run -p atom --example finalizer -- --ws-url --http-url --contract //! //! Options: //! --depth Confirmation depth (number of blocks after event before considering finalized). Default: 6. //! --cursor Path to file containing start block (one line, decimal). If missing or invalid, start from 0. -use stem::{FinalizerBuilder, IndexerConfig, StemIndexer}; +use atom::{FinalizerBuilder, IndexerConfig, AtomIndexer}; use std::io::BufRead; use std::sync::Arc; @@ -74,7 +74,7 @@ fn main() -> Result<(), Box> { } "--help" | "-h" => { eprintln!( - "Usage: finalizer --ws-url --http-url --contract [--depth K] [--cursor ]\n\ + "Usage: finalizer --ws-url --http-url --contract [--depth K] [--cursor ]\n\ Prints one-line JSON per finalized HeadUpdated event (confirmation-depth strategy).\n\ --depth K Confirmation depth (blocks after event before finalized). Default: 6.\n\ --cursor Path to file with start block (one line, decimal). Optional.\n\ @@ -87,7 +87,7 @@ fn main() -> Result<(), Box> { i += 1; } if ws_url.is_empty() || http_url.is_empty() || contract.is_empty() { - eprintln!("Usage: finalizer --ws-url --http-url --contract [--depth K] [--cursor ]"); + eprintln!("Usage: finalizer --ws-url --http-url --contract [--depth K] [--cursor ]"); std::process::exit(1); } let contract_address = match parse_contract_address(&contract) { @@ -111,7 +111,7 @@ fn main() -> Result<(), Box> { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); std::thread::spawn(move || { diff --git a/crates/stem/examples/membrane_poll.rs b/crates/atom/examples/membrane_poll.rs similarity index 96% rename from crates/stem/examples/membrane_poll.rs rename to crates/atom/examples/membrane_poll.rs index 1520a93..1ea8e83 100644 --- a/crates/stem/examples/membrane_poll.rs +++ b/crates/atom/examples/membrane_poll.rs @@ -7,15 +7,15 @@ //! //! Usage: //! -//! cargo run -p stem --example membrane_poll -- --ws-url --http-url --contract +//! cargo run -p atom --example membrane_poll -- --ws-url --http-url --contract //! //! Options: //! --depth Confirmation depth (blocks after event before finalized). Default: 2. use capnp_rpc::new_client; -use stem::stem_capnp; -use stem::{current_block_number, FinalizerBuilder, IndexerConfig, StemIndexer, Epoch}; -use stem::{FinalizedEvent, membrane_client}; +use atom::stem_capnp; +use atom::{current_block_number, FinalizerBuilder, IndexerConfig, AtomIndexer, Epoch}; +use atom::{FinalizedEvent, membrane_client}; use std::sync::Arc; use tokio::sync::watch; @@ -83,7 +83,7 @@ fn main() -> Result<(), Box> { } "--help" | "-h" => { eprintln!( - "Usage: membrane_poll --ws-url --http-url --contract [--depth K]\n\ + "Usage: membrane_poll --ws-url --http-url --contract [--depth K]\n\ Runs indexer → finalizer → membrane → graft → pollStatus (live-only from current block). Use small --depth (1–2) on a dev chain.\n\ After first epoch, run the printed cast send in another terminal to trigger staleness + re-graft." ); @@ -94,7 +94,7 @@ fn main() -> Result<(), Box> { i += 1; } if ws_url.is_empty() || http_url.is_empty() || contract.is_empty() { - eprintln!("Usage: membrane_poll --ws-url --http-url --contract [--depth K]"); + eprintln!("Usage: membrane_poll --ws-url --http-url --contract [--depth K]"); std::process::exit(1); } let contract_address = match parse_contract_address(&contract) { @@ -127,7 +127,7 @@ fn main() -> Result<(), Box> { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); std::thread::spawn(move || { diff --git a/crates/stem/src/abi.rs b/crates/atom/src/abi.rs similarity index 99% rename from crates/stem/src/abi.rs rename to crates/atom/src/abi.rs index 9a1e916..a883ce4 100644 --- a/crates/stem/src/abi.rs +++ b/crates/atom/src/abi.rs @@ -1,4 +1,4 @@ -//! ABI types and decoding for the Stem contract. +//! ABI types and decoding for the Atom contract. //! //! HeadUpdated event and head() view. Decode from JSON-RPC log shape and eth_call return. //! Uses alloy sol-types for ABI decoding; head() returns (uint64, bytes), event HeadUpdated(seq, writer, cid, cidHash). diff --git a/crates/stem/src/config.rs b/crates/atom/src/config.rs similarity index 96% rename from crates/stem/src/config.rs rename to crates/atom/src/config.rs index 120b9f3..4ab02cf 100644 --- a/crates/stem/src/config.rs +++ b/crates/atom/src/config.rs @@ -7,7 +7,7 @@ pub struct IndexerConfig { pub ws_url: String, /// HTTP RPC URL for backfill (eth_getLogs, eth_blockNumber, eth_call). pub http_url: String, - /// Stem contract address (20 bytes). + /// Atom contract address (20 bytes). pub contract_address: [u8; 20], /// First block to backfill from on startup. pub start_block: u64, diff --git a/crates/stem/src/cursor.rs b/crates/atom/src/cursor.rs similarity index 100% rename from crates/stem/src/cursor.rs rename to crates/atom/src/cursor.rs diff --git a/crates/stem/src/finalizer.rs b/crates/atom/src/finalizer.rs similarity index 99% rename from crates/stem/src/finalizer.rs rename to crates/atom/src/finalizer.rs index bc1b6de..20cbc30 100644 --- a/crates/stem/src/finalizer.rs +++ b/crates/atom/src/finalizer.rs @@ -1,7 +1,7 @@ //! Finalizer: reorg-safe finalization of observed HeadUpdated events. //! //! Consumes observed [HeadUpdatedObserved] from the indexer and outputs only events that are -//! eligible per the configured [Strategy] and pass the canonical cross-check (`Stem.head()`). +//! eligible per the configured [Strategy] and pass the canonical cross-check (`Atom.head()`). //! Dedup key is `(tx_hash, log_index)` (globally unique per log; stable across reconnects/backfill). //! Configure via [Strategy]; use [ConfirmationDepth] for depth-K finalization. See the //! `finalizer` example for a full pipeline (indexer → finalizer → JSON output). @@ -217,7 +217,7 @@ impl Finalizer { } /// Drain events that are eligible per strategy and pass the canonical cross-check. - /// Eligibility is checked with `strategy.is_eligible(ev, tip)`; then we call `Stem.head()` + /// Eligibility is checked with `strategy.is_eligible(ev, tip)`; then we call `Atom.head()` /// and only emit if (seq, cid) matches the candidate. Dedup by (tx_hash, log_index). pub async fn drain_eligible(&mut self, tip: u64) -> Result, FinalizerError> { // Collect eligible in order (block_number, log_index), then remove them from pending. diff --git a/crates/stem/src/indexer.rs b/crates/atom/src/indexer.rs similarity index 98% rename from crates/stem/src/indexer.rs rename to crates/atom/src/indexer.rs index 9513f28..8f02abc 100644 --- a/crates/stem/src/indexer.rs +++ b/crates/atom/src/indexer.rs @@ -1,4 +1,4 @@ -//! StemIndexer: observed-only indexing of Stem HeadUpdated events. +//! AtomIndexer: observed-only indexing of Atom HeadUpdated events. //! //! Subscribes via WebSocket, backfills via HTTP on startup/reconnect, maintains //! in-memory cursor and current HEAD. No reorg safety or confirmations in the indexer @@ -111,14 +111,14 @@ async fn eth_get_logs( Ok(arr.clone()) } -/// Stem indexer: follows HeadUpdated logs, backfills via HTTP, maintains current HEAD. -pub struct StemIndexer { +/// Atom indexer: follows HeadUpdated logs, backfills via HTTP, maintains current HEAD. +pub struct AtomIndexer { config: IndexerConfig, event_tx: broadcast::Sender, current_head: Arc>>, } -impl StemIndexer { +impl AtomIndexer { pub fn new(config: IndexerConfig) -> Self { let (event_tx, _) = broadcast::channel(256); Self { @@ -159,7 +159,7 @@ impl StemIndexer { sleep(Duration::from_secs(reconnection.initial_backoff_secs)).await; } Err(e) => { - tracing::warn!(reason = %e, "StemIndexer failed, reconnecting..."); + tracing::warn!(reason = %e, "AtomIndexer failed, reconnecting..."); let base = std::cmp::min( Duration::from_secs(reconnection.initial_backoff_secs) * 2, Duration::from_secs(reconnection.max_backoff_secs), @@ -173,7 +173,7 @@ impl StemIndexer { } async fn run_once( - indexer: Arc, + indexer: Arc, http_client: &reqwest::Client, cursor: &mut Cursor, config: &IndexerConfig, diff --git a/crates/stem/src/lib.rs b/crates/atom/src/lib.rs similarity index 79% rename from crates/stem/src/lib.rs rename to crates/atom/src/lib.rs index fefdf05..3e7fc55 100644 --- a/crates/stem/src/lib.rs +++ b/crates/atom/src/lib.rs @@ -1,10 +1,10 @@ -//! Off-chain Stem runtime: head-following, indexing, and finalization for the Stem contract. +//! Off-chain Atom runtime: head-following, indexing, and finalization for the Atom contract. //! -//! - **StemIndexer**: observed-only indexing of HeadUpdated events (WebSocket + HTTP backfill; +//! - **AtomIndexer**: observed-only indexing of HeadUpdated events (WebSocket + HTTP backfill; //! no reorg safety or confirmations in the indexer itself). //! - **Finalizer**: consumes indexer output and emits only events that are eligible per a //! configurable [Strategy] (e.g. [ConfirmationDepth]) and pass the canonical cross-check -//! (`Stem.head()`), giving reorg-safe finalized output. +//! (`Atom.head()`), giving reorg-safe finalized output. #[allow(unused_parens)] // generated capnp code pub mod stem_capnp { @@ -24,7 +24,7 @@ pub use cursor::Cursor; pub use finalizer::{ ConfirmationDepth, FinalizedEvent, Finalizer, FinalizerBuilder, FinalizerError, Strategy, }; -pub use indexer::{current_block_number, StemIndexer}; +pub use indexer::{current_block_number, AtomIndexer}; pub use membrane::{ membrane_client, Epoch, EpochGuard, MembraneServer, NoExtension, SessionExtensionBuilder, StatusPollerServer, fill_epoch_builder, diff --git a/crates/stem/src/membrane.rs b/crates/atom/src/membrane.rs similarity index 100% rename from crates/stem/src/membrane.rs rename to crates/atom/src/membrane.rs diff --git a/crates/stem/tests/common/mod.rs b/crates/atom/tests/common/mod.rs similarity index 96% rename from crates/stem/tests/common/mod.rs rename to crates/atom/tests/common/mod.rs index 6073a42..2e72533 100644 --- a/crates/stem/tests/common/mod.rs +++ b/crates/atom/tests/common/mod.rs @@ -82,9 +82,9 @@ pub async fn eth_get_transaction_count(http_url: &str, address: &str) -> Result< u64::from_str_radix(s, 16).context("parse nonce") } -/// Call Stem.head() via eth_call and decode. For optional post-revert assertion (e.g. seq == 0). -pub async fn stem_head_http(http_url: &str, contract_address: &[u8; 20]) -> Result { - use stem::abi::{decode_head_return, HEAD_SELECTOR}; +/// Call Atom.head() via eth_call and decode. For optional post-revert assertion (e.g. seq == 0). +pub async fn atom_head_http(http_url: &str, contract_address: &[u8; 20]) -> Result { + use atom::abi::{decode_head_return, HEAD_SELECTOR}; let client = http_client(); let params = json!([{ "to": format!("0x{}", hex::encode(contract_address)), @@ -145,9 +145,9 @@ async fn wait_for_rpc(url: &str) -> Result<()> { anyhow::bail!("RPC not ready"); } -/// Deploy Stem contract via forge script. Run from repo root (where src/Stem.sol lives). +/// Deploy Atom contract via forge script. Run from repo root (where src/Atom.sol lives). /// Parses the deployed address from the broadcast artifact (works across Foundry versions). -pub fn deploy_stem(repo_root: &std::path::Path, rpc_url: &str) -> Result { +pub fn deploy_atom(repo_root: &std::path::Path, rpc_url: &str) -> Result { let out = Command::new("forge") .current_dir(repo_root) .args([ @@ -187,7 +187,7 @@ pub fn deploy_stem(repo_root: &std::path::Path, rpc_url: &str) -> Result anyhow::bail!("no CREATE transaction in broadcast artifact"); } -/// Call setHead via cast send. Matches deployed Stem.sol ABI. +/// Call setHead via cast send. Matches deployed Atom.sol ABI. /// - signature "setHead(bytes)": args = [cid_hex]; cid_kind ignored. /// - signature "setHead(uint8,bytes)": args = [cid_kind, cid_hex]; cid_kind required. /// Prefer set_head_bytes when you have raw bytes so encoding is unambiguous. @@ -206,7 +206,7 @@ pub fn set_head( /// Anvil default account 0 (used for eth_sendTransaction when node signs). const ANVIL_DEFAULT_FROM: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; -/// First 4 bytes of keccak256("setHead(bytes)"). Matches [Stem.sol](src/Stem.sol) setHead(bytes calldata newCid). +/// First 4 bytes of keccak256("setHead(bytes)"). Matches [Atom.sol](src/Atom.sol) setHead(bytes calldata newCid). const SET_HEAD_BYTES_SELECTOR: [u8; 4] = [0x43, 0xea, 0xe8, 0x23]; /// Build ABI-encoded calldata for setHead(bytes)(cid_bytes) per Solidity ABI spec (dynamic type: offset then enc(k) pad_right(X)). diff --git a/crates/stem/tests/finalizer_integration.rs b/crates/atom/tests/finalizer_integration.rs similarity index 90% rename from crates/stem/tests/finalizer_integration.rs rename to crates/atom/tests/finalizer_integration.rs index cc9e4bc..6521d92 100644 --- a/crates/stem/tests/finalizer_integration.rs +++ b/crates/atom/tests/finalizer_integration.rs @@ -2,8 +2,8 @@ mod common; -use common::{deploy_stem, eth_block_number, evm_mine, set_head_bytes, spawn_anvil, stem_head_http}; -use stem::{FinalizerBuilder, IndexerConfig, StemIndexer}; +use common::{deploy_atom, eth_block_number, evm_mine, set_head_bytes, spawn_anvil, atom_head_http}; +use atom::{FinalizerBuilder, IndexerConfig, AtomIndexer}; use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -34,20 +34,20 @@ async fn test_finalizer_confirmation_depth_gates_and_adopts() { return; } let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("stem=debug".parse().unwrap())) + .with_env_filter(EnvFilter::from_default_env().add_directive("atom=debug".parse().unwrap())) .with_test_writer() .try_init(); - // CARGO_MANIFEST_DIR is crates/stem, so ancestors().nth(2) is repo root (script/, broadcast/). + // CARGO_MANIFEST_DIR is crates/atom, so ancestors().nth(2) is repo root (script/, broadcast/). let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap(); let (anvil_process, rpc_url) = spawn_anvil().await.expect("spawn anvil"); - let contract_addr = deploy_stem(repo_root, &rpc_url).expect("deploy Stem"); + let contract_addr = deploy_atom(repo_root, &rpc_url).expect("deploy Atom"); let addr_bytes = hex::decode(contract_addr.strip_prefix("0x").unwrap_or(&contract_addr)).expect("hex"); let mut contract_address = [0u8; 20]; contract_address.copy_from_slice(&addr_bytes); // Sanity: head() right after deploy should be seq=0, cid=b"ipfs-initial" - let head_after_deploy = stem_head_http(&rpc_url, &contract_address).await.expect("head after deploy"); + let head_after_deploy = atom_head_http(&rpc_url, &contract_address).await.expect("head after deploy"); assert_eq!(head_after_deploy.seq, 0, "seq after deploy"); assert_eq!( head_after_deploy.cid.as_slice(), @@ -66,7 +66,7 @@ async fn test_finalizer_confirmation_depth_gates_and_adopts() { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); let indexer_task = tokio::spawn(async move { @@ -80,7 +80,7 @@ async fn test_finalizer_confirmation_depth_gates_and_adopts() { // setHead("cid-1") — raw bytes + eth_sendRawTransaction (in-process EIP-155 signing). const CID_1_BYTES: &[u8] = b"cid-1"; set_head_bytes(repo_root, &rpc_url, &contract_addr, "setHead(bytes)", CID_1_BYTES, None).await.expect("setHead"); - let head_after_set = stem_head_http(&rpc_url, &contract_address).await.expect("stem head after setHead"); + let head_after_set = atom_head_http(&rpc_url, &contract_address).await.expect("stem head after setHead"); assert_eq!(head_after_set.seq, 1, "contract head().seq after setHead"); assert_eq!(head_after_set.cid.as_slice(), CID_1_BYTES, "contract head().cid after setHead (got {:?})", head_after_set.cid); diff --git a/crates/stem/tests/integration.rs b/crates/atom/tests/integration.rs similarity index 88% rename from crates/stem/tests/integration.rs rename to crates/atom/tests/integration.rs index a4478d7..dab9e2a 100644 --- a/crates/stem/tests/integration.rs +++ b/crates/atom/tests/integration.rs @@ -1,9 +1,9 @@ -//! Integration test: Anvil + deploy Stem + setHead + StemIndexer. +//! Integration test: Anvil + deploy Atom + setHead + AtomIndexer. mod common; -use common::{deploy_stem, set_head, spawn_anvil}; -use stem::{IndexerConfig, StemIndexer}; +use common::{deploy_atom, set_head, spawn_anvil}; +use atom::{IndexerConfig, AtomIndexer}; use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -17,13 +17,13 @@ async fn test_indexer_against_anvil() { return; } let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("stem=debug".parse().unwrap())) + .with_env_filter(EnvFilter::from_default_env().add_directive("atom=debug".parse().unwrap())) .with_test_writer() .try_init(); let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap(); let (mut anvil_process, rpc_url) = spawn_anvil().await.expect("spawn anvil"); - let contract_addr = deploy_stem(repo_root, &rpc_url).expect("deploy Stem"); + let contract_addr = deploy_atom(repo_root, &rpc_url).expect("deploy Atom"); let addr_bytes = hex::decode(contract_addr.strip_prefix("0x").unwrap_or(&contract_addr)).expect("hex"); let mut contract_address = [0u8; 20]; contract_address.copy_from_slice(&addr_bytes); @@ -41,7 +41,7 @@ async fn test_indexer_against_anvil() { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); let task = tokio::spawn(async move { diff --git a/crates/stem/tests/membrane_integration.rs b/crates/atom/tests/membrane_integration.rs similarity index 94% rename from crates/stem/tests/membrane_integration.rs rename to crates/atom/tests/membrane_integration.rs index 1dbb6a2..1e33f3d 100644 --- a/crates/stem/tests/membrane_integration.rs +++ b/crates/atom/tests/membrane_integration.rs @@ -4,12 +4,12 @@ mod common; use capnp_rpc::new_client; -use common::{deploy_stem, set_head, spawn_anvil}; +use common::{deploy_atom, set_head, spawn_anvil}; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use stem::stem_capnp; -use stem::{membrane_client, Epoch, IndexerConfig, StemIndexer}; +use atom::stem_capnp; +use atom::{membrane_client, Epoch, IndexerConfig, AtomIndexer}; use tokio::sync::watch; use tokio::time::timeout; use tracing_subscriber::EnvFilter; @@ -29,7 +29,7 @@ impl stem_capnp::signer::Server for StubSigner { } } -fn observed_to_epoch(ev: &stem::HeadUpdatedObserved) -> Epoch { +fn observed_to_epoch(ev: &atom::HeadUpdatedObserved) -> Epoch { Epoch { seq: ev.seq, head: ev.cid.clone(), @@ -44,13 +44,13 @@ async fn test_membrane_graft_poll_status_against_anvil() { return; } let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("stem=debug".parse().unwrap())) + .with_env_filter(EnvFilter::from_default_env().add_directive("atom=debug".parse().unwrap())) .with_test_writer() .try_init(); let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap(); let (mut anvil_process, rpc_url) = spawn_anvil().await.expect("spawn anvil"); - let contract_addr = deploy_stem(repo_root, &rpc_url).expect("deploy Stem"); + let contract_addr = deploy_atom(repo_root, &rpc_url).expect("deploy Atom"); let addr_bytes = hex::decode(contract_addr.strip_prefix("0x").unwrap_or(&contract_addr)).expect("hex"); let mut contract_address = [0u8; 20]; contract_address.copy_from_slice(&addr_bytes); @@ -66,7 +66,7 @@ async fn test_membrane_graft_poll_status_against_anvil() { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); let indexer_task = tokio::spawn(async move { diff --git a/crates/stem/tests/reorg_finalizer.rs b/crates/atom/tests/reorg_finalizer.rs similarity index 90% rename from crates/stem/tests/reorg_finalizer.rs rename to crates/atom/tests/reorg_finalizer.rs index cb79d02..211f4f7 100644 --- a/crates/stem/tests/reorg_finalizer.rs +++ b/crates/atom/tests/reorg_finalizer.rs @@ -3,10 +3,10 @@ mod common; use common::{ - deploy_stem, eth_block_number, evm_mine, evm_revert, evm_snapshot, set_head_bytes, spawn_anvil, - stem_head_http, + deploy_atom, eth_block_number, evm_mine, evm_revert, evm_snapshot, set_head_bytes, spawn_anvil, + atom_head_http, }; -use stem::{FinalizerBuilder, IndexerConfig, StemIndexer}; +use atom::{FinalizerBuilder, IndexerConfig, AtomIndexer}; use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -37,14 +37,14 @@ async fn test_reorg_indexer_false_positive_finalizer_filters() { return; } let _ = tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("stem=debug".parse().unwrap())) + .with_env_filter(EnvFilter::from_default_env().add_directive("atom=debug".parse().unwrap())) .with_test_writer() .try_init(); - // CARGO_MANIFEST_DIR is crates/stem, so ancestors().nth(2) is repo root (script/, broadcast/). + // CARGO_MANIFEST_DIR is crates/atom, so ancestors().nth(2) is repo root (script/, broadcast/). let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap(); let (anvil_process, rpc_url) = spawn_anvil().await.expect("spawn anvil"); - let contract_addr = deploy_stem(repo_root, &rpc_url).expect("deploy Stem"); + let contract_addr = deploy_atom(repo_root, &rpc_url).expect("deploy Atom"); let addr_bytes = hex::decode(contract_addr.strip_prefix("0x").unwrap_or(&contract_addr)).expect("hex"); let mut contract_address = [0u8; 20]; contract_address.copy_from_slice(&addr_bytes); @@ -59,7 +59,7 @@ async fn test_reorg_indexer_false_positive_finalizer_filters() { getlogs_max_range: 1000, reconnection: Default::default(), }; - let indexer = Arc::new(StemIndexer::new(config)); + let indexer = Arc::new(AtomIndexer::new(config)); let mut recv = indexer.subscribe(); let indexer_clone = Arc::clone(&indexer); let indexer_task = tokio::spawn(async move { @@ -123,6 +123,6 @@ async fn test_reorg_indexer_false_positive_finalizer_filters() { finalized.len() ); - let head = stem_head_http(&rpc_url, &contract_address).await.expect("stem head()"); + let head = atom_head_http(&rpc_url, &contract_address).await.expect("stem head()"); assert_eq!(head.seq, 0, "canonical head must be seq 0 after revert"); } diff --git a/crates/stem/tests/reorg_naive.rs b/crates/atom/tests/reorg_naive.rs similarity index 95% rename from crates/stem/tests/reorg_naive.rs rename to crates/atom/tests/reorg_naive.rs index fc15400..d8413a9 100644 --- a/crates/stem/tests/reorg_naive.rs +++ b/crates/atom/tests/reorg_naive.rs @@ -10,9 +10,9 @@ mod common; use anyhow::{Context, Result}; -use common::{deploy_stem, evm_revert, evm_snapshot, set_head, spawn_anvil}; +use common::{deploy_atom, evm_revert, evm_snapshot, set_head, spawn_anvil}; use serde_json::{json, Value}; -use stem::abi::{decode_head_return, decode_log_to_observed, CurrentHead, HEAD_SELECTOR, HEAD_UPDATED_TOPIC0}; +use atom::abi::{decode_head_return, decode_log_to_observed, CurrentHead, HEAD_SELECTOR, HEAD_UPDATED_TOPIC0}; use std::path::Path; async fn http_json_rpc(client: &reqwest::Client, url: &str, method: &str, params: Value, id: u64) -> Result { @@ -61,7 +61,7 @@ async fn eth_get_logs( Ok(logs) } -async fn stem_head( +async fn atom_head( client: &reqwest::Client, http_url: &str, contract_address: &[u8; 20], @@ -88,7 +88,7 @@ async fn test_reorg_naive_observer_mismatch() { let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).ancestors().nth(2).unwrap(); let (mut anvil_process, rpc_url) = spawn_anvil().await.expect("spawn anvil"); - let contract_addr = deploy_stem(repo_root, &rpc_url).expect("deploy Stem"); + let contract_addr = deploy_atom(repo_root, &rpc_url).expect("deploy Atom"); let addr_bytes = hex::decode(contract_addr.strip_prefix("0x").unwrap_or(&contract_addr)).expect("hex"); let mut contract_address = [0u8; 20]; contract_address.copy_from_slice(&addr_bytes); @@ -143,7 +143,7 @@ async fn test_reorg_naive_observer_mismatch() { }); assert!(!has_our_tx, "canonical logs must not contain the reverted setHead tx"); - let canonical_head = stem_head(&client, &rpc_url, &contract_address).await.expect("stem head()"); + let canonical_head = atom_head(&client, &rpc_url, &contract_address).await.expect("atom head()"); assert_eq!(canonical_head.seq, 0, "canonical chain must no longer reflect the reverted setHead"); // After evm_revert, canonical head is back to initial state (seq 0); cid may be initial or empty depending on node. diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 752bd1a..44cb957 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -2,13 +2,13 @@ pragma solidity ^0.8.24; import "forge-std/Script.sol"; -import "../src/Stem.sol"; +import "../src/Atom.sol"; contract Deploy is Script { function run() external { vm.startBroadcast(); - Stem stem = new Stem(bytes("ipfs-initial")); - console.log("Stem deployed at:", address(stem)); + Atom atom = new Atom(bytes("ipfs-initial")); + console.log("Atom deployed at:", address(atom)); vm.stopBroadcast(); } } diff --git a/src/Stem.sol b/src/Atom.sol similarity index 84% rename from src/Stem.sol rename to src/Atom.sol index 84c5101..e833c0d 100644 --- a/src/Stem.sol +++ b/src/Atom.sol @@ -1,20 +1,20 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; -/// @title Stem -/// @notice The on-chain bootstrap facet of Stem. +/// @title Atom +/// @notice The on-chain bootstrap facet of Atom. /// -/// Stem anchors the authoritative "head" pointer for Stem. -/// Off-chain systems (Stem runtime) watch Stem events to learn when +/// Atom anchors the authoritative "head" pointer for Atom. +/// Off-chain systems (Atom runtime) watch Atom events to learn when /// the global configuration root has advanced. /// /// Semantics: -/// - There is exactly one Stem per deployment. +/// - There is exactly one Atom per deployment. /// - The head is advanced monotonically via `seq`. /// - Every advance emits an event suitable for deterministic replay. /// - Authority to advance the head is gated (owner for now). -contract Stem { - /// @notice Emitted whenever the Stem head advances. +contract Atom { + /// @notice Emitted whenever the Atom head advances. /// @param seq Monotonic sequence number (epoch index) /// @param writer Caller who advanced the head /// @param cid New head pointer bytes (binary CID or name bytes) @@ -50,7 +50,7 @@ contract Stem { return (seq, _cid); } - /// @notice Advance the Stem head. + /// @notice Advance the Atom head. /// Emits a HeadUpdated event that off-chain watchers consume. function setHead(bytes calldata newCid) external { if (msg.sender != owner) revert NotOwner(); diff --git a/test/Stem.t.sol b/test/Atom.t.sol similarity index 52% rename from test/Stem.t.sol rename to test/Atom.t.sol index e996901..791ff80 100644 --- a/test/Stem.t.sol +++ b/test/Atom.t.sol @@ -2,23 +2,23 @@ pragma solidity ^0.8.24; import { Test } from "forge-std/Test.sol"; -import { Stem } from "../src/Stem.sol"; +import { Atom } from "../src/Atom.sol"; -contract StemTest is Test { - Stem public stem; +contract AtomTest is Test { + Atom public atom; address public owner; address public user; function setUp() public { owner = address(this); user = address(0x123); - stem = new Stem(bytes("ipfs-initial")); + atom = new Atom(bytes("ipfs-initial")); } function test_Constructor() public view { - assertEq(stem.owner(), owner); - assertEq(stem.seq(), 0); - (uint64 s, bytes memory c) = stem.head(); + assertEq(atom.owner(), owner); + assertEq(atom.seq(), 0); + (uint64 s, bytes memory c) = atom.head(); assertEq(s, 0); assertEq(keccak256(c), keccak256(bytes("ipfs-initial"))); } @@ -27,47 +27,47 @@ contract StemTest is Test { bytes memory newCid = bytes("ipfs://new"); bytes32 expectedCidHash = keccak256(newCid); vm.expectEmit(true, true, true, true); - emit Stem.HeadUpdated(1, owner, newCid, expectedCidHash); + emit Atom.HeadUpdated(1, owner, newCid, expectedCidHash); - stem.setHead(newCid); + atom.setHead(newCid); - assertEq(stem.seq(), 1); - (uint64 seq, bytes memory cid) = stem.head(); + assertEq(atom.seq(), 1); + (uint64 seq, bytes memory cid) = atom.head(); assertEq(seq, 1); assertEq(cid, newCid); } function test_SetHead_NotOwner() public { vm.prank(user); - vm.expectRevert(Stem.NotOwner.selector); - stem.setHead(bytes("ipfs://new")); + vm.expectRevert(Atom.NotOwner.selector); + atom.setHead(bytes("ipfs://new")); } function test_SetHead_NoChange() public { // Same cid as initial head -> revert NoChange, seq unchanged - vm.expectRevert(Stem.NoChange.selector); - stem.setHead(bytes("ipfs-initial")); - assertEq(stem.seq(), 0); + vm.expectRevert(Atom.NoChange.selector); + atom.setHead(bytes("ipfs-initial")); + assertEq(atom.seq(), 0); // One real update, then no-op again -> revert NoChange, seq still 1 - stem.setHead(bytes("ipfs://new")); - assertEq(stem.seq(), 1); - vm.expectRevert(Stem.NoChange.selector); - stem.setHead(bytes("ipfs://new")); - assertEq(stem.seq(), 1); + atom.setHead(bytes("ipfs://new")); + assertEq(atom.seq(), 1); + vm.expectRevert(Atom.NoChange.selector); + atom.setHead(bytes("ipfs://new")); + assertEq(atom.seq(), 1); } function test_SetHead_MultipleUpdates() public { - stem.setHead(bytes("ipfs://first")); - assertEq(stem.seq(), 1); + atom.setHead(bytes("ipfs://first")); + assertEq(atom.seq(), 1); - stem.setHead(bytes("ipld://second")); - assertEq(stem.seq(), 2); + atom.setHead(bytes("ipld://second")); + assertEq(atom.seq(), 2); - stem.setHead(bytes("blob://third")); - assertEq(stem.seq(), 3); + atom.setHead(bytes("blob://third")); + assertEq(atom.seq(), 3); - (uint64 seq, bytes memory cid) = stem.head(); + (uint64 seq, bytes memory cid) = atom.head(); assertEq(seq, 3); assertEq(cid, bytes("blob://third")); } @@ -76,12 +76,12 @@ contract StemTest is Test { bytes memory cid = bytes("QmIPFS"); bytes32 cidHash = keccak256(cid); vm.expectEmit(true, true, true, true); - emit Stem.HeadUpdated(1, owner, cid, cidHash); + emit Atom.HeadUpdated(1, owner, cid, cidHash); - stem.setHead(cid); + atom.setHead(cid); - assertEq(stem.seq(), 1); - (uint64 seq, bytes memory c) = stem.head(); + assertEq(atom.seq(), 1); + (uint64 seq, bytes memory c) = atom.head(); assertEq(seq, 1); assertEq(c, cid); } @@ -90,12 +90,12 @@ contract StemTest is Test { bytes memory cid = bytes("QmIPLD"); bytes32 cidHash = keccak256(cid); vm.expectEmit(true, true, true, true); - emit Stem.HeadUpdated(1, owner, cid, cidHash); + emit Atom.HeadUpdated(1, owner, cid, cidHash); - stem.setHead(cid); + atom.setHead(cid); - assertEq(stem.seq(), 1); - (uint64 seq, bytes memory c) = stem.head(); + assertEq(atom.seq(), 1); + (uint64 seq, bytes memory c) = atom.head(); assertEq(seq, 1); assertEq(c, cid); } @@ -104,12 +104,12 @@ contract StemTest is Test { bytes memory cid = bytes("blob-data"); bytes32 cidHash = keccak256(cid); vm.expectEmit(true, true, true, true); - emit Stem.HeadUpdated(1, owner, cid, cidHash); + emit Atom.HeadUpdated(1, owner, cid, cidHash); - stem.setHead(cid); + atom.setHead(cid); - assertEq(stem.seq(), 1); - (uint64 seq, bytes memory c) = stem.head(); + assertEq(atom.seq(), 1); + (uint64 seq, bytes memory c) = atom.head(); assertEq(seq, 1); assertEq(c, cid); } @@ -118,12 +118,12 @@ contract StemTest is Test { bytes memory cid = bytes("k51qzi5uqu5..."); bytes32 cidHash = keccak256(cid); vm.expectEmit(true, true, true, true); - emit Stem.HeadUpdated(1, owner, cid, cidHash); + emit Atom.HeadUpdated(1, owner, cid, cidHash); - stem.setHead(cid); + atom.setHead(cid); - assertEq(stem.seq(), 1); - (uint64 seq, bytes memory c) = stem.head(); + assertEq(atom.seq(), 1); + (uint64 seq, bytes memory c) = atom.head(); assertEq(seq, 1); assertEq(c, cid); } From bd69e8e7aaa251cb93c6f0a92f795800cec97d46 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Wed, 18 Feb 2026 18:47:41 -0500 Subject: [PATCH 2/3] Switch atom to depend on membrane-core Replace inline membrane module with re-exports from the membrane-core crate. Removes build.rs (no local capnp codegen), membrane.rs (196 lines), and the capnpc build-dep. All 15 tests + 3 examples + membrane integration tests pass. Closes #2 --- Cargo.lock | 13 ++- crates/atom/Cargo.toml | 4 +- crates/atom/build.rs | 10 -- crates/atom/src/lib.rs | 15 ++- crates/atom/src/membrane.rs | 195 ------------------------------------ 5 files changed, 19 insertions(+), 218 deletions(-) delete mode 100644 crates/atom/build.rs delete mode 100644 crates/atom/src/membrane.rs diff --git a/Cargo.lock b/Cargo.lock index aea8d01..b00e68e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,10 +710,10 @@ dependencies = [ "anyhow", "capnp", "capnp-rpc", - "capnpc", "futures-util", "hex", "k256", + "membrane-core", "rand 0.8.5", "reqwest", "rlp", @@ -1976,6 +1976,17 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "membrane-core" +version = "0.1.0" +source = "git+https://github.com/wetware/membrane.git#cfd88022ccb59f4cff49a91501807af430d3dcc5" +dependencies = [ + "capnp", + "capnp-rpc", + "capnpc", + "tokio", +] + [[package]] name = "memchr" version = "2.7.6" diff --git a/crates/atom/Cargo.toml b/crates/atom/Cargo.toml index e42d642..7c2093d 100644 --- a/crates/atom/Cargo.toml +++ b/crates/atom/Cargo.toml @@ -5,10 +5,8 @@ edition = "2021" description = "Off-chain Atom runtime: head-following, finalization, and caching for the Atom contract" -[build-dependencies] -capnpc = "0.23.3" - [dependencies] +membrane-core = { git = "https://github.com/wetware/membrane.git" } alloy = { version = "0.4", features = ["rpc-types", "sol-types"] } capnp = "0.23.2" anyhow = "1" diff --git a/crates/atom/build.rs b/crates/atom/build.rs deleted file mode 100644 index a216c7a..0000000 --- a/crates/atom/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::path::PathBuf; - -fn main() { - let schema = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../capnp/stem.capnp"); - capnpc::CompilerCommand::new() - .src_prefix("../../") - .file(schema) - .run() - .expect("capnp compile"); -} diff --git a/crates/atom/src/lib.rs b/crates/atom/src/lib.rs index 3e7fc55..3f5c70c 100644 --- a/crates/atom/src/lib.rs +++ b/crates/atom/src/lib.rs @@ -6,17 +6,18 @@ //! configurable [Strategy] (e.g. [ConfirmationDepth]) and pass the canonical cross-check //! (`Atom.head()`), giving reorg-safe finalized output. -#[allow(unused_parens)] // generated capnp code -pub mod stem_capnp { - include!(concat!(env!("OUT_DIR"), "/capnp/stem_capnp.rs")); -} +pub use membrane_core::stem_capnp; +pub use membrane_core::{ + Epoch, EpochGuard, fill_epoch_builder, + membrane_client, MembraneServer, NoExtension, + SessionExtensionBuilder, StatusPollerServer, +}; pub mod abi; pub mod config; pub mod cursor; pub mod finalizer; pub mod indexer; -pub mod membrane; pub use abi::{CurrentHead, HeadUpdatedObserved}; pub use config::{IndexerConfig, ReconnectionConfig}; @@ -25,10 +26,6 @@ pub use finalizer::{ ConfirmationDepth, FinalizedEvent, Finalizer, FinalizerBuilder, FinalizerError, Strategy, }; pub use indexer::{current_block_number, AtomIndexer}; -pub use membrane::{ - membrane_client, Epoch, EpochGuard, MembraneServer, NoExtension, - SessionExtensionBuilder, StatusPollerServer, fill_epoch_builder, -}; /// Current head state (alias for ABI CurrentHead). pub type Head = CurrentHead; diff --git a/crates/atom/src/membrane.rs b/crates/atom/src/membrane.rs deleted file mode 100644 index effec1a..0000000 --- a/crates/atom/src/membrane.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Pure-Rust Membrane server: epoch validity via seq equality (Approach A), -//! backed by `watch::Receiver`, exposed over capnp-rpc. - -use crate::stem_capnp; -use capnp::capability::Promise; -use capnp::Error; -use capnp_rpc::new_client; -use tokio::sync::watch; - -/// Epoch value used by the membrane (matches capnp struct Epoch). -#[derive(Clone, Debug)] -pub struct Epoch { - pub seq: u64, - pub head: Vec, - pub adopted_block: u64, -} - -pub fn fill_epoch_builder( - builder: &mut stem_capnp::epoch::Builder<'_>, - epoch: &Epoch, -) -> Result<(), Error> { - builder.set_seq(epoch.seq); - builder.set_adopted_block(epoch.adopted_block); - let head_builder = builder.reborrow().init_head(epoch.head.len() as u32); - head_builder.copy_from_slice(epoch.head.as_slice()); - Ok(()) -} - -/// Guard that checks whether the epoch under which a capability was issued is -/// still current. Shared by all session-scoped capability servers so that -/// every RPC hard-fails once the epoch advances. -#[derive(Clone)] -pub struct EpochGuard { - pub issued_seq: u64, - pub receiver: watch::Receiver, -} - -impl EpochGuard { - pub fn check(&self) -> Result<(), Error> { - let current = self.receiver.borrow(); - if current.seq != self.issued_seq { - return Err(Error::failed("staleEpoch: session epoch no longer current".to_string())); - } - Ok(()) - } -} - -/// Callback trait for filling the session extension during graft. -/// -/// Implementors receive the EpochGuard and a builder for the extension field, -/// allowing platform-specific capabilities (e.g. Host, Executor) to be injected -/// into the session. -pub trait SessionExtensionBuilder: 'static -where - SessionExt: capnp::traits::Owned, -{ - fn build( - &self, - guard: &EpochGuard, - builder: ::Builder<'_>, - ) -> Result<(), Error>; -} - -/// No-op extension builder for sessions without platform-specific capabilities. -pub struct NoExtension; - -impl SessionExtensionBuilder for NoExtension { - fn build( - &self, - _guard: &EpochGuard, - _builder: capnp::any_pointer::Builder<'_>, - ) -> Result<(), Error> { - Ok(()) - } -} - -/// Membrane server: stable across epochs, backed by a watch receiver for the adopted epoch. -/// -/// Generic over `SessionExt`: the type parameter for the Session's extension field. -/// The `ext_builder` callback fills the extension when a session is issued. -pub struct MembraneServer -where - SessionExt: capnp::traits::Owned, - F: SessionExtensionBuilder, -{ - receiver: watch::Receiver, - ext_builder: F, - _phantom: std::marker::PhantomData, -} - -impl MembraneServer -where - SessionExt: capnp::traits::Owned, - F: SessionExtensionBuilder, -{ - pub fn new(receiver: watch::Receiver, ext_builder: F) -> Self { - Self { - receiver, - ext_builder, - _phantom: std::marker::PhantomData, - } - } - - fn get_current_epoch(&self) -> Epoch { - self.receiver.borrow().clone() - } -} - -#[allow(refining_impl_trait)] -impl stem_capnp::membrane::Server for MembraneServer -where - SessionExt: capnp::traits::Owned + 'static, - F: SessionExtensionBuilder, -{ - fn graft( - self: capnp::capability::Rc, - _params: stem_capnp::membrane::GraftParams, - mut results: stem_capnp::membrane::GraftResults, - ) -> Promise<(), Error> { - let epoch = self.get_current_epoch(); - let mut session_builder = results.get().init_session(); - if fill_epoch_builder(&mut session_builder.reborrow().init_issued_epoch(), &epoch).is_err() { - return Promise::err(Error::failed("fill issued epoch".to_string())); - } - let guard = EpochGuard { - issued_seq: epoch.seq, - receiver: self.receiver.clone(), - }; - let poller = StatusPollerServer { guard: guard.clone() }; - session_builder.reborrow().set_status_poller(new_client(poller)); - - if let Err(e) = self.ext_builder.build(&guard, session_builder.reborrow().init_extension()) { - return Promise::err(e); - } - - Promise::ok(()) - } -} - -/// StatusPoller server: epoch-scoped; pollStatus returns an RPC error when the -/// epoch has advanced past the one under which this capability was issued. -pub struct StatusPollerServer { - pub guard: EpochGuard, -} - -#[allow(refining_impl_trait)] -impl stem_capnp::status_poller::Server for StatusPollerServer { - fn poll_status( - self: capnp::capability::Rc, - _: stem_capnp::status_poller::PollStatusParams, - mut results: stem_capnp::status_poller::PollStatusResults, - ) -> Promise<(), Error> { - if let Err(e) = self.guard.check() { - return Promise::err(e); - } - results.get().set_status(stem_capnp::Status::Ok); - Promise::ok(()) - } -} - -/// Builds a Membrane capability client from a watch receiver (for use over capnp-rpc). -/// -/// Uses `NoExtension` — the session's extension field is left empty. -/// For platform-specific extensions (e.g. wetware Host+Executor), construct -/// `MembraneServer::new(receiver, your_ext_builder)` directly. -pub fn membrane_client(receiver: watch::Receiver) -> stem_capnp::membrane::Client { - new_client(MembraneServer::new(receiver, NoExtension)) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn epoch(seq: u64, head: &[u8], adopted_block: u64) -> Epoch { - Epoch { - seq, - head: head.to_vec(), - adopted_block, - } - } - - #[tokio::test] - async fn status_poller_check_epoch_fails_when_seq_differs() { - let (tx, rx) = watch::channel(epoch(1, b"head1", 100)); - let guard = EpochGuard { - issued_seq: 1, - receiver: rx.clone(), - }; - assert!(guard.check().is_ok()); - tx.send(epoch(2, b"head2", 101)).unwrap(); - let res = guard.check(); - assert!(res.is_err()); - assert!(res.unwrap_err().to_string().contains("staleEpoch")); - } -} From e701043cc13b7dd2768bcbac07a7371bf970d7f9 Mon Sep 17 00:00:00 2001 From: Louis Thibault Date: Wed, 18 Feb 2026 20:08:12 -0500 Subject: [PATCH 3/3] Point membrane dep at wetware/rs membrane-core (from wetware/membrane) is now just membrane (in wetware/rs/crates/membrane). Update dep and re-export paths. All 15 tests pass. --- Cargo.lock | 6 +++--- crates/atom/Cargo.toml | 2 +- crates/atom/src/lib.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b00e68e..88d9d5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,7 +713,7 @@ dependencies = [ "futures-util", "hex", "k256", - "membrane-core", + "membrane", "rand 0.8.5", "reqwest", "rlp", @@ -1977,9 +1977,9 @@ dependencies = [ ] [[package]] -name = "membrane-core" +name = "membrane" version = "0.1.0" -source = "git+https://github.com/wetware/membrane.git#cfd88022ccb59f4cff49a91501807af430d3dcc5" +source = "git+https://github.com/wetware/rs.git?branch=feat%2Fmembrane-crate#ce1030034cde7f87ffe2a4ee5d6c47341f08b035" dependencies = [ "capnp", "capnp-rpc", diff --git a/crates/atom/Cargo.toml b/crates/atom/Cargo.toml index 7c2093d..7e32eb0 100644 --- a/crates/atom/Cargo.toml +++ b/crates/atom/Cargo.toml @@ -6,7 +6,7 @@ description = "Off-chain Atom runtime: head-following, finalization, and caching [dependencies] -membrane-core = { git = "https://github.com/wetware/membrane.git" } +membrane = { git = "https://github.com/wetware/rs.git", branch = "feat/membrane-crate" } alloy = { version = "0.4", features = ["rpc-types", "sol-types"] } capnp = "0.23.2" anyhow = "1" diff --git a/crates/atom/src/lib.rs b/crates/atom/src/lib.rs index 3f5c70c..862a243 100644 --- a/crates/atom/src/lib.rs +++ b/crates/atom/src/lib.rs @@ -6,8 +6,8 @@ //! configurable [Strategy] (e.g. [ConfirmationDepth]) and pass the canonical cross-check //! (`Atom.head()`), giving reorg-safe finalized output. -pub use membrane_core::stem_capnp; -pub use membrane_core::{ +pub use membrane::stem_capnp; +pub use membrane::{ Epoch, EpochGuard, fill_epoch_builder, membrane_client, MembraneServer, NoExtension, SessionExtensionBuilder, StatusPollerServer,