-
-
Save denniswon/e716fd0542c617caeff89bfcb17c291d to your computer and use it in GitHub Desktop.
| Now I have the complete picture. Let me deliver the design analysis. | |
| β Insight βββββββββββββββββββββββββββββββββββββ | |
| ELIP-008 does NOT specify cross-chain slashing. It only covers one direction: L1βL2 operator table transport and L2 certificate | |
| verification. The reverse direction (L2 challenge β L1 slashing) is entirely AVS-specific. This means Newton has full design | |
| freedom here β there's no EigenLayer standard to conform to for the relay mechanism. | |
| Critical finding from the contract analysis: The source chain's ChallengeVerifier.raiseAndResolveChallenge() (line 109) | |
| requires allTaskHashes[taskResponse.taskId] to be non-zero β meaning the task must exist on the source chain. Destination-chain | |
| tasks are never registered on the source chain's TaskManager. This means you cannot simply call the source chain's existing | |
| raiseAndResolveChallenge() with a dest-chain task. A new contract function or a new approach is required. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Design: Destination Chain Challenge Relay to L1 for Slashing | |
| The Core Problem | |
| ELIP-008 only defines L1βL2 transport (operator tables down to destination chains). It does not define L2βL1 challenge relay. | |
| This is intentional β slashing enforcement is AVS-specific, not part of the EigenLayer multichain standard. | |
| Newton's current contracts have a blocking constraint: the source chain's ChallengeVerifier.raiseAndResolveChallenge() at | |
| ChallengeVerifier.sol:109 checks: | |
| require( | |
| TaskLib.taskHash(task) == allTaskHashes[taskResponse.taskId], | |
| TaskLib.TaskMismatch(...) | |
| ); | |
| Destination-chain tasks never exist in the source chain's allTaskHashes mapping. This means reusing the existing function is | |
| impossible. | |
| Two Design Options | |
| Option A: New Contract Function β slashForCrossChainChallenge() | |
| Add a new function to ChallengeVerifier that accepts a proof of successful dest-chain challenge: | |
| function slashForCrossChainChallenge( | |
| uint256 destChainId, | |
| INewtonProverTaskManager.Task calldata task, | |
| INewtonProverTaskManager.TaskResponse calldata taskResponse, | |
| INewtonProverTaskManager.ResponseCertificate calldata responseCertificate, | |
| INewtonProverTaskManager.ChallengeData calldata challenge, | |
| BN254.G1Point[] calldata pubkeysOfNonSigningOperators, | |
| bytes calldata destChainChallengeProof // proof that dest chain accepted the challenge | |
| ) external nonReentrant returns (bool) | |
| The destChainChallengeProof would prove that taskSuccesfullyChallenged[taskId] == true on the destination chain. This could be: | |
| - A storage proof (Merkle proof of the dest chain's state root containing the challenged mapping) | |
| - A cross-chain message via a bridge (Optimism/Arbitrum native messaging, LayerZero, etc.) | |
| - Or simply a re-execution of the ZK proof on the source chain (since the source chain has the same RegoVerifier) | |
| Re-executing the ZK proof is the simplest and most trust-minimized approach β the source chain independently verifies the | |
| challenge is valid without trusting any bridge or relay. The only difference from the existing raiseAndResolveChallenge is | |
| skipping the allTaskHashes check (since the task doesn't exist on source) and replacing it with a different validation: | |
| // Instead of checking allTaskHashes (task doesn't exist on source chain), | |
| // verify the task was legitimately created on a registered destination chain | |
| require(registeredDestinationChains[destChainId], "unregistered dest chain"); | |
| // Re-verify the ZK proof on source chain (same RegoVerifier, same logic) | |
| IRegoVerifier.RegoContext memory context = | |
| RegoVerifier(regoVerifier).verifyRegoProof(challenge.data, challenge.proof); | |
| // Verify proof output mismatches the task response | |
| require( | |
| keccak256(abi.encode(context.evaluation)) != keccak256(abi.encode(taskResponse.evaluationResult)), | |
| ChallengeFailed() | |
| ); | |
| // Then slash (same ChallengeLib flow) | |
| ChallengeLib.slashSigningOperators(ctx, task.quorumNumbers, ...); | |
| Option B: Register Dest Tasks on Source Chain | |
| Before challenging on source, the relay would first call createNewTask() on the source chain's TaskManager to register the | |
| task, then call the existing raiseAndResolveChallenge(). This is problematic because: | |
| - createNewTask is onlyTaskGenerator β the challenger isn't the task generator | |
| - It pollutes the source chain's task registry with foreign tasks | |
| - It creates ordering dependencies | |
| Recommendation: Option A (new contract function with ZK proof re-execution). | |
| Detailed Design | |
| 1. Contract Changes (Solidity) | |
| ChallengeVerifier.sol β Add new function: | |
| /// @notice Slash operators for a challenge that was proven on a destination chain | |
| /// @dev Re-executes ZK proof on source chain; does not require task to exist in allTaskHashes | |
| /// @param destChainId The destination chain where the task was created | |
| /// @param task The original task (from dest chain) | |
| /// @param taskResponse The task response being challenged | |
| /// @param challenge ZK proof data proving the response was incorrect | |
| /// @param pubkeysOfNonSigningOperators BLS pubkeys of non-signing operators for slashing | |
| function slashForCrossChainChallenge( | |
| uint256 destChainId, | |
| INewtonProverTaskManager.Task calldata task, | |
| INewtonProverTaskManager.TaskResponse calldata taskResponse, | |
| INewtonProverTaskManager.ChallengeData calldata challenge, | |
| BN254.G1Point[] calldata pubkeysOfNonSigningOperators | |
| ) external nonReentrant returns (bool) | |
| Key differences from raiseAndResolveChallenge: | |
| - No allTaskHashes check (task doesn't exist on source chain) | |
| - No responseCertificate needed for challenge window check (the dest chain already validated timing) | |
| - Adds destChainId param to verify against registered destination chains | |
| - Only callable when serviceManager != address(0) (source chains only) | |
| - Uses a separate crossChainChallenged[destChainId][taskId] mapping to prevent double-slashing | |
| - Still re-verifies the ZK proof independently (trust-minimized) | |
| NewtonCrossChainRegistry.sol or a new mapping β needs registeredDestinationChains[chainId] to validate the destChainId | |
| parameter. | |
| 2. Non-Signer Pub Keys (The Hard Part) | |
| On the destination chain, DestChallengeSubmitter passes pub_keys_of_non_signing_operators = vec![] because slashing is skipped. | |
| For the L1 relay, the source chain's ChallengeLib.slashSigningOperators() needs the non-signer list to identify who signed (by | |
| exclusion from the full operator set). | |
| The non-signer data is available in the dest chain's ResponseCertificate.signatureData, which contains the BN254 certificate. | |
| The certificate includes non-signer witness data (per ELIP-008's BN254 certificate format). The relayer needs to: | |
| 1. Read the ResponseCertificate from the dest chain's TaskResponded event | |
| 2. Decode the BN254 certificate from responseCertificate.signatureData | |
| 3. Extract non-signer pubkeys from the certificate's witness data | |
| 4. Pass these to the source chain's slashForCrossChainChallenge() | |
| The source chain's BLSApkRegistry maps pubkey hashes β operator addresses, so the pubkeys from the cert can be resolved to | |
| addresses for slashing. | |
| 3. Rust Implementation | |
| βββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| β Component β File β Purpose β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β L1ChallengeRelayer β crates/challenger/src/dest/relayer.rs β Holds source chain signer, submits β | |
| β β (new) β slashForCrossChainChallenge β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β Config extension β crates/challenger/src/config.rs β Add source_chain section (rpc_url, task_manager, β | |
| β β β optional separate signer) β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β Non-signer β crates/challenger/src/dest/relayer.rs β Decode BN254 cert from ResponseCertificate, extract β | |
| β extraction β β non-signer G1Points β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β Runner integration β crates/challenger/src/dest/runner.rs β After submitter.submit_challenge() succeeds β β | |
| β β β relayer.relay_to_source() β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β Module registration β crates/challenger/src/dest/mod.rs β Add pub mod relayer; β | |
| βββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ | |
| β Contract bindings β Auto-generated after Solidity changes β make contracts to regenerate β | |
| βββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| L1ChallengeRelayer sketch: | |
| pub struct L1ChallengeRelayer { | |
| signer: SdkSigner, // source chain signer | |
| source_task_manager: Address, // source chain TaskManager | |
| dest_chain_id: u64, // which dest chain this challenger monitors | |
| } | |
| impl L1ChallengeRelayer { | |
| pub async fn relay_to_source( | |
| &self, | |
| proof: ChallengeProof, // same proof used for dest chain | |
| non_signer_pubkeys: Vec<G1Point>, // extracted from ResponseCertificate | |
| ) -> eyre::Result<TransactionReceipt> { | |
| // Call slashForCrossChainChallenge on source chain TaskManager | |
| } | |
| } | |
| Runner integration β fire-and-forget after dest chain success: | |
| // In runner.rs, after successful dest chain challenge: | |
| ChallengeTrigger::TaskResponded { .. } => { | |
| match evaluator.evaluate_response(task_response, response_certificate).await { | |
| Ok(Some(proof)) => { | |
| // Step 1: Invalidate on dest chain | |
| if let Ok(receipt) = submitter.submit_challenge(proof.clone()).await { | |
| // Step 2: Relay to source for slashing (best-effort) | |
| let non_signers = extract_non_signers(&response_certificate); | |
| if let Err(e) = relayer.relay_to_source(proof, non_signers).await { | |
| error!("L1 relay failed (attestation still invalidated on dest): {}", e); | |
| } | |
| } | |
| } | |
| // ... | |
| } | |
| } | |
| 4. Config Changes | |
| # challenger-dest.toml (destination chain challenger) | |
| [signer] | |
| private_key_env = "CHALLENGER_KEY" # needs gas on BOTH chains | |
| [source_chain] | |
| chain_id = 11155111 | |
| rpc_url = "https://eth-sepolia.g.alchemy.com/v2/..." | |
| task_manager = "0x..." # source chain TaskManager address | |
| # Optional: separate signer for L1 relay (defaults to main signer) | |
| # relay_signer_private_key_env = "CHALLENGER_L1_RELAY_KEY" | |
| Docker compose addition for challenger-dest: | |
| environment: | |
| - RPC_HTTP_URL_31337=http://anvil:8545 # source chain RPC | |
| - SOURCE_TASK_MANAGER=0x... # from deployment JSON | |
| Implementation Order | |
| 1. Solidity first: Add slashForCrossChainChallenge() to ChallengeVerifier.sol and NewtonProverTaskManagerShared.sol, add | |
| crossChainChallenged mapping, add dest chain registry check. Write forge tests. | |
| 2. make contracts to regenerate Rust bindings. | |
| 3. Rust relayer.rs: Implement L1ChallengeRelayer using the new contract binding. | |
| 4. Config: Extend ChallengerConfig with optional source_chain section. | |
| 5. Runner integration: Wire relayer into run_dest_challenger(). | |
| 6. E2E test: Dual-anvil test that creates a bad response on dest chain, challenges it, verifies slashing on source chain. | |
| Open Questions for Your Decision | |
| 1. Double-slash prevention: Should the source chain contract track crossChainChallenged[destChainId][taskId] to prevent the | |
| same challenge being relayed multiple times? (Recommended: yes) | |
| 2. Challenge window on source: The dest chain has its own challenge window. Should the source chain also enforce a time limit | |
| for the relay, or accept any valid ZK proof at any time? (Recommendation: no time limit on source β if the ZK proof is valid, | |
| the slash should happen regardless of when it's relayed) | |
| 3. Signer sharing: Use the same ECDSA key for both dest and source chain submissions (simpler, needs gas on both chains) or | |
| separate keys? (Recommendation: same key, per MULTICHAIN.md line 1299) | |
| 4. Non-signer extraction format: The dest chain's ResponseCertificate.signatureData encodes a BN254 certificate (not | |
| NonSignerStakesAndSignature). Need to confirm the exact struct layout in DestinationTaskResponseHandler to decode correctly. |
β
Insight βββββββββββββββββββββββββββββββββββββ
Here's a summary of everything completed in this session for the cross-chain challenge relay feature:
Solidity Tests (new file):
- contracts/test/CrossChainChallenge.t.sol β 12 new tests covering:
- setRegisteredDestinationChain: owner registration/unregistration, non-owner revert, multi-chain support
- slashForCrossChainChallenge guards: challenge disabled, unregistered chain, re-unregistered chain, onlyTaskManager
enforcement - crossChainChallenged key isolation: defaults to false, different chains don't collide
- Error/event selector stability: verified keccak256 signatures for off-chain ABI matching
Documentation Updates (5 files):
- docs/TRANSPORTER.md β Removed all TODO references for L1 relay; updated sequence diagram to show slashForCrossChainChallenge;
added relayer.rs to crate structure; updated ChallengerConfig to show source_chain field; added setRegisteredDestinationChain
to destination chain setup checklist; updated key management table - .claude/rules/architecture.md β Updated challenger section: destination mode description, added relay responsibility, added
relayer.rs and updated config.rs description in key files
βββββββββββββββββββββββββββββββββββββββββββββββββ
The cross-chain challenge relay implementation is now fully complete:
- Solidity: ChallengeVerifier.slashForCrossChainChallenge(), interface, TaskManager entry point
- Rust: L1ChallengeRelayer, SourceChainConfig, wired into run_dest_challenger() event loop
- Tests: 12 Forge tests for access control and state management
- Docs: TRANSPORTER.md and architecture.md updated, all TODO references removed
- Bindings: Regenerated with make generate-bindings
β
Insight βββββββββββββββββββββββββββββββββββββ
Challenger E2E test design choices:
- Config resolution tests (test_source_challenger_config_loads, test_dest_challenger_config_loads) verify the chain
classification plumbing without any network calls. Chain 31337 is source, 31338 is destination with source_chain_id=31337 β
this is the foundation the relay relies on. - Component construction tests (test_dest_challenger_components_build) verify that every dest-mode component can be
instantiated β including L1ChallengeRelayer which resolves the source TaskManager from deployment JSON. This is the key
assertion of this PR: no manual address config needed. - Lifecycle tests (test_source_challenger_lifecycle, test_dest_challenger_lifecycle) start the full challenger daemon, verify
it connects to WebSocket, then cancel and assert clean shutdown. These catch startup regressions (bad config, missing
contracts, WS connection failures) without requiring a full gateway+operator stack.
The tests intentionally do not exercise ZK proof generation (SP1) β that's expensive and the Forge tests already cover the
contract-level challenge flow. The E2E tests focus on the Rust infrastructure: config loading, deployment JSON resolution,
component wiring, and daemon lifecycle.
βββββββββββββββββββββββββββββββββββββββββββββββββ
Files changed:
- integration-tests/tests/challenger.rs β 6 E2E tests for source and destination challenger modes
- Makefile β challenger-e2e target (dual-anvil, follows transporter-e2e pattern)
- CLAUDE.md, .claude/CLAUDE.md, .claude/rules/testing.md β documented make challenger-e2e
Run with:
make challenger-e2e # uses cached deployment
make challenger-e2e force=true # fresh deployment
β Insight βββββββββββββββββββββββββββββββββββββ
Cross-Chain Challenge Relay Architecture
The implementation follows a trust-minimized pattern that avoids bridge dependencies:
raiseAndResolveChallenge() on the dest chain to invalidate the attestation locally.
the source chain's TaskManager. The source chain independently re-verifies the ZK proof via RegoVerifier.verifyRegoProof() β it
never trusts the dest-chain result.
relayer decodes nonSignerWitnesses[].operatorInfo.pubkey to get the BLS G1Points that ChallengeLib.processNonSigners() needs
for slashing on L1.
βββββββββββββββββββββββββββββββββββββββββββββββββ
Summary of Changes
Solidity (contracts)
(crossChainChallenged, registeredDestinationChains), errors, and events
Rust (crates/challenger)
slashForCrossChainChallenge() on source chain
Documentation
fmt
Verification