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
26 changes: 26 additions & 0 deletions api/v1alpha1/seinetwork_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@ type GenesisAccount struct {
// Balance is the initial balance in coin notation (e.g. "1000000usei").
// +kubebuilder:validation:MinLength=1
Balance string `json:"balance"`

// Vesting, when set, locks part of Balance on an unlock schedule instead
// of funding a fully-spendable account; nil funds a standard account.
// The locked coins still count toward the balance and can be staked, but
// cannot be transferred until they unlock.
// +optional
Vesting *GenesisAccountVesting `json:"vesting,omitempty"`
}

// GenesisAccountVesting locks part of a GenesisAccount's Balance on an unlock
// schedule that completes at EndTime.
type GenesisAccountVesting struct {
// Amount is the vesting-locked portion of the account's Balance, in coin
// notation (e.g. "1000000usei"). Must not exceed Balance.
// +kubebuilder:validation:MinLength=1
Amount string `json:"amount"`

// EndTime is the unix timestamp at which the locked Amount fully unlocks.
// Must be after the network's genesis time.
// +kubebuilder:validation:Minimum=1
EndTime int64 `json:"endTime"`

// Delayed unlocks the full Amount all at once at EndTime. The default
// (false) unlocks it linearly from the network's genesis time to EndTime.
// +optional
Delayed bool `json:"delayed,omitempty"`
}

// ---------------------------------------------------------------------------
Expand Down
24 changes: 23 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

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

