Skip to content

Instantly share code, notes, and snippets.

@denniswon
Created March 5, 2026 03:15
Show Gist options
  • Select an option

  • Save denniswon/e716fd0542c617caeff89bfcb17c291d to your computer and use it in GitHub Desktop.

Select an option

Save denniswon/e716fd0542c617caeff89bfcb17c291d to your computer and use it in GitHub Desktop.
Design: Destination Chain Challenge Relay to L1 for Slashing
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.
@denniswon
Copy link
Author

β˜… Insight ─────────────────────────────────────
Cross-Chain Challenge Relay Architecture

The implementation follows a trust-minimized pattern that avoids bridge dependencies:

  1. Destination chain (Step 1 β€” already existed): Challenger detects a mismatch via evaluate_and_prove(), submits
    raiseAndResolveChallenge() on the dest chain to invalidate the attestation locally.
  2. Source chain relay (Step 2 β€” new): After dest-chain success, the L1ChallengeRelayer calls slashForCrossChainChallenge() on
    the source chain's TaskManager. The source chain independently re-verifies the ZK proof via RegoVerifier.verifyRegoProof() β€” it
    never trusts the dest-chain result.
  3. Non-signer extraction: The dest chain uses BN254Certificate (ELIP-008 format) instead of NonSignerStakesAndSignature. The
    relayer decodes nonSignerWitnesses[].operatorInfo.pubkey to get the BLS G1Points that ChallengeLib.processNonSigners() needs
    for slashing on L1.
  4. Double-slash prevention: crossChainChallenged[keccak256(destChainId, taskId)] prevents replaying the same challenge.
    ─────────────────────────────────────────────────

Summary of Changes

Solidity (contracts)

  • ChallengeVerifier.sol: Added slashForCrossChainChallenge(), setRegisteredDestinationChain(), storage mappings
    (crossChainChallenged, registeredDestinationChains), errors, and events
  • NewtonProverTaskManagerShared.sol: Added public entry point delegating to ChallengeVerifier (line 171-187)
  • INewtonProverTaskManager.sol: Added slashForCrossChainChallenge to interface

Rust (crates/challenger)

  • dest/relayer.rs (new): L1ChallengeRelayer β€” extracts non-signer pubkeys from BN254Certificate and calls
    slashForCrossChainChallenge() on source chain
  • dest/runner.rs: Wired relayer into event loop β€” relay fires after successful dest-chain challenge
  • dest/mod.rs: Added pub mod relayer
  • dest/evaluator.rs: Added Clone derive to ChallengeProof
  • dest/submitter.rs: Removed TODO comments (relay is now implemented)
  • config.rs: Added SourceChainConfig with task_manager field, optional source_chain on ChallengerConfig

Documentation

  • CLAUDE.md + .claude/CLAUDE.md: make contracts β†’ make generate-bindings (7 occurrences)
  • .claude/rules/solidity.md: Fixed stale make contracts/contracts-test/contracts-fmt β†’ make generate-bindings/forge test/forge
    fmt
  • .claude/rules/agent-guide.md: make contracts β†’ make generate-bindings
  • .claude/rules/lessons.md: make contracts β†’ make generate-bindings
  • .claude/rules/ci-cd.md: Replaced 3 stale rows with single make generate-bindings row

Verification

  • Solidity: forge build βœ“, forge test βœ“ (137/137 pass)
  • Rust: cargo check βœ“, cargo clippy -D warnings βœ“ (zero warnings)
  • Bindings: make generate-bindings βœ“ (new function in all 3 binding files)

@denniswon
Copy link
Author

denniswon commented Mar 5, 2026

β˜… 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

@denniswon
Copy link
Author

β˜… Insight ─────────────────────────────────────
Challenger E2E test design choices:

  1. 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.
  2. 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.
  3. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment