Repository: base/node
Branch: main
Commit: f14551c15cb8
Files: 28
Total size: 73.0 KB
Directory structure:
gitextract_5b8jeo_f/
├── .dockerignore
├── .github/
│ └── workflows/
│ ├── docker.yml
│ ├── pr.yml
│ ├── stale.yml
│ └── update-dependencies.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── dependency_updater/
│ ├── dependency_updater.go
│ ├── go.mod
│ ├── go.sum
│ ├── version.go
│ └── version_test.go
├── docker-compose.yml
├── geth/
│ ├── Dockerfile
│ └── geth-entrypoint
├── go.mod
├── nethermind/
│ ├── Dockerfile
│ └── nethermind-entrypoint
├── op-node-entrypoint
├── reth/
│ ├── Dockerfile
│ ├── README.md
│ └── reth-entrypoint
├── supervisord.conf
├── versions.env
└── versions.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
geth-data/
reth-data/
nethermind-data/
================================================
FILE: .github/workflows/docker.yml
================================================
name: Tag Docker image
on:
push:
branches:
- "main"
tags:
- "v*"
workflow_dispatch: {}
env:
REGISTRY: ghcr.io
NAMESPACE: ghcr.io/base
GETH_DEPRECATED_IMAGE_NAME: node
GETH_IMAGE_NAME: node-geth
RETH_IMAGE_NAME: node-reth
NETHERMIND_IMAGE_NAME: node-nethermind
permissions:
contents: read
packages: write
jobs:
geth:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.settings.runs-on }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }}
${{ env.NAMESPACE }}/${{ env.GETH_IMAGE_NAME }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push the Docker image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: geth/Dockerfile
tags: ${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }},${{ env.NAMESPACE }}/${{ env.GETH_IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.settings.arch }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Prepare
run: |
platform=${{ matrix.settings.arch }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digests-geth-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
reth:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
features: jemalloc,asm-keccak,optimism
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
features: jemalloc,optimism
runs-on: ${{ matrix.settings.runs-on }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.RETH_IMAGE_NAME }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push the Docker image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: reth/Dockerfile
tags: ${{ env.NAMESPACE }}/${{ env.RETH_IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
FEATURES=${{ matrix.settings.features }}
platforms: ${{ matrix.settings.arch }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Prepare
run: |
platform=${{ matrix.settings.arch }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digests-reth-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
nethermind:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.settings.runs-on }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.NETHERMIND_IMAGE_NAME }}
- name: Build and push the Docker image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: nethermind/Dockerfile
tags: ${{ env.NAMESPACE }}/${{ env.NETHERMIND_IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.settings.arch }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Prepare
run: |
platform=${{ matrix.settings.arch }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digests-nethermind-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge-geth:
runs-on: ubuntu-latest
needs:
- geth
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: ${{ runner.temp }}/digests
pattern: digests-geth-*
merge-multiple: true
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }}
${{ env.NAMESPACE }}/${{ env.GETH_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=long
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.NAMESPACE }}/${{ env.GETH_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.NAMESPACE }}/${{ env.GETH_DEPRECATED_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${{ env.NAMESPACE }}/${{ env.GETH_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-reth:
runs-on: ubuntu-latest
needs:
- reth
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: ${{ runner.temp }}/digests
pattern: digests-reth-*
merge-multiple: true
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.RETH_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=long
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.NAMESPACE }}/${{ env.RETH_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.NAMESPACE }}/${{ env.RETH_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-nethermind:
runs-on: ubuntu-latest
needs:
- nethermind
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: ${{ runner.temp }}/digests
pattern: digests-nethermind-*
merge-multiple: true
- name: Log into the Container registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Extract metadata for the Docker image
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: |
${{ env.NAMESPACE }}/${{ env.NETHERMIND_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,format=long
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.NAMESPACE }}/${{ env.NETHERMIND_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.NAMESPACE }}/${{ env.NETHERMIND_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
================================================
FILE: .github/workflows/pr.yml
================================================
name: Pull Request
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
geth:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.settings.runs-on }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build the Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: geth/Dockerfile
push: false
platforms: ${{ matrix.settings.arch }}
reth:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
features: jemalloc,asm-keccak,optimism
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
features: jemalloc,optimism
runs-on: ${{ matrix.settings.runs-on}}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build the Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: reth/Dockerfile
push: false
build-args: |
FEATURES=${{ matrix.settings.features }}
platforms: ${{ matrix.settings.arch }}
nethermind:
strategy:
matrix:
settings:
- arch: linux/amd64
runs-on: ubuntu-24.04
- arch: linux/arm64
runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.settings.runs-on}}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build the Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: nethermind/Dockerfile
push: false
platforms: ${{ matrix.settings.arch }}
================================================
FILE: .github/workflows/stale.yml
================================================
name: Mark stale issues and PRs
on:
schedule:
- cron: '30 0 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
stale:
runs-on: ubuntu-latest
permissions:
actions: write
issues: write
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
days-before-stale: 14
days-before-close: 5
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
close-issue-message: 'This issue was closed because it has been inactive for 5 days since being marked as stale.'
close-pr-message: 'This pull request was closed because it has been inactive for 5 days since being marked as stale.'
================================================
FILE: .github/workflows/update-dependencies.yml
================================================
name: Update Dockerfile Dependencies
on:
schedule:
- cron: '0 13 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
update:
name: update
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: build dependency updater
run: cd dependency_updater && go build
- name: run dependency updater
id: run_dependency_updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: cd dependency_updater && ./dependency_updater --repo ../ --github-action true
- name: create pull request
if: ${{ steps.run_dependency_updater.outputs.TITLE != '' }}
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
title: ${{ steps.run_dependency_updater.outputs.TITLE }}
commit-message: ${{ steps.run_dependency_updater.outputs.TITLE }}
body: "${{ steps.run_dependency_updater.outputs.DESC }}"
branch: run-dependency-updater
delete-branch: true
================================================
FILE: .gitignore
================================================
/.idea/
/geth-data/
/reth-data/
/nethermind-data/
/dependency_updater/dependency_updater
.DS_Store
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Base Node
## Code of Conduct
All interactions with this project follow our [Code of Conduct][code-of-conduct].
By participating, you are expected to honor this code. Violators can be banned
from further participation in this project, or potentially all Base and/or
Coinbase
projects.
[code-of-conduct]: https://github.com/coinbase/code-of-conduct
## Bug Reports
* Ensure your issue [has not already been reported][1]. It may already be fixed!
* Include the steps you carried out to produce the problem.
* Include the behavior you observed along with the behavior you expected, and
why you expected it.
* Include any relevant stack traces or debugging output.
## Feature Requests
We welcome feedback with or without pull requests. If you have an idea for how
to improve the project, great! All we ask is that you take the time to write a
clear and concise explanation of what need you are trying to solve. If you have
thoughts on _how_ it can be solved, include those too!
The best way to see a feature added, however, is to submit a pull request.
## Pull Requests
* Before creating your pull request, it's usually worth asking if the code
you're planning on writing will actually be considered for merging. You can
do this by [opening an issue][1] and asking. It may also help give the
maintainers context for when the time comes to review your code.
* Ensure your [commit messages are well-written][2]. This can double as your
pull request message, so it pays to take the time to write a clear message.
* Add tests for your feature. You should be able to look at other tests for
examples. If you're unsure, don't hesitate to [open an issue][1] and ask!
* Submit your pull request!
## Support Requests
For security reasons, any communication referencing support tickets for Coinbase
products will be ignored. The request will have its content redacted and will
be locked to prevent further discussion.
All support requests must be made via [our support team][3].
[1]: https://github.com/base/node/issues
[2]: https://medium.com/brigade-engineering/the-secrets-to-great-commit-messages-106fc0a92a25
[3]: https://support.coinbase.com/customer/en/portal/articles/2288496-how-can-i-contact-coinbase-support-
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023-2025 base.org contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

# Base Node
Base is a secure, low-cost, developer-friendly Ethereum L2 built on Optimism's [OP Stack](https://docs.optimism.io/). This repository contains Docker builds to run your own node on the Base network.
[](https://base.org)
[](https://docs.base.org/)
[](https://base.org/discord)
[](https://x.com/Base)
[](https://farcaster.xyz/base)
## Quick Start
1. Ensure you have an Ethereum L1 full node RPC available
2. Choose your network:
- For mainnet: Use `.env.mainnet`
- For testnet: Use `.env.sepolia`
3. Configure your L1 endpoints in the appropriate `.env` file:
```bash
OP_NODE_L1_ETH_RPC=<your-preferred-l1-rpc>
OP_NODE_L1_BEACON=<your-preferred-l1-beacon>
OP_NODE_L1_BEACON_ARCHIVER=<your-preferred-l1-beacon-archiver>
```
4. Start the node:
```bash
# For mainnet (default):
docker compose up --build
# For testnet:
NETWORK_ENV=.env.sepolia docker compose up --build
# To use a specific client (optional):
CLIENT=reth docker compose up --build
# For testnet with a specific client:
NETWORK_ENV=.env.sepolia CLIENT=reth docker compose up --build
```
### Supported Clients
- `reth` (default)
- `geth`
- `nethermind`
## Requirements
### Minimum Requirements
- Modern Multicore CPU
- 32GB RAM (64GB Recommended)
- NVMe SSD drive
- Storage: (2 \* [current chain size](https://base.org/stats) + [snapshot size](https://basechaindata.vercel.app) + 20% buffer) (to accommodate future growth)
- Docker and Docker Compose
### Production Hardware Specifications
The following are the hardware specifications we use in production:
#### Reth Archive Node (recommended)
- **Instance**: AWS i7i.12xlarge
- **Storage**: RAID 0 of all local NVMe drives (`/dev/nvme*`)
- **Filesystem**: ext4
#### Geth Full Node
- **Instance**: AWS i7i.12xlarge
- **Storage**: RAID 0 of all local NVMe drives (`/dev/nvme*`)
- **Filesystem**: ext4
> [!NOTE]
To run the node using a supported client, you can use the following command:
`CLIENT=supported_client docker compose up --build`
Supported clients:
- reth (runs vanilla node by default, Flashblocks mode enabled by providing RETH_FB_WEBSOCKET_URL, see [Reth Node README](./reth/README.md))
- geth
- nethermind
## Configuration
### Required Settings
- L1 Configuration:
- `OP_NODE_L1_ETH_RPC`: Your Ethereum L1 node RPC endpoint
- `OP_NODE_L1_BEACON`: Your L1 beacon node endpoint
- `OP_NODE_L1_BEACON_ARCHIVER`: Your L1 beacon archiver endpoint
- `OP_NODE_L1_RPC_KIND`: The type of RPC provider being used (default: "debug_geth"). Supported values:
- `alchemy`: Alchemy RPC provider
- `quicknode`: QuickNode RPC provider
- `infura`: Infura RPC provider
- `parity`: Parity RPC provider
- `nethermind`: Nethermind RPC provider
- `debug_geth`: Debug Geth RPC provider
- `erigon`: Erigon RPC provider
- `basic`: Basic RPC provider (standard receipt fetching only)
- `any`: Any available RPC method
- `standard`: Standard RPC methods including newer optimized methods
### Network Settings
- Mainnet:
- `RETH_CHAIN=base`
- `OP_NODE_NETWORK=base-mainnet`
- Sequencer: `https://mainnet-sequencer.base.org`
### Performance Settings
- Cache Settings:
- `GETH_CACHE="20480"` (20GB)
- `GETH_CACHE_DATABASE="20"` (4GB)
- `GETH_CACHE_GC="12"`
- `GETH_CACHE_SNAPSHOT="24"`
- `GETH_CACHE_TRIE="44"`
### Optional Features
- EthStats Monitoring (uncomment to enable)
- Trusted RPC Mode (uncomment to enable)
- Snap Sync (experimental)
For full configuration options, see the `.env.mainnet` file.
## Snapshots
Snapshots are available to help you sync your node more quickly. See [docs.base.org](https://docs.base.org/chain/run-a-base-node#snapshots) for links and more details on how to restore from a snapshot.
## Supported Networks
| Network | Status |
| ------- | ------ |
| Mainnet | ✅ |
| Testnet | ✅ |
## Troubleshooting
For support please join our [Discord](https://discord.gg/buildonbase) post in `🛠|node-operators`. You can alternatively open a new GitHub issue.
## Disclaimer
THE NODE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. We make no guarantees about asset protection or security. Usage is subject to applicable laws and regulations.
For more information, visit [docs.base.org](https://docs.base.org/).
================================================
FILE: SECURITY.md
================================================
# Security
## Bug bounty program
In line with our strategy of being the safest way for users to access crypto:
+ Coinbase will be extending our [best-in-industry][1] million-dollar [HackerOne bug bounty program][2]
to cover the Base network, the Base bridge contracts, and Base infrastructure.
+ Coinbase will be working in tandem with OP Labs to harden the security
guarantees of Bedrock and accelerate the timeline for decentralized
fault-proofs on the [OP Stack][3].
+ Coinbase's bug bounty program will run alongside Optimism's existing [Immunefi Bedrock bounty program][4]
to support the open source [Bedrock][5] OP Stack framework.
## Reporting vulnerabilities
All potential vulnerability reports can be submitted via the [HackerOne][6]
platform.
The HackerOne platform allows us to have a centralized and single reporting
source for us to deliver optimized SLA's and results. All reports submitted to
the platform are triaged around the clock by our team of Coinbase engineers
with domain knowledge, ensuring the best quality of review.
For more information on reporting vulnerabilities and our HackerOne bug bounty
program, view our [security program policies][7].
[1]: https://www.coinbase.com/blog/celebrating-10-years-of-our-bug-bounty-program
[2]: https://hackerone.com/coinbase?type=team
[3]: https://docs.optimism.io/
[4]: https://immunefi.com/bounty/optimism/
[5]: https://docs.optimism.io/docs/releases/bedrock/
[6]: https://hackerone.com/coinbase
[7]: https://hackerone.com/coinbase?view_policy=true
================================================
FILE: dependency_updater/dependency_updater.go
================================================
package main
import (
"context"
"encoding/json"
"fmt"
"slices"
"time"
"github.com/ethereum-optimism/optimism/op-service/retry"
"github.com/google/go-github/v72/github"
"github.com/urfave/cli/v3"
"log"
"os"
"os/exec"
"strings"
)
type Info struct {
Tag string `json:"tag,omitempty"`
Commit string `json:"commit"`
TagPrefix string `json:"tagPrefix,omitempty"`
Owner string `json:"owner"`
Repo string `json:"repo"`
Branch string `json:"branch,omitempty"`
Tracking string `json:"tracking"`
}
type VersionUpdateInfo struct {
Repo string
From string
To string
DiffUrl string
}
type Dependencies = map[string]*Info
func main() {
cmd := &cli.Command{
Name: "updater",
Usage: "Updates the dependencies in the geth, nethermind and reth Dockerfiles",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "token",
Usage: "Auth token used to make requests to the Github API must be set using export",
Sources: cli.EnvVars("GITHUB_TOKEN"),
Required: true,
},
&cli.StringFlag{
Name: "repo",
Usage: "Specifies repo location to run the version updater on",
Required: true,
},
&cli.BoolFlag{
Name: "commit",
Usage: "Stages updater changes and creates commit message",
Required: false,
},
&cli.BoolFlag{
Name: "github-action",
Usage: "Specifies whether tool is being used through github action workflow",
Required: false,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
err := updater(cmd.String("token"), cmd.String("repo"), cmd.Bool("commit"), cmd.Bool("github-action"))
if err != nil {
return fmt.Errorf("failed to run updater: %s", err)
}
return nil
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}
func updater(token string, repoPath string, commit bool, githubAction bool) error {
var err error
var dependencies Dependencies
var updatedDependencies []VersionUpdateInfo
f, err := os.ReadFile(repoPath + "/versions.json")
if err != nil {
return fmt.Errorf("error reading versions JSON: %s", err)
}
client := github.NewClient(nil).WithAuthToken(token)
ctx := context.Background()
err = json.Unmarshal(f, &dependencies)
if err != nil {
return fmt.Errorf("error unmarshalling versions JSON to dependencies: %s", err)
}
for dependency := range dependencies {
var updatedDependency VersionUpdateInfo
err := retry.Do0(context.Background(), 3, retry.Fixed(1*time.Second), func() error {
updatedDependency, err = getAndUpdateDependency(
ctx,
client,
dependency,
repoPath,
dependencies,
)
return err
})
if err != nil {
return fmt.Errorf("error getting and updating version/commit for "+dependency+": %s", err)
}
if updatedDependency != (VersionUpdateInfo{}) {
updatedDependencies = append(updatedDependencies, updatedDependency)
}
}
e := createVersionsEnv(repoPath, dependencies)
if e != nil {
return fmt.Errorf("error creating versions.env: %s", e)
}
if (commit && updatedDependencies != nil) || (githubAction && updatedDependencies != nil) {
err := createCommitMessage(updatedDependencies, repoPath, githubAction)
if err != nil {
return fmt.Errorf("error creating commit message: %s", err)
}
}
return nil
}
func createCommitMessage(updatedDependencies []VersionUpdateInfo, repoPath string, githubAction bool) error {
var repos []string
descriptionLines := []string{
"### Dependency Updates",
}
commitTitle := "chore: updated "
for _, dependency := range updatedDependencies {
repo, tag := dependency.Repo, dependency.To
descriptionLines = append(descriptionLines, fmt.Sprintf("**%s** - %s: [diff](%s)", repo, tag, dependency.DiffUrl))
repos = append(repos, repo)
}
commitDescription := strings.Join(descriptionLines, "\n")
commitTitle += strings.Join(repos, ", ")
if githubAction {
err := writeToGithubOutput(commitTitle, commitDescription, repoPath)
if err != nil {
return fmt.Errorf("error creating git commit message: %s", err)
}
} else {
cmd := exec.Command("git", "commit", "-am", commitTitle, "-m", commitDescription)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run git commit -m: %s", err)
}
}
return nil
}
func getAndUpdateDependency(ctx context.Context, client *github.Client, dependencyType string, repoPath string, dependencies Dependencies) (VersionUpdateInfo, error) {
version, commit, updatedDependency, err := getVersionAndCommit(ctx, client, dependencies, dependencyType)
if err != nil {
return VersionUpdateInfo{}, err
}
if updatedDependency != (VersionUpdateInfo{}) {
e := updateVersionTagAndCommit(commit, version, dependencyType, repoPath, dependencies)
if e != nil {
return VersionUpdateInfo{}, fmt.Errorf("error updating version tag and commit: %s", e)
}
}
return updatedDependency, nil
}
func getVersionAndCommit(ctx context.Context, client *github.Client, dependencies Dependencies, dependencyType string) (string, string, VersionUpdateInfo, error) {
var selectedTag *github.RepositoryTag
var commit string
var diffUrl string
var updatedDependency VersionUpdateInfo
options := &github.ListOptions{Page: 1}
currentTag := dependencies[dependencyType].Tag
tagPrefix := dependencies[dependencyType].TagPrefix
if dependencies[dependencyType].Tracking == "tag" || dependencies[dependencyType].Tracking == "release" {
// Collect all valid tags across all pages, then find the max version
var validTags []*github.RepositoryTag
trackingMode := dependencies[dependencyType].Tracking
for {
tags, resp, err := client.Repositories.ListTags(
ctx,
dependencies[dependencyType].Owner,
dependencies[dependencyType].Repo,
options)
if err != nil {
return "", "", VersionUpdateInfo{}, fmt.Errorf("error getting tags: %s", err)
}
for _, tag := range tags {
// Skip if tagPrefix is set and doesn't match
if tagPrefix != "" && !strings.HasPrefix(*tag.Name, tagPrefix) {
continue
}
// Filter based on tracking mode:
// - "release": only stable releases (no prerelease suffix)
// - "tag": releases and RC versions only (exclude -synctest, -alpha, etc.)
if trackingMode == "release" {
if !IsReleaseVersion(*tag.Name, tagPrefix) {
continue
}
} else if trackingMode == "tag" {
if !IsReleaseOrRCVersion(*tag.Name, tagPrefix) {
continue
}
}
// Check if this is a valid upgrade (not a downgrade)
if err := ValidateVersionUpgrade(currentTag, *tag.Name, tagPrefix); err != nil {
continue
}
validTags = append(validTags, tag)
}
if resp.NextPage == 0 {
break
}
options.Page = resp.NextPage
}
// Find the maximum version among valid tags
for _, tag := range validTags {
// Skip if this tag can't be parsed
if _, err := ParseVersion(*tag.Name, tagPrefix); err != nil {
log.Printf("Skipping unparseable tag %s: %v", *tag.Name, err)
continue
}
if selectedTag == nil {
selectedTag = tag
continue
}
cmp, err := CompareVersions(*tag.Name, *selectedTag.Name, tagPrefix)
if err != nil {
log.Printf("Error comparing versions %s and %s: %v", *tag.Name, *selectedTag.Name, err)
continue
}
if cmp > 0 {
selectedTag = tag
}
}
// If no valid version found, keep current version
if selectedTag == nil {
log.Printf("No valid upgrade found for %s, keeping %s", dependencyType, currentTag)
return currentTag, dependencies[dependencyType].Commit, VersionUpdateInfo{}, nil
}
if *selectedTag.Name != currentTag {
diffUrl = generateGithubRepoUrl(dependencies, dependencyType) + "/compare/" +
currentTag + "..." + *selectedTag.Name
}
// Get commit SHA from the tag
commit = *selectedTag.Commit.SHA
}
if diffUrl != "" {
updatedDependency = VersionUpdateInfo{
dependencies[dependencyType].Repo,
dependencies[dependencyType].Tag,
*selectedTag.Name,
diffUrl,
}
}
if dependencies[dependencyType].Tracking == "branch" {
branchCommit, _, err := client.Repositories.ListCommits(
ctx,
dependencies[dependencyType].Owner,
dependencies[dependencyType].Repo,
&github.CommitsListOptions{
SHA: dependencies[dependencyType].Branch,
},
)
if err != nil {
return "", "", VersionUpdateInfo{}, fmt.Errorf("error listing commits for "+dependencyType+": %s", err)
}
commit = *branchCommit[0].SHA
if dependencies[dependencyType].Commit != commit {
from, to := dependencies[dependencyType].Commit, commit
diffUrl = fmt.Sprintf("%s/compare/%s...%s", generateGithubRepoUrl(dependencies, dependencyType), from, to)
updatedDependency = VersionUpdateInfo{
dependencies[dependencyType].Repo,
dependencies[dependencyType].Tag,
commit,
diffUrl,
}
}
}
if selectedTag != nil {
return *selectedTag.Name, commit, updatedDependency, nil
}
return "", commit, updatedDependency, nil
}
func updateVersionTagAndCommit(
commit string,
tag string,
dependencyType string,
repoPath string,
dependencies Dependencies) error {
dependencies[dependencyType].Tag = tag
dependencies[dependencyType].Commit = commit
err := writeToVersionsJson(repoPath, dependencies)
if err != nil {
return fmt.Errorf("error writing to versions "+dependencyType+": %s", err)
}
return nil
}
func writeToVersionsJson(repoPath string, dependencies Dependencies) error {
// formatting json
updatedJson, err := json.MarshalIndent(dependencies, "", " ")
if err != nil {
return fmt.Errorf("error marshaling dependencies json: %s", err)
}
e := os.WriteFile(repoPath+"/versions.json", updatedJson, 0644)
if e != nil {
return fmt.Errorf("error writing to versions.json: %s", e)
}
return nil
}
func createVersionsEnv(repoPath string, dependencies Dependencies) error {
envLines := []string{}
for dependency := range dependencies {
repoUrl := generateGithubRepoUrl(dependencies, dependency) + ".git"
dependencyPrefix := strings.ToUpper(dependency)
if dependencies[dependency].Tracking == "branch" {
dependencies[dependency].Tag = dependencies[dependency].Branch
}
envLines = append(envLines, fmt.Sprintf("export %s_%s=%s",
dependencyPrefix, "TAG", dependencies[dependency].Tag))
envLines = append(envLines, fmt.Sprintf("export %s_%s=%s",
dependencyPrefix, "COMMIT", dependencies[dependency].Commit))
envLines = append(envLines, fmt.Sprintf("export %s_%s=%s",
dependencyPrefix, "REPO", repoUrl))
}
slices.Sort(envLines)
file, err := os.Create(repoPath + "/versions.env")
if err != nil {
return fmt.Errorf("error creating versions.env file: %s", err)
}
defer file.Close()
_, err = file.WriteString(strings.Join(envLines, "\n"))
if err != nil {
return fmt.Errorf("error writing to versions.env file: %s", err)
}
return nil
}
func writeToGithubOutput(title string, description string, repoPath string) error {
file := os.Getenv("GITHUB_OUTPUT")
f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open GITHUB_OUTPUT file: %s", err)
}
defer f.Close()
titleToWrite := fmt.Sprintf("%s=%s\n", "TITLE", title)
_, err = f.WriteString(titleToWrite)
if err != nil {
return fmt.Errorf("failed to write to GITHUB_OUTPUT file: %s", err)
}
delimiter := "EOF"
descToWrite := fmt.Sprintf("%s<<%s\n%s\n%s\n", "DESC", delimiter, description, delimiter)
_, err = f.WriteString(descToWrite)
if err != nil {
return fmt.Errorf("failed to write to GITHUB_OUTPUT file: %s", err)
}
return nil
}
func generateGithubRepoUrl(dependencies Dependencies, dependencyType string) string {
return "https://github.com/" + dependencies[dependencyType].Owner + "/" + dependencies[dependencyType].Repo
}
================================================
FILE: dependency_updater/go.mod
================================================
module github.com/base/node/dependency_updater
go 1.24.3
require (
github.com/ethereum-optimism/optimism v1.13.3
github.com/google/go-github/v72 v72.0.0
github.com/urfave/cli/v3 v3.3.8
)
require (
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
)
================================================
FILE: dependency_updater/go.sum
================================================
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ethereum-optimism/optimism v1.13.3 h1:rfPx7OembMnoEASU1ozA/Foa7Am7UA+h0SB+OUrxn7s=
github.com/ethereum-optimism/optimism v1.13.3/go.mod h1:WrVFtk3cP45tvHs7MARn9KGQu35XIoXo/IOWU6K/rzk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM=
github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: dependency_updater/version.go
================================================
package main
import (
"fmt"
"regexp"
"strings"
"github.com/Masterminds/semver/v3"
)
// rcPattern matches various RC formats: -rc1, -rc.1, -rc-1, -RC1, etc.
var rcPattern = regexp.MustCompile(`(?i)-rc[.-]?(\d+)`)
// rcOnlyPattern is used to check if a version contains ONLY an RC prerelease (not -synctest, -alpha, etc.)
var rcOnlyPattern = regexp.MustCompile(`(?i)^-rc[.-]?\d+$`)
// ParseVersion extracts and normalizes a semantic version from a tag string.
// It handles tagPrefix stripping, v-prefix normalization, and RC format normalization.
func ParseVersion(tag string, tagPrefix string) (*semver.Version, error) {
versionStr := tag
// Step 1: Strip tagPrefix if present (e.g., "op-node/v1.16.2" -> "v1.16.2")
if tagPrefix != "" && strings.HasPrefix(tag, tagPrefix) {
versionStr = strings.TrimPrefix(tag, tagPrefix)
versionStr = strings.TrimPrefix(versionStr, "/")
}
// Step 2: Normalize RC formats to semver-compatible format
// "-rc1" -> "-rc.1", "-rc-1" -> "-rc.1"
versionStr = normalizeRCFormat(versionStr)
// Step 3: Parse using Masterminds/semver (handles v prefix automatically)
v, err := semver.NewVersion(versionStr)
if err != nil {
return nil, fmt.Errorf("invalid version format %q: %w", tag, err)
}
return v, nil
}
// normalizeRCFormat converts various RC formats to semver-compatible format.
// Examples: "-rc1" -> "-rc.1", "-rc-2" -> "-rc.2"
func normalizeRCFormat(version string) string {
return rcPattern.ReplaceAllString(version, "-rc.$1")
}
// ValidateVersionUpgrade checks if transitioning from currentTag to newTag
// is a valid upgrade (not a downgrade).
// Returns nil if valid, error explaining why if invalid.
func ValidateVersionUpgrade(currentTag, newTag, tagPrefix string) error {
// First-time setup: no current version, any valid version is acceptable
if currentTag == "" {
_, err := ParseVersion(newTag, tagPrefix)
return err
}
// Parse current version
currentVersion, err := ParseVersion(currentTag, tagPrefix)
if err != nil {
// Current version unparseable - still validate new version is parseable
_, newErr := ParseVersion(newTag, tagPrefix)
return newErr
}
// Parse new version
newVersion, err := ParseVersion(newTag, tagPrefix)
if err != nil {
return fmt.Errorf("new version %q is not a valid semver: %w", newTag, err)
}
// Check for downgrade
if newVersion.LessThan(currentVersion) {
return fmt.Errorf(
"version downgrade detected: %s -> %s",
currentTag, newTag,
)
}
return nil
}
// CompareVersions compares two version tags and returns:
// -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
// Returns 0 and error if either version cannot be parsed.
func CompareVersions(v1Tag, v2Tag, tagPrefix string) (int, error) {
v1, err := ParseVersion(v1Tag, tagPrefix)
if err != nil {
return 0, err
}
v2, err := ParseVersion(v2Tag, tagPrefix)
if err != nil {
return 0, err
}
return v1.Compare(v2), nil
}
// IsReleaseVersion returns true if the tag is a stable release (no prerelease suffix).
// Examples:
// - "v1.0.0" -> true
// - "v1.0.0-rc1" -> false
// - "v1.0.0-synctest.0" -> false
func IsReleaseVersion(tag string, tagPrefix string) bool {
v, err := ParseVersion(tag, tagPrefix)
if err != nil {
return false
}
return v.Prerelease() == ""
}
// IsRCVersion returns true if the tag is a release candidate version.
// This matches versions with -rc, -rc.N, -rc-N, -rcN suffixes.
// Examples:
// - "v1.0.0-rc1" -> true
// - "v1.0.0-rc.2" -> true
// - "v1.0.0" -> false (stable release, not RC)
// - "v1.0.0-synctest.0" -> false (not an RC)
// - "v1.0.0-alpha" -> false (not an RC)
func IsRCVersion(tag string, tagPrefix string) bool {
v, err := ParseVersion(tag, tagPrefix)
if err != nil {
return false
}
prerelease := v.Prerelease()
if prerelease == "" {
return false
}
// Check if the prerelease is ONLY an RC format (e.g., "rc.1", "rc1", "rc-1")
// We need to check the original format before normalization
return rcOnlyPattern.MatchString("-" + prerelease)
}
// IsReleaseOrRCVersion returns true if the tag is either a stable release or an RC version.
// This excludes other prereleases like -alpha, -beta, -synctest, etc.
func IsReleaseOrRCVersion(tag string, tagPrefix string) bool {
return IsReleaseVersion(tag, tagPrefix) || IsRCVersion(tag, tagPrefix)
}
================================================
FILE: dependency_updater/version_test.go
================================================
package main
import (
"testing"
)
func TestNormalizeRCFormat(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"v0.3.0-rc1", "v0.3.0-rc.1"},
{"v0.3.0-rc.1", "v0.3.0-rc.1"},
{"v0.3.0-rc-1", "v0.3.0-rc.1"},
{"v0.3.0-RC1", "v0.3.0-rc.1"},
{"v0.3.0-rc12", "v0.3.0-rc.12"},
{"v0.3.0", "v0.3.0"},
{"v0.3.0-alpha", "v0.3.0-alpha"},
{"v0.3.0-beta.1", "v0.3.0-beta.1"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := normalizeRCFormat(tt.input)
if result != tt.expected {
t.Errorf("normalizeRCFormat(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
func TestParseVersion(t *testing.T) {
tests := []struct {
tag string
tagPrefix string
wantErr bool
}{
// Standard versions
{"v0.2.2", "", false},
{"v0.3.0", "", false},
{"1.35.3", "", false}, // nethermind style - no v prefix
// RC versions
{"v0.3.0-rc1", "", false},
{"v0.3.0-rc.1", "", false},
{"v0.3.0-rc-1", "", false},
{"v0.3.0-rc.2", "", false},
// With tagPrefix
{"op-node/v1.16.2", "op-node", false},
{"op-node/v1.16.3-rc1", "op-node", false},
// Non-standard but parseable
{"v1.101603.5", "", false}, // op-geth style
// Invalid
{"not-a-version", "", true},
{"", "", true},
}
for _, tt := range tests {
t.Run(tt.tag, func(t *testing.T) {
_, err := ParseVersion(tt.tag, tt.tagPrefix)
if (err != nil) != tt.wantErr {
t.Errorf("ParseVersion(%q, %q) error = %v, wantErr %v", tt.tag, tt.tagPrefix, err, tt.wantErr)
}
})
}
}
func TestValidateVersionUpgrade(t *testing.T) {
tests := []struct {
name string
currentTag string
newTag string
tagPrefix string
wantErr bool
}{
// Valid upgrades
{"stable to rc", "v0.2.2", "v0.3.0-rc1", "", false},
{"rc to rc", "v0.3.0-rc1", "v0.3.0-rc2", "", false},
{"rc to stable", "v0.3.0-rc2", "v0.3.0", "", false},
{"stable to stable", "v0.2.2", "v0.3.0", "", false},
{"patch upgrade", "v0.2.2", "v0.2.3", "", false},
{"minor upgrade", "v0.2.2", "v0.3.0", "", false},
{"major upgrade", "v0.2.2", "v1.0.0", "", false},
{"same version", "v0.2.2", "v0.2.2", "", false},
// With tagPrefix
{"prefix upgrade", "op-node/v1.16.2", "op-node/v1.16.3", "op-node", false},
{"prefix rc upgrade", "op-node/v1.16.2", "op-node/v1.17.0-rc1", "op-node", false},
// No v prefix (nethermind style)
{"no v prefix upgrade", "1.35.3", "1.35.4", "", false},
// Invalid downgrades
{"downgrade major", "v0.3.0", "v0.2.2", "", true},
{"downgrade minor", "v0.3.0", "v0.2.9", "", true},
{"downgrade patch", "v0.3.1", "v0.3.0", "", true},
{"stable to rc same version", "v0.3.0", "v0.3.0-rc2", "", true},
{"stable to rc older version", "v0.3.0", "v0.2.0-rc1", "", true},
// Edge cases
{"empty current - valid new", "", "v0.3.0", "", false},
{"empty current - invalid new", "", "not-a-version", "", true},
{"unparseable current allows update", "not-semver", "v0.3.0", "", false},
// Unparseable current with unparseable new should fail
{"unparseable current - unparseable new", "rollup-boost/v0.7.11", "websocket-proxy/v0.0.2", "", true},
{"unparseable current - valid new", "rollup-boost/v0.7.11", "v0.8.0", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateVersionUpgrade(tt.currentTag, tt.newTag, tt.tagPrefix)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateVersionUpgrade(%q, %q, %q) error = %v, wantErr %v",
tt.currentTag, tt.newTag, tt.tagPrefix, err, tt.wantErr)
}
})
}
}
func TestCompareVersions(t *testing.T) {
tests := []struct {
name string
v1 string
v2 string
tagPrefix string
want int
}{
{"v1 less than v2", "v0.2.2", "v0.3.0", "", -1},
{"v1 greater than v2", "v0.3.0", "v0.2.2", "", 1},
{"equal versions", "v0.3.0", "v0.3.0", "", 0},
{"rc less than stable", "v0.3.0-rc1", "v0.3.0", "", -1},
{"rc1 less than rc2", "v0.3.0-rc1", "v0.3.0-rc2", "", -1},
{"stable greater than rc", "v0.3.0", "v0.3.0-rc2", "", 1},
{"with prefix", "op-node/v1.16.2", "op-node/v1.16.3", "op-node", -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CompareVersions(tt.v1, tt.v2, tt.tagPrefix)
if err != nil {
t.Errorf("CompareVersions(%q, %q, %q) unexpected error: %v", tt.v1, tt.v2, tt.tagPrefix, err)
return
}
if got != tt.want {
t.Errorf("CompareVersions(%q, %q, %q) = %d, want %d", tt.v1, tt.v2, tt.tagPrefix, got, tt.want)
}
})
}
}
func TestIsReleaseVersion(t *testing.T) {
tests := []struct {
tag string
tagPrefix string
want bool
}{
// Stable releases
{"v1.0.0", "", true},
{"v0.2.2", "", true},
{"1.35.3", "", true}, // nethermind style
{"v1.101603.5", "", true}, // op-geth style
// With prefix
{"op-node/v1.16.2", "op-node", true},
// Pre-release versions (should return false)
{"v1.0.0-rc1", "", false},
{"v1.0.0-rc.1", "", false},
{"v1.0.0-rc-1", "", false},
{"v1.0.0-synctest.0", "", false},
{"v1.0.0-alpha", "", false},
{"v1.0.0-beta.1", "", false},
{"op-node/v1.16.6-synctest.0", "op-node", false},
{"op-node/v1.16.3-rc1", "op-node", false},
// Invalid versions (should return false)
{"not-a-version", "", false},
{"", "", false},
}
for _, tt := range tests {
t.Run(tt.tag, func(t *testing.T) {
got := IsReleaseVersion(tt.tag, tt.tagPrefix)
if got != tt.want {
t.Errorf("IsReleaseVersion(%q, %q) = %v, want %v", tt.tag, tt.tagPrefix, got, tt.want)
}
})
}
}
func TestIsRCVersion(t *testing.T) {
tests := []struct {
tag string
tagPrefix string
want bool
}{
// RC versions
{"v1.0.0-rc1", "", true},
{"v1.0.0-rc.1", "", true},
{"v1.0.0-rc-1", "", true},
{"v1.0.0-RC1", "", true},
{"v1.0.0-rc12", "", true},
{"op-node/v1.16.3-rc1", "op-node", true},
{"op-node/v1.16.3-rc.2", "op-node", true},
// Stable releases (not RC)
{"v1.0.0", "", false},
{"v0.2.2", "", false},
{"op-node/v1.16.2", "op-node", false},
// Other pre-release versions (not RC)
{"v1.0.0-synctest.0", "", false},
{"op-node/v1.16.6-synctest.0", "op-node", false},
{"v1.0.0-alpha", "", false},
{"v1.0.0-beta.1", "", false},
{"v1.0.0-alpha.rc1", "", false}, // rc is part of another prerelease
// Invalid versions
{"not-a-version", "", false},
{"", "", false},
}
for _, tt := range tests {
t.Run(tt.tag, func(t *testing.T) {
got := IsRCVersion(tt.tag, tt.tagPrefix)
if got != tt.want {
t.Errorf("IsRCVersion(%q, %q) = %v, want %v", tt.tag, tt.tagPrefix, got, tt.want)
}
})
}
}
func TestIsReleaseOrRCVersion(t *testing.T) {
tests := []struct {
tag string
tagPrefix string
want bool
}{
// Stable releases - should pass
{"v1.0.0", "", true},
{"v0.2.2", "", true},
{"op-node/v1.16.2", "op-node", true},
// RC versions - should pass
{"v1.0.0-rc1", "", true},
{"v1.0.0-rc.1", "", true},
{"op-node/v1.16.3-rc1", "op-node", true},
// Other pre-release versions - should NOT pass
{"v1.0.0-synctest.0", "", false},
{"op-node/v1.16.6-synctest.0", "op-node", false},
{"v1.0.0-alpha", "", false},
{"v1.0.0-beta.1", "", false},
// Invalid versions
{"not-a-version", "", false},
}
for _, tt := range tests {
t.Run(tt.tag, func(t *testing.T) {
got := IsReleaseOrRCVersion(tt.tag, tt.tagPrefix)
if got != tt.want {
t.Errorf("IsReleaseOrRCVersion(%q, %q) = %v, want %v", tt.tag, tt.tagPrefix, got, tt.want)
}
})
}
}
func TestRCVersionOrdering(t *testing.T) {
// Verify that RC versions are ordered correctly
versions := []string{
"v0.2.2",
"v0.3.0-rc.1",
"v0.3.0-rc.2",
"v0.3.0",
"v0.3.1",
}
for i := 0; i < len(versions)-1; i++ {
current := versions[i]
next := versions[i+1]
t.Run(current+" -> "+next, func(t *testing.T) {
err := ValidateVersionUpgrade(current, next, "")
if err != nil {
t.Errorf("Expected %s -> %s to be valid upgrade, got error: %v", current, next, err)
}
})
}
// Verify reverse order is invalid
for i := len(versions) - 1; i > 0; i-- {
current := versions[i]
previous := versions[i-1]
t.Run(current+" -> "+previous+" (downgrade)", func(t *testing.T) {
err := ValidateVersionUpgrade(current, previous, "")
if err == nil {
t.Errorf("Expected %s -> %s to be invalid downgrade", current, previous)
}
})
}
}
================================================
FILE: docker-compose.yml
================================================
services:
execution:
build:
context: .
dockerfile: ${CLIENT:-geth}/Dockerfile
restart: unless-stopped
ports:
- "8545:8545" # RPC
- "8546:8546" # websocket
- "7301:6060" # metrics
- "30303:30303" # P2P TCP
- "30303:30303/udp" # P2P UDP
command: ["bash", "./execution-entrypoint"]
volumes:
- ${HOST_DATA_DIR}:/data
env_file:
- ${NETWORK_ENV:-.env.mainnet} # Use .env.mainnet by default, override with .env.sepolia for testnet
node:
build:
context: .
dockerfile: ${CLIENT:-geth}/Dockerfile
restart: unless-stopped
depends_on:
- execution
ports:
- "7545:8545" # RPC
- "9222:9222" # P2P TCP
- "9222:9222/udp" # P2P UDP
- "7300:7300" # metrics
- "6060:6060" # pprof
command: ["bash", "./op-node-entrypoint"]
env_file:
- ${NETWORK_ENV:-.env.mainnet} # Use .env.mainnet by default, override with .env.sepolia for testnet
================================================
FILE: geth/Dockerfile
================================================
FROM golang:1.24 AS op
RUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN . /tmp/versions.env && git clone $OP_NODE_REPO --branch $OP_NODE_TAG --single-branch . && \
git switch -c branch-$OP_NODE_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$OP_NODE_COMMIT" ]'
RUN . /tmp/versions.env && cd op-node && \
make VERSION=$OP_NODE_TAG op-node
FROM golang:1.24 AS geth
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN . /tmp/versions.env && git clone $OP_GETH_REPO --branch $OP_GETH_TAG --single-branch . && \
git switch -c branch-$OP_GETH_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$OP_GETH_COMMIT" ]'
RUN go run build/ci.go install -static ./cmd/geth
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y jq curl supervisor && \
rm -rf /var/lib/apt/lists
RUN mkdir -p /var/log/supervisor
WORKDIR /app
COPY --from=op /app/op-node/bin/op-node ./
COPY --from=geth /app/build/bin/geth ./
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY geth/geth-entrypoint ./execution-entrypoint
COPY op-node-entrypoint .
CMD ["/usr/bin/supervisord"]
================================================
FILE: geth/geth-entrypoint
================================================
#!/bin/bash
set -eu
VERBOSITY=${GETH_VERBOSITY:-3}
GETH_DATA_DIR=${GETH_DATA_DIR:-/data}
RPC_PORT="${RPC_PORT:-8545}"
WS_PORT="${WS_PORT:-8546}"
AUTHRPC_PORT="${AUTHRPC_PORT:-8551}"
METRICS_PORT="${METRICS_PORT:-6060}"
HOST_IP="" # put your external IP address here and open port 30303 to improve peer connectivity
P2P_PORT="${P2P_PORT:-30303}"
DISCOVERY_PORT="${DISCOVERY_PORT:-30303}"
ADDITIONAL_ARGS=""
OP_GETH_GCMODE="${OP_GETH_GCMODE:-full}"
OP_GETH_SYNCMODE="${OP_GETH_SYNCMODE:-full}"
# Add cache optimizations with defaults
GETH_CACHE="${GETH_CACHE:-20480}"
GETH_CACHE_DATABASE="${GETH_CACHE_DATABASE:-20}"
GETH_CACHE_GC="${GETH_CACHE_GC:-12}"
GETH_CACHE_SNAPSHOT="${GETH_CACHE_SNAPSHOT:-24}"
GETH_CACHE_TRIE="${GETH_CACHE_TRIE:-44}"
if [[ -z "$OP_NODE_NETWORK" ]]; then
echo "expected OP_NODE_NETWORK to be set" 1>&2
exit 1
fi
mkdir -p $GETH_DATA_DIR
echo "$OP_NODE_L2_ENGINE_AUTH_RAW" > "$OP_NODE_L2_ENGINE_AUTH"
if [ "${OP_GETH_ETH_STATS+x}" = x ]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --ethstats=$OP_GETH_ETH_STATS"
fi
if [ "${OP_GETH_ALLOW_UNPROTECTED_TXS+x}" = x ]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --rpc.allow-unprotected-txs=$OP_GETH_ALLOW_UNPROTECTED_TXS"
fi
if [ "${OP_GETH_STATE_SCHEME+x}" = x ]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --state.scheme=$OP_GETH_STATE_SCHEME"
fi
if [ "${OP_GETH_BOOTNODES+x}" = x ]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --bootnodes=$OP_GETH_BOOTNODES"
fi
if [ "${HOST_IP:+x}" = x ]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --nat=extip:$HOST_IP"
fi
exec ./geth \
--datadir="$GETH_DATA_DIR" \
--verbosity="$VERBOSITY" \
--http \
--http.corsdomain="*" \
--http.vhosts="*" \
--http.addr=0.0.0.0 \
--http.port="$RPC_PORT" \
--http.api=web3,debug,eth,net,engine \
--authrpc.addr=0.0.0.0 \
--authrpc.port="$AUTHRPC_PORT" \
--authrpc.vhosts="*" \
--authrpc.jwtsecret="$OP_NODE_L2_ENGINE_AUTH" \
--ws \
--ws.addr=0.0.0.0 \
--ws.port="$WS_PORT" \
--ws.origins="*" \
--ws.api=debug,eth,net,engine \
--metrics \
--metrics.addr=0.0.0.0 \
--metrics.port="$METRICS_PORT" \
--syncmode="$OP_GETH_SYNCMODE" \
--gcmode="$OP_GETH_GCMODE" \
--maxpeers=100 \
--rollup.sequencerhttp="$OP_GETH_SEQUENCER_HTTP" \
--rollup.halt=major \
--op-network="$OP_NODE_NETWORK" \
--discovery.port="$DISCOVERY_PORT" \
--port="$P2P_PORT" \
--rollup.disabletxpoolgossip=true \
--cache="$GETH_CACHE" \
--cache.database="$GETH_CACHE_DATABASE" \
--cache.gc="$GETH_CACHE_GC" \
--cache.snapshot="$GETH_CACHE_SNAPSHOT" \
--cache.trie="$GETH_CACHE_TRIE" \
$ADDITIONAL_ARGS # intentionally unquoted
================================================
FILE: go.mod
================================================
module dependency_updater
go 1.24.3
================================================
FILE: nethermind/Dockerfile
================================================
FROM golang:1.24 AS op
RUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN . /tmp/versions.env && git clone $OP_NODE_REPO --branch $OP_NODE_TAG --single-branch . && \
git switch -c branch-$OP_NODE_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$OP_NODE_COMMIT" ]'
RUN . /tmp/versions.env && cd op-node && \
just VERSION=$OP_NODE_TAG op-node
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
ARG BUILD_CONFIG=release
ARG TARGETARCH
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN . /tmp/versions.env && git clone $NETHERMIND_REPO --branch $NETHERMIND_TAG --single-branch . && \
git switch -c $NETHERMIND_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$NETHERMIND_COMMIT" ]'
RUN TARGETARCH=${TARGETARCH#linux/} && \
arch=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
echo "Using architecture: $arch" && \
dotnet publish src/Nethermind/Nethermind.Runner -c $BUILD_CONFIG -a $arch -o /publish --sc false
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble
RUN apt-get update && \
apt-get install -y jq curl supervisor && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/log/supervisor
WORKDIR /app
COPY --from=build /publish ./
COPY --from=op /app/op-node/bin/op-node ./
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY nethermind/nethermind-entrypoint ./execution-entrypoint
COPY op-node-entrypoint .
CMD ["/usr/bin/supervisord"]
================================================
FILE: nethermind/nethermind-entrypoint
================================================
#!/bin/bash
set -eu
# Default configurations
NETHERMIND_DATA_DIR=${NETHERMIND_DATA_DIR:-/data}
NETHERMIND_LOG_LEVEL=${NETHERMIND_LOG_LEVEL:-Info}
RPC_PORT="${RPC_PORT:-8545}"
WS_PORT="${WS_PORT:-8546}"
AUTHRPC_PORT="${AUTHRPC_PORT:-8551}"
METRICS_PORT="${METRICS_PORT:-6060}"
DISCOVERY_PORT="${DISCOVERY_PORT:-30303}"
P2P_PORT="${P2P_PORT:-30303}"
ADDITIONAL_ARGS=""
# Check if required variables are set
if [[ -z "$OP_NODE_NETWORK" ]]; then
echo "Expected OP_NODE_NETWORK to be set" 1>&2
exit 1
fi
# Create necessary directories
mkdir -p "$NETHERMIND_DATA_DIR"
# Write the JWT secret
if [[ -z "$OP_NODE_L2_ENGINE_AUTH_RAW" ]]; then
echo "Expected OP_NODE_L2_ENGINE_AUTH_RAW to be set" 1>&2
exit 1
fi
echo "$OP_NODE_L2_ENGINE_AUTH_RAW" > "$OP_NODE_L2_ENGINE_AUTH"
# Additional arguments based on environment variables
if [[ -n "${OP_NETHERMIND_BOOTNODES:-}" ]]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --Network.Bootnodes=$OP_NETHERMIND_BOOTNODES"
fi
if [[ -n "${OP_NETHERMIND_ETHSTATS_ENABLED:-}" ]]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --EthStats.Enabled=$OP_NETHERMIND_ETHSTATS_ENABLED"
fi
if [[ -n "${OP_NETHERMIND_ETHSTATS_ENDPOINT:-}" ]]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --EthStats.NodeName=${OP_NETHERMIND_ETHSTATS_NODE_NAME:-NethermindNode} --EthStats.Endpoint=$OP_NETHERMIND_ETHSTATS_ENDPOINT"
fi
# Execute Nethermind
exec ./nethermind \
--config="$OP_NODE_NETWORK" \
--datadir="$NETHERMIND_DATA_DIR" \
--Optimism.SequencerUrl=$OP_SEQUENCER_HTTP \
--log="$NETHERMIND_LOG_LEVEL" \
--JsonRpc.Enabled=true \
--JsonRpc.Host=0.0.0.0 \
--JsonRpc.WebSocketsPort="$WS_PORT" \
--JsonRpc.Port="$RPC_PORT" \
--JsonRpc.JwtSecretFile="$OP_NODE_L2_ENGINE_AUTH" \
--JsonRpc.EngineHost=0.0.0.0 \
--JsonRpc.EnginePort="$AUTHRPC_PORT" \
--HealthChecks.Enabled=true \
--Metrics.Enabled=true \
--Metrics.ExposePort="$METRICS_PORT" \
--Network.P2PPort="$P2P_PORT" \
--Network.DiscoveryPort="$DISCOVERY_PORT" \
$ADDITIONAL_ARGS
================================================
FILE: op-node-entrypoint
================================================
#!/bin/bash
set -eu
get_public_ip() {
# Define a list of HTTP-based providers
local PROVIDERS=(
"http://ifconfig.me"
"http://api.ipify.org"
"http://ipecho.net/plain"
"http://v4.ident.me"
)
# Iterate through the providers until an IP is found or the list is exhausted
for provider in "${PROVIDERS[@]}"; do
local IP
IP=$(curl -s --max-time 10 --connect-timeout 5 "$provider")
# Check if IP contains a valid format (simple regex for an IPv4 address)
if [[ $IP =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$IP"
return 0
fi
done
return 1
}
if [[ -z "$OP_NODE_NETWORK" && -z "$OP_NODE_ROLLUP_CONFIG" ]]; then
echo "expected OP_NODE_NETWORK to be set" 1>&2
exit 1
fi
# wait until local execution client comes up (authed so will return 401 without token)
until [ "$(curl -s --max-time 10 --connect-timeout 5 -w '%{http_code}' -o /dev/null "${OP_NODE_L2_ENGINE_RPC/ws/http}")" -eq 401 ]; do
echo "waiting for execution client to be ready"
sleep 5
done
# public-facing P2P node, advertise public IP address
if PUBLIC_IP=$(get_public_ip); then
echo "fetched public IP is: $PUBLIC_IP"
else
echo "Could not retrieve public IP."
exit 8
fi
export OP_NODE_P2P_ADVERTISE_IP=$PUBLIC_IP
echo "$OP_NODE_L2_ENGINE_AUTH_RAW" > "$OP_NODE_L2_ENGINE_AUTH"
exec ./op-node
================================================
FILE: reth/Dockerfile
================================================
FROM golang:1.24 AS op
RUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN . /tmp/versions.env && git clone $OP_NODE_REPO --branch $OP_NODE_TAG --single-branch . && \
git switch -c branch-$OP_NODE_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$OP_NODE_COMMIT" ]'
RUN . /tmp/versions.env && cd op-node && \
make VERSION=$OP_NODE_TAG op-node
FROM rust:1.88 AS reth-base
WORKDIR /app
COPY versions.env /tmp/versions.env
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y git libclang-dev pkg-config curl build-essential && \
rm -rf /var/lib/apt/lists/*
RUN . /tmp/versions.env && git clone $BASE_RETH_NODE_REPO . && \
git checkout tags/$BASE_RETH_NODE_TAG && \
bash -c '[ "$(git rev-parse HEAD)" = "$BASE_RETH_NODE_COMMIT" ]' || (echo "Commit hash verification failed" && exit 1)
RUN cargo build --bin base-reth-node --profile maxperf
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y jq curl supervisor && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/log/supervisor
WORKDIR /app
COPY --from=op /app/op-node/bin/op-node ./
COPY --from=reth-base /app/target/maxperf/base-reth-node ./
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY ./reth/reth-entrypoint ./execution-entrypoint
COPY op-node-entrypoint .
CMD ["/usr/bin/supervisord"]
================================================
FILE: reth/README.md
================================================
# Running a Reth Node
This is an implementation of the Reth node setup that supports Flashblocks mode based on configuration.
## Setup
- See hardware requirements mentioned in the master README
- For Flashblocks mode: Access to a Flashblocks websocket endpoint (for `RETH_FB_WEBSOCKET_URL`)
- We provide public websocket endpoints for mainnet and devnet, included in `.env.mainnet` and `.env.sepolia`
## Node Type Selection
The node determines its mode based on the presence of the `RETH_FB_WEBSOCKET_URL` environment variable:
- **Vanilla Mode** (default): When no `RETH_FB_WEBSOCKET_URL` is provided.
- **Flashblocks Mode**: When `RETH_FB_WEBSOCKET_URL` is provided.
## Running the Node
The node follows the standard `docker-compose` workflow in the master README.
```bash
# To run Reth node with Flashblocks support, set RETH_FB_WEBSOCKET_URL in your .env file
CLIENT=reth docker-compose up
```
## Testing Flashblocks RPC Methods
When running in Flashblocks mode (with `RETH_FB_WEBSOCKET_URL` configured), you can query a pending block using the Flashblocks RPC:
```bash
curl -X POST \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["pending", false],"id":1}' \
http://localhost:8545
```
## Additional RPC Methods
For a complete list of supported RPC methods, refer to:
- [Standard Ethereum JSON-RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/)
- [Flashblocks RPC Methods](https://docs.base.org/chain/flashblocks#rpc-api) (Flashblocks mode only)
================================================
FILE: reth/reth-entrypoint
================================================
#!/bin/bash
set -eu
IPC_PATH="/data/reth.ipc"
RETH_DATA_DIR=/data
RPC_PORT="${RPC_PORT:-8545}"
WS_PORT="${WS_PORT:-8546}"
AUTHRPC_PORT="${AUTHRPC_PORT:-8551}"
METRICS_PORT="${METRICS_PORT:-6060}"
DISCOVERY_PORT="${DISCOVERY_PORT:-30303}"
P2P_PORT="${P2P_PORT:-30303}"
ADDITIONAL_ARGS=""
BINARY="./base-reth-node"
RETH_HISTORICAL_PROOFS="${RETH_HISTORICAL_PROOFS:-false}"
RETH_HISTORICAL_PROOFS_STORAGE_PATH="${RETH_HISTORICAL_PROOFS_STORAGE_PATH:-}"
LOG_LEVEL="${LOG_LEVEL:-info}"
if [[ -z "${RETH_CHAIN:-}" ]]; then
echo "expected RETH_CHAIN to be set" 1>&2
exit 1
fi
# Enable Flashblocks support if websocket URL is provided
if [[ -n "${RETH_FB_WEBSOCKET_URL:-}" ]]; then
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --websocket-url=$RETH_FB_WEBSOCKET_URL"
echo "Enabling Flashblocks support with endpoint: $RETH_FB_WEBSOCKET_URL"
else
echo "Running in vanilla node mode (no Flashblocks URL provided)"
fi
case "$LOG_LEVEL" in
"error")
LOG_LEVEL="v"
;;
"warn")
LOG_LEVEL="vv"
;;
"info"|*)
LOG_LEVEL="vvv"
;;
"debug")
LOG_LEVEL="vvvv"
;;
"trace")
LOG_LEVEL="vvvvv"
;;
esac
# Add pruning for base
if [[ "${RETH_PRUNING_ARGS+x}" = x ]]; then
echo "Adding pruning arguments: $RETH_PRUNING_ARGS"
ADDITIONAL_ARGS="$ADDITIONAL_ARGS $RETH_PRUNING_ARGS"
fi
if [[ "$RETH_HISTORICAL_PROOFS" == "true" && -n "$RETH_HISTORICAL_PROOFS_STORAGE_PATH" ]]; then
# reth doesn't like starting an old database in RO mode, so we have to start the reth node, wait for it to start up, then shut it down first
"$BINARY" node \
-$LOG_LEVEL \
--datadir="$RETH_DATA_DIR" \
--log.stdout.format json \
--http \
--http.addr=127.0.0.1 \
--http.port="$RPC_PORT" \
--http.api=eth \
--chain "$RETH_CHAIN" &
PID=$!
MAX_WAIT=$((60 * 60 * 6)) # 6 hours (static file manager init is slow)
# wait for json-rpc to return a block number greater than 0 (synced beyond genesis)
while true; do
RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", false],"id":1}' http://127.0.0.1:"$RPC_PORT" 2>/dev/null || true)
if echo "$RESPONSE" | grep -q '"number":"0x0"'; then
echo "waiting for reth node to sync beyond genesis block"
elif echo "$RESPONSE" | grep -q '"result"'; then
# curl succeeded and returned a valid result with block number != 0x0
break
else
echo "waiting for reth node to start up"
fi
sleep 1
MAX_WAIT=$((MAX_WAIT - 1))
if [ "$MAX_WAIT" -eq 0 ]; then
echo "timed out waiting for reth node to start up"
kill "$PID"
exit 1
fi
done
# shut down gracefully
kill "$PID"
(wait "$PID" && echo "reth node initialized") || echo "warning: reth node exited with code $?"
ADDITIONAL_ARGS="$ADDITIONAL_ARGS --proofs-history --proofs-history.storage-path=$RETH_HISTORICAL_PROOFS_STORAGE_PATH"
# in this case, we need to run the init script first (idempotent)
"$BINARY" proofs init \
-$LOG_LEVEL \
--log.stdout.format json \
--chain "$RETH_CHAIN" \
--datadir="$RETH_DATA_DIR" \
--proofs-history.storage-path=$RETH_HISTORICAL_PROOFS_STORAGE_PATH
fi
mkdir -p "$RETH_DATA_DIR"
echo "Starting reth with additional args: $ADDITIONAL_ARGS"
echo "$OP_NODE_L2_ENGINE_AUTH_RAW" > "$OP_NODE_L2_ENGINE_AUTH"
exec "$BINARY" node \
-$LOG_LEVEL \
--datadir="$RETH_DATA_DIR" \
--log.stdout.format json \
--ws \
--ws.origins="*" \
--ws.addr=0.0.0.0 \
--ws.port="$WS_PORT" \
--ws.api=web3,debug,eth,net,txpool \
--http \
--http.corsdomain="*" \
--http.addr=0.0.0.0 \
--http.port="$RPC_PORT" \
--http.api=web3,debug,eth,net,txpool,miner \
--ipcpath="$IPC_PATH" \
--authrpc.addr=0.0.0.0 \
--authrpc.port="$AUTHRPC_PORT" \
--authrpc.jwtsecret="$OP_NODE_L2_ENGINE_AUTH" \
--metrics=0.0.0.0:"$METRICS_PORT" \
--max-outbound-peers=100 \
--chain "$RETH_CHAIN" \
--rollup.sequencer-http="$RETH_SEQUENCER_HTTP" \
--rollup.disable-tx-pool-gossip \
--discovery.port="$DISCOVERY_PORT" \
--port="$P2P_PORT" \
$ADDITIONAL_ARGS
================================================
FILE: supervisord.conf
================================================
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:op-node]
command=/app/op-node-entrypoint
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
stopwaitsecs=300
[program:op-execution]
command=/app/execution-entrypoint
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
stopwaitsecs=300
================================================
FILE: versions.env
================================================
export BASE_RETH_NODE_COMMIT=6e54e8cbcbd27cb86bde54d3a5b0e9b4e9ea960a
export BASE_RETH_NODE_REPO=https://github.com/base/base.git
export BASE_RETH_NODE_TAG=v0.5.1
export NETHERMIND_COMMIT=31cb81b7328026791cdfaccd9db230c82f1db02d
export NETHERMIND_REPO=https://github.com/NethermindEth/nethermind.git
export NETHERMIND_TAG=1.36.0
export OP_GETH_COMMIT=878a554cc84cead647d20ba366043c6fd41ebf1c
export OP_GETH_REPO=https://github.com/ethereum-optimism/op-geth.git
export OP_GETH_TAG=v1.101609.1
export OP_NODE_COMMIT=3019251e80aa248e91743addd3e833190acb26f1
export OP_NODE_REPO=https://github.com/ethereum-optimism/optimism.git
export OP_NODE_TAG=op-node/v1.16.7
================================================
FILE: versions.json
================================================
{
"base_reth_node": {
"tag": "v0.5.1",
"commit": "6e54e8cbcbd27cb86bde54d3a5b0e9b4e9ea960a",
"owner": "base",
"repo": "base",
"tracking": "release"
},
"nethermind": {
"tag": "1.36.0",
"commit": "31cb81b7328026791cdfaccd9db230c82f1db02d",
"owner": "NethermindEth",
"repo": "nethermind",
"tracking": "release"
},
"op_geth": {
"tag": "v1.101609.1",
"commit": "878a554cc84cead647d20ba366043c6fd41ebf1c",
"owner": "ethereum-optimism",
"repo": "op-geth",
"tracking": "release"
},
"op_node": {
"tag": "op-node/v1.16.7",
"commit": "3019251e80aa248e91743addd3e833190acb26f1",
"tagPrefix": "op-node",
"owner": "ethereum-optimism",
"repo": "optimism",
"tracking": "release"
}
}
gitextract_5b8jeo_f/ ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── docker.yml │ ├── pr.yml │ ├── stale.yml │ └── update-dependencies.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── dependency_updater/ │ ├── dependency_updater.go │ ├── go.mod │ ├── go.sum │ ├── version.go │ └── version_test.go ├── docker-compose.yml ├── geth/ │ ├── Dockerfile │ └── geth-entrypoint ├── go.mod ├── nethermind/ │ ├── Dockerfile │ └── nethermind-entrypoint ├── op-node-entrypoint ├── reth/ │ ├── Dockerfile │ ├── README.md │ └── reth-entrypoint ├── supervisord.conf ├── versions.env └── versions.json
SYMBOL INDEX (27 symbols across 3 files)
FILE: dependency_updater/dependency_updater.go
type Info (line 20) | type Info struct
type VersionUpdateInfo (line 30) | type VersionUpdateInfo struct
function main (line 39) | func main() {
function updater (line 80) | func updater(token string, repoPath string, commit bool, githubAction bo...
function createCommitMessage (line 134) | func createCommitMessage(updatedDependencies []VersionUpdateInfo, repoPa...
function getAndUpdateDependency (line 164) | func getAndUpdateDependency(ctx context.Context, client *github.Client, ...
function getVersionAndCommit (line 179) | func getVersionAndCommit(ctx context.Context, client *github.Client, dep...
function updateVersionTagAndCommit (line 316) | func updateVersionTagAndCommit(
function writeToVersionsJson (line 332) | func writeToVersionsJson(repoPath string, dependencies Dependencies) err...
function createVersionsEnv (line 347) | func createVersionsEnv(repoPath string, dependencies Dependencies) error {
function writeToGithubOutput (line 385) | func writeToGithubOutput(title string, description string, repoPath stri...
function generateGithubRepoUrl (line 409) | func generateGithubRepoUrl(dependencies Dependencies, dependencyType str...
FILE: dependency_updater/version.go
function ParseVersion (line 19) | func ParseVersion(tag string, tagPrefix string) (*semver.Version, error) {
function normalizeRCFormat (line 43) | func normalizeRCFormat(version string) string {
function ValidateVersionUpgrade (line 50) | func ValidateVersionUpgrade(currentTag, newTag, tagPrefix string) error {
function CompareVersions (line 85) | func CompareVersions(v1Tag, v2Tag, tagPrefix string) (int, error) {
function IsReleaseVersion (line 102) | func IsReleaseVersion(tag string, tagPrefix string) bool {
function IsRCVersion (line 118) | func IsRCVersion(tag string, tagPrefix string) bool {
function IsReleaseOrRCVersion (line 134) | func IsReleaseOrRCVersion(tag string, tagPrefix string) bool {
FILE: dependency_updater/version_test.go
function TestNormalizeRCFormat (line 7) | func TestNormalizeRCFormat(t *testing.T) {
function TestParseVersion (line 32) | func TestParseVersion(t *testing.T) {
function TestValidateVersionUpgrade (line 71) | func TestValidateVersionUpgrade(t *testing.T) {
function TestCompareVersions (line 124) | func TestCompareVersions(t *testing.T) {
function TestIsReleaseVersion (line 155) | func TestIsReleaseVersion(t *testing.T) {
function TestIsRCVersion (line 195) | func TestIsRCVersion(t *testing.T) {
function TestIsReleaseOrRCVersion (line 237) | func TestIsReleaseOrRCVersion(t *testing.T) {
function TestRCVersionOrdering (line 273) | func TestRCVersionOrdering(t *testing.T) {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (81K chars).
[
{
"path": ".dockerignore",
"chars": 39,
"preview": "geth-data/\nreth-data/\nnethermind-data/\n"
},
{
"path": ".github/workflows/docker.yml",
"chars": 13415,
"preview": "name: Tag Docker image\n\non:\n push:\n branches:\n - \"main\"\n tags:\n - \"v*\"\n workflow_dispatch: {}\n\nenv:\n "
},
{
"path": ".github/workflows/pr.yml",
"chars": 3299,
"preview": "name: Pull Request\n\non:\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n geth:\n strategy:"
},
{
"path": ".github/workflows/stale.yml",
"chars": 1285,
"preview": "name: Mark stale issues and PRs\n\non:\n schedule:\n - cron: '30 0 * * *'\n workflow_dispatch:\npermissions:\n contents: "
},
{
"path": ".github/workflows/update-dependencies.yml",
"chars": 1314,
"preview": "name: Update Dockerfile Dependencies\non:\n schedule:\n - cron: '0 13 * * *'\n workflow_dispatch:\n\npermissions:\n conte"
},
{
"path": ".gitignore",
"chars": 98,
"preview": "/.idea/\n/geth-data/\n/reth-data/\n/nethermind-data/\n/dependency_updater/dependency_updater\n.DS_Store"
},
{
"path": "CONTRIBUTING.md",
"chars": 2252,
"preview": "# Contributing to Base Node\n\n## Code of Conduct\n\nAll interactions with this project follow our [Code of Conduct][code-of"
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "MIT License\n\nCopyright (c) 2023-2025 base.org contributors\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 4689,
"preview": "\n\n# Base Node\n\nBase is a secure, low-cost, developer-friendly Ethereum L2 built on Optimism's [OP Stac"
},
{
"path": "SECURITY.md",
"chars": 1530,
"preview": "# Security\n\n## Bug bounty program\n\nIn line with our strategy of being the safest way for users to access crypto:\n\n+ Coin"
},
{
"path": "dependency_updater/dependency_updater.go",
"chars": 11783,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/ethereum-optimism/optimism/op-"
},
{
"path": "dependency_updater/go.mod",
"chars": 311,
"preview": "module github.com/base/node/dependency_updater\n\ngo 1.24.3\n\nrequire (\n\tgithub.com/ethereum-optimism/optimism v1.13.3\n\tgit"
},
{
"path": "dependency_updater/go.sum",
"chars": 2070,
"preview": "github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3"
},
{
"path": "dependency_updater/version.go",
"chars": 4317,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Masterminds/semver/v3\"\n)\n\n// rcPattern matches various "
},
{
"path": "dependency_updater/version_test.go",
"chars": 8375,
"preview": "package main\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNormalizeRCFormat(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t"
},
{
"path": "docker-compose.yml",
"chars": 973,
"preview": "services:\n execution:\n build:\n context: .\n dockerfile: ${CLIENT:-geth}/Dockerfile\n restart: unless-stop"
},
{
"path": "geth/Dockerfile",
"chars": 1190,
"preview": "FROM golang:1.24 AS op\n\nRUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin\n\nWORKDIR /app\n"
},
{
"path": "geth/geth-entrypoint",
"chars": 2691,
"preview": "#!/bin/bash\nset -eu\n\nVERBOSITY=${GETH_VERBOSITY:-3}\nGETH_DATA_DIR=${GETH_DATA_DIR:-/data}\nRPC_PORT=\"${RPC_PORT:-8545}\"\nW"
},
{
"path": "go.mod",
"chars": 37,
"preview": "module dependency_updater\n\ngo 1.24.3\n"
},
{
"path": "nethermind/Dockerfile",
"chars": 1519,
"preview": "FROM golang:1.24 AS op\n\nRUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin\n\nWORKDIR /app\n"
},
{
"path": "nethermind/nethermind-entrypoint",
"chars": 2029,
"preview": "#!/bin/bash\nset -eu\n\n# Default configurations\nNETHERMIND_DATA_DIR=${NETHERMIND_DATA_DIR:-/data}\nNETHERMIND_LOG_LEVEL=${N"
},
{
"path": "op-node-entrypoint",
"chars": 1334,
"preview": "#!/bin/bash\nset -eu\n\nget_public_ip() {\n # Define a list of HTTP-based providers\n local PROVIDERS=(\n \"http://ifconfi"
},
{
"path": "reth/Dockerfile",
"chars": 1412,
"preview": "FROM golang:1.24 AS op\n\nRUN curl -sSfL 'https://just.systems/install.sh' | bash -s -- --to /usr/local/bin\n\nWORKDIR /app\n"
},
{
"path": "reth/README.md",
"chars": 1502,
"preview": "# Running a Reth Node\n\nThis is an implementation of the Reth node setup that supports Flashblocks mode based on configur"
},
{
"path": "reth/reth-entrypoint",
"chars": 4344,
"preview": "#!/bin/bash\nset -eu\n\nIPC_PATH=\"/data/reth.ipc\"\nRETH_DATA_DIR=/data\nRPC_PORT=\"${RPC_PORT:-8545}\"\nWS_PORT=\"${WS_PORT:-8546"
},
{
"path": "supervisord.conf",
"chars": 352,
"preview": "[supervisord]\nnodaemon=true\nlogfile=/dev/null\nlogfile_maxbytes=0\n\n[program:op-node]\ncommand=/app/op-node-entrypoint\nstdo"
},
{
"path": "versions.env",
"chars": 659,
"preview": "export BASE_RETH_NODE_COMMIT=6e54e8cbcbd27cb86bde54d3a5b0e9b4e9ea960a\nexport BASE_RETH_NODE_REPO=https://github.com/base"
},
{
"path": "versions.json",
"chars": 816,
"preview": "{\n\t \"base_reth_node\": {\n\t \t \"tag\": \"v0.5.1\",\n\t \t \"commit\": \"6e54e8cbcbd27cb86bde54d3a5b0e9b4e9ea960a\",\n\t \t \"owner"
}
]
About this extraction
This page contains the full source code of the base/node GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (73.0 KB), approximately 23.3k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.