29 changes: 29 additions & 0 deletions config/crd/sei.io_seinetworks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ spec:
(e.g. "1000000usei").
minLength: 1
type: string
vesting:
description: |-
Vesting, when set, locks part of Balance on an unlock schedule instead
of funding a fully-spendable account; nil funds a standard account.
The locked coins still count toward the balance and can be staked, but
cannot be transferred until they unlock.
properties:
amount:
description: |-
Amount is the vesting-locked portion of the account's Balance, in coin
notation (e.g. "1000000usei"). Must not exceed Balance.
minLength: 1
type: string
delayed:
description: |-
Delayed unlocks the full Amount all at once at EndTime. The default
(false) unlocks it linearly from the network's genesis time to EndTime.
type: boolean
endTime:
description: |-
EndTime is the unix timestamp at which the locked Amount fully unlocks.
Must be after the network's genesis time.
format: int64
minimum: 1
type: integer
required:
- amount
- endTime
type: object
required:
- address
- balance
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ require (
github.com/google/uuid v1.6.0
github.com/onsi/gomega v1.39.1
github.com/sei-protocol/sei-config v0.0.23
github.com/sei-protocol/seictl v0.0.64
github.com/sei-protocol/seictl v0.0.68
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
go.opentelemetry.io/otel/exporters/prometheus v0.65.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,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.23 h1:pGFxRnKoXZLK3Ew/Vd5lgC5RvH9Wo58NnL19CuMgFIk=
github.com/sei-protocol/sei-config v0.0.23/go.mod h1:zcEdLzyIH2AyP0/QRBE3s4Y9eGn0C/qAUx1c4o4EROU=
github.com/sei-protocol/seictl v0.0.64 h1:GfAA/2br7nDTlpiav1BC7Z2fUS9iv7gXYhECXKClPiY=
github.com/sei-protocol/seictl v0.0.64/go.mod h1:YJYgm3yl1fTpLWdPrpwgAqyONK5IwSZ3Dlw4dzH6U6U=
github.com/sei-protocol/seictl v0.0.68 h1:dCT94Ys4OjiPt5Y6TWqtgJ8GKTiWv7tN87D8HqIQxbk=
github.com/sei-protocol/seictl v0.0.68/go.mod h1:kI3HIAIWzuJSme8LqJH1WZiITrSIx5yDtJJea07Xt8Y=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
Expand Down
10 changes: 9 additions & 1 deletion internal/planner/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ func (p *genesisGroupPlanner) BuildPlan(

accounts := make([]sidecar.GenesisAccountEntry, len(network.Spec.Genesis.Accounts))
for i, a := range network.Spec.Genesis.Accounts {
accounts[i] = sidecar.GenesisAccountEntry{Address: a.Address, Balance: a.Balance}
entry := sidecar.GenesisAccountEntry{Address: a.Address, Balance: a.Balance}
if a.Vesting != nil {
entry.Vesting = &sidecar.GenesisAccountVesting{
Amount: a.Vesting.Amount,
EndTime: a.Vesting.EndTime,
Delayed: a.Vesting.Delayed,
}
}
accounts[i] = entry
}

// Validate at planner-time so bech32 / shape errors hit
Expand Down
41 changes: 41 additions & 0 deletions internal/planner/group_accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,47 @@ func TestBuildPlan_NilAccountsOmitsField(t *testing.T) {
}
}

func TestBuildPlan_PropagatesVesting(t *testing.T) {
group := groupWithAccounts([]seiv1alpha1.GenesisAccount{
{
Address: validSeiAddr,
Balance: "2000000usei",
Vesting: &seiv1alpha1.GenesisAccountVesting{
Amount: testAccountBalance,
EndTime: 1893456000,
Delayed: true,
},
},
{Address: validSeiAddr2, Balance: testBalance1000usei}, // no Vesting: must stay nil, not zero-valued
})
p, err := ForGroup(group)
if err != nil {
t.Fatalf("ForGroup: %v", err)
}
plan, err := p.BuildPlan(group)
if err != nil {
t.Fatalf("BuildPlan: %v", err)
}

var params sidecar.AssembleAndUploadGenesisTask
if err := json.Unmarshal(plan.Tasks[0].Params.Raw, &params); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(params.Accounts) != 2 {
t.Fatalf("Accounts: got %d, want 2", len(params.Accounts))
}
v := params.Accounts[0].Vesting
if v == nil {
t.Fatalf("Accounts[0].Vesting: got nil, want set")
}
if v.Amount != testAccountBalance || v.EndTime != 1893456000 || !v.Delayed {
t.Errorf("Accounts[0].Vesting = %+v", v)
}
if params.Accounts[1].Vesting != nil {
t.Errorf("Accounts[1].Vesting: got %+v, want nil", params.Accounts[1].Vesting)
}
}

func TestBuildPlan_RejectsBadBech32_PlannerTime(t *testing.T) {
// Validate() runs at planner time so a bad address surfaces in
// kubectl describe rather than burning a sidecar Job pod.
Expand Down
29 changes: 29 additions & 0 deletions manifests/sei.io_seinetworks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ spec:
(e.g. "1000000usei").
minLength: 1
type: string
vesting:
description: |-
Vesting, when set, locks part of Balance on an unlock schedule instead
of funding a fully-spendable account; nil funds a standard account.
The locked coins still count toward the balance and can be staked, but
cannot be transferred until they unlock.
properties:
amount:
description: |-
Amount is the vesting-locked portion of the account's Balance, in coin
notation (e.g. "1000000usei"). Must not exceed Balance.
minLength: 1
type: string
delayed:
description: |-
Delayed unlocks the full Amount all at once at EndTime. The default
(false) unlocks it linearly from the network's genesis time to EndTime.
type: boolean
endTime:
description: |-
EndTime is the unix timestamp at which the locked Amount fully unlocks.
Must be after the network's genesis time.
format: int64
minimum: 1
type: integer
required:
- amount
- endTime
type: object
required:
- address
- balance
Expand Down
2 changes: 2 additions & 0 deletions sdk/sei/provider/k8s/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
testNet = "chaos-net"
testImage = "img:1"

testGenesisAddr = "sei1abc" // non-validator genesis account fixture

rpc0Name = "rpc-0"
rpc1Name = "rpc-1"
resultKey = "result"
Expand Down
11 changes: 9 additions & 2 deletions sdk/sei/provider/k8s/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,15 @@ func renderNetwork(spec sei.NetworkSpec, namespace string) *seiv1alpha1.SeiNetwo
net.Spec.ConfigOverrides = maps.Clone(spec.Config)
}
for _, a := range spec.Accounts {
net.Spec.Genesis.Accounts = append(net.Spec.Genesis.Accounts,
seiv1alpha1.GenesisAccount{Address: a.Address, Balance: a.Balance})
acc := seiv1alpha1.GenesisAccount{Address: a.Address, Balance: a.Balance}
if a.Vesting != nil {
acc.Vesting = &seiv1alpha1.GenesisAccountVesting{
Amount: a.Vesting.Amount,
EndTime: a.Vesting.EndTime,
Delayed: a.Vesting.Delayed,
}
}
net.Spec.Genesis.Accounts = append(net.Spec.Genesis.Accounts, acc)
}
if spec.SidecarImage != "" {
// Pin the seictl sidecar image on this network; the controller propagates
Expand Down
33 changes: 31 additions & 2 deletions sdk/sei/provider/k8s/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,41 @@ import (
"github.com/sei-protocol/sei-k8s-controller/sdk/sei"
)

func TestRenderNetwork_PropagatesVesting(t *testing.T) {
spec := sei.NetworkSpec{
Name: testNet, Image: testImage, Validators: 1,
Accounts: []sei.GenesisAccount{
{
Address: testGenesisAddr,
Balance: "2000000usei",
Vesting: &sei.GenesisAccountVesting{Amount: "1000000usei", EndTime: 1893456000, Delayed: true},
},
{Address: "sei1def", Balance: "100usei"}, // no Vesting: must stay nil, not zero-valued
},
}
net := renderNetwork(spec, testNS)

if len(net.Spec.Genesis.Accounts) != 2 {
t.Fatalf("genesis accounts = %+v", net.Spec.Genesis.Accounts)
}
v := net.Spec.Genesis.Accounts[0].Vesting
if v == nil {
t.Fatalf("Accounts[0].Vesting: got nil, want set")
}
if v.Amount != "1000000usei" || v.EndTime != 1893456000 || !v.Delayed {
t.Errorf("Accounts[0].Vesting = %+v", v)
}
if net.Spec.Genesis.Accounts[1].Vesting != nil {
t.Errorf("Accounts[1].Vesting: got %+v, want nil", net.Spec.Genesis.Accounts[1].Vesting)
}
}

func TestRenderNetwork_ChainIDDefaultsToName(t *testing.T) {
spec := sei.NetworkSpec{
Name: testNet, Image: testImage, Validators: 4,
Genesis: map[string]string{"staking.params.unbonding_time": "60s"},
Config: map[string]string{"app.pruning": "nothing"},
Accounts: []sei.GenesisAccount{{Address: "sei1abc", Balance: "100usei"}},
Accounts: []sei.GenesisAccount{{Address: testGenesisAddr, Balance: "100usei"}},
Labels: map[string]string{testRunLabel: testRunID},
DeletionPolicy: sei.DeletionDelete,
}
Expand All @@ -34,7 +63,7 @@ func TestRenderNetwork_ChainIDDefaultsToName(t *testing.T) {
if got := net.Spec.ConfigOverrides["app.pruning"]; got != "nothing" {
t.Errorf("configOverrides = %v, want app.pruning=nothing", net.Spec.ConfigOverrides)
}
if len(net.Spec.Genesis.Accounts) != 1 || net.Spec.Genesis.Accounts[0].Address != "sei1abc" {
if len(net.Spec.Genesis.Accounts) != 1 || net.Spec.Genesis.Accounts[0].Address != testGenesisAddr {
t.Errorf("genesis accounts = %+v", net.Spec.Genesis.Accounts)
}
// DeletionPolicy threads through so an ephemeral chain cascades to its
Expand Down
12 changes: 12 additions & 0 deletions sdk/sei/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ type NetworkSpec struct {
type GenesisAccount struct {
Address string
Balance string

// Vesting, when set, locks part of Balance under a vesting schedule
// instead of funding a standard account; nil produces a plain account.
Vesting *GenesisAccountVesting
}

// GenesisAccountVesting is the vesting schedule locking part of a
// GenesisAccount's Balance. Mirrors seiv1alpha1.GenesisAccountVesting.
type GenesisAccountVesting struct {
Amount string // vesting-locked portion of Balance, coin notation; must not exceed Balance
EndTime int64 // unix timestamp the vesting schedule completes
Delayed bool // DelayedVestingAccount (all-at-once at EndTime) instead of the default ContinuousVestingAccount (linear from genesis time)
}

// NodeSpec is the typed input to CreateNode — one RPC node peered to a network.
Expand Down
Loading