Skip to content
Closed
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
419 changes: 419 additions & 0 deletions .tide/genesis-execution-plan.md

Large diffs are not rendered by default.

799 changes: 799 additions & 0 deletions .tide/network-deployments.md

Large diffs are not rendered by default.

489 changes: 489 additions & 0 deletions .tide/network-upgrades.md

Large diffs are not rendered by default.

926 changes: 926 additions & 0 deletions .tide/observability.md

Large diffs are not rendered by default.

699 changes: 699 additions & 0 deletions .tide/seinodegroup-scheduling.md

Large diffs are not rendered by default.

411 changes: 411 additions & 0 deletions .tide/validator-migration.md

Large diffs are not rendered by default.

79 changes: 74 additions & 5 deletions api/v1alpha1/seinodegroup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type SeiNodeGroupSpec struct {
// +kubebuilder:default=Delete
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`

// Genesis configures genesis ceremony orchestration for this group.
// When set, the controller generates GenesisCeremonyNodeConfig for each
// child SeiNode and coordinates assembly of the final genesis.json.
// +optional
Genesis *GenesisCeremonyConfig `json:"genesis,omitempty"`

// Networking controls how the group is exposed to traffic.
// Networking resources are shared across all replicas.
// +optional
Expand All @@ -37,6 +43,56 @@ type SeiNodeGroupSpec struct {
Monitoring *MonitoringConfig `json:"monitoring,omitempty"`
}

// GenesisCeremonyConfig configures genesis ceremony orchestration for a node group.
type GenesisCeremonyConfig struct {
// ChainID for the new network.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:Pattern=`^[a-z0-9][a-z0-9-]*[a-z0-9]$`
ChainID string `json:"chainId"`

// StakingAmount is the amount each validator self-delegates in its gentx.
// +optional
// +kubebuilder:default="10000000usei"
StakingAmount string `json:"stakingAmount,omitempty"`

// AccountBalance is the initial balance for each validator's genesis account.
// +optional
// +kubebuilder:default="1000000000000000000000usei,1000000000000000000000uusdc,1000000000000000000000uatom"
AccountBalance string `json:"accountBalance,omitempty"`

// Accounts adds non-validator genesis accounts (e.g. for load test funding).
// +optional
Accounts []GenesisAccount `json:"accounts,omitempty"`

// Overrides is a flat map of dotted key paths merged on top of sei-config's
// GenesisDefaults(). Applied BEFORE gentx generation.
// +optional
Overrides map[string]string `json:"overrides,omitempty"`

// GenesisS3 configures where genesis artifacts are stored.
// When omitted, inferred: bucket = "sei-genesis-artifacts",
// prefix = "<chainId>/<group-name>/", region from PlatformConfig.
// +optional
GenesisS3 *GenesisS3Destination `json:"genesisS3,omitempty"`

// MaxCeremonyDuration is the maximum time from group creation to genesis
// assembly completion. Default: "15m".
// +optional
MaxCeremonyDuration *metav1.Duration `json:"maxCeremonyDuration,omitempty"`
}

// GenesisAccount represents a non-validator genesis account to fund.
type GenesisAccount struct {
// Address is the bech32-encoded account address.
// +kubebuilder:validation:MinLength=1
Address string `json:"address"`

// Balance is the initial balance in coin notation (e.g. "1000000usei").
// +kubebuilder:validation:MinLength=1
Balance string `json:"balance"`
}

// SeiNodeTemplate wraps a SeiNodeSpec for use in the group template.
type SeiNodeTemplate struct {
// Metadata allows setting labels and annotations on child SeiNodes.
Expand Down Expand Up @@ -98,6 +154,18 @@ type SeiNodeGroupStatus struct {
// +optional
Nodes []GroupNodeStatus `json:"nodes,omitempty"`

// InitPlan tracks group-level initialization tasks (e.g. genesis assembly).
// +optional
InitPlan *TaskPlan `json:"initPlan,omitempty"`

// GenesisHash is the SHA-256 hex digest of the assembled genesis.json.
// +optional
GenesisHash string `json:"genesisHash,omitempty"`

// GenesisS3URI is the S3 URI of the uploaded genesis.
// +optional
GenesisS3URI string `json:"genesisS3URI,omitempty"`

// NetworkingStatus reports the observed state of networking resources.
// +optional
NetworkingStatus *NetworkingStatus `json:"networkingStatus,omitempty"`
Expand Down Expand Up @@ -131,11 +199,12 @@ type NetworkingStatus struct {

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

// +kubebuilder:object:root=true
Expand Down
50 changes: 50 additions & 0 deletions api/v1alpha1/validator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,54 @@ type ValidatorSpec struct {
// When absent the node block-syncs from genesis.
// +optional
Snapshot *SnapshotSource `json:"snapshot,omitempty"`

// GenesisCeremony indicates this validator participates in a group genesis
// ceremony. Set by the SeiNodeGroup controller — not intended for direct use.
// +optional
GenesisCeremony *GenesisCeremonyNodeConfig `json:"genesisCeremony,omitempty"`
}

// GenesisCeremonyNodeConfig holds per-node genesis ceremony parameters.
// Populated by the SeiNodeGroup controller when genesis is configured.
type GenesisCeremonyNodeConfig struct {
// ChainID of the genesis network.
// +kubebuilder:validation:MinLength=1
ChainID string `json:"chainId"`

// StakingAmount is the self-delegation amount for this validator's gentx.
// +kubebuilder:validation:MinLength=1
StakingAmount string `json:"stakingAmount"`

// AccountBalance is the initial coin balance to fund this validator's
// genesis account. The node's own address is discovered during identity
// generation — no cross-node coordination needed.
// +kubebuilder:validation:MinLength=1
AccountBalance string `json:"accountBalance"`

// GenesisParams is a JSON string of genesis parameter overrides merged
// on top of sei-config's GenesisDefaults(). Applied before gentx generation.
// +optional
GenesisParams string `json:"genesisParams,omitempty"`

// Index is the node's ordinal within the group (0-based).
// +kubebuilder:validation:Minimum=0
Index int32 `json:"index"`

// ArtifactS3 is the S3 location where this node uploads its genesis artifacts.
ArtifactS3 GenesisS3Destination `json:"artifactS3"`
}

// GenesisS3Destination configures where genesis artifacts are stored in S3.
type GenesisS3Destination struct {
// Bucket is the S3 bucket name.
// +kubebuilder:validation:MinLength=1
Bucket string `json:"bucket"`

// Prefix is an optional key prefix within the bucket.
// +optional
Prefix string `json:"prefix,omitempty"`

// Region is the AWS region for S3 access.
// +kubebuilder:validation:MinLength=1
Region string `json:"region"`
}
98 changes: 98 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.

7 changes: 6 additions & 1 deletion internal/controller/node/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,16 @@ func (r *SeiNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}

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

node.Status.PreInitPlan = buildPreInitPlan(node, planner)
if needsPreInit(node) {
if isGenesisCeremonyNode(node) {
node.Status.InitPlan = buildGenesisInitPlan()
} else if needsPreInit(node) {
node.Status.InitPlan = buildPostBootstrapInitPlan(node)
} else {
node.Status.InitPlan = planner.BuildPlan(node)
Expand Down
13 changes: 11 additions & 2 deletions internal/controller/node/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ func preInitJobName(node *seiv1alpha1.SeiNode) string {

func generatePreInitJob(node *seiv1alpha1.SeiNode, platform PlatformConfig) *batchv1.Job {
labels := preInitLabelsForNode(node)

snap := snapshotSourceFor(node)
podSpec := buildPreInitPodSpec(node, snap, platform)

return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -41,7 +43,7 @@ func generatePreInitJob(node *seiv1alpha1.SeiNode, platform PlatformConfig) *bat
"karpenter.sh/do-not-disrupt": "true",
},
},
Spec: buildPreInitPodSpec(node, snap, platform),
Spec: podSpec,
},
},
}
Expand Down Expand Up @@ -72,8 +74,15 @@ func generatePreInitService(node *seiv1alpha1.SeiNode) *corev1.Service {

// preInitSidecarURL returns the in-cluster DNS URL for the pre-init Job's sidecar.
func preInitSidecarURL(node *seiv1alpha1.SeiNode) string {
return PreInitSidecarURL(node.Name, node.Namespace, sidecarPort(node))
}

// PreInitSidecarURL builds the in-cluster DNS URL for a pre-init Job's sidecar
// given the node name, namespace, and port. Exported for use by the group controller.
func PreInitSidecarURL(nodeName, namespace string, port int32) string {
jobName := fmt.Sprintf("%s-pre-init", nodeName)
return fmt.Sprintf("http://%s.%s.%s.svc.cluster.local:%d",
preInitPodHostname, preInitJobName(node), node.Namespace, sidecarPort(node))
preInitPodHostname, jobName, namespace, port)
}

// preInitWaitCommand returns a shell command that waits for the sidecar
Expand Down
13 changes: 13 additions & 0 deletions internal/controller/node/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ func needsPreInit(node *seiv1alpha1.SeiNode) bool {
snap.S3 != nil && snap.S3.TargetHeight > 0
}

// isGenesisCeremonyNode returns true when the node participates in a group genesis ceremony.
func isGenesisCeremonyNode(node *seiv1alpha1.SeiNode) bool {
return node.Spec.Validator != nil && node.Spec.Validator.GenesisCeremony != nil
}

// snapshotGeneration extracts the SnapshotGenerationConfig from the populated
// mode sub-spec. Returns nil when the mode doesn't support it.
func snapshotGeneration(node *seiv1alpha1.SeiNode) *seiv1alpha1.SnapshotGenerationConfig {
Expand Down Expand Up @@ -197,6 +202,14 @@ func buildSharedTask(
return sidecar.MarkReadyTask{}, nil
case taskAwaitCondition:
return awaitConditionTask(node)
case taskGenerateIdentity:
return generateIdentityTaskBuilder(node), nil
case taskGenerateGentx:
return generateGentxTaskBuilder(node), nil
case taskUploadGenesisArtifacts:
return uploadGenesisArtifactsTaskBuilder(node), nil
case taskAwaitGenesisAssembly:
return awaitGenesisAssemblyTaskBuilder(node), nil
default:
return nil, fmt.Errorf("buildSharedTask: unhandled task type %q", taskType)
}
Expand Down
Loading
Loading