Created
February 27, 2026 15:39
-
-
Save denniswon/b5a49011c8633ee61a2a4d65f2cd51a8 to your computer and use it in GitHub Desktop.
transporter analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ★ Insight ───────────────────────────────────── | |
| They don't need to share a key. My analysis conflated "per-chain signer" with "one signer per chain for all operations." In | |
| reality, the dest chain contracts don't enforce that table updates and challenges come from the same address. These are | |
| independent on-chain roles. | |
| ───────────────────────────────────────────────── | |
| Let me verify what the contracts actually enforce: | |
| ⏺ Searched for 4 patterns, read 1 file (ctrl+o to expand) | |
| ⎿ Loaded contracts/CLAUDE.md | |
| ⎿ Loaded contracts/src/CLAUDE.md | |
| ⎿ Loaded .claude/rules/solidity.md | |
| ⏺ The contracts confirm it — they don't need to share a key at all: | |
| ┌──────────────────────────┬────────────────────────────────────────┬─────────────────────────────────┐ | |
| │ Operation │ Access Control │ Who Can Call │ | |
| ├──────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ | |
| │ confirmGlobalTableRoot │ onlyOwner on ECDSAOperatorTableUpdater │ Only the updater contract owner │ | |
| ├──────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ | |
| │ updateOperatorTable │ (same contract, same onlyOwner) │ Only the updater contract owner │ | |
| ├──────────────────────────┼────────────────────────────────────────┼─────────────────────────────────┤ | |
| │ raiseAndResolveChallenge │ whenNotPaused — no caller restriction │ Anyone │ | |
| └──────────────────────────┴────────────────────────────────────────┴─────────────────────────────────┘ | |
| ★ Insight ───────────────────────────────────── | |
| Challenge submission is permissionless. raiseAndResolveChallenge on NewtonProverTaskManagerShared has no msg.sender check — | |
| anyone can submit a valid challenge with a correct ZK proof. The ChallengeVerifier only checks that msg.sender == taskManager, | |
| meaning the call must go through the TaskManager, but the TaskManager itself doesn't restrict who initiates it. This means the | |
| challenger doesn't need the transport signer key at all — it can use any funded address. | |
| ───────────────────────────────────────────────── | |
| This significantly weakens the nonce-contention argument I made earlier for combining. Let me revise: | |
| Revised Coupling Analysis | |
| ┌───────────────────────┬──────────────────────┬────────────────────────────────┬──────────────────────────────────────────┐ | |
| │ Resource │ Transport │ Challenge │ Still Coupled? │ | |
| ├───────────────────────┼──────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤ | |
| │ Dest chain RPC │ Yes │ Yes │ Shared infra, but separate connections │ | |
| │ provider │ │ │ are fine │ | |
| ├───────────────────────┼──────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤ | |
| │ Source chain RPC │ Yes (read operator │ Yes (re-evaluate policy, L1 │ Shared infra │ | |
| │ provider │ set) │ relay) │ │ | |
| ├───────────────────────┼──────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤ | |
| │ ECDSA signer │ Owner key │ Any funded key │ No coupling │ | |
| │ │ (privileged) │ (permissionless) │ │ | |
| ├───────────────────────┼──────────────────────┼────────────────────────────────┼──────────────────────────────────────────┤ | |
| │ Nonce sequence │ Owner key nonces │ Separate key nonces │ No coupling │ | |
| └───────────────────────┴──────────────────────┴────────────────────────────────┴──────────────────────────────────────────┘ | |
| Since the challenger can use a completely separate (even a throwaway funded) key, the nonce contention problem disappears. The | |
| two strongest arguments for combining were: | |
| 1. ~~Shared nonce sequence~~ — Not a real constraint | |
| 2. Shared RPC connections — Marginal cost saving, not architectural | |
| What This Means for the Architecture Decision | |
| The case for separating is now stronger than I originally presented. The remaining arguments for combining are purely operational | |
| convenience (fewer containers, single config), not fundamental architectural coupling. | |
| That said, the decision still depends on how you weigh: | |
| ┌─────────────────────┬───────────────────────────────────────────────────────────┬─────────────────────────────────────────┐ | |
| │ Factor │ Combined │ Separated │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ Containers per │ 1 │ 2 │ | |
| │ chain │ │ │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ Key management │ 1 key per chain (transport = owner key, challenge = same │ 2 keys per chain (owner key + │ | |
| │ │ or different) │ challenger key) │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ ZK prover blast │ Must isolate internally (catch_unwind) │ Process-level isolation (free) │ | |
| │ radius │ │ │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ Operational │ Better │ Worse │ | |
| │ simplicity │ │ │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ Independent scaling │ Requires internal tuning │ Natural (give challenger more memory) │ | |
| ├─────────────────────┼───────────────────────────────────────────────────────────┼─────────────────────────────────────────┤ | |
| │ Code clarity │ Mixed concerns in one binary │ Clean single-responsibility │ | |
| └─────────────────────┴───────────────────────────────────────────────────────────┴─────────────────────────────────────────┘ | |
| The per-chain signer requirement you chose doesn't force combination. It's a genuine trade-off now, not a forced hand. Would you | |
| like me to revise the recommendation with this corrected understanding, or does this change your thinking on the combined vs. | |
| separated question? | |
| ✻ Worked for 51s | |
| ❯ let's actually just separate the two into separate | |
| ⏺ Good call. With permissionless challenge submission, there's no architectural reason to keep them coupled. Let me lay out the | |
| clean separation design. | |
| ★ Insight ───────────────────────────────────── | |
| Separation is natural here because the two services have different operational profiles: the transporter is a low-frequency, | |
| privileged writer (owner key, daily syncs), while the challenger is a high-frequency, permissionless monitor (any key, every task | |
| response). Different keys, different timing, different failure modes — classic case for separate processes. | |
| ───────────────────────────────────────────────── | |
| Separated Architecture | |
| flowchart TB | |
| subgraph source["SOURCE CHAIN (Ethereum/Sepolia)"] | |
| OPS["Operator Set State<br/>(BN254TableCalculator,<br/>NewtonCrossChainRegistry)"] | |
| L1_TM["NewtonProverTaskManager<br/>(slashing via raiseAndResolveChallenge)"] | |
| end | |
| subgraph per_chain["PER DESTINATION CHAIN"] | |
| subgraph transporter["Transporter Container"] | |
| CALC["WeightCalculator"] | |
| UPD["TableUpdater"] | |
| SRC_L["SourceChainListener<br/>(operator set events)"] | |
| TX_T["Tx Submitter<br/>(owner key)"] | |
| end | |
| subgraph challenger["Challenger Container"] | |
| DST_L["DestChainListener<br/>(NewTask + TaskResponded)"] | |
| EVAL["ChallengeEvaluator<br/>(re-evaluate policy)"] | |
| ZK["ZK Prover<br/>(SP1, spawn_blocking)"] | |
| SUB_D["Dest ChallengeSubmitter<br/>(any funded key)"] | |
| SUB_L1["L1 ChallengeRelayer<br/>(any funded key on source)"] | |
| end | |
| end | |
| subgraph dest["DESTINATION CHAIN (L2)"] | |
| OTU["ECDSAOperatorTableUpdater<br/>(onlyOwner)"] | |
| DEST_TM["NewtonProverDestTaskManager<br/>(permissionless challenge)"] | |
| end | |
| OPS --> CALC | |
| SRC_L --> CALC | |
| CALC --> UPD | |
| UPD --> TX_T | |
| TX_T --> OTU | |
| DEST_TM -->|"NewTask / TaskResponded events"| DST_L | |
| DST_L --> EVAL | |
| EVAL --> ZK | |
| ZK --> SUB_D | |
| SUB_D --> DEST_TM | |
| SUB_D -->|"on success"| SUB_L1 | |
| SUB_L1 --> L1_TM | |
| What Changes in the Codebase | |
| ┌─────────────────────────────────────┬──────────────────────────────────────────────────────┬───────────────────────────────┐ | |
| │ Component │ Current State │ Target State │ | |
| ├─────────────────────────────────────┼──────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ │ Contains transport + embedded challenger │ Transport only — remove │ | |
| │ crates/transporter/ │ (challenge_evaluator.rs, challenge_submitter.rs, │ challenge modules │ | |
| │ │ dest_listener.rs) │ │ | |
| ├─────────────────────────────────────┼──────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ │ Source-chain challenger only (Challenger struct) + │ Shared evaluation logic stays │ | |
| │ crates/challenger/ │ shared evaluate_and_prove() │ here; add dest-chain │ | |
| │ │ │ challenger mode │ | |
| ├─────────────────────────────────────┼──────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ bin/newton-prover-transporter/ │ Runs transporter::run() which does both │ Transport only │ | |
| ├─────────────────────────────────────┼──────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ New: │ Doesn't exist │ New binary — dest-chain │ | |
| │ bin/newton-prover-dest-challenger/ │ │ challenger with L1 relay │ | |
| └─────────────────────────────────────┴──────────────────────────────────────────────────────┴───────────────────────────────┘ | |
| Key Design Decisions | |
| 1. Where does evaluate_and_prove() live? | |
| It stays in crates/challenger/ as a shared free function. Both the source-chain Challenger struct and the new dest-chain | |
| challenger import it. No crate dependency changes needed for the dest challenger binary — it already depends on | |
| newton-prover-challenger. | |
| The transporter crate drops its dependency on newton-prover-challenger entirely. | |
| 2. Three separate signer keys per chain | |
| ┌─────────────────────┬─────────────────┬─────────────────────────────────────┬──────────────┐ | |
| │ Key │ Used By │ Access Level │ Chain │ | |
| ├─────────────────────┼─────────────────┼─────────────────────────────────────┼──────────────┤ | |
| │ Transport owner key │ Transporter │ Privileged (onlyOwner) │ Dest chain │ | |
| ├─────────────────────┼─────────────────┼─────────────────────────────────────┼──────────────┤ | |
| │ Dest challenger key │ Dest challenger │ Permissionless (any funded address) │ Dest chain │ | |
| ├─────────────────────┼─────────────────┼─────────────────────────────────────┼──────────────┤ | |
| │ L1 relay key │ Dest challenger │ Permissionless (any funded address) │ Source chain │ | |
| └─────────────────────┴─────────────────┴─────────────────────────────────────┴──────────────┘ | |
| The transport owner key is the most sensitive (can push arbitrary operator state). The challenger keys just need gas — they can | |
| be hot wallets. | |
| 3. Transporter config simplifies | |
| # transporter-arbitrum.toml (transport only, no challenger flags) | |
| update_frequency = 86400 | |
| operator_set_id = 0 | |
| [dest_chain] | |
| chain_id = 421614 | |
| rpc_url = "wss://arb-sepolia.g.alchemy.com/v2/..." | |
| [source_chain] | |
| chain_id = 11155111 | |
| rpc_url = "wss://eth-sepolia.g.alchemy.com/v2/..." | |
| [signer] | |
| private_key_env = "TRANSPORTER_ARB_OWNER_KEY" | |
| 4. New dest-challenger config | |
| # dest-challenger-arbitrum.toml | |
| [dest_chain] | |
| chain_id = 421614 | |
| rpc_url = "wss://arb-sepolia.g.alchemy.com/v2/..." | |
| [source_chain] | |
| chain_id = 11155111 | |
| rpc_url = "wss://eth-sepolia.g.alchemy.com/v2/..." | |
| [signer] | |
| # Permissionless — any funded key | |
| private_key_env = "CHALLENGER_ARB_KEY" | |
| [l1_relay_signer] | |
| # For relaying challenges to source chain | |
| private_key_env = "L1_RELAY_KEY" | |
| [ipfs] | |
| gateway_url = "https://gateway.pinata.cloud" | |
| Files to Create / Modify / Remove | |
| ┌──────────┬────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────┐ | |
| │ Action │ File │ Notes │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Remove │ crates/transporter/src/challenge_evaluator.rs │ Moves to dest challenger │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Remove │ crates/transporter/src/challenge_submitter.rs │ Moves to dest challenger │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Remove │ crates/transporter/src/dest_listener.rs │ Moves to dest challenger │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Simplify │ crates/transporter/src/lib.rs │ Remove enable_challenger flag, challenge event loop arms, │ | |
| │ │ │ ChallengeEvaluator │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Simplify │ crates/transporter/src/config.rs │ Remove enable_challenger field │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Simplify │ crates/transporter/Cargo.toml │ Drop newton-prover-challenger dependency │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Create │ bin/newton-prover-dest-challenger/ │ New binary entry point │ | |
| ├──────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────┤ | |
| │ Create │ crates/dest-challenger/ (or expand │ Dest-chain challenge logic + L1 relay │ | |
| │ │ crates/challenger/) │ │ | |
| └──────────┴────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────┘ | |
| Open Question: New Crate or Expand Existing? | |
| Two ways to structure the dest-chain challenger: | |
| ┌───────────────────────────────────┬─────────────────────────────────────────┬─────────────────────────────────────────────┐ | |
| │ Approach │ Pros │ Cons │ | |
| ├───────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────────────────┤ | |
| │ Expand crates/challenger/ with a │ Shared evaluate_and_prove() stays │ Mixes source-chain and dest-chain concerns │ | |
| │ dest module │ in-crate, less duplication │ in one crate │ | |
| ├───────────────────────────────────┼─────────────────────────────────────────┼─────────────────────────────────────────────┤ | |
| │ New crates/dest-challenger/ crate │ Clean separation, independent │ evaluate_and_prove() either moves to core │ | |
| │ │ versioning │ or is imported cross-crate │ | |
| └───────────────────────────────────┴─────────────────────────────────────────┴─────────────────────────────────────────────┘ | |
| ⏺ User answered Claude's questions: | |
| ⎿ · How should we structure the dest-chain challenger code? → Expand crates/challenger/ | |
| ⏺ Good choice — keeps evaluate_and_prove() in-crate with zero dependency changes for the shared logic. | |
| Final Architecture Summary | |
| flowchart TB | |
| subgraph crates["Crate Structure"] | |
| direction TB | |
| subgraph challenger_crate["crates/challenger/"] | |
| shared["evaluate_and_prove()<br/>(shared free function)"] | |
| src_mod["src/source.rs<br/>Source-chain Challenger struct<br/>(existing logic, renamed)"] | |
| dest_mod["src/dest.rs (new)<br/>DestChainChallenger struct<br/>• DestChainListener<br/>• ChallengeEvaluator<br/>• | |
| ChallengeSubmitter<br/>• L1ChallengeRelayer"] | |
| end | |
| subgraph transporter_crate["crates/transporter/"] | |
| transport["Transport only<br/>• WeightCalculator<br/>• TableUpdater<br/>• SourceChainListener"] | |
| end | |
| end | |
| subgraph bins["Binaries"] | |
| bin_src["bin/newton-prover-challenger/<br/>(source chain, existing)"] | |
| bin_dest["bin/newton-prover-dest-challenger/<br/>(dest chain, new)"] | |
| bin_trans["bin/newton-prover-transporter/<br/>(transport only, simplified)"] | |
| end | |
| src_mod --> bin_src | |
| dest_mod --> bin_dest | |
| transport --> bin_trans | |
| shared --> src_mod | |
| shared --> dest_mod | |
| Per Destination Chain: Two Containers | |
| flowchart LR | |
| subgraph arb["Arbitrum Sepolia"] | |
| T_ARB["Transporter<br/>Owner key<br/>Daily sync + event-driven"] | |
| C_ARB["Dest Challenger<br/>Hot wallet key<br/>Continuous monitoring"] | |
| end | |
| subgraph op["OP Sepolia"] | |
| T_OP["Transporter<br/>Owner key<br/>Daily sync + event-driven"] | |
| C_OP["Dest Challenger<br/>Hot wallet key<br/>Continuous monitoring"] | |
| end | |
| SRC["Source Chain<br/>(Sepolia)"] --> T_ARB | |
| SRC --> T_OP | |
| SRC --> SRC_C["Source Challenger<br/>(single instance)"] | |
| C_ARB -.->|"L1 relay"| SRC | |
| C_OP -.->|"L1 relay"| SRC | |
| Deployment Summary for 8 Chains | |
| ┌──────────────────┬──────────────────────┬─────────────────────────────────────────────────────┬────────────────────────────┐ | |
| │ Service │ Instances │ Key Type │ Key Count │ | |
| ├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────┼────────────────────────────┤ | |
| │ Source │ 1 │ Any funded key (source chain) │ 1 │ | |
| │ Challenger │ │ │ │ | |
| ├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────┼────────────────────────────┤ | |
| │ Transporter │ 8 (one per dest │ Owner key (privileged, per dest chain) │ 8 │ | |
| │ │ chain) │ │ │ | |
| ├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────┼────────────────────────────┤ | |
| │ Dest Challenger │ 8 (one per dest │ Hot wallet (permissionless, per dest chain) + L1 │ 8 dest + 1 shared L1 relay │ | |
| │ │ chain) │ relay key │ = 9 │ | |
| ├──────────────────┼──────────────────────┼─────────────────────────────────────────────────────┼────────────────────────────┤ | |
| │ Total │ 17 containers │ │ 18 keys │ | |
| └──────────────────┴──────────────────────┴─────────────────────────────────────────────────────┴────────────────────────────┘ | |
| The L1 relay key can be shared across all dest challengers since raiseAndResolveChallenge is permissionless on the source chain | |
| too — nonce contention is the only concern, but challenge relays are rare events so contention is negligible in practice. | |
| ★ Insight ───────────────────────────────────── | |
| The separation creates a clean security boundary: transport owner keys are high-value targets (can push malicious operator | |
| state), while challenger keys are low-value (can only submit valid ZK-proven challenges). This lets you put transport keys in | |
| HSMs/multi-sig while keeping challenger keys as simple hot wallets, matching the risk profile of each operation. | |
| ───────────────────────────────────────────────── | |
| Crate File Layout After Refactor | |
| crates/challenger/ | |
| ├── src/ | |
| │ ├── lib.rs # evaluate_and_prove() + module exports | |
| │ ├── config.rs # ChallengerConfig (source) + DestChallengerConfig (new) | |
| │ ├── error.rs # ChallengerError (shared) | |
| │ ├── source.rs # Existing Challenger struct (renamed from current lib.rs logic) | |
| │ └── dest/ | |
| │ ├── mod.rs # DestChainChallenger struct + run loop | |
| │ ├── listener.rs # DestChainListener (moved from transporter/dest_listener.rs) | |
| │ ├── evaluator.rs # ChallengeEvaluator (moved from transporter/challenge_evaluator.rs) | |
| │ ├── submitter.rs # ChallengeSubmitter (moved from transporter/challenge_submitter.rs) | |
| │ └── l1_relay.rs # L1ChallengeRelayer (new — completes the TODO) | |
| ├── challenger.toml | |
| └── Cargo.toml | |
| crates/transporter/ | |
| ├── src/ | |
| │ ├── lib.rs # run() — transport_update loop only (simplified) | |
| │ ├── config.rs # TransporterConfig — no enable_challenger field | |
| │ ├── calculator.rs # WeightCalculator (unchanged) | |
| │ ├── updater.rs # TableUpdater (unchanged) | |
| │ └── listener.rs # SourceChainListener (unchanged) | |
| ├── transporter.toml | |
| └── Cargo.toml | |
| bin/newton-prover-dest-challenger/ | |
| ├── src/ | |
| │ └── main.rs # Instantiates DestChainChallenger, runs event loop | |
| └── Cargo.toml |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment