Skip to content

Commit 91a9ad7

Browse files
authored
fix(cosmos): Migrate bundle chunking params on upgrade (#12621)
## Description Chunked artifact bundle uploads fail on chains that upgraded to bundle-chunking without a migration: the `InstallationDeadlineSeconds` and `InstallationDeadlineBlocks` params were defined in the protobuf but never registered in `ParamSetPairs`, so they persisted as `0` (Go zero value). A deadline of `0` means "expire immediately", causing `PruneExpiredBundleInstalls` to remove every pending upload before the first chunk can land. - **Register deadline params in `ParamSetPairs`** so the legacy param store round-trips them correctly and they become governable. - **Add `Migrate2to3`** (consensus version 2 → 3) that calls `MigrateParams`, which treats `0` as "unset" and replaces it with the default (24 h / unlimited blocks). This fixes existing chains (e.g. devnet) that already have `0` stored. - **Validate deadline params** in `ValidateBasic` — values below `-1` are rejected. - **Improve observability** in `ChunkedArtifactStatus` gRPC query — error messages now include the artifact ID and distinguish "expired/finalized" from "state inconsistency". ### Security Considerations No new authorities or trust boundaries are introduced. The migration only replaces a zero (immediate-expiry) deadline with the intended default (24 h), restoring the designed behavior. ### Scaling Considerations No change to resource consumption. The migration touches only the param store (a single key-value write per param). ### Documentation Considerations Operators upgrading from consensus version 2 to 3 will automatically get the deadline params populated. No manual steps required. Existing deployments that already have non-zero values are unaffected. ### Testing Considerations - `TestValidateInstallationDeadlineBounds` covers boundary validation. - All 34 pre-existing `x/swingset` Go tests continue to pass. ### Upgrade Considerations The module's `ConsensusVersion` is bumped from 2 to 3. The `Migrate2to3` migration is registered and will run automatically during the upgrade handler. To verify on a target chain: query `swingset params` and confirm `installation_deadline_seconds` is `86400` and `installation_deadline_blocks` is `-1` (unless governance has set different values).
2 parents 28c592b + 76254e2 commit 91a9ad7

3 files changed

Lines changed: 110 additions & 2 deletions

File tree

golang/cosmos/x/swingset/keeper/grpc_query.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,18 @@ func (k Querier) ChunkedArtifactStatus(c context.Context, req *types.QueryChunke
7070

7171
msg := k.GetPendingBundleInstall(ctx, req.ChunkedArtifactId)
7272
if msg == nil {
73-
return nil, status.Error(codes.NotFound, "pending chunked artifact not found")
73+
return nil, status.Errorf(codes.NotFound,
74+
"no pending chunked artifact with id %d; it may have expired or been finalized",
75+
req.ChunkedArtifactId,
76+
)
7477
}
7578

7679
can := k.GetChunkedArtifactNode(ctx, req.ChunkedArtifactId)
7780
if can == nil {
78-
return nil, status.Error(codes.NotFound, "pending chunked artifact node not found")
81+
return nil, status.Errorf(codes.NotFound,
82+
"pending chunked artifact %d exists but its tracking node is missing (possible state inconsistency)",
83+
req.ChunkedArtifactId,
84+
)
7985
}
8086

8187
return &types.QueryChunkedArtifactStatusResponse{

golang/cosmos/x/swingset/types/params.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ var (
2020
ParamStoreKeyVatCleanupBudget = []byte("vat_cleanup_budget")
2121
ParamStoreKeyBundleUncompressedSizeLimitBytes = []byte("bundle_uncompressed_size_limit_bytes")
2222
ParamStoreKeyChunkSizeLimitBytes = []byte("chunk_size_limit_bytes")
23+
ParamStoreKeyInstallationDeadlineSeconds = []byte("installation_deadline_seconds")
24+
ParamStoreKeyInstallationDeadlineBlocks = []byte("installation_deadline_blocks")
2325
)
2426

2527
func NewStringBeans(key string, beans sdkmath.Uint) StringBeans {
@@ -59,6 +61,8 @@ func DefaultParams() Params {
5961
VatCleanupBudget: DefaultVatCleanupBudget,
6062
BundleUncompressedSizeLimitBytes: DefaultBundleUncompressedSizeLimitBytes,
6163
ChunkSizeLimitBytes: DefaultChunkSizeLimitBytes,
64+
InstallationDeadlineSeconds: DefaultInstallationDeadlineSeconds, // 86400 (24h)
65+
InstallationDeadlineBlocks: DefaultInstallationDeadlineBlocks, // -1 (unlimited)
6266
}
6367
}
6468

@@ -78,6 +82,8 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs {
7882
paramtypes.NewParamSetPair(ParamStoreKeyVatCleanupBudget, &p.VatCleanupBudget, validateVatCleanupBudget),
7983
paramtypes.NewParamSetPair(ParamStoreKeyBundleUncompressedSizeLimitBytes, &p.BundleUncompressedSizeLimitBytes, validateBundleUncompressedSizeLimitBytes),
8084
paramtypes.NewParamSetPair(ParamStoreKeyChunkSizeLimitBytes, &p.ChunkSizeLimitBytes, validateChunkSizeLimitBytes),
85+
paramtypes.NewParamSetPair(ParamStoreKeyInstallationDeadlineSeconds, &p.InstallationDeadlineSeconds, validateInstallationDeadlineSeconds),
86+
paramtypes.NewParamSetPair(ParamStoreKeyInstallationDeadlineBlocks, &p.InstallationDeadlineBlocks, validateInstallationDeadlineBlocks),
8187
}
8288
}
8389

@@ -101,6 +107,12 @@ func (p Params) ValidateBasic() error {
101107
if err := validateVatCleanupBudget(p.VatCleanupBudget); err != nil {
102108
return err
103109
}
110+
if err := validateInstallationDeadlineBlocks(p.InstallationDeadlineBlocks); err != nil {
111+
return err
112+
}
113+
if err := validateInstallationDeadlineSeconds(p.InstallationDeadlineSeconds); err != nil {
114+
return err
115+
}
104116
if err := validateBundleUncompressedSizeLimitBytes(p.BundleUncompressedSizeLimitBytes); err != nil {
105117
return err
106118
}
@@ -219,6 +231,34 @@ func validateChunkSizeLimitBytes(i interface{}) error {
219231
return nil
220232
}
221233

234+
func validateInstallationDeadlineSeconds(i interface{}) error {
235+
value, ok := i.(int64)
236+
if !ok {
237+
return fmt.Errorf("installation_deadline_seconds must be int64, got %#v", i)
238+
}
239+
if value < -1 {
240+
return fmt.Errorf(
241+
"installation_deadline_seconds must be -1 (unlimited), 0 (expire immediately), or positive, got %d",
242+
value,
243+
)
244+
}
245+
return nil
246+
}
247+
248+
func validateInstallationDeadlineBlocks(i interface{}) error {
249+
value, ok := i.(int64)
250+
if !ok {
251+
return fmt.Errorf("installation_deadline_blocks must be int64, got %#v", i)
252+
}
253+
if value < -1 {
254+
return fmt.Errorf(
255+
"installation_deadline_blocks must be -1 (unlimited), 0 (expire immediately), or positive, got %d",
256+
value,
257+
)
258+
}
259+
return nil
260+
}
261+
222262
// UpdateParams appends any missing params, configuring them to their defaults,
223263
// then returning the updated params or an error.
224264
// Existing params are not

golang/cosmos/x/swingset/types/params_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package types
22

33
import (
4+
"strings"
45
"reflect"
56
"testing"
67

@@ -211,3 +212,64 @@ func TestValidateParams(t *testing.T) {
211212
t.Errorf("unexpected ValidateBasic() error with empty VatCleanupBudget: %v", params.VatCleanupBudget)
212213
}
213214
}
215+
216+
func TestValidateInstallationDeadlineBounds(t *testing.T) {
217+
const minInt64 int64 = -1 << 63
218+
219+
for _, tc := range []struct {
220+
name string
221+
seconds int64
222+
blocks int64
223+
wantErrSubstr string
224+
}{
225+
{name: "both unlimited", seconds: -1, blocks: -1},
226+
{name: "both immediate", seconds: 0, blocks: 0},
227+
{name: "both positive", seconds: 1, blocks: 1},
228+
{
229+
name: "seconds below minimum",
230+
seconds: -2,
231+
blocks: -1,
232+
wantErrSubstr: "installation_deadline_seconds must be -1 (unlimited), 0 (expire immediately), or positive, got -2",
233+
},
234+
{
235+
name: "blocks below minimum",
236+
seconds: -1,
237+
blocks: -2,
238+
wantErrSubstr: "installation_deadline_blocks must be -1 (unlimited), 0 (expire immediately), or positive, got -2",
239+
},
240+
{
241+
name: "seconds at int64 minimum",
242+
seconds: minInt64,
243+
blocks: -1,
244+
wantErrSubstr: "installation_deadline_seconds must be -1 (unlimited), 0 (expire immediately), or positive, got -9223372036854775808",
245+
},
246+
{
247+
name: "blocks at int64 minimum",
248+
seconds: -1,
249+
blocks: minInt64,
250+
wantErrSubstr: "installation_deadline_blocks must be -1 (unlimited), 0 (expire immediately), or positive, got -9223372036854775808",
251+
},
252+
} {
253+
t.Run(tc.name, func(t *testing.T) {
254+
params := DefaultParams()
255+
params.BootstrapVatConfig = "foo"
256+
params.FeeUnitPrice = sdk.NewCoins(sdk.NewInt64Coin("denom", 789))
257+
params.InstallationDeadlineSeconds = tc.seconds
258+
params.InstallationDeadlineBlocks = tc.blocks
259+
260+
err := params.ValidateBasic()
261+
if tc.wantErrSubstr == "" {
262+
if err != nil {
263+
t.Fatalf("unexpected ValidateBasic() error: %v", err)
264+
}
265+
return
266+
}
267+
if err == nil {
268+
t.Fatal("expected ValidateBasic() error, got nil")
269+
}
270+
if !strings.Contains(err.Error(), tc.wantErrSubstr) {
271+
t.Fatalf("expected error containing %q, got %q", tc.wantErrSubstr, err)
272+
}
273+
})
274+
}
275+
}

0 commit comments

Comments
 (0)