Skip to content

Commit a9d71d2

Browse files
bdchathamclaude
andauthored
refactor: simplify genesis resolution and clean up sidecar task handlers (#49)
## Summary - **Genesis simplification**: All genesis/ceremony task handlers now derive S3 coordinates from environment variables (`SEI_GENESIS_BUCKET`, `SEI_GENESIS_REGION`, `SEI_CHAIN_ID`) instead of receiving them as task params. Genesis resolution: embedded sei-config first, S3 fallback at `{bucket}/{chainID}/genesis.json`. - **Bump sei-config to v0.0.9**: Corrected embedded genesis files for pacific-1, atlantic-2, arctic-1 - **Remove dead code**: `CommandRunner` type, `parseUploadConfig`, stale S3 fields from client task types - **Fix `readLocalNodeID`**: Now derives node ID from Ed25519 key (matching CometBFT's `p2p.PubKeyToID`), instead of assuming a top-level `"id"` field in `node_key.json` - **Consistency fixes**: ConfigPatcher rejects empty files, `SEI_SNAPSHOT_UPLOAD_INTERVAL` fails fast on invalid value, stale doc comments cleaned up - **Env var validation**: `serve.go` validates `SEI_CHAIN_ID`, `SEI_GENESIS_BUCKET`, `SEI_GENESIS_REGION` at startup ## Deployment notes - Requires sei-k8s-controller to inject `SEI_GENESIS_BUCKET` and `SEI_GENESIS_REGION` into sidecar pod specs (see sei-protocol/sei-k8s-controller `refactor/genesis-env-driven` branch) - Controller must deploy before this sidecar — new sidecar requires env vars that the new controller injects ## Test plan - [x] `go test ./sidecar/...` — all pass (pre-existing `TestExportS3UploaderFactoryError` unrelated) - [x] Genesis embedded path tested (pacific-1, atlantic-2) - [x] Genesis S3 fallback tested (unknown chain) - [x] Ceremony tasks tested (upload artifacts, assemble genesis, set peers) - [x] TypedHandler deserialization integration tests pass for all 17 task types - [ ] Deploy controller with env var injection, then deploy new sidecar image - [ ] Verify genesis ceremony end-to-end on dev cluster 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4822d57 commit a9d71d2

23 files changed

Lines changed: 227 additions & 488 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ require (
1414
github.com/oapi-codegen/runtime v1.2.0
1515
github.com/pelletier/go-toml/v2 v2.2.4
1616
github.com/sei-protocol/sei-chain v0.0.29-fix.0.20260326202429-c9b42951fef7
17-
github.com/sei-protocol/sei-config v0.0.9-0.20260327015454-7cf35ff77daa
17+
github.com/sei-protocol/sei-config v0.0.9
1818
github.com/sei-protocol/seilog v0.0.3
1919
github.com/urfave/cli/v3 v3.6.1
2020
modernc.org/sqlite v1.18.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,8 +1820,8 @@ github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQp
18201820
github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8=
18211821
github.com/sei-protocol/sei-chain v0.0.29-fix.0.20260326202429-c9b42951fef7 h1:kW+yxMoSm4RvpP5VT5lFfSa+dqnOE9ZpapE2qNIJwvc=
18221822
github.com/sei-protocol/sei-chain v0.0.29-fix.0.20260326202429-c9b42951fef7/go.mod h1:R1/EoOY+MKvwH0k5ivTtG9mLYOQvm0Ni/Trjxrep3t4=
1823-
github.com/sei-protocol/sei-config v0.0.9-0.20260327015454-7cf35ff77daa h1:bi0qHl2E8TxpOpBZZJmMl7eZc04sMJe5E11nC+uF7IM=
1824-
github.com/sei-protocol/sei-config v0.0.9-0.20260327015454-7cf35ff77daa/go.mod h1:IEAv5ynYw8Gu2F2qNfE4MQR0PPihAT6g7RWLpWdw5O0=
1823+
github.com/sei-protocol/sei-config v0.0.9 h1:ELCpE0XnsTvgjOfWe1fWU43vqqFGL2tnlWuF9U1A2l8=
1824+
github.com/sei-protocol/sei-config v0.0.9/go.mod h1:IEAv5ynYw8Gu2F2qNfE4MQR0PPihAT6g7RWLpWdw5O0=
18251825
github.com/sei-protocol/sei-load v0.0.0-20251007135253-78fbdc141082 h1:f2sY8OcN60UL1/6POx+HDMZ4w04FTZtSScnrFSnGZHg=
18261826
github.com/sei-protocol/sei-load v0.0.0-20251007135253-78fbdc141082/go.mod h1:V0fNURAjS6A8+sA1VllegjNeSobay3oRUW5VFZd04bA=
18271827
github.com/sei-protocol/sei-tm-db v0.0.5 h1:3WONKdSXEqdZZeLuWYfK5hP37TJpfaUa13vAyAlvaQY=

serve.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,26 @@ var serveCmd = cli.Command{
4040
}
4141
port := cmd.String("port")
4242
chainID := os.Getenv("SEI_CHAIN_ID")
43+
genesisBucket := os.Getenv("SEI_GENESIS_BUCKET")
44+
genesisRegion := os.Getenv("SEI_GENESIS_REGION")
45+
46+
for _, kv := range []struct{ name, val string }{
47+
{"SEI_CHAIN_ID", chainID},
48+
{"SEI_GENESIS_BUCKET", genesisBucket},
49+
{"SEI_GENESIS_REGION", genesisRegion},
50+
} {
51+
if kv.val == "" {
52+
return fmt.Errorf("required environment variable %s is not set", kv.name)
53+
}
54+
}
4355

4456
var snapshotUploadInterval time.Duration
4557
if raw := os.Getenv("SEI_SNAPSHOT_UPLOAD_INTERVAL"); raw != "" {
46-
if parsed, err := time.ParseDuration(raw); err == nil {
47-
snapshotUploadInterval = parsed
58+
parsed, err := time.ParseDuration(raw)
59+
if err != nil {
60+
return fmt.Errorf("invalid SEI_SNAPSHOT_UPLOAD_INTERVAL %q: %w", raw, err)
4861
}
62+
snapshotUploadInterval = parsed
4963
}
5064

5165
if err := tasks.EnsureDefaultConfig(homeDir); err != nil {
@@ -65,16 +79,16 @@ var serveCmd = cli.Command{
6579
engine.TaskConfigValidate: tasks.NewConfigValidator(homeDir).Handler(),
6680
engine.TaskConfigReload: tasks.NewConfigReloader(homeDir).Handler(),
6781
engine.TaskMarkReady: tasks.MarkReadyHandler(),
68-
engine.TaskConfigureGenesis: tasks.NewGenesisFetcher(homeDir, chainID, nil).Handler(),
82+
engine.TaskConfigureGenesis: tasks.NewGenesisFetcher(homeDir, chainID, genesisBucket, genesisRegion, nil).Handler(),
6983
engine.TaskConfigureStateSync: tasks.NewStateSyncConfigurer(homeDir, nil).Handler(),
7084
engine.TaskSnapshotUpload: tasks.NewSnapshotUploader(homeDir, snapshotUploadInterval, nil).Handler(),
7185
engine.TaskResultExport: tasks.NewResultExporter(homeDir, nil).Handler(),
7286
engine.TaskAwaitCondition: tasks.NewConditionWaiter(nil).Handler(),
73-
engine.TaskGenerateIdentity: tasks.NewIdentityGenerator(homeDir, nil).Handler(),
74-
engine.TaskGenerateGentx: tasks.NewGentxGenerator(homeDir, nil).Handler(),
75-
engine.TaskUploadGenesisArtifacts: tasks.NewGenesisArtifactUploader(homeDir, nil).Handler(),
76-
engine.TaskAssembleAndUploadGenesis: tasks.NewGenesisAssembler(homeDir, nil, nil, nil).Handler(),
77-
engine.TaskSetGenesisPeers: tasks.NewGenesisPeersSetter(homeDir, nil).Handler(),
87+
engine.TaskGenerateIdentity: tasks.NewIdentityGenerator(homeDir).Handler(),
88+
engine.TaskGenerateGentx: tasks.NewGentxGenerator(homeDir).Handler(),
89+
engine.TaskUploadGenesisArtifacts: tasks.NewGenesisArtifactUploader(homeDir, genesisBucket, genesisRegion, chainID, nil).Handler(),
90+
engine.TaskAssembleAndUploadGenesis: tasks.NewGenesisAssembler(homeDir, genesisBucket, genesisRegion, chainID, nil, nil).Handler(),
91+
engine.TaskSetGenesisPeers: tasks.NewGenesisPeersSetter(homeDir, genesisBucket, genesisRegion, chainID, nil).Handler(),
7892
}
7993

8094
eng := engine.NewEngine(ctx, handlers, store)

sidecar/client/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func (c *SidecarClient) SubmitTask(ctx context.Context, task TaskRequest) (uuid.
132132
}
133133
}
134134

135-
// ListTasks returns recent task results and active scheduled tasks.
135+
// ListTasks returns recent task results.
136136
func (c *SidecarClient) ListTasks(ctx context.Context) ([]TaskResult, error) {
137137
resp, err := c.inner.ListTasksWithResponse(ctx)
138138
if err != nil {

sidecar/client/tasks.go

Lines changed: 20 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package client
22

33
import (
44
"fmt"
5-
"net/url"
65

76
"github.com/google/uuid"
87
seiconfig "github.com/sei-protocol/sei-config"
@@ -156,61 +155,28 @@ func SnapshotUploadTaskFromParams(params map[string]interface{}) SnapshotUploadT
156155
}
157156
}
158157

159-
// ConfigureGenesisTask configures genesis.json for a node. When URI and Region
160-
// are set, the sidecar downloads from S3. When they are empty, the sidecar
161-
// falls back to writing the embedded genesis for the chain ID it was started
162-
// with (set via SEI_CHAIN_ID environment variable).
158+
// ConfigureGenesisTask instructs the sidecar to resolve and write genesis.json.
159+
// The sidecar resolves genesis from its chain ID: embedded config is checked
160+
// first, then S3 fallback at {bucket}/{chainID}/genesis.json using env vars.
161+
// No parameters are needed from the controller.
163162
type ConfigureGenesisTask struct {
164163
TaskMeta
165-
URI string
166-
Region string
167164
}
168165

169166
func (t ConfigureGenesisTask) TaskType() string { return TaskTypeConfigureGenesis }
170167

171-
func (t ConfigureGenesisTask) Validate() error {
172-
if t.URI == "" {
173-
return nil
174-
}
175-
if t.Region == "" {
176-
return fmt.Errorf("configure-genesis: Region is required when URI is set")
177-
}
178-
parsed, err := url.Parse(t.URI)
179-
if err != nil {
180-
return fmt.Errorf("configure-genesis: invalid URI %q: %w", t.URI, err)
181-
}
182-
if parsed.Scheme != "s3" {
183-
return fmt.Errorf("configure-genesis: URI must use s3:// scheme, got %q", parsed.Scheme)
184-
}
185-
if parsed.Host == "" || parsed.Path == "" || parsed.Path == "/" {
186-
return fmt.Errorf("configure-genesis: URI must be s3://bucket/key, got %q", t.URI)
187-
}
188-
return nil
189-
}
168+
func (t ConfigureGenesisTask) Validate() error { return nil }
190169

191170
func (t ConfigureGenesisTask) ToTaskRequest() TaskRequest {
192-
var req TaskRequest
193-
if t.URI == "" {
194-
req = TaskRequest{Type: t.TaskType()}
195-
} else {
196-
p := map[string]interface{}{
197-
"uri": t.URI,
198-
"region": t.Region,
199-
}
200-
req = TaskRequest{Type: t.TaskType(), Params: &p}
201-
}
171+
req := TaskRequest{Type: t.TaskType()}
202172
t.applyMeta(&req)
203173
return req
204174
}
205175

206176
// ConfigureGenesisTaskFromParams reconstructs a ConfigureGenesisTask from
207177
// a generic params map.
208-
func ConfigureGenesisTaskFromParams(params map[string]interface{}) ConfigureGenesisTask {
209-
s := func(k string) string { v, _ := params[k].(string); return v }
210-
return ConfigureGenesisTask{
211-
URI: s("uri"),
212-
Region: s("region"),
213-
}
178+
func ConfigureGenesisTaskFromParams(_ map[string]interface{}) ConfigureGenesisTask {
179+
return ConfigureGenesisTask{}
214180
}
215181

216182
// PeerSourceType identifies the peer discovery mechanism.
@@ -645,23 +611,15 @@ func (t GenerateGentxTask) ToTaskRequest() TaskRequest {
645611
}
646612

647613
// UploadGenesisArtifactsTask uploads identity.json and gentx.json to S3.
614+
// S3 coordinates are derived by the sidecar from its environment.
648615
type UploadGenesisArtifactsTask struct {
649616
TaskMeta
650-
S3Bucket string
651-
S3Prefix string
652-
S3Region string
653617
NodeName string
654618
}
655619

656620
func (t UploadGenesisArtifactsTask) TaskType() string { return TaskTypeUploadGenesisArtifacts }
657621

658622
func (t UploadGenesisArtifactsTask) Validate() error {
659-
if t.S3Bucket == "" {
660-
return fmt.Errorf("upload-genesis-artifacts: missing required field S3Bucket")
661-
}
662-
if t.S3Region == "" {
663-
return fmt.Errorf("upload-genesis-artifacts: missing required field S3Region")
664-
}
665623
if t.NodeName == "" {
666624
return fmt.Errorf("upload-genesis-artifacts: missing required field NodeName")
667625
}
@@ -670,9 +628,6 @@ func (t UploadGenesisArtifactsTask) Validate() error {
670628

671629
func (t UploadGenesisArtifactsTask) ToTaskRequest() TaskRequest {
672630
p := map[string]interface{}{
673-
"s3Bucket": t.S3Bucket,
674-
"s3Prefix": t.S3Prefix,
675-
"s3Region": t.S3Region,
676631
"nodeName": t.NodeName,
677632
}
678633
req := TaskRequest{Type: t.TaskType(), Params: &p}
@@ -686,26 +641,22 @@ type GenesisNodeParam struct {
686641
}
687642

688643
// AssembleAndUploadGenesisTask collects per-node artifacts and produces final genesis.json.
644+
// S3 coordinates are derived by the sidecar from its environment.
689645
type AssembleAndUploadGenesisTask struct {
690646
TaskMeta
691-
S3Bucket string
692-
S3Prefix string
693-
S3Region string
694-
ChainID string
695-
Nodes []GenesisNodeParam
647+
AccountBalance string
648+
Namespace string
649+
Nodes []GenesisNodeParam
696650
}
697651

698652
func (t AssembleAndUploadGenesisTask) TaskType() string { return TaskTypeAssembleGenesis }
699653

700654
func (t AssembleAndUploadGenesisTask) Validate() error {
701-
if t.S3Bucket == "" {
702-
return fmt.Errorf("assemble-and-upload-genesis: missing required field S3Bucket")
703-
}
704-
if t.S3Region == "" {
705-
return fmt.Errorf("assemble-and-upload-genesis: missing required field S3Region")
655+
if t.AccountBalance == "" {
656+
return fmt.Errorf("assemble-and-upload-genesis: missing required field AccountBalance")
706657
}
707-
if t.ChainID == "" {
708-
return fmt.Errorf("assemble-and-upload-genesis: missing required field ChainID")
658+
if t.Namespace == "" {
659+
return fmt.Errorf("assemble-and-upload-genesis: missing required field Namespace")
709660
}
710661
if len(t.Nodes) == 0 {
711662
return fmt.Errorf("assemble-and-upload-genesis: at least one node is required")
@@ -719,11 +670,9 @@ func (t AssembleAndUploadGenesisTask) ToTaskRequest() TaskRequest {
719670
nodes[i] = map[string]interface{}{"name": n.Name}
720671
}
721672
p := map[string]interface{}{
722-
"s3Bucket": t.S3Bucket,
723-
"s3Prefix": t.S3Prefix,
724-
"s3Region": t.S3Region,
725-
"chainId": t.ChainID,
726-
"nodes": nodes,
673+
"accountBalance": t.AccountBalance,
674+
"namespace": t.Namespace,
675+
"nodes": nodes,
727676
}
728677
req := TaskRequest{Type: t.TaskType(), Params: &p}
729678
t.applyMeta(&req)

sidecar/client/tasks_test.go

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,7 @@ func genSnapshotUploadTask() gopter.Gen {
4444
}
4545

4646
func genConfigureGenesisTask() gopter.Gen {
47-
return gopter.CombineGens(
48-
genNonEmptyString(),
49-
genNonEmptyString(),
50-
genNonEmptyString(),
51-
).Map(func(v []interface{}) ConfigureGenesisTask {
52-
return ConfigureGenesisTask{
53-
URI: "s3://" + v[0].(string) + "/" + v[1].(string),
54-
Region: v[2].(string),
55-
}
56-
})
47+
return gen.Const(ConfigureGenesisTask{})
5748
}
5849

5950
func genEC2TagsSource() gopter.Gen {
@@ -154,37 +145,19 @@ func TestSnapshotUploadRoundTrip(t *testing.T) {
154145

155146
func TestConfigureGenesisRoundTrip_S3(t *testing.T) {
156147
properties := gopter.NewProperties(gopter.DefaultTestParameters())
157-
properties.Property("ConfigureGenesisTask (S3) round-trips through TaskRequest", prop.ForAll(
148+
properties.Property("ConfigureGenesisTask round-trips through TaskRequest", prop.ForAll(
158149
func(task ConfigureGenesisTask) bool {
159150
if err := task.Validate(); err != nil {
160151
return false
161152
}
162153
req := task.ToTaskRequest()
163-
if req.Type != TaskTypeConfigureGenesis {
164-
return false
165-
}
166-
rebuilt := ConfigureGenesisTaskFromParams(*req.Params)
167-
return rebuilt.URI == task.URI && rebuilt.Region == task.Region
154+
return req.Type == TaskTypeConfigureGenesis && req.Params == nil
168155
},
169156
genConfigureGenesisTask(),
170157
))
171158
properties.TestingRun(t)
172159
}
173160

174-
func TestConfigureGenesisRoundTrip_Empty(t *testing.T) {
175-
task := ConfigureGenesisTask{}
176-
if err := task.Validate(); err != nil {
177-
t.Fatalf("Validate: %v", err)
178-
}
179-
req := task.ToTaskRequest()
180-
if req.Type != TaskTypeConfigureGenesis {
181-
t.Errorf("Type = %q, want %q", req.Type, TaskTypeConfigureGenesis)
182-
}
183-
if req.Params != nil {
184-
t.Errorf("expected nil Params for empty genesis task, got %v", req.Params)
185-
}
186-
}
187-
188161
func TestDiscoverPeersRoundTrip(t *testing.T) {
189162
properties := gopter.NewProperties(gopter.DefaultTestParameters())
190163
properties.Property("DiscoverPeersTask round-trips through TaskRequest", prop.ForAll(
@@ -342,28 +315,9 @@ func TestSnapshotUploadValidation(t *testing.T) {
342315
}
343316

344317
func TestConfigureGenesisValidation(t *testing.T) {
345-
cases := []struct {
346-
name string
347-
task ConfigureGenesisTask
348-
ok bool
349-
}{
350-
{"valid s3", ConfigureGenesisTask{URI: "s3://bucket/key", Region: "us-east-1"}, true},
351-
{"empty (uses embedded)", ConfigureGenesisTask{}, true},
352-
{"uri without region", ConfigureGenesisTask{URI: "s3://bucket/key"}, false},
353-
{"wrong scheme", ConfigureGenesisTask{URI: "https://bucket/key", Region: "us-east-1"}, false},
354-
{"no key", ConfigureGenesisTask{URI: "s3://bucket", Region: "us-east-1"}, false},
355-
{"no key trailing slash", ConfigureGenesisTask{URI: "s3://bucket/", Region: "us-east-1"}, false},
356-
}
357-
for _, tc := range cases {
358-
t.Run(tc.name, func(t *testing.T) {
359-
err := tc.task.Validate()
360-
if tc.ok && err != nil {
361-
t.Errorf("expected no error, got %v", err)
362-
}
363-
if !tc.ok && err == nil {
364-
t.Error("expected validation error, got nil")
365-
}
366-
})
318+
task := ConfigureGenesisTask{}
319+
if err := task.Validate(); err != nil {
320+
t.Errorf("expected no error, got %v", err)
367321
}
368322
}
369323

0 commit comments

Comments
 (0)