Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions .tide/lld-fork-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# LLD: Automated Fork Export via Temporary SeiNode

## Overview

Automates the fork export by creating a temporary "exporter" SeiNode that bootstraps from the source chain using the existing bootstrap pipeline (identical to replayer). The SeiNode controller brings it to the target height. The group plan then submits `export-state` to the exporter's sidecar and uploads the result.

No new fields on FullNodeSpec. The exporter is a plain FullNode with BootstrapImage + TargetHeight — the same config replayers use.

## Key Insight

`seid export --height N` reads committed state at exactly height N from the database, regardless of whether seid has progressed beyond N. This means we don't need to halt the node precisely — we just need it to have reached height N. The export command is deterministic.

## Flow

```
reconcileSeiNodes creates:
- N validator SeiNodes (start init plans, block at configure-genesis)
- 1 exporter SeiNode (FullNode, bootstraps from source chain snapshot)

SeiNode controller drives exporter through standard bootstrap:
Pending → Initializing → Running
(snapshot-restore → config → sync to height → production StatefulSet)

Group plan (once exporter reaches Running):
await-exporter-running ← polls exporter phase == Running
submit-export-state ← submits export-state task to exporter's sidecar
teardown-exporter ← deletes exporter SeiNode
assemble-genesis-fork ← existing: downloads export, strips validators, collect-gentxs
collect-and-set-peers ← existing
await-nodes-running ← existing

Validators unblock: configure-genesis finds genesis.json → proceed → Running
```

## CRD Changes

### ForkConfig (expanded)

```go
type ForkConfig struct {
// SourceChainID is the chain ID of the network being forked.
SourceChainID string `json:"sourceChainId"`

// SourceImage is the seid container image compatible with the source
// chain at ExportHeight.
SourceImage string `json:"sourceImage"`

// ExportHeight is the block height at which to export state.
ExportHeight int64 `json:"exportHeight"`
}
```

No changes to FullNodeSpec, SnapshotSource, or any existing types.

## Exporter SeiNode

Created by `reconcileSeiNodes` as `{group}-exporter`. A plain FullNode:

```go
Spec: SeiNodeSpec{
ChainID: fork.SourceChainID,
Image: fork.SourceImage,
FullNode: &FullNodeSpec{
Snapshot: &SnapshotSource{
S3: &S3SnapshotSource{TargetHeight: fork.ExportHeight},
BootstrapImage: fork.SourceImage,
},
},
}
```

This is the same bootstrap config pattern that replayers use. The SeiNode controller handles it through the standard `fullNodePlanner` → `buildBootstrapPlan` → bootstrap Job → StatefulSet.

Labels: `sei.io/nodegroup: {group}`, `sei.io/role: exporter`
Excluded from `IncumbentNodes`.

## Group Plan Tasks

### `submit-export-state` (new controller-side task)

Submits an `export-state` task to the exporter's sidecar via HTTP API:

```go
type SubmitExportStateParams struct {
ExporterName string `json:"exporterName"`
Namespace string `json:"namespace"`
ExportHeight int64 `json:"exportHeight"`
SourceChainID string `json:"sourceChainId"`
}
```

The task:
1. Builds a sidecar client for the exporter node
2. Submits `export-state` with params: `{height: N, chainId: sourceChainId, s3Bucket: genesisBucket, s3Key: {sourceChainId}/exported-state.json, s3Region: genesisRegion}`
3. Polls the sidecar for task completion
4. Returns Complete when the sidecar task completes

This is the same submit-and-poll pattern used by other sidecar tasks in the plan executor.

### `await-exporter-running` (new controller-side task)

Polls exporter SeiNode phase:

```go
type AwaitExporterRunningParams struct {
ExporterName string `json:"exporterName"`
Namespace string `json:"namespace"`
}
```

Returns Complete when `Phase == Running`, Failed when `Phase == Failed`.

### `teardown-exporter` (new controller-side task)

Deletes the exporter SeiNode:

```go
type TeardownExporterParams struct {
ExporterName string `json:"exporterName"`
Namespace string `json:"namespace"`
}
```

Polls until the SeiNode is gone (ownerReferences cascade the StatefulSet, PVC, etc.).

