Skip to content

Commit a369106

Browse files
committed
Rewrite README with architecture docs and examples
Replace default Foundry template with developer-facing documentation covering the four-stage pipeline (contract, indexer, finalizer, membrane), the epoch-scoped capability model, local deployment, and all three runnable examples.
1 parent dc6d318 commit a369106

1 file changed

Lines changed: 149 additions & 46 deletions

File tree

README.md

Lines changed: 149 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,185 @@
1-
## Foundry
1+
# Stem
22

3-
This repo contains a Foundry Solidity project and a Rust workspace under `crates/`. Run Solidity tests with `forge test` and the Rust crate with `cargo test -p stem`.
3+
Off-chain runtime for the [Stem smart contract](src/Stem.sol).
44

5-
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
5+
Stem indexes `HeadUpdated` events emitted by an on-chain anchor contract,
6+
finalizes them with reorg safety (configurable confirmation depth), and exposes
7+
epoch-scoped authority to clients via Cap'n Proto capabilities. When the
8+
on-chain head advances, every capability issued under the previous epoch
9+
hard-fails — clients re-graft to recover.
610

7-
Foundry consists of:
11+
## Architecture
812

9-
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
10-
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
11-
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
12-
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
13+
```
14+
HeadUpdated → Indexer → Finalizer → Epoch → Membrane
15+
16+
graft(signer)
17+
18+
Session
19+
┌─────┴─────┐
20+
statusPoller (future caps)
21+
22+
Ok / RPC error
23+
```
24+
25+
The runtime is a four-stage pipeline:
26+
27+
### 1. Stem contract (`src/Stem.sol`)
28+
29+
On-chain anchor. The owner calls `setHead(newCid)` to advance a monotonic
30+
`seq` and emit a `HeadUpdated` event. The `head()` view returns the canonical
31+
`(seq, cid)` pair for cross-checking.
32+
33+
```solidity
34+
event HeadUpdated(
35+
uint64 indexed seq,
36+
address indexed writer,
37+
bytes cid,
38+
bytes32 indexed cidHash
39+
);
40+
```
41+
42+
### 2. Indexer (`StemIndexer`)
43+
44+
Subscribes to `HeadUpdated` via WebSocket for live events and backfills
45+
missed blocks via HTTP `eth_getLogs` on startup and reconnect. Broadcasts
46+
`HeadUpdatedObserved` values to downstream consumers. Reconnects with
47+
exponential backoff and jitter. Client-side filtering handles RPC nodes
48+
(e.g. Anvil) that don't support topic filters natively.
49+
50+
The indexer is observation-only — it makes no reorg-safety guarantees.
51+
52+
### 3. Finalizer (`Finalizer` / `FinalizerBuilder`)
53+
54+
Consumes observed events from the indexer and outputs only those that are
55+
**eligible** and **canonical**:
56+
57+
- **Eligibility** is decided by a pluggable `Strategy` trait. The built-in
58+
`ConfirmationDepth(K)` strategy requires `tip >= event.block_number + K`.
59+
- **Canonical cross-check**: after eligibility, the finalizer calls
60+
`Stem.head()` and only emits if the on-chain `(seq, cid)` matches the
61+
candidate event.
62+
- **Deduplication** by `(tx_hash, log_index)` ensures exactly-once delivery
63+
across reconnects and backfills.
64+
65+
Each output is a `FinalizedEvent` containing `seq`, `cid`, `block_number`,
66+
`tx_hash`, `log_index`, and `writer`.
1367

14-
## Documentation
68+
### 4. Membrane (`MembraneServer` / Cap'n Proto RPC)
1569

16-
https://book.getfoundry.sh/
70+
The capability layer. A `MembraneServer` holds a `watch::Receiver<Epoch>`
71+
and exposes a single entry point:
1772

