Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement challenger #355

Merged
merged 3 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions book/fault_proofs/challenger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Fault Proof Challenger

The fault proof challenger is a component responsible for monitoring and challenging invalid OP-Succinct fault dispute games on the L1 chain. It continuously scans for invalid games and challenges them to maintain L2 state validity.

## Prerequisites

Before running the challenger, ensure you have:

1. Rust toolchain installed (latest stable version)
2. Access to L1 and L2 network nodes
3. The DisputeGameFactory contract deployed (See [Deploy](./deploy.md))
4. Sufficient ETH balance for:
- Transaction fees
- Challenge bonds (proof rewards)
5. Required environment variables properly configured (See [Configuration](#configuration))

## Overview

The challenger performs several key functions:

1. **Game Monitoring**: Continuously scans for invalid games that need to be challenged
2. **Game Challenging**: Challenges invalid games by providing counter-proofs
3. **Game Resolution**: Optionally resolves challenged games after their deadline passes
4. **Bond Management**: Handles proof rewards and challenge bonds

## Configuration

The challenger is configured through environment variables. Create a `.env.challenger` file in the project root directory:

### Required Environment Variables

| Variable | Description |
|----------|-------------|
| `L1_RPC` | L1 RPC endpoint URL |
| `L2_RPC` | L2 RPC endpoint URL |
| `FACTORY_ADDRESS` | Address of the DisputeGameFactory contract |
| `GAME_TYPE` | Type identifier for the dispute game |
| `PRIVATE_KEY` | Private key for transaction signing |

### Optional Environment Variables

| Variable | Description | Default Value |
|----------|-------------|---------------|
| `FETCH_INTERVAL` | Polling interval in seconds | `30` |
| `ENABLE_GAME_RESOLUTION` | Whether to enable automatic game resolution | `true` |
| `MAX_GAMES_TO_CHECK_FOR_CHALLENGE` | Maximum number of games to scan for challenges | `100` |
| `MAX_GAMES_TO_CHECK_FOR_RESOLUTION` | Maximum number of games to check for resolution | `100` |

```env
# Required Configuration
L1_RPC= # L1 RPC endpoint URL
L2_RPC= # L2 RPC endpoint URL
FACTORY_ADDRESS= # Address of the DisputeGameFactory contract
GAME_TYPE= # Type identifier for the dispute game
PRIVATE_KEY= # Private key for transaction signing

# Optional Configuration
FETCH_INTERVAL=30 # Polling interval in seconds
ENABLE_GAME_RESOLUTION=true # Whether to enable automatic game resolution
MAX_GAMES_TO_CHECK_FOR_CHALLENGE=100 # Maximum number of games to scan for challenges
MAX_GAMES_TO_CHECK_FOR_RESOLUTION=100 # Maximum number of games to check for resolution
```

## Running

To run the challenger:
```bash
cargo run --bin challenger
```

The challenger will run indefinitely, monitoring for invalid games and challenging them as needed.

## Features

### Game Monitoring
- Continuously scans for invalid games
- Checks game validity against L2 state
- Prioritizes oldest challengeable games
- Maintains efficient scanning through configurable limits

### Game Challenging
- Challenges invalid games with counter-proofs
- Handles proof reward bonds
- Ensures proper transaction confirmation
- Provides detailed logging of challenge actions

### Game Resolution
When enabled (`ENABLE_GAME_RESOLUTION=true`), the challenger:
- Monitors challenged games
- Resolves games after their resolution period expires
- Handles resolution of multiple games efficiently
- Respects game resolution requirements

## Architecture

The challenger is built around the `OPSuccinctChallenger` struct which manages:
- Configuration state
- Wallet management for transactions
- Game challenging and resolution logic
- Chain monitoring and interval management

Key components:
- `ChallengerConfig`: Handles environment-based configuration
- `handle_game_challenging`: Main function for challenging invalid games that:
- Scans for challengeable games
- Determines game validity
- Executes challenge transactions
- `handle_game_resolution`: Main function for resolving games that:
- Checks if resolution is enabled
- Manages resolution of challenged games
- Handles resolution confirmations
- `run`: Main loop that:
- Runs at configurable intervals
- Handles both challenging and resolution
- Provides error isolation between tasks

## Error Handling

The challenger includes robust error handling for:
- RPC connection issues
- Transaction failures
- Contract interaction errors
- Invalid configurations

Errors are logged with appropriate context to aid in debugging.

## Development

When developing or modifying the challenger:
1. Ensure all environment variables are properly set
2. Test with a local L1/L2 setup first
3. Monitor logs for proper operation
4. Test challenging and resolution separately
5. Verify proper handling of edge cases
6. Test with various game states and conditions
1 change: 1 addition & 0 deletions book/fault_proofs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ USE_SP1_MOCK_VERIFIER=true
```

For production, set all of these:

| Variable | Description | Example |
|----------|-------------|---------|
| `VERIFIER_ADDRESS` | Address of the SP1 verifier ([see contract addresses](https://docs.succinct.xyz/docs/sp1/verification/onchain/contract-addresses)) | `0x...` |
Expand Down
23 changes: 20 additions & 3 deletions book/fault_proofs/quick_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Save the output addresses, particularly the `FACTORY_ADDRESS` output as "Factory

## Step 2: Run the Proposer

1. Create a `.env.proposer` file in the fault_proof directory:
1. Create a `.env.proposer` file in the project root directory:
```env
# Required Configuration
L1_RPC=<YOUR_L1_RPC_URL>
Expand All @@ -57,11 +57,28 @@ PRIVATE_KEY=<YOUR_PRIVATE_KEY>
cargo run --bin proposer
```

## Step 3: Monitor Games
## Step 3: Run the Challenger

1. Create a `.env.challenger` file in the project root directory:
```env
# Required Configuration
L1_RPC=<YOUR_L1_RPC_URL>
L2_RPC=<YOUR_L2_RPC_URL>
FACTORY_ADDRESS=<FACTORY_ADDRESS_FROM_DEPLOYMENT>
GAME_TYPE=42
PRIVATE_KEY=<YOUR_PRIVATE_KEY>
```

2. Run the challenger:
```bash
cargo run --bin challenger
```

## Step 4: Monitor Games

1. The proposer will automatically create new games at regular intervals (every 1800 blocks with the default config)
2. You can view created games on a block explorer using the factory address and the game address in the proposer logs
3. The proposer will also attempt to resolve unchallenged games after the challenge period expires
3. Both the proposer and challenger will attempt to resolve unchallenged games after the challenge period expires

## Troubleshooting

Expand Down
5 changes: 5 additions & 0 deletions contracts/src/fp/OPSuccinctFaultDisputeGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -634,4 +634,9 @@ contract OPSuccinctFaultDisputeGame is Clone, ISemver, IDisputeGame {
function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_) {
registry_ = ANCHOR_STATE_REGISTRY;
}

/// @notice Returns the proof reward.
function proofReward() external view returns (uint256 proofReward_) {
proofReward_ = PROOF_REWARD;
}
}
4 changes: 4 additions & 0 deletions fault_proof/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ path = "src/lib.rs"
name = "proposer"
path = "bin/proposer.rs"

[[bin]]
name = "challenger"
path = "bin/challenger.rs"

[dependencies]
# alloy
alloy-contract.workspace = true
Expand Down
183 changes: 183 additions & 0 deletions fault_proof/bin/challenger.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use std::{env, time::Duration};

use alloy_network::Ethereum;
use alloy_primitives::{Address, U256};
use alloy_provider::{fillers::TxFiller, Provider, ProviderBuilder};
use alloy_signer_local::PrivateKeySigner;
use alloy_transport_http::reqwest::Url;
use anyhow::{Context, Result};
use clap::Parser;
use op_alloy_network::EthereumWallet;
use tokio::time;

use fault_proof::{
config::ChallengerConfig,
contract::{
DisputeGameFactory::{self, DisputeGameFactoryInstance},
OPSuccinctFaultDisputeGame,
},
utils::setup_logging,
FactoryTrait, L1Provider, L1ProviderWithWallet, L2Provider, Mode, NUM_CONFIRMATIONS,
TIMEOUT_SECONDS,
};

#[derive(Parser)]
struct Args {
#[clap(long, default_value = ".env.challenger")]
env_file: String,
}

struct OPSuccinctChallenger<F, P>
where
F: TxFiller<Ethereum>,
P: Provider<Ethereum> + Clone,
{
config: ChallengerConfig,
l1_provider: L1Provider,
l2_provider: L2Provider,
l1_provider_with_wallet: L1ProviderWithWallet<F, P>,
factory: DisputeGameFactoryInstance<(), L1ProviderWithWallet<F, P>>,
proof_reward: U256,
}

impl<F, P> OPSuccinctChallenger<F, P>
where
F: TxFiller<Ethereum>,
P: Provider<Ethereum> + Clone,
{
/// Creates a new challenger instance with the provided L1 provider with wallet and factory contract instance.
pub async fn new(
l1_provider_with_wallet: L1ProviderWithWallet<F, P>,
factory: DisputeGameFactoryInstance<(), L1ProviderWithWallet<F, P>>,
) -> Result<Self> {
let config = ChallengerConfig::from_env()?;
let l1_provider = ProviderBuilder::default().on_http(config.l1_rpc.clone());

Ok(Self {
config: config.clone(),
l1_provider: l1_provider.clone(),
l2_provider: ProviderBuilder::default().on_http(config.l2_rpc.clone()),
l1_provider_with_wallet: l1_provider_with_wallet.clone(),
factory: factory.clone(),
proof_reward: factory.fetch_proof_reward(config.game_type).await?,
})
}

/// Challenges a specific game at the given address.
async fn challenge_game(&self, game_address: Address) -> Result<()> {
let game =
OPSuccinctFaultDisputeGame::new(game_address, self.l1_provider_with_wallet.clone());

// TODO(fakedev9999): Potentially need to add a gas provider.
let receipt = game
.challenge()
.value(self.proof_reward)
.send()
.await
.context("Failed to send challenge transaction")?
.with_required_confirmations(NUM_CONFIRMATIONS)
.with_timeout(Some(Duration::from_secs(TIMEOUT_SECONDS)))
.get_receipt()
.await
.context("Failed to get transaction receipt for challenge")?;

tracing::info!(
"Successfully challenged game {:?} with tx {:?}",
game_address,
receipt.transaction_hash
);

Ok(())
}

/// Handles challenging of invalid games by scanning recent games for potential challenges.
async fn handle_game_challenging(&self) -> Result<()> {
let _span = tracing::info_span!("[[Challenging]]").entered();

if let Some(game_address) = self
.factory
.get_oldest_challengable_game_address(
self.config.max_games_to_check_for_challenge,
self.l1_provider.clone(),
self.l2_provider.clone(),
)
.await?
{
tracing::info!("Attempting to challenge game {:?}", game_address);
self.challenge_game(game_address).await?;
}

Ok(())
}

/// Handles resolution of challenged games that are ready to be resolved.
async fn handle_game_resolution(&self) -> Result<()> {
// Only resolve games if the config is enabled
if !self.config.enable_game_resolution {
return Ok(());
}

let _span = tracing::info_span!("[[Resolving]]").entered();

self.factory
.resolve_games(
Mode::Challenger,
self.config.max_games_to_check_for_resolution,
self.l1_provider_with_wallet.clone(),
self.l2_provider.clone(),
)
.await
}

/// Runs the challenger in an infinite loop, periodically checking for games to challenge and resolve.
async fn run(&mut self) -> Result<()> {
tracing::info!("OP Succinct Challenger running...");
let mut interval = time::interval(Duration::from_secs(self.config.fetch_interval));

// Each loop, check the oldest challengeable game and challenge it if it exists.
// Eventually, all games will be challenged (as long as the rate at which games are being created is slower than the fetch interval).
loop {
interval.tick().await;

if let Err(e) = self.handle_game_challenging().await {
tracing::warn!("Failed to handle game challenging: {:?}", e);
}

if let Err(e) = self.handle_game_resolution().await {
tracing::warn!("Failed to handle game resolution: {:?}", e);
}
}
}
}

#[tokio::main]
async fn main() {
setup_logging();

let args = Args::parse();
dotenv::from_filename(args.env_file).ok();

let wallet = EthereumWallet::from(
env::var("PRIVATE_KEY")
.expect("PRIVATE_KEY must be set")
.parse::<PrivateKeySigner>()
.unwrap(),
);

let l1_provider_with_wallet = ProviderBuilder::new()
.wallet(wallet.clone())
.on_http(env::var("L1_RPC").unwrap().parse::<Url>().unwrap());

let factory = DisputeGameFactory::new(
env::var("FACTORY_ADDRESS")
.expect("FACTORY_ADDRESS must be set")
.parse::<Address>()
.unwrap(),
l1_provider_with_wallet.clone(),
);

let mut challenger = OPSuccinctChallenger::new(l1_provider_with_wallet, factory)
.await
.unwrap();
challenger.run().await.expect("Runs in an infinite loop");
}
Loading
Loading