## Full Group Plan Sequence

When `ForkGenesisCeremonyNeeded`:

```
await-exporter-running (controller: poll exporter phase)
submit-export-state (controller: submit to exporter sidecar, poll completion)
teardown-exporter (controller: delete exporter SeiNode)
assemble-genesis-fork (sidecar: download export, strip validators, collect-gentxs)
collect-and-set-peers (existing)
await-nodes-running (existing)
```

When export already exists in S3 (checked by `needsForkExporter`):

```
assemble-genesis-fork (sidecar: download export, strip validators, collect-gentxs)
collect-and-set-peers (existing)
await-nodes-running (existing)
```

## Edge Case: Export Already Exists

`needsForkExporter` checks S3 for `{sourceChainId}/exported-state.json`. If it exists, no exporter SeiNode is created and the group plan skips the first three tasks. This supports:
- Re-reconciling after a failed assembly (export already done)
- Multiple fork groups from the same source chain (export once, fork many)
- Pre-uploaded exports for faster iteration

## Example YAML

```yaml
apiVersion: sei.io/v1alpha1
kind: SeiNodeGroup
metadata:
name: private-fork
spec:
replicas: 4
genesis:
chainId: private-fork-1
stakingAmount: "10000000usei"
fork:
sourceChainId: pacific-1
sourceImage: ghcr.io/sei-protocol/seid:v5.9.0
exportHeight: 200000000
template:
spec:
image: ghcr.io/sei-protocol/seid:v6.0.0
validator: {}
```

## Files Affected

### Controller
| File | Change |
|------|--------|
| `api/v1alpha1/seinodegroup_types.go` | ForkConfig: add SourceImage, ExportHeight |
| `internal/controller/nodegroup/nodes.go` | ensureForkExporter, needsForkExporter, filter populateIncumbentNodes |
| `internal/planner/group.go` | Prepend export tasks to fork plan when exporter exists |
| `internal/task/fork_export.go` | New: SubmitExportStateParams, AwaitExporterRunningParams, TeardownExporterParams + executions |
| `internal/task/task.go` | Deserialize: 3 new cases |

### Seictl
| File | Change |
|------|--------|
| (export-state task already exists from earlier work) | No changes needed if export-state handler is already registered |

### What's Reused (no changes)
- `fullNodePlanner` — builds the exporter's bootstrap plan as-is
- `buildBootstrapPlan` — standard bootstrap task sequence
- `GenerateBootstrapJob` — standard bootstrap Job
- `SeiNode controller` — drives the exporter's full lifecycle
- `SnapshotSource` / `S3SnapshotSource` / `BootstrapImage` — existing types
45 changes: 38 additions & 7 deletions api/v1alpha1/seinodegroup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ type GenesisCeremonyConfig struct {
// assembly completion. Default: "15m".
// +optional
MaxCeremonyDuration *metav1.Duration `json:"maxCeremonyDuration,omitempty"`

// Fork configures this genesis ceremony to fork from an existing
// chain's exported state rather than building genesis from scratch.
// When set, the assembler downloads the exported state, rewrites
// the chain identity, strips old validators, and runs collect-gentxs
// with the new validator set.
// +optional
Fork *ForkConfig `json:"fork,omitempty"`
}

// ForkConfig configures forking from an existing chain. The controller
// creates a temporary exporter SeiNode that bootstraps from the source
// chain (using the same pipeline as replayers), then the group plan
// submits seid export to the exporter's sidecar and uploads the result.
type ForkConfig struct {
// SourceChainID is the chain ID of the network being forked.
// +kubebuilder:validation:MinLength=1
SourceChainID string `json:"sourceChainId"`

// SourceImage is the seid container image compatible with the source
// chain at ExportHeight. Used as both the bootstrap and main image
// for the temporary exporter node.
// +kubebuilder:validation:MinLength=1
SourceImage string `json:"sourceImage"`

// ExportHeight is the block height at which to export state.
// seid export --height N reads committed state at exactly this height.
// +kubebuilder:validation:Minimum=1
ExportHeight int64 `json:"exportHeight"`
}

