Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 5 additions & 0 deletions playground/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ POSTGRES_PASSWORD=123
TOML_TRACE_ERROR=1
CHAIN=1
ETHFLOW_CONTRACTS=0x04501b9b1d52e67f6862d157e00d13419d2d6e95

# Otterscan Sourcify source: "cloud" (default) or "local"
# - cloud: Shows publicly verified contracts from sourcify.dev
# - local: Shows contracts verified on the local Sourcify instance
SOURCIFY_MODE=cloud
2 changes: 1 addition & 1 deletion playground/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM debian:bookworm AS chef
WORKDIR /src/
RUN apt-get update && apt-get install -y curl git clang mold libssl-dev pkg-config git && apt-get clean
RUN apt-get update && apt-get install -y curl git clang mold libssl-dev pkg-config git make && apt-get clean
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="$PATH:/root/.cargo/bin"
RUN rustup component add clippy rustfmt
Expand Down
6 changes: 6 additions & 0 deletions playground/Dockerfile.otterscan
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM otterscan/otterscan:latest

COPY otterscan-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
31 changes: 31 additions & 0 deletions playground/Dockerfile.sourcify
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM node:22-bookworm-slim

# Install dependencies
RUN apt-get update && apt-get install -y git curl postgresql-client && \
curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/download/v2.21.0/dbmate-linux-amd64 && \
chmod +x /usr/local/bin/dbmate && \
apt-get clean && rm -rf /var/lib/apt/lists/*

# Clone Sourcify with submodules
RUN git clone --depth 1 --recurse-submodules https://github.com/ethereum/sourcify.git /sourcify

WORKDIR /sourcify

# Install dependencies and build
RUN npm install && npm run build:lerna

# Prepare migrations
WORKDIR /sourcify/services/database
RUN mkdir -p /migrations && \
cp -r database-specs/migrations/* /migrations/ 2>/dev/null || true && \
cp -r migrations/* /migrations/ 2>/dev/null || true

WORKDIR /sourcify/services/server

# Copy custom entrypoint
COPY sourcify-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 5555

ENTRYPOINT ["/entrypoint.sh"]
50 changes: 49 additions & 1 deletion playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ await window.ethereum.request({
| Postgres | postgres | 5432 | 5432 | N/A | Local/Fork |
| Adminer | adminer | 8082 | 8080 | N/A | Local/Fork |
| Grafana | grafana | 3000 | 3000 | N/A | Local/Fork |
| Otterscan | otterscan | 8003 | 80 | N/A | Local/Fork |
| Sourcify | sourcify | 5555 | 5555 | N/A | Local/Fork |
| Sourcify DB | sourcify-db | N/A | 5432 | N/A | Local/Fork |

**NOTE**: Currently only **FORK** mode is supported.

Expand All @@ -137,7 +140,8 @@ In this mode, the stack will spin up:
- Postgres (with migrations)
- Adminer
- RPC (forked from `reth` or `erigon` node)
- Otterscan (*not yet implemented*)
- Otterscan
- Sourcify (contract verification)
- Orderbook
- Autopilot
- Driver
Expand All @@ -150,3 +154,47 @@ In this mode, the stack will spin up:
**NOT YET IMPLEMENTED**

- As per fork, but with a local node (not forked from Erigon)

## Contract Verification with Sourcify

The playground includes a local [Sourcify](https://sourcify.dev/) instance for contract verification. Sourcify is a decentralized contract verification service that matches deployed bytecode with source code. Verified contracts display their source code in Otterscan.

**How it works:**

- **Cloud mode** (`SOURCIFY_MODE=cloud`): Otterscan fetches verified source code from the public Sourcify repository. This shows source code for well-known contracts (CoW Protocol, USDC, etc.) that have been publicly verified.
- **Local mode** (`SOURCIFY_MODE=local`): Otterscan fetches from your local Sourcify instance. Use this when testing contracts you deploy and verify locally.

### Sourcify Sources Configuration

Configure which Sourcify source Otterscan uses in your `.env` file:

```bash
# Use public Sourcify (default) - shows publicly verified contracts
SOURCIFY_MODE=cloud

# Use local Sourcify - shows contracts verified on your local instance
SOURCIFY_MODE=local
```

After changing this value, recreate the Otterscan container:

```bash
docker compose -f docker-compose.fork.yml up -d otterscan
```

or

```bash
docker compose -f docker-compose.non-interactive.yml up -d otterscan
```

> **Note**: A simple `docker compose restart` won't work because it doesn't re-read `.env` - you need to recreate the container.

### Verifying Contracts

You can verify contracts on the local Sourcify instance using:

1. **Sourcify API**: POST to `http://localhost:5555/verify` with your contract address, chain ID, and source files
2. **Foundry**: Use `forge verify-contract` with `--verifier sourcify --verifier-url http://localhost:5555`

After verification, view the contract source in Otterscan at `http://localhost:8003/address/<contract_address>`.
60 changes: 60 additions & 0 deletions playground/docker-compose.fork.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,65 @@ services:
ports:
- 8000:80

# Sourcify - Contract verification service
sourcify-db:
image: postgres:15-alpine
restart: always
environment:
- POSTGRES_USER=sourcify
- POSTGRES_PASSWORD=sourcify
- POSTGRES_DB=sourcify
volumes:
- sourcify-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sourcify"]
interval: 5s
timeout: 5s
retries: 5

sourcify:
build:
context: .
dockerfile: Dockerfile.sourcify
restart: always
depends_on:
sourcify-db:
condition: service_healthy
environment:
- SOURCIFY_POSTGRES_HOST=sourcify-db
- SOURCIFY_POSTGRES_PORT=5432
- SOURCIFY_POSTGRES_USER=sourcify
- SOURCIFY_POSTGRES_PASSWORD=sourcify
- SOURCIFY_POSTGRES_DB=sourcify
- NODE_ENV=development
volumes:
- ./sourcify-chains.json:/sourcify/services/server/dist/sourcify-chains.json
ports:
- 5555:5555
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5555/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

otterscan:
build:
context: .
dockerfile: Dockerfile.otterscan
restart: always
environment:
- ERIGON_URL=http://127.0.0.1:8545
- SOURCIFY_MODE=${SOURCIFY_MODE:-cloud}
- LOCAL_SOURCIFY_URL=http://sourcify:5555
ports:
- 8003:80
depends_on:
chain:
condition: service_healthy
sourcify:
condition: service_healthy

explorer:
build:
context: .
Expand Down Expand Up @@ -253,3 +312,4 @@ services:

volumes:
postgres:
sourcify-postgres:
62 changes: 61 additions & 1 deletion playground/docker-compose.non-interactive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,66 @@ services:
ports:
- 8000:80

# Sourcify - Contract verification service
sourcify-db:
image: postgres:15-alpine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using postgres-16, I wonder if we can use the same image here to avoid downloading two postgres images

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point for image optimization! I've updated both compose files to use postgres:16 for the sourcify-db service to match the main db service.
✅ Fixed

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sourcify DB is failing to get up, I'm guessing it's targeting the same port as the main DB.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't actually a port conflict - sourcify-db doesn't expose any external ports (only the main db exposes 5432:5432).

The error is a PostgreSQL version mismatch that happens when containers aren't rebuilt after pulling changes.

Solution - reset volumes and rebuild:
docker compose -f docker-compose.non-interactive.yml down --remove-orphans --volumes
docker compose -f docker-compose.non-interactive.yml up --build

I confirmed this fixes the issue locally - sourcify-db comes up healthy after the rebuild.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've ran these, they worked (nice find on the volumes, I cleaned my env but forgot them!)

The sourcify image is still not cooperating (bunch of panics like the one below):

sourcify-1       | goroutine 37 gp=0xc000458700 m=nil [chan receive]:
sourcify-1       | runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/proc.go:424 +0xce fp=0xc0001b1f18 sp=0xc0001b1ef8 pc=0x47096e
sourcify-1       | runtime.chanrecv(0xc0000da540, 0x0, 0x1)
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/chan.go:639 +0x41c fp=0xc0001b1f90 sp=0xc0001b1f18 pc=0x40b87c
sourcify-1       | runtime.chanrecv1(0x0?, 0x0?)
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/chan.go:489 +0x12 fp=0xc0001b1fb8 sp=0xc0001b1f90 pc=0x40b432
sourcify-1       | runtime.unique_runtime_registerUniqueMapCleanup.func1(...)
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/mgc.go:1732
sourcify-1       | runtime.unique_runtime_registerUniqueMapCleanup.gowrap1()
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/mgc.go:1735 +0x2f fp=0xc0001b1fe0 sp=0xc0001b1fb8 pc=0x41edcf
sourcify-1       | runtime.goexit({})
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/asm_amd64.s:1700 +0x1 fp=0xc0001b1fe8 sp=0xc0001b1fe0 pc=0x478b61
sourcify-1       | created by unique.runtime_registerUniqueMapCleanup in goroutine 1
sourcify-1       |      /home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/mgc.go:1730 +0x96

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My docker versions are the following:

- Docker: 27.2.1
- Docker Compose: v2.40.3-desktop.1

What versions are you using? Also have you tried running the playground in the main branch? meaning, without our changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmg-duarte were you able test this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay. I find it weird that you can run this as is, the playground driver.toml actually has an issue.

And I still can't run sourcify/otterscan on my Mac, I was able to run this on my Linux machine (x86) though which hints me that the go images are not being correctly built for ARM

Maybe docker is able to emulate or cross build correctly but not everyone can be expected to run docker (as you see, I dont, and I've met others running lima, etc)

Would be nice to have this working with alternative container runtimes too, to avoid "it works on my machine"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jmg-duarte, I've pushed a fix that should help with the ARM architecture issues.

What changed:

The Sourcify Dockerfile was hardcoding the dbmate-linux-amd64 binary, which doesn't work natively on ARM machines. I've updated it to:

  • Auto-detect the host architecture
  • Map to the correct dbmate binary (supports amd64, arm64, i386, armhf)
  • Fail fast with a clear error if the architecture is unsupported
  • Verify the binary works during build

Please try building again with a clean slate:
docker compose -f docker-compose.non-interactive.yml down --remove-orphans --volumes
docker compose -f docker-compose.non-interactive.yml up --build

A couple of questions:

  1. Were you able to run the playground on main branch (without this PR's changes)? That would help us isolate whether this is a pre-existing issue or something introduced in this PR.
  2. Did you try running Otterscan with SOURCIFY_MODE=cloud (pointing to the cloud Sourcify instead of local)? If so, did that work?
  3. You mentioned there's an issue with driver.toml - could you elaborate on what's broken there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update, it works now (on Podman)! The issue was clearly something to do with emulation and possibly cross building.

The driver issue is due to the driver.toml missing the tx-gas-limit field at the top of the file, like the example.toml — this is not related to the PR, I just found it curious

Details

2026-01-19T11:32:33.117Z ERROR observe::tracing: thread 'main' panicked at /src/crates/driver/src/infra/config/file/load.rs:36:13:
failed to parse TOML config at "/driver.toml": Error {
    inner: Error {
        inner: TomlError {
            message: "missing field `tx-gas-limit`",
            raw: Some(
                "[[solver]]\nname = \"baseline\" # Arbitrary name given to this solver, must be unique\nendpoint = \"http://baseline\"\nabsolute-slippage = \"40000000000000000\" # Denominated in wei, optional\nrelative-slippage = \"0.1\" # Percentage in the [0, 1] range\naccount = \"0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6\" # Known test private key\n\n[submission]\ngas-price-cap = \"1000000000000\"\n\n[[submission.mempool]]\nmempool = \"public\"\n\n[liquidity]\nbase-tokens = [\n    \"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\", # WETH\n    \"0x6B175474E89094C44Da98b954EedeAC495271d0F\", # DAI\n    \"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\", # USDC\n    \"0xdAC17F958D2ee523a2206206994597C13D831ec7\", # USDT\n    \"0xc00e94Cb662C3520282E6f5717214004A7f26888\", # COMP\n    \"0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2\", # MKR\n    \"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599\", # WBTC\n    \"0x6810e776880C02933D47DB1b9fc05908e5386b96\", # GNO\n]\n\n[[liquidity.uniswap-v2]] # Uniswap V2 configuration\npreset = \"uniswap-v2\"\n",
            ),
            keys: [],
            span: Some(
                0..0,
            ),
        },
    },
}

restart: always
environment:
- POSTGRES_USER=sourcify
- POSTGRES_PASSWORD=sourcify
- POSTGRES_DB=sourcify
volumes:
- sourcify-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sourcify"]
interval: 5s
timeout: 5s
retries: 5

sourcify:
build:
context: .
dockerfile: Dockerfile.sourcify
restart: always
depends_on:
sourcify-db:
condition: service_healthy
environment:
- SOURCIFY_POSTGRES_HOST=sourcify-db
- SOURCIFY_POSTGRES_PORT=5432
- SOURCIFY_POSTGRES_USER=sourcify
- SOURCIFY_POSTGRES_PASSWORD=sourcify
- SOURCIFY_POSTGRES_DB=sourcify
- NODE_ENV=development
volumes:
- ./sourcify-chains.json:/sourcify/services/server/dist/sourcify-chains.json
ports:
- 5555:5555
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5555/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s

# Otterscan - Local blockchain block explorer for Anvil
otterscan:
build:
context: .
dockerfile: Dockerfile.otterscan
restart: always
environment:
- ERIGON_URL=http://127.0.0.1:8545
- SOURCIFY_MODE=${SOURCIFY_MODE:-cloud}
- LOCAL_SOURCIFY_URL=http://sourcify:5555
ports:
- 8003:80
depends_on:
chain:
condition: service_healthy
sourcify:
condition: service_healthy

explorer:
build:
context: .
Expand Down Expand Up @@ -231,7 +291,6 @@ services:
volumes:
- ./grafana-prometheus.yml:/etc/grafana/provisioning/datasources/prometheus.yml


prometheus:
image: prom/prometheus:latest
container_name: prometheus
Expand All @@ -243,3 +302,4 @@ services:

volumes:
postgres:
sourcify-postgres:
51 changes: 51 additions & 0 deletions playground/otterscan-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/sh
# Otterscan entrypoint that configures Sourcify based on SOURCIFY_MODE

CONFIG_FILE="/usr/share/nginx/html/config.json"
ERIGON_URL="${ERIGON_URL:-http://127.0.0.1:8545}"
LOCAL_SOURCIFY_URL="${LOCAL_SOURCIFY_URL:-http://localhost:5555}"

echo "=== Otterscan Entrypoint ==="
echo "SOURCIFY_MODE: ${SOURCIFY_MODE}"

case "${SOURCIFY_MODE:-cloud}" in
local)
echo "Using LOCAL Sourcify as primary source"
cat > "$CONFIG_FILE" << 'EOF'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to remove the ' to enable variable expansion. With 'EOF' this command literally wrote ${ERIGON_URL} into the file instead of http://127.0.0.1:8545.

Suggested change
cat > "$CONFIG_FILE" << 'EOF'
cat > "$CONFIG_FILE" << EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed

{
"erigonURL": "${ERIGON_URL}",
"sourcify": {
"sources": {
"Local Sourcify": {
"url": "${LOCAL_SOURCIFY_URL}/repository",
"backendFormat": "RepositoryV1"
}
}
}
}
EOF
;;
cloud|*)
echo "Using CLOUD Sourcify as primary source"
cat > "$CONFIG_FILE" << 'EOF'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Suggested change
cat > "$CONFIG_FILE" << 'EOF'
cat > "$CONFIG_FILE" << EOF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed

{
"erigonURL": "${ERIGON_URL}",
"sourcify": {
"sources": {
"Sourcify": {
"url": "https://repo.sourcify.dev",
"backendFormat": "RepositoryV1"
}
}
}
}
EOF
;;
esac

echo "Config written to $CONFIG_FILE:"
cat "$CONFIG_FILE"
echo ""
echo "=== Starting nginx ==="

exec nginx -g "daemon off;"
12 changes: 12 additions & 0 deletions playground/sourcify-chains.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"1": {
"sourcifyName": "Mainnet (Forked)",
"supported": true,
"rpc": ["http://chain:8545"]
},
"31337": {
"sourcifyName": "Anvil Local",
"supported": true,
"rpc": ["http://chain:8545"]
}
}
22 changes: 22 additions & 0 deletions playground/sourcify-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
set -e

echo "=== Sourcify Server Starting ==="

# Wait for database to be ready
echo "Waiting for database..."
until pg_isready -h "$SOURCIFY_POSTGRES_HOST" -p "$SOURCIFY_POSTGRES_PORT" -U "$SOURCIFY_POSTGRES_USER" -d "$SOURCIFY_POSTGRES_DB" > /dev/null 2>&1; do
echo "Database not ready, waiting..."
sleep 2
done
echo "Database is ready!"

# Run migrations
echo "Running database migrations..."
DATABASE_URL="postgres://${SOURCIFY_POSTGRES_USER}:${SOURCIFY_POSTGRES_PASSWORD}@${SOURCIFY_POSTGRES_HOST}:${SOURCIFY_POSTGRES_PORT}/${SOURCIFY_POSTGRES_DB}?sslmode=disable"
dbmate --url "$DATABASE_URL" --migrations-dir /migrations --no-dump-schema up
echo "Migrations complete!"

# Start the server
echo "Starting Sourcify server..."
exec node dist/server/cli.js
Loading