Spin up proposer

After you have spun up your sequencer and batcher, you need to attach a proposer to post your L2 state roots data back onto L1 so we can prove withdrawal validity. The proposer is a critical component that enables trustless L2-to-L1 messaging and creates the authoritative view of L2 state from L1's perspective.

Step 4 of 5: This tutorial is designed to be followed step-by-step. Each step builds on the previous one.

This guide assumes you already have a functioning sequencer, batcher, and the necessary L1 contracts deployed using op-deployer. If you haven't set up your sequencer and batcher yet, please refer to the sequencer guide and batcher guide first.

To see configuration info for the proposer, check out the configuration page.

Understanding the proposer's role

The proposer (op-proposer) serves as a crucial bridge between your L2 chain and L1. Its primary responsibilities include:

  • State commitment: Proposing L2 state roots to L1 at regular intervals
  • Withdrawal enablement: Providing the necessary commitments for users to prove and finalize withdrawals

The proposer creates dispute games via the DisputeGameFactory contract.

Prerequisites

Before setting up your proposer, ensure you have:

Running infrastructure:

  • An operational sequencer node
  • Access to a L1 RPC endpoint

Network information:

  • Your L2 chain ID and network configuration
  • L1 network details (chain ID, RPC endpoints)

For setting up the proposer, we recommend using Docker as it provides a consistent and isolated environment. Building from source is also available as an option.

If you prefer containerized deployment, you can use the official Docker images and do the following:

Set up directory structure and copy configuration files

# Create a proposer directory inside rollup
cd ../    # Go back to rollup directory if you're in batcher
mkdir proposer
cd proposer
# inside the proposer directory, copy the state.json file from the op-deployer setup
# Copy configuration files from deployer
cp ../deployer/.deployer/state.json .
 
# Extract the DisputeGameFactory address
GAME_FACTORY_ADDRESS=$(cat state.json | jq -r '.opChainDeployments[0].disputeGameFactoryProxyAddress')
echo "DisputeGameFactory Address: $GAME_FACTORY_ADDRESS"

Create environment variables file

# Create .env file with your actual values
cat > .env << 'EOF'
# L1 Configuration - Replace with your actual RPC URLs
L1_RPC_URL=https://sepolia.infura.io/v3/YOUR_ACTUAL_INFURA_KEY
 
# L2 Configuration - Should match your sequencer setup
  L2_RPC_URL=http://op-geth:8545
  ROLLUP_RPC_URL=http://op-node:8547
 
# Contract addresses - Extract from your op-deployer output
GAME_FACTORY_ADDRESS=YOUR_ACTUAL_GAME_FACTORY_ADDRESS
 
# Private key - Replace with your actual private key
PRIVATE_KEY=YOUR_ACTUAL_PRIVATE_KEY
 
# Proposer configuration
PROPOSAL_INTERVAL=3600s
GAME_TYPE=0
POLL_INTERVAL=20s
 
# RPC configuration
PROPOSER_RPC_PORT=8560
EOF

Important: Replace ALL placeholder values (YOUR_ACTUAL_*) with your real configuration values.

Create docker-compose.yml

If you get "failed to dial address" errors, ensure your proposer is in the same Docker network as your sequencer.

Common fixes:

  • Add networks: - sequencer-node_default to your proposer's docker-compose.yml
  • Use service names like op-geth:8545 and op-node:8547 in your .env file
  • Verify your sequencer network name with docker network ls
 
services:
  op-proposer:
    image: us-docker.pkg.dev/oplabs-tools-artifacts/images/op-proposer:v1.10.0
    volumes:
      - .:/workspace
    working_dir: /workspace
    ports:
      - "8560:8560"
    env_file:
      - .env
    command:
      - "op-proposer"
      - "--poll-interval=${POLL_INTERVAL}"
      - "--rpc.port=${PROPOSER_RPC_PORT}"
      - "--rpc.enable-admin"
      - "--rollup-rpc=${ROLLUP_RPC_URL}"
      - "--l1-eth-rpc=${L1_RPC_URL}"
      - "--private-key=${PRIVATE_KEY}"
      - "--game-factory-address=${GAME_FACTORY_ADDRESS}"
      - "--game-type=${GAME_TYPE}"
      - "--proposal-interval=${PROPOSAL_INTERVAL}"
      - "--num-confirmations=1"
      - "--resubmission-timeout=30s"
      - "--wait-node-sync=true"
      - "--log.level=info"
    restart: unless-stopped
    networks:
      - sequencer-node_default
 
networks:
  sequencer-node_default:
    external: false
 

Start the proposer service

# Make sure your sequencer network exists
docker network create op-stack 2>/dev/null || true
 
# Start the proposer
docker-compose up -d
 
# View logs
docker-compose logs -f op-proposer

Verify proposer is running

# Check container status
docker-compose ps

Final directory structure

rollup/
├── deployer/            # From previous step
   └── .deployer/      # Contains state.json
├── sequencer/          # From previous step
├── batcher/           # From previous step
└── proposer/         # You are here
    ├── state.json    # Copied from deployer
    ├── .env          # Environment variables
    └── docker-compose.yml # Docker configuration
Understanding proposer startup logs

When you first start your proposer, you'll see several types of log messages:

  1. Initialization messages (normal):

    lvl=info msg="Initializing L2Output Submitter"
    lvl=info msg="Connected to DisputeGameFactory"
    lvl=info msg="Starting JSON-RPC server"
  2. Sync status messages (expected during startup):

    msg="rollup current L1 block still behind target, retrying"
    current_l1=...:9094035 target_l1=9132815

    This is normal! It means:

    • Your rollup is still syncing with L1 (e.g., Sepolia)
    • The proposer is waiting until sync is closer to L1 tip
    • You'll see the current_l1 number increasing as it catches up
    • Once caught up, the proposer will start submitting proposals

Don't worry about the "retrying" messages - they show healthy progress as your rollup catches up to the latest L1 blocks.

Common log patterns:

  • Startup: You'll see initialization messages as services start
  • Sync: "block still behind target" messages while catching up
  • Normal operation: Regular proposal submissions once synced
  • Network: Connection messages to L1/L2 endpoints

If you see errors about "failed to dial" or connection issues:

  • For source build: Verify your localhost ports and services

Your proposer is now operational and will continuously submit state roots to L1!

What's Next?

Perfect! Your proposer is submitting state roots to L1. The final step is to set up the challenger to monitor and respond to disputes.

Spin up challenger →

Need Help?