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
1 change: 1 addition & 0 deletions api/v1alpha1/seinodedeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
//
// +kubebuilder:validation:XValidation:rule="!has(self.genesis) || has(self.template.spec.validator)",message="genesis is meaningful only for validator-role deployments (full nodes inherit genesis from the validator ceremony's S3 artifact); remove spec.genesis or set template.spec.validator: {}"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.genesis) || (has(self.genesis) && self.genesis == oldSelf.genesis)",message="spec.genesis is immutable once set; the ceremony's outputs (chain ID, validator gentxs, account balances) are baked into chain state and cannot be retroactively rewritten by editing the spec"
// +kubebuilder:validation:XValidation:rule="!has(self.template.spec.validator) || !has(self.template.spec.validator.signingKey) || self.replicas == 1",message="a validator with a signingKey must have replicas: 1 — every replica mounts the same priv_validator_key.json, so >1 replica double-signs (equivocation) and tombstones/slashes the validator"
type SeiNodeDeploymentSpec struct {
// Replicas is the number of SeiNode instances to create.
// +kubebuilder:validation:Minimum=1
Expand Down
5 changes: 5 additions & 0 deletions config/crd/sei.io_seinodedeployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,11 @@ spec:
state and cannot be retroactively rewritten by editing the spec
rule: '!has(oldSelf.genesis) || (has(self.genesis) && self.genesis ==
oldSelf.genesis)'
- message: 'a validator with a signingKey must have replicas: 1 — every
replica mounts the same priv_validator_key.json, so >1 replica double-signs
(equivocation) and tombstones/slashes the validator'
rule: '!has(self.template.spec.validator) || !has(self.template.spec.validator.signingKey)
|| self.replicas == 1'
status:
description: SeiNodeDeploymentStatus defines the observed state of a SeiNodeDeployment.
properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//go:build envtest

package envtest_test

import (
"testing"

. "github.com/onsi/gomega"

"sigs.k8s.io/controller-runtime/pkg/client"

seiv1alpha1 "github.com/sei-protocol/sei-k8s-controller/api/v1alpha1"
"github.com/sei-protocol/sei-k8s-controller/internal/controller/nodedeployment/envtest/fixtures"
)

// withSigningAndNodeKey attaches a signingKey + nodeKey to an existing
// validator spec, referencing distinct Secrets. Distinct names satisfy the
// signingKey/nodeKey-paired CEL rules so a test can isolate the
// replicas==1 rule. Requires WithValidator() to have set Validator first.
func withSigningAndNodeKey(snd *seiv1alpha1.SeiNodeDeployment) {
snd.Spec.Template.Spec.Validator.SigningKey = &seiv1alpha1.SigningKeySource{
Secret: &seiv1alpha1.SecretSigningKeySource{SecretName: "test-signing-key"},
}
snd.Spec.Template.Spec.Validator.NodeKey = &seiv1alpha1.NodeKeySource{
Secret: &seiv1alpha1.SecretNodeKeySource{SecretName: "test-node-key"},
}
}

// TestValidator_SigningKeyRequiresSingleReplica asserts the spec-level CEL
// rule that rejects replicas > 1 when validator.signingKey is set. Every
// replica mounts the same priv_validator_key.json, so more than one replica
// double-signs (equivocation) and tombstones/slashes the validator.
func TestValidator_SigningKeyRequiresSingleReplica(t *testing.T) {
t.Run("signingKey with replicas 1 is accepted", func(t *testing.T) {
g := NewWithT(t)
ns := makeNamespace(t)
snd := fixtures.NewSND(ns, "val-keys-r1", fixtures.WithValidator(), fixtures.WithReplicas(1))
withSigningAndNodeKey(snd)
g.Expect(testCli.Create(testCtx, snd)).To(Succeed(),
"a signing validator with replicas:1 must be accepted")
})

t.Run("signingKey with replicas 2 is rejected", func(t *testing.T) {
g := NewWithT(t)
ns := makeNamespace(t)
snd := fixtures.NewSND(ns, "val-keys-r2", fixtures.WithValidator(), fixtures.WithReplicas(2))
withSigningAndNodeKey(snd)
err := testCli.Create(testCtx, snd)
g.Expect(err).To(HaveOccurred(), "a signing validator with replicas:2 must be rejected")
g.Expect(err.Error()).To(ContainSubstring("must have replicas: 1"),
"rejection must carry the CEL rule message; got: %s", err.Error())
})

t.Run("validator without a signingKey allows replicas > 1", func(t *testing.T) {
g := NewWithT(t)
ns := makeNamespace(t)
// Genesis-ceremony / non-signing observer: keys are generated
// on-cluster, no fixed signingKey is mounted, so multiple replicas
// are legitimate. The rule must not catch this case.
snd := fixtures.NewSND(ns, "val-nokeys-r3", fixtures.WithValidator(), fixtures.WithReplicas(3))
g.Expect(testCli.Create(testCtx, snd)).To(Succeed(),
"a validator without a signingKey must allow replicas > 1")
})

t.Run("scaling a signing validator above 1 is rejected on update", func(t *testing.T) {
g := NewWithT(t)
ns := makeNamespace(t)
snd := fixtures.NewSND(ns, "val-keys-scale", fixtures.WithValidator(), fixtures.WithReplicas(1))
withSigningAndNodeKey(snd)
g.Expect(testCli.Create(testCtx, snd)).To(Succeed())

err := updateSNDWithRetry(t, client.ObjectKeyFromObject(snd), func(cur *seiv1alpha1.SeiNodeDeployment) {
cur.Spec.Replicas = 2
})
g.Expect(err).To(HaveOccurred(), "scaling a signing validator above 1 must be rejected")
g.Expect(err.Error()).To(ContainSubstring("must have replicas: 1"))
})
}
5 changes: 5 additions & 0 deletions manifests/sei.io_seinodedeployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,11 @@ spec:
state and cannot be retroactively rewritten by editing the spec
rule: '!has(oldSelf.genesis) || (has(self.genesis) && self.genesis ==
oldSelf.genesis)'
- message: 'a validator with a signingKey must have replicas: 1 — every
replica mounts the same priv_validator_key.json, so >1 replica double-signs
(equivocation) and tombstones/slashes the validator'
rule: '!has(self.template.spec.validator) || !has(self.template.spec.validator.signingKey)
|| self.replicas == 1'
status:
description: SeiNodeDeploymentStatus defines the observed state of a SeiNodeDeployment.
properties:
Expand Down
Loading