Skip to content

Commit 80c4338

Browse files
authored
Merge branch 'main' into julien/catchup-base
2 parents 976bb69 + a5ef771 commit 80c4338

12 files changed

Lines changed: 434 additions & 165 deletions

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ function sidebarHome() {
269269
text: "Testnet",
270270
link: "/guides/deploy/testnet",
271271
},
272+
{
273+
text: "Mainnet",
274+
link: "/guides/deploy/mainnet",
275+
},
272276
],
273277
},
274278
{

docs/guides/deploy/mainnet.md

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
---
2+
description: Checklist and guide for launching an EVM mainnet using ev-node and ev-reth.
3+
---
4+
5+
# EVM Mainnet Checklist
6+
7+
This guide covers launching a mainnet using **ev-reth** and **ev-node**.
8+
9+
## Ev-node
10+
11+
Ev-node is the sequencer that creates blocks, propagates them to other peers, and submits them to the DA layer.
12+
13+
### Chain ID
14+
15+
- Pick a unique EVM chain ID for your network
16+
- Verify it does not collide with existing chains at [chainlist.org](https://chainlist.org)
17+
18+
### Block Time
19+
20+
- Pick a block time for your chain
21+
- **Optional:** Decide if you would like lazy blocks
22+
23+
### Data Availability (DA)
24+
25+
| Configuration | Description |
26+
|---|---|
27+
| Header Namespace | Required. Namespace for block headers. |
28+
| Data Namespace | Required. Namespace for block data. Two namespaces are recommended, but one can be used. |
29+
| Forced Inclusion Namespace | Optional. For censorship resistance. |
30+
| DA Block Time | Used for syncing with the DA layer. Set this to the block time of the underlying DA chain. |
31+
32+
#### Batching Strategy
33+
34+
Blob submission to the DA layer is controlled by the batching strategy, not the DA block time. Choose a strategy based on your latency vs. cost trade-off:
35+
36+
| Strategy | Behavior |
37+
|---|---|
38+
| `immediate` | Submits as soon as any items are available. Lowest latency, highest cost. |
39+
| `size` | Waits until the batch reaches a size threshold (fraction of max blob size). |
40+
| `time` | Waits for a time interval (`batch_max_delay`) before submitting. Default strategy. |
41+
| `adaptive` | Submits when either the size threshold or the max delay is reached, whichever comes first. |
42+
43+
Related configuration flags:
44+
45+
- `da.batching_strategy` -- Strategy name (default: `time`)
46+
- `da.batch_size_threshold` -- Fraction of max blob size before submitting, 0.0-1.0 (default: 0.8). Applies to `size` and `adaptive`.
47+
- `da.batch_max_delay` -- Maximum wait time before submitting regardless of size (default: DA block time). Applies to `time` and `adaptive`.
48+
- `da.batch_min_items` -- Minimum items to accumulate before considering submission (default: 1).
49+
50+
#### DA Account Funding
51+
52+
The DA account needs tokens to submit blobs to the DA layer. If the account runs dry, blob submission stops and your chain halts.
53+
54+
- Fund the DA account with sufficient tokens before launch
55+
- Set up balance monitoring with alerts at a threshold that gives you enough runway to top up (e.g. alert when balance drops below 48 hours of estimated submission costs)
56+
- Establish a process for topping up the DA account
57+
58+
### Sequencer Key Management
59+
60+
The sequencer signing key is the most security-critical component of your chain. A compromised key allows an attacker to produce arbitrary blocks.
61+
62+
- Use an HSM or remote signer for the sequencer key in production -- do not store plaintext keys on disk
63+
- Restrict access to the sequencer machine to a minimal set of operators
64+
- Have a key rotation plan ready before launch
65+
66+
### P2P
67+
68+
- Configure p2p peers for a stable network
69+
- The sequencer should be connected to **at least two full nodes you control**
70+
- Third-party full nodes should connect to your full nodes, **not** directly to the sequencer
71+
72+
### Network Security
73+
74+
- Place the sequencer behind a firewall; only allow p2p connections from your own full nodes
75+
- Apply rate limiting on public RPC endpoints to prevent abuse
76+
- Consider DDoS mitigation (e.g. Cloudflare, HAProxy) in front of public-facing full nodes
77+
- Restrict SSH and management ports to a VPN or bastion host
78+
79+
### Metrics and Monitoring
80+
81+
- Set up a metric gathering system (Prometheus + Grafana recommended)
82+
- ev-metrics was created to help with basic metrics and alerting
83+
84+
Key metrics to monitor:
85+
86+
| Metric | Why |
87+
|---|---|
88+
| Block production rate | Detect if the sequencer has stalled |
89+
| DA submission lag / failures | Catch blob submission issues before they become critical |
90+
| Peer count | Ensure network connectivity is healthy |
91+
| Mempool depth | Detect congestion or spam |
92+
| Disk usage | Prevent nodes from running out of storage |
93+
| Sequencer balance | Ensure the sequencer can pay for DA submissions |
94+
| DA account balance | Chain halts if this runs dry |
95+
| RPC latency / error rate | Catch degraded user experience |
96+
97+
### RPC
98+
99+
- Use the full nodes connected to the sequencer for public/application RPCs
100+
- **Do not expose the sequencer directly**
101+
102+
## Ev-reth
103+
104+
Ev-reth is the execution engine. It uses reth as a library to make custom configurations for the Evolve use case. Changes are documented in the readme.
105+
106+
### Precompiles
107+
108+
Ev-reth comes with a set of optional precompiles:
109+
110+
| Precompile | Description |
111+
|---|---|
112+
| Basefee Redirect | Redirects the basefee (burned under EIP-1559) to a specified address |
113+
| Native Mint & Burn | Allows minting and burning the native token. Can be used with a bridge like Hyperlane. |
114+
115+
### Checklist
116+
117+
- Decide which precompiles are needed
118+
- Set admin accounts for precompiles / basefee redirect
119+
- Decide if a proxy contract is needed (provided proxy contract)
120+
- Decide on EIP-1559 configurations
121+
- Configure basefee
122+
- Optional: Feevault contract
123+
124+
### Backup and Recovery
125+
126+
Establish a backup and recovery strategy before launch. See the [Reth State Backup](../evm/reth-backup.md) guide for detailed instructions.
127+
128+
- Take periodic state snapshots (frequency depends on your RTO requirements)
129+
- Test the recovery procedure on a staging environment before mainnet launch
130+
- Keep at least one full node with archival state as a fallback
131+
- Document the recovery runbook so any operator can execute it
132+
133+
### Upgrade Strategy
134+
135+
Plan how you will ship new versions of ev-node and ev-reth to mainnet.
136+
137+
- **Rolling restart order:** Upgrade full nodes first, then the sequencer. This ensures full nodes can handle the new version before the sequencer starts producing blocks with it.
138+
- **Hard fork coordination:** If a release includes consensus-breaking changes, coordinate an activation height with all node operators in advance.
139+
- **Rollback plan:** Know how to revert to the previous binary and state if an upgrade causes issues. Test this on a staging network.
140+
- **Communication:** Establish a channel (Telegram, Discord, etc.) to notify node operators of upcoming upgrades and activation heights.
141+
142+
## Chain Startup Flow
143+
144+
### Genesis
145+
146+
Configuring the genesis is the first step to starting the chain.
147+
148+
If using the proxy admin contract alongside both native mint/burn and basefee redirect, set the admin of those to the proxy contract. This allows the chain to modify the admin to a multisig later.
149+
150+
#### Genesis Token Distribution
151+
152+
Define the initial token supply and allocation before generating the genesis file.
153+
154+
- Decide the total initial supply and how it is split (team, treasury, partners, bridge reserves, etc.)
155+
- Configure genesis balances in the `alloc` section of the genesis file
156+
- Ensure the sequencer EOA has enough balance to submit transactions
157+
- If using a bridge (e.g. Hyperlane), reserve sufficient supply for the bridge contract
158+
159+
:::info
160+
All flows below assume usage of the proxy admin contract.
161+
:::
162+
163+
### Flow 1: Full Setup
164+
165+
**Basefee redirect + feevault + native mint/burn + bridge (Hyperlane)**
166+
167+
#### Genesis Setup
168+
169+
Embed the proxy contract with an EOA address as admin. The EOA must have at least one token to submit transactions. The proxy contract will have a predictable address, which is added to the `EvolveConfig` in the Chain Config as admin for feevault and native mint/burn.
170+
171+
**Steps:**
172+
173+
1. Pick an EOA as admin of the proxy contract
174+
2. Set EOA and create alloc of proxy contract for the genesis file
175+
3. Set the admin proxy contract as admin in Evolve config:
176+
177+
```json
178+
{
179+
"evolve": {
180+
"baseFeeSink": "<EOA_to_receive_funds>",
181+
"baseFeeRedirectActivationHeight": 0,
182+
"mintAdmin": "<EOA_admin/proxyContract>",
183+
"mintPrecompileActivationHeight": 0,
184+
"contractSizeLimit": 131072,
185+
"contractSizeLimitActivationHeight": 0
186+
}
187+
}
188+
```
189+
190+
4. Pick a max contract size (24kb default, 128kb is a safe upgrade)
191+
5. Pick EIP-1559 config:
192+
193+
```json
194+
{
195+
"baseFeeMaxChangeDenominator": 8,
196+
"baseFeeElasticityMultiplier": 2,
197+
"initialBaseFeePerGas": 1000000000
198+
}
199+
```
200+
201+
#### Post Genesis
202+
203+
- Deploy Hyperlane native mint contract (if using Hyperlane)
204+
- Provide funds to partner wallets deploying on your chain
205+
- Connect full nodes via reth p2p on top of the Evolve system (consult reth documentation for key discovery and connection)
206+
207+
### Flow 2: Minimal Setup
208+
209+
**Basefee redirect only, with an EOA receiving all funds.**
210+
211+
#### Genesis Setup
212+
213+
1. Set EOA address as the sink in the Evolve config
214+
2. Pick a max contract size (24kb default, 128kb is a safe upgrade)
215+
3. Pick EIP-1559 config:
216+
217+
```json
218+
{
219+
"baseFeeMaxChangeDenominator": 8,
220+
"baseFeeElasticityMultiplier": 2,
221+
"initialBaseFeePerGas": 1000000000
222+
}
223+
```
224+
225+
#### Post Genesis
226+
227+
- Provide funds to partner wallets deploying on your chain
228+
- Connect full nodes via reth p2p on top of the Evolve system (consult reth documentation for key discovery and connection)

execution/evm/test/execution_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"time"
99

10+
tastoradocker "github.com/celestiaorg/tastora/framework/docker"
1011
"github.com/ethereum/go-ethereum/common"
1112
ethTypes "github.com/ethereum/go-ethereum/core/types"
1213
"github.com/ethereum/go-ethereum/ethclient"
@@ -59,8 +60,10 @@ func TestEngineExecution(t *testing.T) {
5960
genesisStateRoot := common.HexToHash(GENESIS_STATEROOT)
6061
GenesisStateRoot := genesisStateRoot[:]
6162

63+
dockerClient, dockerNetworkID := tastoradocker.Setup(t)
64+
6265
t.Run("Build chain", func(tt *testing.T) {
63-
rethNode := SetupTestRethNode(t)
66+
rethNode := SetupTestRethNode(t, dockerClient, dockerNetworkID)
6467

6568
ni, err := rethNode.GetNetworkInfo(context.TODO())
6669
require.NoError(tt, err)
@@ -158,7 +161,7 @@ func TestEngineExecution(t *testing.T) {
158161

159162
// start new container and try to sync
160163
t.Run("Sync chain", func(tt *testing.T) {
161-
rethNode := SetupTestRethNode(t)
164+
rethNode := SetupTestRethNode(t, dockerClient, dockerNetworkID)
162165

163166
ni, err := rethNode.GetNetworkInfo(context.TODO())
164167
require.NoError(tt, err)

execution/evm/test/test_helpers.go

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ import (
99
mathrand "math/rand"
1010
"net/http"
1111
"strings"
12-
"sync"
1312
"testing"
1413
"time"
1514

16-
"github.com/celestiaorg/tastora/framework/docker"
1715
"github.com/celestiaorg/tastora/framework/docker/evstack/reth"
1816
"github.com/celestiaorg/tastora/framework/types"
1917
"github.com/golang-jwt/jwt/v5"
@@ -22,12 +20,8 @@ import (
2220
"go.uber.org/zap/zaptest"
2321
)
2422

25-
// Test-scoped Docker client/network mapping to avoid conflicts between tests
26-
var (
27-
dockerClients = make(map[string]types.TastoraDockerClient)
28-
dockerNetworks = make(map[string]string)
29-
dockerMutex sync.RWMutex
30-
)
23+
// RethNodeOpt allows tests to customize the reth node builder before building.
24+
type RethNodeOpt func(b *reth.NodeBuilder)
3125

3226
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
3327

@@ -40,52 +34,36 @@ func randomString(n int) string {
4034
return string(b)
4135
}
4236

43-
// getTestScopedDockerSetup returns a Docker client and network ID that are scoped to the specific test.
44-
func getTestScopedDockerSetup(t testing.TB) (types.TastoraDockerClient, string) {
45-
t.Helper()
46-
47-
testKey := t.Name()
48-
dockerMutex.Lock()
49-
defer dockerMutex.Unlock()
50-
51-
dockerCli, exists := dockerClients[testKey]
52-
if !exists {
53-
cli, netID := docker.Setup(t)
54-
dockerClients[testKey] = cli
55-
dockerNetworks[testKey] = netID
56-
dockerCli = cli
57-
}
58-
dockerNetID := dockerNetworks[testKey]
59-
60-
return dockerCli, dockerNetID
61-
}
62-
6337
// SetupTestRethNode creates a single Reth node for testing purposes.
64-
func SetupTestRethNode(t testing.TB) *reth.Node {
38+
func SetupTestRethNode(t testing.TB, client types.TastoraDockerClient, networkID string, opts ...RethNodeOpt) *reth.Node {
6539
t.Helper()
6640
ctx := context.Background()
6741

68-
dockerCli, dockerNetID := getTestScopedDockerSetup(t)
69-
7042
testName := fmt.Sprintf("%s-%s", t.Name(), randomString(6))
7143
logger := zap.NewNop()
7244
if testing.Verbose() {
7345
logger = zaptest.NewLogger(t)
7446
}
75-
n, err := new(reth.NodeBuilder).
47+
b := new(reth.NodeBuilder).
7648
WithTestName(testName).
7749
WithLogger(logger).
7850
WithImage(reth.DefaultImage()).
7951
WithBin("ev-reth").
80-
WithDockerClient(dockerCli).
81-
WithDockerNetworkID(dockerNetID).
82-
WithGenesis([]byte(reth.DefaultEvolveGenesisJSON())).
83-
Build(ctx)
52+
WithDockerClient(client).
53+
WithDockerNetworkID(networkID).
54+
WithGenesis([]byte(reth.DefaultEvolveGenesisJSON()))
55+
56+
for _, opt := range opts {
57+
if opt != nil {
58+
opt(b)
59+
}
60+
}
61+
62+
n, err := b.Build(ctx)
63+
require.NoError(t, err)
8464
t.Cleanup(func() {
8565
_ = n.Remove(context.Background())
8666
})
87-
88-
require.NoError(t, err)
8967
require.NoError(t, n.Start(ctx))
9068

9169
ni, err := n.GetNetworkInfo(ctx)

test/e2e/evm_contract_e2e_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111
"time"
1212

13+
tastoradocker "github.com/celestiaorg/tastora/framework/docker"
1314
"github.com/ethereum/go-ethereum"
1415
"github.com/ethereum/go-ethereum/common"
1516
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -243,7 +244,8 @@ func TestEvmContractEvents(t *testing.T) {
243244
func setupTestSequencer(t testing.TB, homeDir string, extraArgs ...string) (*ethclient.Client, string, func()) {
244245
sut := NewSystemUnderTest(t)
245246

246-
genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, extraArgs...)
247+
dcli, netID := tastoradocker.Setup(t)
248+
genesisHash, seqEthURL := setupSequencerOnlyTest(t, sut, homeDir, dcli, netID, extraArgs...)
247249
t.Logf("Sequencer started at %s (Genesis: %s)", seqEthURL, genesisHash)
248250

249251
client, err := ethclient.Dial(seqEthURL)

test/e2e/evm_da_restart_e2e_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"testing"
2525
"time"
2626

27+
tastoradocker "github.com/celestiaorg/tastora/framework/docker"
2728
"github.com/ethereum/go-ethereum/common"
2829
"github.com/ethereum/go-ethereum/ethclient"
2930
"github.com/stretchr/testify/require"
@@ -56,7 +57,8 @@ func TestEvmDARestartWithPendingBlocksE2E(t *testing.T) {
5657
sut := NewSystemUnderTest(t)
5758

5859
// Setup sequencer and get genesis hash
59-
genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome)
60+
dcli, netID := tastoradocker.Setup(t)
61+
genesisHash, seqURL := setupSequencerOnlyTest(t, sut, nodeHome, dcli, netID)
6062
t.Logf("Genesis hash: %s", genesisHash)
6163

6264
// Connect to EVM

0 commit comments

Comments
 (0)