18-
## Usage
73+
```
74+
graft(signer) → Session { issuedEpoch, statusPoller }
75+
```
76+
77+
All capabilities inside a `Session` share an `EpochGuard` that checks
78+
`current.seq == issued_seq` on every RPC call. When the epoch advances,
79+
every outstanding capability fails with a `staleEpoch` RPC error. Clients
80+
call `graft()` again to obtain a fresh session under the new epoch.
81+
82+
## Getting started
83+
84+
### Prerequisites
85+
86+
- [Rust](https://rustup.rs/) (stable)
87+
- [Foundry](https://getfoundry.sh/) (forge, anvil, cast)
88+
- [Cap'n Proto compiler](https://capnproto.org/install.html) (`capnp`)
1989

2090
### Build
2191

22-
```shell
23-
$ forge build
92+
```bash
93+
forge build
94+
cargo build -p stem
2495
```
2596

2697
### Test
2798

28-
```shell
29-
$ forge test
99+
```bash
100+
forge test
101+
cargo test -p stem
30102
```
31103

32-
### Format
104+
## Deploy (local)
105+
106+
Start Anvil in one terminal:
33107

34-
```shell
35-
$ forge fmt
108+
```bash
109+
anvil
36110
```
37111

38-
### Gas Snapshots
112+
Deploy the contract and set the first head:
39113

40-
```shell
41-
$ forge snapshot
114+
```bash
115+
# Deploy
116+
forge script script/Deploy.s.sol \
117+
--rpc-url http://127.0.0.1:8545 \
118+
--broadcast \
119+
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
120+
121+
# Note the deployed address from the output, then set the first head:
122+
cast send <STEM_ADDRESS> "setHead(bytes)" "0x$(echo -n 'ipfs://first' | xxd -p)" \
123+
--rpc-url http://127.0.0.1:8545 \
124+
--private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
42125
```
43126

44-
### Anvil
127+
## Examples
45128

46-
```shell
47-
$ anvil
48-
```
129+
All examples live in `crates/stem/examples/` and connect to a running node
130+
with a deployed Stem contract.
49131

50-
### Deploy
132+
### `stem_indexer` — raw observed events
51133

52-
Deploy the Stem contract (e.g. to local Anvil). The script prints the deployed address; use it with the stem examples.
134+
Prints every `HeadUpdated` event as it arrives (no finalization).
53135

54-
```shell
55-
$ anvil
56-
$ forge script script/Deploy.s.sol:Deploy --rpc-url http://127.0.0.1:8545 --broadcast --private-key <your_private_key>
57-
# Stem deployed at: 0x... <- use this as --contract below
136+
```bash
137+
cargo run -p stem --example stem_indexer -- \
138+
--rpc-url http://127.0.0.1:8545 \
139+
--contract <STEM_ADDRESS>
58140
```
59141

60-
**Membrane (ocap API):** The membrane is the capability API on top of the Stem finalizer/adopted-epoch stream. Processes may persist across epochs, but privileged capabilities do not—authority is bound to the current adopted epoch. After a staleEpoch, clients must re-graft (e.g. `Membrane.graft`) to obtain a new session.
142+
### `finalizer` — finalized events as JSON
61143

62-
**Stem examples (Rust):**
144+
Runs the full indexer + finalizer pipeline and prints one JSON object per
145+
finalized event.
63146

64-
- **stem_indexer** — observed HeadUpdated events (no confirmations):
65-
`cargo run -p stem --example stem_indexer -- --rpc-url http://127.0.0.1:8545 --contract 0x...`
147+
```bash
148+
cargo run -p stem --example finalizer -- \
149+
--ws-url ws://127.0.0.1:8545 \
150+
--http-url http://127.0.0.1:8545 \
151+
--contract <STEM_ADDRESS> \
152+
--depth 2
153+
```
66154

67-
- **finalizer** — finalized events only (confirmation-depth + canonical cross-check); prints one-line JSON per event:
68-
`cargo run -p stem --example finalizer -- --ws-url ws://127.0.0.1:8545 --http-url http://127.0.0.1:8545 --contract 0x... [--depth 6]`
155+
### `membrane_poll` — epoch expiration and re-graft
69156

70-
### Cast
157+
Demonstrates the full pipeline: indexer, finalizer, membrane, graft, poll.
158+
When a second `setHead` is finalized the existing session's `statusPoller`
159+
fails with a `staleEpoch` error; the example re-grafts and polls successfully
160+
under the new epoch.
71161

72-
```shell
73-
$ cast <subcommand>
162+
```bash
163+
cargo run -p stem --example membrane_poll -- \
164+
--ws-url ws://127.0.0.1:8545 \
165+
--http-url http://127.0.0.1:8545 \
166+
--contract <STEM_ADDRESS> \
167+
--depth 2
74168
```
75169

76-
### Help
170+
## Cap'n Proto schema
77171

78-
```shell
79-
$ forge --help
80-
$ anvil --help
81-
$ cast --help
82-
```
172+
The RPC interface is defined in [`capnp/stem.capnp`](capnp/stem.capnp):
173+
174+
| Type | Kind | Description |
175+
|------|------|-------------|
176+
| `Epoch` | struct | `seq`, `head`, `adoptedBlock` — identifies a finalized head |
177+
| `Status` | enum | `ok`, `unauthorized`, `internalError` |
178+
| `Signer` | interface | `sign(domain, nonce) → sig` — client-supplied signing capability |
179+
| `StatusPoller` | interface | `pollStatus() → status` — epoch-scoped health check |
180+
| `Session` | struct | `issuedEpoch`, `statusPoller` — returned by `graft` |
181+
| `Membrane` | interface | `graft(signer) → session` — the sole entry point |
182+
183+
## License
184+
185+
MIT

0 commit comments

Comments
 (0)