Skip to content

Commit 1c957c7

Browse files
committed
feat: genesis ceremony controller orchestration
Add group-level genesis ceremony support to SeiNodeGroup and per-node genesis configuration to SeiNode/ValidatorSpec. The node controller builds the full Init plan upfront (no deferred steps) and the group controller coordinates assembly, peer collection, and static peer injection. New CRD types: GenesisCeremonyConfig, GenesisCeremonyNodeConfig, GenesisS3Destination, GenesisS3Source. New group status fields: AssemblyPlan, GenesisHash, GenesisS3URI. Depends on seictl genesis ceremony client types (sei-protocol/seictl#40).
1 parent a68ec2b commit 1c957c7

12 files changed

Lines changed: 725 additions & 24 deletions

File tree

api/v1alpha1/seinodegroup_types.go

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type SeiNodeGroupSpec struct {
2626
// +kubebuilder:default=Delete
2727
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
2828

29+
// Genesis configures genesis ceremony orchestration for this group.
30+
// When set, the controller generates GenesisCeremonyNodeConfig for each
31+
// child SeiNode and coordinates assembly of the final genesis.json.
32+
// +optional
33+
Genesis *GenesisCeremonyConfig `json:"genesis,omitempty"`
34+
2935
// Networking controls how the group is exposed to traffic.
3036
// Networking resources are shared across all replicas.
3137
// +optional
@@ -37,6 +43,56 @@ type SeiNodeGroupSpec struct {
3743
Monitoring *MonitoringConfig `json:"monitoring,omitempty"`
3844
}
3945

46+
// GenesisCeremonyConfig configures genesis ceremony orchestration for a node group.
47+
type GenesisCeremonyConfig struct {
48+
// ChainID for the new network.
49+
// +kubebuilder:validation:MinLength=1
50+
// +kubebuilder:validation:MaxLength=64
51+
// +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9-]*[a-z0-9]$`
52+
ChainID string `json:"chainId"`
53+
54+
// StakingAmount is the amount each validator self-delegates in its gentx.
55+
// +optional
56+
// +kubebuilder:default="10000000usei"
57+
StakingAmount string `json:"stakingAmount,omitempty"`
58+
59+
// AccountBalance is the initial balance for each validator's genesis account.
60+
// +optional
61+
// +kubebuilder:default="1000000000000000000000usei,1000000000000000000000uusdc,1000000000000000000000uatom"
62+
AccountBalance string `json:"accountBalance,omitempty"`
63+
64+
// Accounts adds non-validator genesis accounts (e.g. for load test funding).
65+
// +optional
66+
Accounts []GenesisAccount `json:"accounts,omitempty"`
67+
68+
// Overrides is a flat map of dotted key paths merged on top of sei-config's
69+
// GenesisDefaults(). Applied BEFORE gentx generation.
70+
// +optional
71+
Overrides map[string]string `json:"overrides,omitempty"`
72+
73+
// GenesisS3 configures where genesis artifacts are stored.
74+
// When omitted, inferred: bucket = "sei-genesis-artifacts",
75+
// prefix = "<chainId>/<group-name>/", region from PlatformConfig.
76+
// +optional
77+
GenesisS3 *GenesisS3Destination `json:"genesisS3,omitempty"`
78+
79+
// MaxCeremonyDuration is the maximum time from group creation to genesis
80+
// assembly completion. Default: "15m".
81+
// +optional
82+
MaxCeremonyDuration *metav1.Duration `json:"maxCeremonyDuration,omitempty"`
83+
}
84+
85+
// GenesisAccount represents a non-validator genesis account to fund.
86+
type GenesisAccount struct {
87+
// Address is the bech32-encoded account address.
88+
// +kubebuilder:validation:MinLength=1
89+
Address string `json:"address"`
90+
91+
// Balance is the initial balance in coin notation (e.g. "1000000usei").
92+
// +kubebuilder:validation:MinLength=1
93+
Balance string `json:"balance"`
94+
}
95+
4096
// SeiNodeTemplate wraps a SeiNodeSpec for use in the group template.
4197
type SeiNodeTemplate struct {
4298
// Metadata allows setting labels and annotations on child SeiNodes.
@@ -65,16 +121,17 @@ type SeiNodeTemplateMeta struct {
65121
// ---------------------------------------------------------------------------
66122

67123
// SeiNodeGroupPhase represents the high-level lifecycle state.
68-
// +kubebuilder:validation:Enum=Pending;Initializing;Ready;Degraded;Failed;Terminating
124+
// +kubebuilder:validation:Enum=Pending;GenesisCeremony;Initializing;Ready;Degraded;Failed;Terminating
69125
type SeiNodeGroupPhase string
70126

71127
const (
72-
GroupPhasePending SeiNodeGroupPhase = "Pending"
73-
GroupPhaseInitializing SeiNodeGroupPhase = "Initializing"
74-
GroupPhaseReady SeiNodeGroupPhase = "Ready"
75-
GroupPhaseDegraded SeiNodeGroupPhase = "Degraded"
76-
GroupPhaseFailed SeiNodeGroupPhase = "Failed"
77-
GroupPhaseTerminating SeiNodeGroupPhase = "Terminating"
128+
GroupPhasePending SeiNodeGroupPhase = "Pending"
129+
GroupPhaseGenesisCeremony SeiNodeGroupPhase = "GenesisCeremony"
130+
GroupPhaseInitializing SeiNodeGroupPhase = "Initializing"
131+
GroupPhaseReady SeiNodeGroupPhase = "Ready"
132+
GroupPhaseDegraded SeiNodeGroupPhase = "Degraded"
133+
GroupPhaseFailed SeiNodeGroupPhase = "Failed"
134+
GroupPhaseTerminating SeiNodeGroupPhase = "Terminating"
78135
)
79136

80137
// SeiNodeGroupStatus defines the observed state of a SeiNodeGroup.
@@ -98,6 +155,18 @@ type SeiNodeGroupStatus struct {
98155
// +optional
99156
Nodes []GroupNodeStatus `json:"nodes,omitempty"`
100157

158+
// AssemblyPlan tracks the genesis assembly task on index 0's sidecar.
159+
// +optional
160+
AssemblyPlan *TaskPlan `json:"assemblyPlan,omitempty"`
161+
162+
// GenesisHash is the SHA-256 hex digest of the assembled genesis.json.
163+
// +optional
164+
GenesisHash string `json:"genesisHash,omitempty"`
165+
166+
// GenesisS3URI is the S3 URI of the uploaded genesis.
167+
// +optional
168+
GenesisS3URI string `json:"genesisS3URI,omitempty"`
169+
101170
// NetworkingStatus reports the observed state of networking resources.
102171
// +optional
103172
NetworkingStatus *NetworkingStatus `json:"networkingStatus,omitempty"`
@@ -131,11 +200,12 @@ type NetworkingStatus struct {
131200

132201
// Status condition types for SeiNodeGroup.
133202
const (
134-
ConditionNodesReady = "NodesReady"
135-
ConditionExternalServiceReady = "ExternalServiceReady"
136-
ConditionRouteReady = "RouteReady"
137-
ConditionIsolationReady = "IsolationReady"
138-
ConditionServiceMonitorReady = "ServiceMonitorReady"
203+
ConditionNodesReady = "NodesReady"
204+
ConditionExternalServiceReady = "ExternalServiceReady"
205+
ConditionRouteReady = "RouteReady"
206+
ConditionIsolationReady = "IsolationReady"
207+
ConditionServiceMonitorReady = "ServiceMonitorReady"
208+
ConditionGenesisCeremonyComplete = "GenesisCeremonyComplete"
139209
)
140210

141211
// +kubebuilder:object:root=true

api/v1alpha1/validator_types.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,54 @@ type ValidatorSpec struct {
1111
// When absent the node block-syncs from genesis.
1212
// +optional
1313
Snapshot *SnapshotSource `json:"snapshot,omitempty"`
14+
15+
// GenesisCeremony indicates this validator participates in a group genesis
16+
// ceremony. Set by the SeiNodeGroup controller — not intended for direct use.
17+
// +optional
18+
GenesisCeremony *GenesisCeremonyNodeConfig `json:"genesisCeremony,omitempty"`
19+
}
20+
21+
// GenesisCeremonyNodeConfig holds per-node genesis ceremony parameters.
22+
// Populated by the SeiNodeGroup controller when genesis is configured.
23+
type GenesisCeremonyNodeConfig struct {
24+
// ChainID of the genesis network.
25+
// +kubebuilder:validation:MinLength=1
26+
ChainID string `json:"chainId"`
27+
28+
// StakingAmount is the self-delegation amount for this validator's gentx.
29+
// +kubebuilder:validation:MinLength=1
30+
StakingAmount string `json:"stakingAmount"`
31+
32+
// AccountBalance is the initial coin balance to fund this validator's
33+
// genesis account. The node's own address is discovered during identity
34+
// generation — no cross-node coordination needed.
35+
// +kubebuilder:validation:MinLength=1
36+
AccountBalance string `json:"accountBalance"`
37+
38+
// GenesisParams is a JSON string of genesis parameter overrides merged
39+
// on top of sei-config's GenesisDefaults(). Applied before gentx generation.
40+
// +optional
41+
GenesisParams string `json:"genesisParams,omitempty"`
42+
43+
// Index is the node's ordinal within the group (0-based).
44+
// +kubebuilder:validation:Minimum=0
45+
Index int32 `json:"index"`
46+
47+
// ArtifactS3 is the S3 location where this node uploads its genesis artifacts.
48+
ArtifactS3 GenesisS3Destination `json:"artifactS3"`
49+
}
50+
51+
// GenesisS3Destination configures where genesis artifacts are stored in S3.
52+
type GenesisS3Destination struct {
53+
// Bucket is the S3 bucket name.
54+
// +kubebuilder:validation:MinLength=1
55+
Bucket string `json:"bucket"`
56+
57+
// Prefix is an optional key prefix within the bucket.
58+
// +optional
59+
Prefix string `json:"prefix,omitempty"`
60+
61+
// Region is the AWS region for S3 access.
62+
// +kubebuilder:validation:MinLength=1
63+
Region string `json:"region"`
1464
}

internal/controller/node/controller.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,16 @@ func (r *SeiNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
137137
}
138138

139139
// reconcilePending creates all plans up front and selects the starting phase.
140+
// The Init plan is always built here — for genesis ceremony nodes the S3
141+
// source is deterministic and discover-peers is unconditionally included.
142+
// Plan execution order is the single source of truth for orchestration.
140143
func (r *SeiNodeReconciler) reconcilePending(ctx context.Context, node *seiv1alpha1.SeiNode, planner NodePlanner) (ctrl.Result, error) {
141144
patch := client.MergeFrom(node.DeepCopy())
142145

143146
node.Status.PreInitPlan = buildPreInitPlan(node, planner)
144-
if needsPreInit(node) {
147+
if isGenesisCeremonyNode(node) {
148+
node.Status.InitPlan = buildGenesisInitPlan()
149+
} else if needsPreInit(node) {
145150
node.Status.InitPlan = buildPostBootstrapInitPlan(node)
146151
} else {
147152
node.Status.InitPlan = planner.BuildPlan(node)

internal/controller/node/job.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ func preInitJobName(node *seiv1alpha1.SeiNode) string {
2323

2424
func generatePreInitJob(node *seiv1alpha1.SeiNode, platform PlatformConfig) *batchv1.Job {
2525
labels := preInitLabelsForNode(node)
26-
snap := snapshotSourceFor(node)
26+
27+
var podSpec corev1.PodSpec
28+
if isGenesisCeremonyNode(node) {
29+
podSpec = buildGenesisPreInitPodSpec(node, platform)
30+
} else {
31+
snap := snapshotSourceFor(node)
32+
podSpec = buildPreInitPodSpec(node, snap, platform)
33+
}
2734

2835
return &batchv1.Job{
2936
ObjectMeta: metav1.ObjectMeta{
@@ -41,7 +48,7 @@ func generatePreInitJob(node *seiv1alpha1.SeiNode, platform PlatformConfig) *bat
4148
"karpenter.sh/do-not-disrupt": "true",
4249
},
4350
},
44-
Spec: buildPreInitPodSpec(node, snap, platform),
51+
Spec: podSpec,
4552
},
4653
},
4754
}
@@ -72,8 +79,15 @@ func generatePreInitService(node *seiv1alpha1.SeiNode) *corev1.Service {
7279

7380
// preInitSidecarURL returns the in-cluster DNS URL for the pre-init Job's sidecar.
7481
func preInitSidecarURL(node *seiv1alpha1.SeiNode) string {
82+
return PreInitSidecarURL(node.Name, node.Namespace, sidecarPort(node))
83+
}
84+
85+
// PreInitSidecarURL builds the in-cluster DNS URL for a pre-init Job's sidecar
86+
// given the node name, namespace, and port. Exported for use by the group controller.
87+
func PreInitSidecarURL(nodeName, namespace string, port int32) string {
88+
jobName := fmt.Sprintf("%s-pre-init", nodeName)
7589
return fmt.Sprintf("http://%s.%s.%s.svc.cluster.local:%d",
76-
preInitPodHostname, preInitJobName(node), node.Namespace, sidecarPort(node))
90+
preInitPodHostname, jobName, namespace, port)
7791
}
7892

7993
// preInitWaitCommand returns a shell command that waits for the sidecar
@@ -99,6 +113,78 @@ func preInitWaitCommand(port int32, haltHeight int64) (command []string, args []
99113
return []string{"/bin/bash", "-c"}, []string{script}
100114
}
101115

116+
// buildGenesisPreInitPodSpec constructs a PodSpec for the genesis ceremony PreInit Job.
117+
// Unlike the snapshot PreInit, the main container runs "sleep infinity" as a keepalive
118+
// (the sidecar does the real work), and an init container copies the seid binary from
119+
// the node image to the PVC so sidecar tasks can invoke it.
120+
func buildGenesisPreInitPodSpec(node *seiv1alpha1.SeiNode, platform PlatformConfig) corev1.PodSpec {
121+
serviceName := preInitJobName(node)
122+
123+
dataVolume := corev1.Volume{
124+
Name: "data",
125+
VolumeSource: corev1.VolumeSource{
126+
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
127+
ClaimName: nodeDataPVCClaimName(node),
128+
},
129+
},
130+
}
131+
132+
port := sidecarPort(node)
133+
sidecarContainer := corev1.Container{
134+
Name: "sei-sidecar",
135+
Image: sidecarImage(node),
136+
Command: []string{"seictl", "serve"},
137+
RestartPolicy: ptr.To(corev1.ContainerRestartPolicyAlways),
138+
Env: []corev1.EnvVar{
139+
{Name: "SEI_CHAIN_ID", Value: node.Spec.ChainID},
140+
{Name: "SEI_SIDECAR_PORT", Value: fmt.Sprintf("%d", port)},
141+
{Name: "SEI_HOME", Value: dataDir},
142+
},
143+
Ports: []corev1.ContainerPort{
144+
{Name: "sidecar", ContainerPort: port, Protocol: corev1.ProtocolTCP},
145+
},
146+
VolumeMounts: []corev1.VolumeMount{
147+
{Name: "data", MountPath: dataDir},
148+
},
149+
}
150+
if node.Spec.Sidecar != nil && node.Spec.Sidecar.Resources != nil {
151+
sidecarContainer.Resources = *node.Spec.Sidecar.Resources
152+
}
153+
154+
copySeid := corev1.Container{
155+
Name: "copy-seid",
156+
Image: node.Spec.Image,
157+
Command: []string{"sh", "-c", fmt.Sprintf("mkdir -p %s/bin && cp $(which seid) %s/bin/seid", dataDir, dataDir)},
158+
VolumeMounts: []corev1.VolumeMount{
159+
{Name: "data", MountPath: dataDir},
160+
},
161+
}
162+
163+
keepalive := corev1.Container{
164+
Name: "keepalive",
165+
Image: node.Spec.Image,
166+
Command: []string{"sleep", "infinity"},
167+
VolumeMounts: []corev1.VolumeMount{
168+
{Name: "data", MountPath: dataDir},
169+
},
170+
}
171+
172+
return corev1.PodSpec{
173+
Hostname: preInitPodHostname,
174+
Subdomain: serviceName,
175+
ServiceAccountName: platform.ServiceAccount,
176+
ShareProcessNamespace: ptr.To(true),
177+
RestartPolicy: corev1.RestartPolicyNever,
178+
TerminationGracePeriodSeconds: ptr.To(int64(5)),
179+
Tolerations: []corev1.Toleration{
180+
{Key: platform.TolerationKey, Value: platform.TolerationVal, Effect: corev1.TaintEffectNoSchedule},
181+
},
182+
Volumes: []corev1.Volume{dataVolume},
183+
InitContainers: []corev1.Container{copySeid, sidecarContainer},
184+
Containers: []corev1.Container{keepalive},
185+
}
186+
}
187+
102188
func buildPreInitPodSpec(node *seiv1alpha1.SeiNode, snap *seiv1alpha1.SnapshotSource, platform PlatformConfig) corev1.PodSpec {
103189
serviceName := preInitJobName(node)
104190

internal/controller/node/planner.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,19 @@ func peersFor(node *seiv1alpha1.SeiNode) []seiv1alpha1.PeerSource {
8080

8181
// needsPreInit returns true when the node requires a PreInitPlan Job.
8282
func needsPreInit(node *seiv1alpha1.SeiNode) bool {
83+
if isGenesisCeremonyNode(node) {
84+
return true
85+
}
8386
snap := snapshotSourceFor(node)
8487
return snap != nil && snap.BootstrapImage != "" &&
8588
snap.S3 != nil && snap.S3.TargetHeight > 0
8689
}
8790

91+
// isGenesisCeremonyNode returns true when the node participates in a group genesis ceremony.
92+
func isGenesisCeremonyNode(node *seiv1alpha1.SeiNode) bool {
93+
return node.Spec.Validator != nil && node.Spec.Validator.GenesisCeremony != nil
94+
}
95+
8896
// snapshotGeneration extracts the SnapshotGenerationConfig from the populated
8997
// mode sub-spec. Returns nil when the mode doesn't support it.
9098
func snapshotGeneration(node *seiv1alpha1.SeiNode) *seiv1alpha1.SnapshotGenerationConfig {
@@ -197,6 +205,12 @@ func buildSharedTask(
197205
return sidecar.MarkReadyTask{}, nil
198206
case taskAwaitCondition:
199207
return awaitConditionTask(node)
208+
case taskGenerateIdentity:
209+
return generateIdentityTaskBuilder(node), nil
210+
case taskGenerateGentx:
211+
return generateGentxTaskBuilder(node), nil
212+
case taskUploadGenesisArtifacts:
213+
return uploadGenesisArtifactsTaskBuilder(node), nil
200214
default:
201215
return nil, fmt.Errorf("buildSharedTask: unhandled task type %q", taskType)
202216
}

0 commit comments

Comments
 (0)