// GenesisAccount represents a non-validator genesis account to fund.
Expand Down Expand Up @@ -270,13 +299,15 @@ type DeploymentStatus struct {

// Status condition types for SeiNodeGroup.
const (
ConditionNodesReady = "NodesReady"
ConditionExternalServiceReady = "ExternalServiceReady"
ConditionRouteReady = "RouteReady"
ConditionIsolationReady = "IsolationReady"
ConditionServiceMonitorReady = "ServiceMonitorReady"
ConditionGenesisCeremonyComplete = "GenesisCeremonyComplete"
ConditionPlanInProgress = "PlanInProgress"
ConditionNodesReady = "NodesReady"
ConditionExternalServiceReady = "ExternalServiceReady"
ConditionRouteReady = "RouteReady"
ConditionIsolationReady = "IsolationReady"
ConditionServiceMonitorReady = "ServiceMonitorReady"
ConditionGenesisCeremonyComplete = "GenesisCeremonyComplete"
ConditionPlanInProgress = "PlanInProgress"
ConditionGenesisCeremonyNeeded = "GenesisCeremonyNeeded"
ConditionForkGenesisCeremonyNeeded = "ForkGenesisCeremonyNeeded"
)

// +kubebuilder:object:root=true
Expand Down
20 changes: 20 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions config/crd/sei.io_seinodegroups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,38 @@ spec:
minLength: 1
pattern: ^[a-z0-9][a-z0-9-]*[a-z0-9]$
type: string
fork:
description: |-
Fork configures this genesis ceremony to fork from an existing
chain's exported state rather than building genesis from scratch.
When set, the assembler downloads the exported state, rewrites
the chain identity, strips old validators, and runs collect-gentxs
with the new validator set.
properties:
exportHeight:
description: |-
ExportHeight is the block height at which to export state.
seid export --height N reads committed state at exactly this height.
format: int64
minimum: 1
type: integer
sourceChainId:
description: SourceChainID is the chain ID of the network
being forked.
minLength: 1
type: string
sourceImage:
description: |-
SourceImage is the seid container image compatible with the source
chain at ExportHeight. Used as both the bootstrap and main image
for the temporary exporter node.
minLength: 1
type: string
required:
- exportHeight
- sourceChainId
- sourceImage
type: object
maxCeremonyDuration:
description: |-
MaxCeremonyDuration is the maximum time from group creation to genesis
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/onsi/gomega v1.38.2
github.com/prometheus/client_golang v1.23.2
github.com/sei-protocol/sei-config v0.0.9
github.com/sei-protocol/seictl v0.0.26
github.com/sei-protocol/seictl v0.0.29
k8s.io/api v0.35.0
k8s.io/apiextensions-apiserver v0.35.0
k8s.io/apimachinery v0.35.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sei-protocol/sei-config v0.0.9 h1:ELCpE0XnsTvgjOfWe1fWU43vqqFGL2tnlWuF9U1A2l8=
github.com/sei-protocol/sei-config v0.0.9/go.mod h1:IEAv5ynYw8Gu2F2qNfE4MQR0PPihAT6g7RWLpWdw5O0=
github.com/sei-protocol/seictl v0.0.26 h1:Rp0wcsOARR84d9phWfNvP8Ieup2UJJyGvCfo+Pw9bmA=
github.com/sei-protocol/seictl v0.0.26/go.mod h1:ffv0EkvWfTeJB5LbMdPEe83P5NYRcGW5TMVNo30LteY=
github.com/sei-protocol/seictl v0.0.29 h1:1cHn6hcPcLEVzylcl9/DVU6WVrYQYNm1ntuVduG0KWk=
github.com/sei-protocol/seictl v0.0.29/go.mod h1:ffv0EkvWfTeJB5LbMdPEe83P5NYRcGW5TMVNo30LteY=
github.com/sei-protocol/seilog v0.0.3 h1:Zi7oWXdX5jv92dY8n482xH032LtNebC89Y+qYZlBn0Y=
github.com/sei-protocol/seilog v0.0.3/go.mod h1:CKg58wraWnB3gRxWQ0v1rIVr0gmDHjkfP1bM2giKFFU=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
Expand Down
Loading
Loading