diff --git a/.agents/skills/guidelines/SKILL.md b/.agents/skills/guidelines/SKILL.md new file mode 100644 index 000000000..694bf4e6a --- /dev/null +++ b/.agents/skills/guidelines/SKILL.md @@ -0,0 +1,65 @@ +--- +name: guidelines +description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria. +license: MIT +--- + +# Guidelines + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. diff --git a/.agents/skills/upgrade-changelog/SKILL.md b/.agents/skills/upgrade-changelog/SKILL.md new file mode 100644 index 000000000..35ab5d668 --- /dev/null +++ b/.agents/skills/upgrade-changelog/SKILL.md @@ -0,0 +1,81 @@ +--- +name: upgrade-changelog +description: "Generates CHANGELOG.md entries for upgrade versions found in upgrades/software/ by parsing init.go and upgrade.go" +--- +# Instructions + +Generate changelog entries in `upgrades/CHANGELOG.md` for any upgrade version under `upgrades/software/` that doesn't already have an entry. + +## Steps + +1. **Find semver directories** — list all directories under `upgrades/software/` whose names match `v..` (e.g., `v2.0.0`, `v1.0.0`). + +2. **Read `upgrades/CHANGELOG.md`** — identify which versions already have entries by scanning for `##### vX.Y.Z` headings. If a version already has a heading, skip it entirely. + +3. **For each new version** (no existing heading), gather data from two files: + + ### 3a. Parse `upgrades/software//init.go` + + Find all `utypes.RegisterMigration(...)` calls. Each call has the form: + ```go + utypes.RegisterMigration(moduleName, version, handlerFn) + ``` + - `moduleName` is a Go constant (e.g., `dv1.ModuleName`). Resolve it: + 1. Find the import alias (e.g., `dv1 "pkg.akt.dev/go/node/deployment/v1"`) + 2. Search the imported package for `ModuleName` constant definition to get the actual string value + - `version` is a `uint64` — this is the **from** version. The **to** version is `version + 1`. + - Record each migration as: `moduleName version -> version+1` + + ### 3b. Parse `upgrades/software//upgrade.go` + + Find the `StoreLoader()` method. It returns a `*storetypes.StoreUpgrades` struct with optional fields: + - `Added: []string{...}` — new stores + - `Renamed: []storetypes.StoreRename{...}` — renamed stores + - `Deleted: []string{...}` — removed stores + + If `StoreLoader()` returns `nil`, there are no store changes. + + For each store key constant (e.g., `epochstypes.StoreKey`, `ttypes.ModuleName`): + 1. Find the import alias in the file + 2. Search the imported package for the constant definition to get the actual string value + +4. **Insert the new entry** in `upgrades/CHANGELOG.md` immediately after the line: + ``` + Add new upgrades after this line based on the template above + ``` + followed by `-----`. + + Use this format (newest entries go first, right after the delimiter): + + ```markdown + + ##### vX.Y.Z + + ###### Description + + - Stores + - added + - `storeName`: brief description if available + - renamed + - `oldName` -> `newName` + - deleted + - `storeName`: brief description if available + + - Migrations + - moduleName `from -> to` + ``` + + **Omission rules** (match existing CHANGELOG style): + - Omit the entire `Stores` section if there are no added, renamed, or deleted stores + - Omit `added`/`renamed`/`deleted` subsections individually if empty + - Omit the entire `Migrations` section if there are no migrations + - Always include the `###### Description` heading (leave it for the user to fill in) + +5. **Report results** — list each version processed and what was added. For skipped versions (already in CHANGELOG), mention they were skipped. + +## Important notes + +- Store key constants may be named `StoreKey` or `ModuleName` — both are used as store identifiers. Resolve whichever constant appears in the code. +- When resolving Go constants from external packages, search under the Go module cache or use `go doc` if needed. The packages typically follow the pattern `pkg.akt.dev/go/node//`. +- If a constant cannot be resolved, use the raw Go expression as a placeholder (e.g., `` `epochstypes.StoreKey` ``) and warn the user. +- Multiple versions may need entries — process them all in one run, inserting newest first after the delimiter. diff --git a/app/app.go b/app/app.go index efcb0ce93..4f9168ddf 100644 --- a/app/app.go +++ b/app/app.go @@ -53,7 +53,10 @@ import ( transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" ibchost "github.com/cosmos/ibc-go/v10/modules/core/exported" + gogogrpc "github.com/cosmos/gogoproto/grpc" + cflags "pkg.akt.dev/go/cli/flags" + aclient "pkg.akt.dev/go/node/client" epochstypes "pkg.akt.dev/go/node/epochs/v1beta1" "pkg.akt.dev/go/sdkutil" @@ -532,6 +535,11 @@ func (app *AkashApp) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIC // Register node gRPC service for grpc-gateway. nodeservice.RegisterGRPCGatewayRoutes(cctx, apiSvr.GRPCGatewayRouter) + // Register Akash Discovery gRPC-Gateway route for REST access at GET /akash/discovery/v1/info. + if err := aclient.RegisterDiscoveryHandlerServer(cctx.CmdContext, apiSvr.GRPCGatewayRouter, aclient.NewDiscoveryServer(aclient.GetRegistry())); err != nil { + panic(fmt.Errorf("failed to register discovery gRPC-Gateway routes: %w", err)) + } + // register swagger API from root so that other applications can override easily if apiConfig.Swagger { RegisterSwaggerAPI(cctx, apiSvr.Router) @@ -557,6 +565,18 @@ func (app *AkashApp) RegisterNodeService(cctx client.Context, cfg config.Config) nodeservice.RegisterNodeService(cctx, app.GRPCQueryRouter(), cfg) } +// RegisterGRPCServerWithSkipCheckHeader registers all gRPC services including +// the Akash Discovery service for version negotiation. +func (app *AkashApp) RegisterGRPCServerWithSkipCheckHeader(server gogogrpc.Server, skipCheckHeader bool) { + // Register all standard Cosmos SDK module query services. + app.BaseApp.RegisterGRPCServerWithSkipCheckHeader(server, skipCheckHeader) + + // Register Akash Discovery service directly on the gRPC server. + // This enables version discovery for clients via gRPC at akash.discovery.v1.Discovery/GetInfo + // and via REST through gRPC-Gateway at GET /akash/discovery/v1/info. + aclient.RegisterDiscoveryService(server, aclient.GetRegistry()) +} + // RegisterSwaggerAPI registers swagger route with API Server func RegisterSwaggerAPI(_ client.Context, rtr *mux.Router) { statikFS, err := fs.New() diff --git a/cmd/akash/cmd/app_creator.go b/cmd/akash/cmd/app_creator.go index 449b4794c..560d0e678 100644 --- a/cmd/akash/cmd/app_creator.go +++ b/cmd/akash/cmd/app_creator.go @@ -19,7 +19,10 @@ import ( sdkserver "github.com/cosmos/cosmos-sdk/server" servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/version" + cflags "pkg.akt.dev/go/cli/flags" + aclient "pkg.akt.dev/go/node/client" "pkg.akt.dev/go/sdkutil" akash "pkg.akt.dev/node/v2/app" @@ -81,6 +84,14 @@ func (a appCreator) newApp( cast.ToUint32(appOpts.Get(cflags.FlagStateSyncSnapshotKeepRecent)), ) + // Configure version discovery registry with chain metadata. + // This is used by the CometBFT JSON-RPC "akash" route (automatic via init()) + // and the gRPC Discovery service (registered in AkashApp.RegisterGRPCServerWithSkipCheckHeader). + aclient.SetRegistry(aclient.DefaultRegistry( + aclient.WithChainID(chainID), + aclient.WithNodeVersion(version.Version), + )) + baseAppOptions := []func(*baseapp.BaseApp){ baseapp.SetChainID(chainID), baseapp.SetPruning(pruningOpts), diff --git a/go.mod b/go.mod index 79dd01e13..b27ef1e10 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module pkg.akt.dev/node/v2 -go 1.25.9 +go 1.26.2 require ( cosmossdk.io/api v0.9.2 @@ -35,7 +35,7 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/rakyll/statik v0.1.7 github.com/regen-network/cosmos-proto v0.3.1 - github.com/rs/zerolog v1.34.0 + github.com/rs/zerolog v1.35.0 github.com/spf13/cast v1.10.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -48,7 +48,7 @@ require ( google.golang.org/grpc v1.76.0 gopkg.in/yaml.v3 v3.0.1 gotest.tools/v3 v3.5.2 - pkg.akt.dev/go v0.2.9 + pkg.akt.dev/go v0.2.10 pkg.akt.dev/go/cli v0.2.2 pkg.akt.dev/go/sdl v0.2.0 ) @@ -59,12 +59,12 @@ replace ( // use cosmos fork of keyring github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 - github.com/bytedance/sonic => github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic => github.com/bytedance/sonic v1.15.0 // use akash fork of cometbft github.com/cometbft/cometbft => github.com/akash-network/cometbft v0.38.21-akash.1 // use akash fork of cosmos sdk - github.com/cosmos/cosmos-sdk => github.com/akash-network/cosmos-sdk v0.53.6-akash.1 + github.com/cosmos/cosmos-sdk => github.com/akash-network/cosmos-sdk v0.53.7-akash.1 github.com/cosmos/gogoproto => github.com/akash-network/gogoproto v1.7.0-akash.2 @@ -118,8 +118,8 @@ require ( github.com/bits-and-blooms/bitset v1.24.3 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.2 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -210,7 +210,7 @@ require ( github.com/jmhodges/levigo v1.0.1-0.20191019112844-b572e7f4cdac // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -262,25 +262,25 @@ require ( github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v1.0.1 // indirect go.etcd.io/bbolt v1.4.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/sdk v1.37.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/arch v0.17.0 // indirect + golang.org/x/arch v0.24.0 // indirect golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect diff --git a/go.sum b/go.sum index 1de6faf76..fb35a318b 100644 --- a/go.sum +++ b/go.sum @@ -1287,8 +1287,8 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3 github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/akash-network/cometbft v0.38.21-akash.1 h1:li8x87YansHyID6VBTOxe/yBLRcdb6lQnjAeTvnMn/w= github.com/akash-network/cometbft v0.38.21-akash.1/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= -github.com/akash-network/cosmos-sdk v0.53.6-akash.1 h1:Da37/oS0rbO77FfUNbcNeYp9oKZr2ktV3jhA1sjv+KY= -github.com/akash-network/cosmos-sdk v0.53.6-akash.1/go.mod h1:ZKpVIHvKJgJJqOO2DpIJTwSyuKC0ekgIySUJsEPKnFs= +github.com/akash-network/cosmos-sdk v0.53.7-akash.1 h1:1Ck4izBdr50qPHFueBCICVxRO1CXqZShAyOyLCXh5pU= +github.com/akash-network/cosmos-sdk v0.53.7-akash.1/go.mod h1:ZKpVIHvKJgJJqOO2DpIJTwSyuKC0ekgIySUJsEPKnFs= github.com/akash-network/gogoproto v1.7.0-akash.2 h1:zY5seM6kBOLMBWn15t8vrY1ao4J1HjrhNaEeO/Soro0= github.com/akash-network/gogoproto v1.7.0-akash.2/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/akash-network/ledger-go v0.16.0 h1:75oasauaV0dNGOgMB3jr/rUuxJC0gHDdYYnQW+a4bvg= @@ -1346,10 +1346,10 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -1424,7 +1424,6 @@ github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvA github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= @@ -1599,7 +1598,6 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= @@ -1893,8 +1891,8 @@ github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrD github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -1929,7 +1927,6 @@ github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GW github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -1938,7 +1935,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -2116,9 +2112,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= +github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -2251,8 +2246,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw= go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= @@ -2263,14 +2258,14 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/X go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= @@ -2279,8 +2274,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -2311,8 +2306,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= +golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -2655,8 +2650,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -3293,8 +3288,8 @@ nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= pgregory.net/rapid v0.5.5 h1:jkgx1TjbQPD/feRoK+S/mXw9e1uj6WilpHrXJowi6oA= pgregory.net/rapid v0.5.5/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -pkg.akt.dev/go v0.2.9 h1:2oU971PeW/f9UY2pBL4tmFTrsk4CvSroGlTQ38uKQjY= -pkg.akt.dev/go v0.2.9/go.mod h1:x9Cku9yibLk4aGLTE/Yy7eEkkda0uOSRWmYwQeFw23o= +pkg.akt.dev/go v0.2.10 h1:oRrzQIaolZiaL3+U9fgpfod9E7NT2OzHMs+7fPEB+Dc= +pkg.akt.dev/go v0.2.10/go.mod h1:6yV8oyP8xFm4ocyuf+9nx8S3T4mJz3e64Cl7ln/BczM= pkg.akt.dev/go/cli v0.2.2 h1:PWDAAeHkVtcZ9qE76yh4IhJ2J/42ekhwSyrGWLPGi/g= pkg.akt.dev/go/cli v0.2.2/go.mod h1:MHm9lU8hb+xQ8BX3b9c9S1pMyZKUob5tVjHXQ4T1uwU= pkg.akt.dev/go/sdl v0.2.0 h1:hY74GjN4itV92REf8HqGt1rQDtXsruzE/iIzd/FpUB8= diff --git a/make/setup-cache.mk b/make/setup-cache.mk index 2a2b01910..1bc4c4c8a 100644 --- a/make/setup-cache.mk +++ b/make/setup-cache.mk @@ -50,7 +50,7 @@ $(STATIK): $(STATIK_VERSION_FILE) $(COSMOVISOR_VERSION_FILE): $(AKASH_DEVCACHE) @echo "installing cosmovisor $(COSMOVISOR_VERSION) ..." rm -f $(COSMOVISOR) - GOBIN=$(AKASH_DEVCACHE_BIN) $(GO) install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@$(COSMOVISOR_VERSION) + GOTOOLCHAIN=go1.25.9 GOBIN=$(AKASH_DEVCACHE_BIN) $(GO) install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@$(COSMOVISOR_VERSION) rm -rf "$(dir $@)" mkdir -p "$(dir $@)" touch $@ diff --git a/tests/upgrade/docker-compose.hermes.yaml b/tests/upgrade/docker-compose.hermes.yaml new file mode 100644 index 000000000..dcc4c8597 --- /dev/null +++ b/tests/upgrade/docker-compose.hermes.yaml @@ -0,0 +1,66 @@ +# Docker Compose for Akash Local Development with Pyth Oracle +# +# This setup includes: +# - akash-node: Single-node validator with Wormhole and Pyth contracts +# - hermes-client: Price relayer that submits Pyth prices to the oracle +# +# Prerequisites: +# - Build contracts first: cd contracts && make build +# - Hermes repo at ../../hermes (relative to node repo) +# +# Usage: +# docker-compose -f _build/docker-compose.yaml up -d # Start all services +# docker-compose -f _build/docker-compose.yaml logs -f # View all logs +# docker-compose -f _build/docker-compose.yaml logs -f validator # View node logs +# docker-compose -f _build/docker-compose.yaml logs -f hermes-client # View hermes logs +# docker-compose -f _build/docker-compose.yaml down # Stop services +# docker-compose -f _build/docker-compose.yaml down -v # Stop and clean volumes +# +# Verify: +# curl http://localhost:26657/status # Check node status +# curl http://localhost:1317/cosmos/base/tendermint/v1beta1/blocks/latest # Check latest block +# +--- +services: + # ============================================================================= + # Hermes Price Relayer + # ============================================================================= + hermes-client: + image: ghcr.io/akash-network/hermes:latest + container_name: hermes-client + hostname: hermes-client + network_mode: "host" + environment: + # RPC endpoint (internal docker network) + - RPC_ENDPOINT=http://host.docker.internal:26657 + # Pyth Hermes API + - HERMES_ENDPOINT=https://hermes.pyth.network + # Update interval (1 minute for testing, use 5+ minutes in production) + - UPDATE_INTERVAL_MS=10000 + # Gas configuration + - GAS_PRICE=0.025uakt + - DENOM=uakt + - NODE_ENV=development + env_file: + - ${AKASH_RUN_DIR}/hermes.env + ports: + - "3000:3000" + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "================================================" + echo "Hermes Price Relayer - Waiting for configuration" + echo "================================================" + + echo "Contract address: $$CONTRACT_ADDRESS" + echo "Starting Hermes daemon..." + echo "" + + # Run the hermes daemon + exec node dist/cli.js daemon + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/tests/upgrade/test-cases.json b/tests/upgrade/test-cases.json index dc95d41f5..3a926102c 100644 --- a/tests/upgrade/test-cases.json +++ b/tests/upgrade/test-cases.json @@ -10,6 +10,12 @@ "to": "8" } ], + "market": [ + { + "from": "8", + "to": "9" + } + ], "oracle": [ { "from": "1", diff --git a/testutil/network/rpc.go b/testutil/network/rpc.go index f936e83fd..3f0f06ed4 100644 --- a/testutil/network/rpc.go +++ b/testutil/network/rpc.go @@ -14,19 +14,17 @@ import ( // needed by DiscoverClient to detect the API version. type LocalRPCClient struct { *local.Local + registry *aclient.VersionRegistry } // NewLocalRPCClient creates a new LocalRPCClient wrapping the local client -func NewLocalRPCClient(lc *local.Local) *LocalRPCClient { - return &LocalRPCClient{Local: lc} +// with a local registry instance to avoid mutating global discovery state. +func NewLocalRPCClient(lc *local.Local, registry *aclient.VersionRegistry) *LocalRPCClient { + return &LocalRPCClient{Local: lc, registry: registry} } // Akash implements the RPCClient interface required by chain-sdk. -// Returns client info with the current API version. +// Returns version discovery info from the local registry. func (c *LocalRPCClient) Akash(_ context.Context) (*aclient.Akash, error) { - return &aclient.Akash{ - ClientInfo: aclient.ClientInfo{ - ApiVersion: "v1beta3", - }, - }, nil + return c.registry.ToAkash(), nil } diff --git a/testutil/network/util.go b/testutil/network/util.go index 492b274f2..79541ebcb 100644 --- a/testutil/network/util.go +++ b/testutil/network/util.go @@ -23,10 +23,13 @@ import ( "github.com/cosmos/cosmos-sdk/server/api" servergrpc "github.com/cosmos/cosmos-sdk/server/grpc" servercmtlog "github.com/cosmos/cosmos-sdk/server/log" + "github.com/cosmos/cosmos-sdk/version" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + + aclient "pkg.akt.dev/go/node/client" ) func startInProcess(cfg Config, val *Validator) error { @@ -46,6 +49,11 @@ func startInProcess(cfg Config, val *Validator) error { app := cfg.AppConstructor(*val) val.app = app + registry := aclient.DefaultRegistry( + aclient.WithChainID(cfg.ChainID), + aclient.WithNodeVersion(version.Version), + ) + appGenesisProvider := func() (node.ChecksummedGenesisDoc, error) { appGenesis, err := genutiltypes.AppGenesisFromFile(cmtCfg.GenesisFile()) if err != nil { @@ -84,7 +92,7 @@ func startInProcess(cfg Config, val *Validator) error { val.tmNode = tmNode if val.RPCAddress != "" { - val.RPCClient = NewLocalRPCClient(local.New(tmNode)) + val.RPCClient = NewLocalRPCClient(local.New(tmNode), registry) } // We'll need a RPC client if the validator exposes a gRPC or REST endpoint. diff --git a/upgrades/software/v2.1.0/init.go b/upgrades/software/v2.1.0/init.go index 2c5553a91..bc0fe80f3 100644 --- a/upgrades/software/v2.1.0/init.go +++ b/upgrades/software/v2.1.0/init.go @@ -4,6 +4,7 @@ package v2_1_0 import ( dv1 "pkg.akt.dev/go/node/deployment/v1" + mv1 "pkg.akt.dev/go/node/market/v1" otypes "pkg.akt.dev/go/node/oracle/v2" utypes "pkg.akt.dev/node/v2/upgrades/types" @@ -14,4 +15,5 @@ func init() { utypes.RegisterMigration(otypes.ModuleName, 1, newOracleMigration) utypes.RegisterMigration(dv1.ModuleName, 7, newDeploymentMigration) + utypes.RegisterMigration(mv1.ModuleName, 8, newMarketMigration) } diff --git a/upgrades/software/v2.1.0/market.go b/upgrades/software/v2.1.0/market.go new file mode 100644 index 000000000..9f4a637d2 --- /dev/null +++ b/upgrades/software/v2.1.0/market.go @@ -0,0 +1,25 @@ +package v2_1_0 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkmodule "github.com/cosmos/cosmos-sdk/types/module" + + utypes "pkg.akt.dev/node/v2/upgrades/types" +) + +type marketMigrations struct { + utypes.Migrator +} + +func newMarketMigration(m utypes.Migrator) utypes.Migration { + return marketMigrations{Migrator: m} +} + +func (m marketMigrations) GetHandler() sdkmodule.MigrationHandler { + return m.handler +} + +// handler marketMigrations deployment from version 7 to 8. +func (m marketMigrations) handler(_ sdk.Context) error { + return nil +} diff --git a/upgrades/software/v2.1.0/upgrade.go b/upgrades/software/v2.1.0/upgrade.go index 6b585a5fa..59a8426cf 100644 --- a/upgrades/software/v2.1.0/upgrade.go +++ b/upgrades/software/v2.1.0/upgrade.go @@ -16,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" etypes "pkg.akt.dev/go/node/escrow/module" + mvbeta "pkg.akt.dev/go/node/market/v1beta5" otypes "pkg.akt.dev/go/node/oracle/v2" "pkg.akt.dev/go/sdkutil" @@ -119,6 +120,20 @@ func (up *upgrade) UpgradeHandler() upgradetypes.UpgradeHandler { } } + // Set default reclamation params for market module + mparams, err := up.Keepers.Akash.Market.GetParams(sctx) + if err != nil { + return toVM, fmt.Errorf("failed to get market params: %w", err) + } + + if mparams.MinReclamationWindow == 0 { + mparams.MinReclamationWindow = mvbeta.DefaultMinReclamationWindow + mparams.MaxReclamationWindow = mvbeta.DefaultMaxReclamationWindow + if err = up.Keepers.Akash.Market.SetParams(sctx, mparams); err != nil { + return toVM, fmt.Errorf("failed to set market params: %w", err) + } + } + return toVM, err } } diff --git a/x/deployment/handler/server.go b/x/deployment/handler/server.go index 0f821e864..56e88e15d 100644 --- a/x/deployment/handler/server.go +++ b/x/deployment/handler/server.go @@ -61,11 +61,29 @@ func (ms msgServer) CreateDeployment(goCtx context.Context, msg *types.MsgCreate return nil, v1.ErrInvalidDeposit } + // Validate reclamation window against market module params + if msg.Reclamation != nil { + marketParams, err := ms.market.GetParams(ctx) + if err != nil { + return nil, err + } + if msg.Reclamation.MinWindow < 0 { + return nil, v1.ErrInvalidReclamation.Wrap("min_window must be >= 0") + } + if msg.Reclamation.MinWindow < marketParams.MinReclamationWindow { + return nil, v1.ErrInvalidReclamation.Wrap("min_window below governance minimum") + } + if msg.Reclamation.MinWindow > marketParams.MaxReclamationWindow { + return nil, v1.ErrInvalidReclamation.Wrap("min_window above governance maximum") + } + } + deployment := v1.Deployment{ - ID: did, - State: v1.DeploymentActive, - Hash: msg.Hash, - CreatedAt: ctx.BlockHeight(), + ID: did, + State: v1.DeploymentActive, + Hash: msg.Hash, + CreatedAt: ctx.BlockHeight(), + Reclamation: msg.Reclamation, } if err := types.ValidateDeploymentGroups(msg.Groups); err != nil { @@ -98,7 +116,7 @@ func (ms msgServer) CreateDeployment(goCtx context.Context, msg *types.MsgCreate // create orders for _, group := range groups { - if _, err := ms.market.CreateOrder(ctx, group.ID, group.GroupSpec); err != nil { + if _, err := ms.market.CreateOrder(ctx, group.ID, group.GroupSpec, msg.Reclamation); err != nil { return &types.MsgCreateDeploymentResponse{}, err } } @@ -223,7 +241,12 @@ func (ms msgServer) StartGroup(goCtx context.Context, msg *types.MsgStartGroup) if err != nil { return &types.MsgStartGroupResponse{}, err } - if _, err := ms.market.CreateOrder(ctx, group.ID, group.GroupSpec); err != nil { + deployment, found := ms.deployment.GetDeployment(ctx, msg.ID.DeploymentID()) + if !found { + return &types.MsgStartGroupResponse{}, v1.ErrDeploymentNotFound + } + + if _, err := ms.market.CreateOrder(ctx, group.ID, group.GroupSpec, deployment.Reclamation); err != nil { return &types.MsgStartGroupResponse{}, err } diff --git a/x/deployment/imports/keepers.go b/x/deployment/imports/keepers.go index 13f7ffaca..3769920f6 100644 --- a/x/deployment/imports/keepers.go +++ b/x/deployment/imports/keepers.go @@ -19,7 +19,8 @@ import ( // MarketKeeper is the subset of the market keeper needed for denom migration. type MarketKeeper interface { - CreateOrder(ctx sdk.Context, id dv1.GroupID, spec dvbeta.GroupSpec) (mvbeta.Order, error) + CreateOrder(ctx sdk.Context, id dv1.GroupID, spec dvbeta.GroupSpec, reclamation *dv1.DeploymentReclamation) (mvbeta.Order, error) + GetParams(ctx sdk.Context) (mvbeta.Params, error) OnGroupClosed(ctx sdk.Context, id dv1.GroupID, state dvbeta.Group_State) error WithOrdersForGroup(ctx sdk.Context, id dv1.GroupID, state mvbeta.Order_State, fn func(mvbeta.Order) bool) WithBidsForOrder(ctx sdk.Context, id mv1.OrderID, state mvbeta.Bid_State, fn func(mvbeta.Bid) bool) diff --git a/x/market/handler/handler.go b/x/market/handler/handler.go index 2ac6e7d16..b055432a7 100644 --- a/x/market/handler/handler.go +++ b/x/market/handler/handler.go @@ -29,6 +29,9 @@ func NewHandler(keepers Keepers) baseapp.MsgServiceHandler { case *mvbeta.MsgCloseLease: res, err := ms.CloseLease(ctx, msg) return sdk.WrapServiceResult(ctx, res, err) + case *mvbeta.MsgLeaseStartReclaim: + res, err := ms.LeaseStartReclaim(ctx, msg) + return sdk.WrapServiceResult(ctx, res, err) default: return nil, sdkerrors.ErrUnknownRequest } diff --git a/x/market/handler/handler_reclamation_test.go b/x/market/handler/handler_reclamation_test.go new file mode 100644 index 000000000..b6e6be124 --- /dev/null +++ b/x/market/handler/handler_reclamation_test.go @@ -0,0 +1,703 @@ +package handler_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + dv1 "pkg.akt.dev/go/node/deployment/v1" + dtypes "pkg.akt.dev/go/node/deployment/v1beta4" + mv1 "pkg.akt.dev/go/node/market/v1" + mvbeta "pkg.akt.dev/go/node/market/v1beta5" + deposit "pkg.akt.dev/go/node/types/deposit/v1" + "pkg.akt.dev/go/testutil" + + "pkg.akt.dev/node/v2/testutil/state" + bmemodule "pkg.akt.dev/node/v2/x/bme" +) + +// =========================== +// Helpers +// =========================== + +// prepareBlanketMocks sets up blanket bank mocks for tests that need escrow operations. +func prepareBlanketMocks(suite *testSuite) { + suite.PrepareMocks(func(ts *state.TestSuite) { + bkeeper := ts.BankKeeper() + bkeeper.On("SendCoinsFromAccountToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + bkeeper.On("SendCoinsFromModuleToAccount", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + bkeeper.On("SendCoinsFromModuleToModule", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe() + bkeeper.On("MintCoins", mock.Anything, bmemodule.ModuleName, mock.Anything).Return(nil).Maybe() + bkeeper.On("BurnCoins", mock.Anything, bmemodule.ModuleName, mock.Anything).Return(nil).Maybe() + }) +} + +// setupEscrowAccount creates only the escrow account for a deployment, +// without creating the payment. Use this when the handler will create the payment. +func (st *testSuite) setupEscrowAccount(bid mvbeta.Bid, order mvbeta.Order) { + st.t.Helper() + ctx := st.Context() + + owner, err := sdk.AccAddressFromBech32(bid.ID.Owner) + require.NoError(st.t, err) + + denom := bid.Price.Denom + defaultDeposit, err := dtypes.DefaultParams().MinDepositFor(denom) + if err != nil { + defaultDeposit, err = dtypes.DefaultParams().MinDepositFor("uact") + require.NoError(st.t, err) + } + + msg := &dtypes.MsgCreateDeployment{ + ID: order.ID.GroupID().DeploymentID(), + Deposit: deposit.Deposit{ + Amount: defaultDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + deposits, err := st.EscrowKeeper().AuthorizeDeposits(ctx, msg) + require.NoError(st.t, err) + + err = st.EscrowKeeper().AccountCreate(ctx, bid.ID.DeploymentID().ToEscrowAccountID(), owner, deposits) + require.NoError(st.t, err) +} + +// setupLeaseEscrow creates the escrow account and payment for a lease. +func (st *testSuite) setupLeaseEscrow(bid mvbeta.Bid, order mvbeta.Order) { + st.t.Helper() + ctx := st.Context() + + owner, err := sdk.AccAddressFromBech32(bid.ID.Owner) + require.NoError(st.t, err) + provider, err := sdk.AccAddressFromBech32(bid.ID.Provider) + require.NoError(st.t, err) + + // Use the bid price denom to determine the deposit denom + denom := bid.Price.Denom + defaultDeposit, err := dtypes.DefaultParams().MinDepositFor(denom) + if err != nil { + // Fallback: try uact + defaultDeposit, err = dtypes.DefaultParams().MinDepositFor("uact") + require.NoError(st.t, err) + } + + msg := &dtypes.MsgCreateDeployment{ + ID: order.ID.GroupID().DeploymentID(), + Deposit: deposit.Deposit{ + Amount: defaultDeposit, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + } + + deposits, err := st.EscrowKeeper().AuthorizeDeposits(ctx, msg) + require.NoError(st.t, err) + + err = st.EscrowKeeper().AccountCreate(ctx, bid.ID.DeploymentID().ToEscrowAccountID(), owner, deposits) + require.NoError(st.t, err) + + err = st.EscrowKeeper().PaymentCreate(ctx, bid.ID.LeaseID().ToEscrowPaymentID(), provider, bid.Price) + require.NoError(st.t, err) +} + +func (st *testSuite) createOrderWithReclamation(resources dtypes.ResourceUnits, reclamation *dv1.DeploymentReclamation) (mvbeta.Order, dtypes.GroupSpec) { + st.t.Helper() + + deployment := testutil.Deployment(st.t) + deployment.Reclamation = reclamation + group := testutil.DeploymentGroup(st.t, deployment.ID, 0) + group.GroupSpec.Resources = resources + + err := st.DeploymentKeeper().Create(st.Context(), deployment, []dtypes.Group{group}) + require.NoError(st.t, err) + + order, err := st.MarketKeeper().CreateOrder(st.Context(), group.ID, group.GroupSpec, reclamation) + require.NoError(st.t, err) + + return order, group.GroupSpec +} + +func (st *testSuite) createBidWithReclamation(reclaimWindow *time.Duration) (mvbeta.Bid, mvbeta.Order) { + st.t.Helper() + order, gspec := st.createOrder(testutil.Resources(st.t, testutil.WithDenom("uact"))) + provider := testutil.AccAddress(st.t) + + price := order.Price() // use order price to ensure bid doesn't exceed it + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + bidID := mv1.MakeBidID(order.ID, provider) + + bid, err := st.MarketKeeper().CreateBid(st.Context(), bidID, price, roffer, reclaimWindow) + require.NoError(st.t, err) + + return bid, order +} + +func (st *testSuite) createLeaseWithReclamation(reclaimWindow *time.Duration) (mv1.LeaseID, mvbeta.Bid, mvbeta.Order) { + st.t.Helper() + bid, order := st.createBidWithReclamation(reclaimWindow) + + err := st.MarketKeeper().CreateLease(st.Context(), bid) + require.NoError(st.t, err) + + // Store reclamation on the lease if bid has it + if reclaimWindow != nil { + lease, found := st.MarketKeeper().GetLease(st.Context(), bid.ID.LeaseID()) + require.True(st.t, found) + lease.Reclamation = &mv1.Reclamation{ + Window: *reclaimWindow, + } + err = st.MarketKeeper().SaveLease(st.Context(), lease) + require.NoError(st.t, err) + } + + st.MarketKeeper().OnBidMatched(st.Context(), bid) + st.MarketKeeper().OnOrderMatched(st.Context(), order) + + lid := mv1.MakeLeaseID(bid.ID) + return lid, bid, order +} + +// =========================== +// LeaseStartReclaim Tests +// =========================== + +func TestLeaseStartReclaim_Success(t *testing.T) { + suite := setupTestSuite(t) + + window := 24 * time.Hour + lid, _, _ := suite.createLeaseWithReclamation(&window) + + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx := suite.Context().WithBlockTime(blockTime) + + msg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(ctx, msg) + require.NoError(t, err) + require.NotNil(t, res) + + // Verify lease state + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + assert.Equal(t, mv1.LeaseReclaiming, lease.State) + assert.Equal(t, int64(100), lease.Reclamation.StartedAt) + assert.Equal(t, blockTime.Add(24*time.Hour).Unix(), lease.Reclamation.Deadline) + assert.Equal(t, mv1.LeaseClosedReason(10001), lease.Reclamation.Reason) +} + +func TestLeaseStartReclaim_NoReclamation(t *testing.T) { + suite := setupTestSuite(t) + + // Create lease without reclamation + lid, _, _ := suite.createLease() + + msg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(suite.Context(), msg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrLeaseNotReclamable) +} + +func TestLeaseStartReclaim_AlreadyReclaiming(t *testing.T) { + suite := setupTestSuite(t) + + window := 24 * time.Hour + lid, _, _ := suite.createLeaseWithReclamation(&window) + + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx := suite.Context().WithBlockTime(blockTime) + + msg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + + // First call succeeds + res, err := suite.handler(ctx, msg) + require.NoError(t, err) + require.NotNil(t, res) + + // Second call fails -- lease is now in LeaseReclaiming state, not LeaseActive + res, err = suite.handler(ctx, msg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrLeaseNotActive) +} + +func TestLeaseStartReclaim_LeaseNotActive(t *testing.T) { + suite := setupTestSuite(t) + + window := 24 * time.Hour + lid, _, _ := suite.createLeaseWithReclamation(&window) + + // Close the lease first + lease, found := suite.MarketKeeper().GetLease(suite.Context(), lid) + require.True(t, found) + _ = suite.MarketKeeper().OnLeaseClosed(suite.Context(), lease, mv1.LeaseClosed, mv1.LeaseClosedReasonOwner) + + msg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(suite.Context(), msg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrLeaseNotActive) +} + +func TestLeaseStartReclaim_UnknownLease(t *testing.T) { + suite := setupTestSuite(t) + + msg := &mvbeta.MsgLeaseStartReclaim{ + ID: testutil.LeaseID(t), + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(suite.Context(), msg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrUnknownLease) +} + +// =========================== +// CloseBid Reclamation Enforcement Tests +// =========================== + +func TestCloseBid_ReclamationNotStarted(t *testing.T) { + suite := setupTestSuite(t) + + window := 24 * time.Hour + lid, bid, _ := suite.createLeaseWithReclamation(&window) + + _ = lid // used implicitly through bid + + msg := &mvbeta.MsgCloseBid{ + ID: bid.ID, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(suite.Context(), msg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationNotStarted) +} + +func TestCloseBid_ReclamationWindowNotElapsed(t *testing.T) { + suite := setupTestSuite(t) + + window := 24 * time.Hour + lid, bid, _ := suite.createLeaseWithReclamation(&window) + + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx := suite.Context().WithBlockTime(blockTime) + + // Start reclamation + reclaimMsg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + res, err := suite.handler(ctx, reclaimMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Try to close DURING the window (advance 12h, but window is 24h) + midWindowTime := blockTime.Add(12 * time.Hour) + ctx = suite.Context().WithBlockTime(midWindowTime) + + closeMsg := &mvbeta.MsgCloseBid{ + ID: bid.ID, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err = suite.handler(ctx, closeMsg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationWindowNotElapsed) +} + +func TestCloseBid_AfterReclamationWindow(t *testing.T) { + suite := setupTestSuite(t) + prepareBlanketMocks(suite) + + window := 1 * time.Hour + lid, bid, order := suite.createLeaseWithReclamation(&window) + suite.setupLeaseEscrow(bid, order) + + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx := suite.Context().WithBlockTime(blockTime) + + // Start reclamation + reclaimMsg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + res, err := suite.handler(ctx, reclaimMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Advance past the window (window is 1h, advance 2h) + afterWindowTime := blockTime.Add(2 * time.Hour) + ctx = suite.Context().WithBlockTime(afterWindowTime) + + closeMsg := &mvbeta.MsgCloseBid{ + ID: bid.ID, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err = suite.handler(ctx, closeMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Verify lease is closed + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) +} + +func TestCloseBid_NoReclamation_StillWorks(t *testing.T) { + suite := setupTestSuite(t) + prepareBlanketMocks(suite) + + // Create lease WITHOUT reclamation + lid, bid, order := suite.createLease() + suite.setupLeaseEscrow(bid, order) + + closeMsg := &mvbeta.MsgCloseBid{ + ID: bid.ID, + Reason: mv1.LeaseClosedReason(10001), + } + + res, err := suite.handler(suite.Context(), closeMsg) + require.NoError(t, err) + require.NotNil(t, res) + + lease, found := suite.MarketKeeper().GetLease(suite.Context(), lid) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) +} + +// =========================== +// CloseLease During Reclamation Tests +// =========================== + +func TestCloseLease_DuringReclamation_TenantCanAlwaysClose(t *testing.T) { + suite := setupTestSuite(t) + prepareBlanketMocks(suite) + + window := 24 * time.Hour + lid, bid, order := suite.createLeaseWithReclamation(&window) + suite.setupLeaseEscrow(bid, order) + + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx := suite.Context().WithBlockTime(blockTime) + + // Start reclamation + reclaimMsg := &mvbeta.MsgLeaseStartReclaim{ + ID: lid, + Reason: mv1.LeaseClosedReason(10001), + } + res, err := suite.handler(ctx, reclaimMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Tenant closes DURING the window (only 1 minute into the 24h window) + earlyTime := blockTime.Add(1 * time.Minute) + ctx = suite.Context().WithBlockTime(earlyTime) + + closeMsg := &mvbeta.MsgCloseLease{ + ID: lid, + } + + res, err = suite.handler(ctx, closeMsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Verify lease is closed + lease, found := suite.MarketKeeper().GetLease(ctx, lid) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) +} + +func TestCloseLease_ActiveWithReclamation_TenantCanClose(t *testing.T) { + suite := setupTestSuite(t) + prepareBlanketMocks(suite) + + window := 24 * time.Hour + lid, bid, order := suite.createLeaseWithReclamation(&window) + suite.setupLeaseEscrow(bid, order) + + // Tenant closes without reclamation being started (lease is Active, has reclamation config) + closeMsg := &mvbeta.MsgCloseLease{ + ID: lid, + } + + res, err := suite.handler(suite.Context(), closeMsg) + require.NoError(t, err) + require.NotNil(t, res) + + lease, found := suite.MarketKeeper().GetLease(suite.Context(), lid) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) +} + +// =========================== +// CreateLease Reclamation Storage Tests +// =========================== + +func TestCreateLease_StoresReclamation(t *testing.T) { + suite := setupTestSuite(t) + prepareBlanketMocks(suite) + + window := 48 * time.Hour + bid, order := suite.createBidWithReclamation(&window) + suite.setupEscrowAccount(bid, order) + + msg := &mvbeta.MsgCreateLease{ + BidID: bid.ID, + } + + res, err := suite.handler(suite.Context(), msg) + require.NoError(t, err) + require.NotNil(t, res) + + lid := mv1.MakeLeaseID(bid.ID) + lease, found := suite.MarketKeeper().GetLease(suite.Context(), lid) + require.True(t, found) + require.NotNil(t, lease.Reclamation) + assert.Equal(t, 48*time.Hour, lease.Reclamation.Window) + assert.Equal(t, int64(0), lease.Reclamation.StartedAt) + assert.Equal(t, int64(0), lease.Reclamation.Deadline) +} + +func TestCreateLease_NoReclamation(t *testing.T) { + suite := setupTestSuite(t) + + lid, _, _ := suite.createLease() + + lease, found := suite.MarketKeeper().GetLease(suite.Context(), lid) + require.True(t, found) + assert.Nil(t, lease.Reclamation) +} + +// =========================== +// CreateBid Reclamation Validation Tests +// =========================== + +func TestCreateBid_ReclamationRequired_NoBidWindow(t *testing.T) { + suite := setupTestSuite(t) + + reclamation := &dv1.DeploymentReclamation{ + MinWindow: 24 * time.Hour, + } + order, gspec := suite.createOrderWithReclamation( + testutil.Resources(t, testutil.WithDenom("uact")), + reclamation, + ) + + provider := suite.createProvider(gspec.Requirements.Attributes) + providerAddr, _ := sdk.AccAddressFromBech32(provider.Owner) + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + suite.PrepareMocks(func(ts *state.TestSuite) { + ts.MockBMEForDeposit(providerAddr, mvbeta.DefaultBidMinDepositACT) + }) + + bmsg := &mvbeta.MsgCreateBid{ + ID: mv1.MakeBidID(order.ID, providerAddr), + Price: order.Price(), + Deposit: deposit.Deposit{ + Amount: mvbeta.DefaultBidMinDepositACT, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + ResourcesOffer: roffer, + ReclamationWindow: nil, // no reclamation offered + } + + res, err := suite.handler(suite.Context(), bmsg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationRequired) +} + +func TestCreateBid_ReclamationWindowTooShort(t *testing.T) { + suite := setupTestSuite(t) + + reclamation := &dv1.DeploymentReclamation{ + MinWindow: 24 * time.Hour, + } + order, gspec := suite.createOrderWithReclamation( + testutil.Resources(t, testutil.WithDenom("uact")), + reclamation, + ) + + provider := suite.createProvider(gspec.Requirements.Attributes) + providerAddr, _ := sdk.AccAddressFromBech32(provider.Owner) + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + suite.PrepareMocks(func(ts *state.TestSuite) { + ts.MockBMEForDeposit(providerAddr, mvbeta.DefaultBidMinDepositACT) + }) + + shortWindow := 1 * time.Hour // less than 24h min + bmsg := &mvbeta.MsgCreateBid{ + ID: mv1.MakeBidID(order.ID, providerAddr), + Price: order.Price(), + Deposit: deposit.Deposit{ + Amount: mvbeta.DefaultBidMinDepositACT, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + ResourcesOffer: roffer, + ReclamationWindow: &shortWindow, + } + + res, err := suite.handler(suite.Context(), bmsg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationWindowTooShort) +} + +func TestCreateBid_ReclamationWindowBelowGovernanceMin(t *testing.T) { + suite := setupTestSuite(t) + + // Order does NOT require reclamation, but provider offers a window below governance min + order, gspec := suite.createOrder(testutil.Resources(t, testutil.WithDenom("uact"))) + + provider := suite.createProvider(gspec.Requirements.Attributes) + providerAddr, _ := sdk.AccAddressFromBech32(provider.Owner) + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + suite.PrepareMocks(func(ts *state.TestSuite) { + ts.MockBMEForDeposit(providerAddr, mvbeta.DefaultBidMinDepositACT) + }) + + tinyWindow := 1 * time.Second // below 1h governance min + bmsg := &mvbeta.MsgCreateBid{ + ID: mv1.MakeBidID(order.ID, providerAddr), + Price: order.Price(), + Deposit: deposit.Deposit{ + Amount: mvbeta.DefaultBidMinDepositACT, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + ResourcesOffer: roffer, + ReclamationWindow: &tinyWindow, + } + + res, err := suite.handler(suite.Context(), bmsg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationWindowInvalid) +} + +func TestCreateBid_ReclamationWindowAboveGovernanceMax(t *testing.T) { + suite := setupTestSuite(t) + + order, gspec := suite.createOrder(testutil.Resources(t, testutil.WithDenom("uact"))) + + provider := suite.createProvider(gspec.Requirements.Attributes) + providerAddr, _ := sdk.AccAddressFromBech32(provider.Owner) + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + suite.PrepareMocks(func(ts *state.TestSuite) { + ts.MockBMEForDeposit(providerAddr, mvbeta.DefaultBidMinDepositACT) + }) + + hugeWindow := 10000 * time.Hour // above 720h governance max + bmsg := &mvbeta.MsgCreateBid{ + ID: mv1.MakeBidID(order.ID, providerAddr), + Price: order.Price(), + Deposit: deposit.Deposit{ + Amount: mvbeta.DefaultBidMinDepositACT, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + ResourcesOffer: roffer, + ReclamationWindow: &hugeWindow, + } + + res, err := suite.handler(suite.Context(), bmsg) + require.Nil(t, res) + require.Error(t, err) + require.ErrorIs(t, err, mv1.ErrReclamationWindowInvalid) +} + +func TestCreateBid_ReclamationWindowValid_NoOrderRequirement(t *testing.T) { + suite := setupTestSuite(t) + + // Order does NOT require reclamation, but provider voluntarily offers it + order, gspec := suite.createOrder(testutil.Resources(t, testutil.WithDenom("uact"))) + + provider := suite.createProvider(gspec.Requirements.Attributes) + providerAddr, _ := sdk.AccAddressFromBech32(provider.Owner) + roffer := mvbeta.ResourceOfferFromRU(gspec.Resources) + + suite.PrepareMocks(func(ts *state.TestSuite) { + ts.MockBMEForDeposit(providerAddr, mvbeta.DefaultBidMinDepositACT) + }) + + validWindow := 24 * time.Hour + bmsg := &mvbeta.MsgCreateBid{ + ID: mv1.MakeBidID(order.ID, providerAddr), + Price: order.Price(), + Deposit: deposit.Deposit{ + Amount: mvbeta.DefaultBidMinDepositACT, + Sources: deposit.Sources{deposit.SourceBalance}, + }, + ResourcesOffer: roffer, + ReclamationWindow: &validWindow, + } + + res, err := suite.handler(suite.Context(), bmsg) + require.NoError(t, err) + require.NotNil(t, res) + + // Verify bid has reclamation window + bidID := mv1.MakeBidID(order.ID, providerAddr) + bid, found := suite.MarketKeeper().GetBid(suite.Context(), bidID) + require.True(t, found) + require.NotNil(t, bid.ReclamationWindow) + assert.Equal(t, 24*time.Hour, *bid.ReclamationWindow) +} + +// =========================== +// Order Reclamation Propagation Tests +// =========================== + +func TestOrder_RequiresReclamation(t *testing.T) { + suite := setupTestSuite(t) + + reclamation := &dv1.DeploymentReclamation{ + MinWindow: 24 * time.Hour, + } + order, _ := suite.createOrderWithReclamation(testutil.Resources(t), reclamation) + + assert.True(t, order.RequiresReclamation()) + require.NotNil(t, order.Reclamation) + assert.Equal(t, 24*time.Hour, order.Reclamation.MinWindow) +} + +func TestOrder_DoesNotRequireReclamation(t *testing.T) { + suite := setupTestSuite(t) + + order, _ := suite.createOrder(testutil.Resources(t)) + + assert.False(t, order.RequiresReclamation()) + assert.Nil(t, order.Reclamation) +} diff --git a/x/market/handler/handler_test.go b/x/market/handler/handler_test.go index 8617a804a..fb72a2603 100644 --- a/x/market/handler/handler_test.go +++ b/x/market/handler/handler_test.go @@ -1354,7 +1354,7 @@ func TestCloseBidUnknownOrder(t *testing.T) { roffer := mvbeta.ResourceOfferFromRU(group.GroupSpec.Resources) bidID := mv1.MakeBidID(orderID, provider) - bid, err := suite.MarketKeeper().CreateBid(suite.Context(), bidID, price, roffer) + bid, err := suite.MarketKeeper().CreateBid(suite.Context(), bidID, price, roffer, nil) require.NoError(t, err) err = suite.MarketKeeper().CreateLease(suite.Context(), bid) @@ -1393,7 +1393,7 @@ func (st *testSuite) createBid() (mvbeta.Bid, mvbeta.Order) { bidID := mv1.MakeBidID(order.ID, provider) - bid, err := st.MarketKeeper().CreateBid(st.Context(), bidID, price, roffer) + bid, err := st.MarketKeeper().CreateBid(st.Context(), bidID, price, roffer, nil) require.NoError(st.t, err) require.Equal(st.t, order.ID, bid.ID.OrderID()) require.Equal(st.t, price, bid.Price) @@ -1411,7 +1411,7 @@ func (st *testSuite) createOrder(resources dtypes.ResourceUnits) (mvbeta.Order, err := st.DeploymentKeeper().Create(st.Context(), deployment, []dtypes.Group{group}) require.NoError(st.t, err) - order, err := st.MarketKeeper().CreateOrder(st.Context(), group.ID, group.GroupSpec) + order, err := st.MarketKeeper().CreateOrder(st.Context(), group.ID, group.GroupSpec, nil) require.NoError(st.t, err) require.Equal(st.t, group.ID, order.ID.GroupID()) require.Equal(st.t, uint32(1), order.ID.OSeq) diff --git a/x/market/handler/server.go b/x/market/handler/server.go index 7d08d4ad8..935b7dd78 100644 --- a/x/market/handler/server.go +++ b/x/market/handler/server.go @@ -101,7 +101,26 @@ func (ms msgServer) CreateBid(goCtx context.Context, msg *mvbeta.MsgCreateBid) ( return nil, err } - bid, err := ms.keepers.Market.CreateBid(ctx, msg.ID, msg.Price, msg.ResourcesOffer) + // Reclamation validation + if order.RequiresReclamation() { + if msg.ReclamationWindow == nil { + return nil, mv1.ErrReclamationRequired + } + if *msg.ReclamationWindow < order.Reclamation.MinWindow { + return nil, mv1.ErrReclamationWindowTooShort + } + } + + if msg.ReclamationWindow != nil { + if *msg.ReclamationWindow < params.MinReclamationWindow { + return nil, mv1.ErrReclamationWindowInvalid + } + if *msg.ReclamationWindow > params.MaxReclamationWindow { + return nil, mv1.ErrReclamationWindowInvalid + } + } + + bid, err := ms.keepers.Market.CreateBid(ctx, msg.ID, msg.Price, msg.ResourcesOffer, msg.ReclamationWindow) if err != nil { return nil, err } @@ -139,7 +158,17 @@ func (ms msgServer) CloseBid(goCtx context.Context, msg *mvbeta.MsgCloseBid) (*m return nil, mv1.ErrUnknownLeaseForBid } - if lease.State != mv1.LeaseActive { + // Reclamation-aware lease state check + switch lease.State { + case mv1.LeaseActive: + if lease.Reclamation != nil { + return nil, mv1.ErrReclamationNotStarted + } + case mv1.LeaseReclaiming: + if ctx.BlockTime().Unix() < lease.Reclamation.Deadline { + return nil, mv1.ErrReclamationWindowNotElapsed + } + default: return nil, mv1.ErrLeaseNotActive } @@ -224,6 +253,19 @@ func (ms msgServer) CreateLease(goCtx context.Context, msg *mvbeta.MsgCreateLeas return &mvbeta.MsgCreateLeaseResponse{}, err } + if bid.ReclamationWindow != nil { + lease, found := ms.keepers.Market.GetLease(ctx, bid.ID.LeaseID()) + if !found { + return &mvbeta.MsgCreateLeaseResponse{}, mv1.ErrLeaseNotFound + } + lease.Reclamation = &mv1.Reclamation{ + Window: *bid.ReclamationWindow, + } + if err = ms.keepers.Market.SaveLease(ctx, lease); err != nil { + return &mvbeta.MsgCreateLeaseResponse{}, err + } + } + ms.keepers.Market.OnOrderMatched(ctx, order) ms.keepers.Market.OnBidMatched(ctx, bid) @@ -264,7 +306,7 @@ func (ms msgServer) CloseLease(goCtx context.Context, msg *mvbeta.MsgCloseLease) if !found { return &mvbeta.MsgCloseLeaseResponse{}, mv1.ErrLeaseNotFound } - if lease.State != mv1.LeaseActive { + if lease.State != mv1.LeaseActive && lease.State != mv1.LeaseReclaiming { return &mvbeta.MsgCloseLeaseResponse{}, mv1.ErrOrderClosed } @@ -286,13 +328,56 @@ func (ms msgServer) CloseLease(goCtx context.Context, msg *mvbeta.MsgCloseLease) return &mvbeta.MsgCloseLeaseResponse{}, nil } - if _, err := ms.keepers.Market.CreateOrder(ctx, group.ID, group.GroupSpec); err != nil { + if _, err := ms.keepers.Market.CreateOrder(ctx, group.ID, group.GroupSpec, order.Reclamation); err != nil { return &mvbeta.MsgCloseLeaseResponse{}, err } return &mvbeta.MsgCloseLeaseResponse{}, nil } +func (ms msgServer) LeaseStartReclaim(goCtx context.Context, msg *mvbeta.MsgLeaseStartReclaim) (*mvbeta.MsgLeaseStartReclaimResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + lease, found := ms.keepers.Market.GetLease(ctx, msg.ID) + if !found { + return nil, mv1.ErrUnknownLease + } + + if lease.State != mv1.LeaseActive { + return nil, mv1.ErrLeaseNotActive + } + + if lease.Reclamation == nil { + return nil, mv1.ErrLeaseNotReclamable + } + + if lease.Reclamation.StartedAt != 0 { + return nil, mv1.ErrLeaseAlreadyReclaiming + } + + blockTime := ctx.BlockTime() + deadline := blockTime.Add(lease.Reclamation.Window) + + lease.Reclamation.StartedAt = ctx.BlockHeight() + lease.Reclamation.Deadline = deadline.Unix() + lease.Reclamation.Reason = msg.Reason + lease.State = mv1.LeaseReclaiming + + if err := ms.keepers.Market.SaveLease(ctx, lease); err != nil { + return nil, err + } + + if err := ctx.EventManager().EmitTypedEvent(&mv1.EventLeaseReclaimStarted{ + ID: lease.ID, + Reason: msg.Reason, + Deadline: deadline.Unix(), + }); err != nil { + return nil, err + } + + return &mvbeta.MsgLeaseStartReclaimResponse{}, nil +} + func (ms msgServer) UpdateParams(goCtx context.Context, req *mvbeta.MsgUpdateParams) (*mvbeta.MsgUpdateParamsResponse, error) { if ms.keepers.Market.GetAuthority() != req.Authority { return nil, govtypes.ErrInvalidSigner.Wrapf("invalid authority; expected %s, got %s", ms.keepers.Market.GetAuthority(), req.Authority) diff --git a/x/market/keeper/grpc_query.go b/x/market/keeper/grpc_query.go index fea3ea644..3ff11f9a4 100644 --- a/x/market/keeper/grpc_query.go +++ b/x/market/keeper/grpc_query.go @@ -758,7 +758,7 @@ func (k Querier) Leases(c context.Context, req *types.QueryLeasesRequest) (*type } states = append(states, byte(stateVal)) } else { - states = append(states, byte(v1.LeaseActive), byte(v1.LeaseInsufficientFunds), byte(v1.LeaseClosed)) + states = append(states, byte(v1.LeaseActive), byte(v1.LeaseInsufficientFunds), byte(v1.LeaseClosed), byte(v1.LeaseReclaiming)) } // Resolve iteration path from filters diff --git a/x/market/keeper/keeper.go b/x/market/keeper/keeper.go index d0cbba069..33bba5118 100644 --- a/x/market/keeper/keeper.go +++ b/x/market/keeper/keeper.go @@ -2,6 +2,7 @@ package keeper import ( "fmt" + "time" "cosmossdk.io/collections" "cosmossdk.io/collections/indexes" @@ -23,8 +24,8 @@ type IKeeper interface { NewQuerier() Querier Codec() codec.BinaryCodec StoreKey() storetypes.StoreKey - CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.GroupSpec) (types.Order, error) - CreateBid(ctx sdk.Context, id mv1.BidID, price sdk.DecCoin, roffer types.ResourcesOffer) (types.Bid, error) + CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.GroupSpec, reclamation *dtypes.DeploymentReclamation) (types.Order, error) + CreateBid(ctx sdk.Context, id mv1.BidID, price sdk.DecCoin, roffer types.ResourcesOffer, reclaimWindow *time.Duration) (types.Bid, error) CreateLease(ctx sdk.Context, bid types.Bid) error OnOrderMatched(ctx sdk.Context, order types.Order) OnBidMatched(ctx sdk.Context, bid types.Bid) @@ -150,7 +151,7 @@ func (k Keeper) GetParams(ctx sdk.Context) (types.Params, error) { } // CreateOrder creates a new order with given group id and specifications. It returns created order -func (k Keeper) CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.GroupSpec) (types.Order, error) { +func (k Keeper) CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.GroupSpec, reclamation *dtypes.DeploymentReclamation) (types.Order, error) { oseq := uint32(1) var err error @@ -185,10 +186,11 @@ func (k Keeper) CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.Gro } order := types.Order{ - ID: orderID, - Spec: spec, - State: types.OrderOpen, - CreatedAt: ctx.BlockHeight(), + ID: orderID, + Spec: spec, + State: types.OrderOpen, + CreatedAt: ctx.BlockHeight(), + Reclamation: reclamation, } if err := k.orders.Set(ctx, pk, order); err != nil { @@ -208,7 +210,7 @@ func (k Keeper) CreateOrder(ctx sdk.Context, gid dtypes.GroupID, spec dvbeta.Gro } // CreateBid creates a bid for a order with given orderID, price for bid and provider -func (k Keeper) CreateBid(ctx sdk.Context, id mv1.BidID, price sdk.DecCoin, roffer types.ResourcesOffer) (types.Bid, error) { +func (k Keeper) CreateBid(ctx sdk.Context, id mv1.BidID, price sdk.DecCoin, roffer types.ResourcesOffer, reclaimWindow *time.Duration) (types.Bid, error) { pk := keys.BidIDToKey(id) has, err := k.bids.Has(ctx, pk) @@ -220,11 +222,12 @@ func (k Keeper) CreateBid(ctx sdk.Context, id mv1.BidID, price sdk.DecCoin, roff } bid := types.Bid{ - ID: id, - State: types.BidOpen, - Price: price, - CreatedAt: ctx.BlockHeight(), - ResourcesOffer: roffer, + ID: id, + State: types.BidOpen, + Price: price, + CreatedAt: ctx.BlockHeight(), + ResourcesOffer: roffer, + ReclamationWindow: reclaimWindow, } if err := k.bids.Set(ctx, pk, bid); err != nil { diff --git a/x/market/keeper/keeper_reclamation_test.go b/x/market/keeper/keeper_reclamation_test.go new file mode 100644 index 000000000..76a4fd3b7 --- /dev/null +++ b/x/market/keeper/keeper_reclamation_test.go @@ -0,0 +1,232 @@ +package keeper_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dv1 "pkg.akt.dev/go/node/deployment/v1" + mv1 "pkg.akt.dev/go/node/market/v1" + mvbeta "pkg.akt.dev/go/node/market/v1beta5" + "pkg.akt.dev/go/testutil" +) + +func Test_CreateOrderWithReclamation(t *testing.T) { + ctx, keeper, _ := setupKeeper(t) + + group := testutil.DeploymentGroup(t, testutil.DeploymentID(t), 0) + reclamation := &dv1.DeploymentReclamation{ + MinWindow: 24 * time.Hour, + } + + order, err := keeper.CreateOrder(ctx, group.ID, group.GroupSpec, reclamation) + require.NoError(t, err) + require.NotNil(t, order.Reclamation) + assert.Equal(t, 24*time.Hour, order.Reclamation.MinWindow) +} + +func Test_CreateOrderWithoutReclamation(t *testing.T) { + ctx, keeper, _ := setupKeeper(t) + + group := testutil.DeploymentGroup(t, testutil.DeploymentID(t), 0) + + order, err := keeper.CreateOrder(ctx, group.ID, group.GroupSpec, nil) + require.NoError(t, err) + assert.Nil(t, order.Reclamation) +} + +func Test_CreateBidWithReclamationWindow(t *testing.T) { + _, _, suite := setupKeeper(t) + ctx := suite.Context() + keeper := suite.MarketKeeper() + + order, _ := createOrder(t, ctx, keeper) + provider := testutil.AccAddress(t) + price := testutil.ACTDecCoinRandom(t) + roffer := mvbeta.ResourceOfferFromRU(order.Spec.Resources) + bidID := mv1.MakeBidID(order.ID, provider) + + window := 48 * time.Hour + bid, err := keeper.CreateBid(ctx, bidID, price, roffer, &window) + require.NoError(t, err) + require.NotNil(t, bid.ReclamationWindow) + assert.Equal(t, 48*time.Hour, *bid.ReclamationWindow) +} + +func Test_CreateBidWithoutReclamationWindow(t *testing.T) { + _, _, suite := setupKeeper(t) + ctx := suite.Context() + keeper := suite.MarketKeeper() + + order, _ := createOrder(t, ctx, keeper) + provider := testutil.AccAddress(t) + price := testutil.ACTDecCoinRandom(t) + roffer := mvbeta.ResourceOfferFromRU(order.Spec.Resources) + bidID := mv1.MakeBidID(order.ID, provider) + + bid, err := keeper.CreateBid(ctx, bidID, price, roffer, nil) + require.NoError(t, err) + assert.Nil(t, bid.ReclamationWindow) +} + +func Test_LeaseReclamationStoredFromBid(t *testing.T) { + _, _, suite := setupKeeper(t) + ctx := suite.Context() + keeper := suite.MarketKeeper() + + // Create order and bid with reclamation window + order, _ := createOrder(t, ctx, keeper) + provider := testutil.AccAddress(t) + price := testutil.ACTDecCoinRandom(t) + roffer := mvbeta.ResourceOfferFromRU(order.Spec.Resources) + bidID := mv1.MakeBidID(order.ID, provider) + window := 24 * time.Hour + + bid, err := keeper.CreateBid(ctx, bidID, price, roffer, &window) + require.NoError(t, err) + + // Create lease + err = keeper.CreateLease(ctx, bid) + require.NoError(t, err) + + // Get lease and manually set reclamation (simulating what the handler does) + lease, found := keeper.GetLease(ctx, bid.ID.LeaseID()) + require.True(t, found) + assert.Equal(t, mv1.LeaseActive, lease.State) + + // Simulate handler storing reclamation on the lease + lease.Reclamation = &mv1.Reclamation{ + Window: *bid.ReclamationWindow, + } + err = keeper.SaveLease(ctx, lease) + require.NoError(t, err) + + // Verify reclamation is persisted + lease, found = keeper.GetLease(ctx, bid.ID.LeaseID()) + require.True(t, found) + require.NotNil(t, lease.Reclamation) + assert.Equal(t, 24*time.Hour, lease.Reclamation.Window) + assert.Equal(t, int64(0), lease.Reclamation.StartedAt) + assert.Equal(t, int64(0), lease.Reclamation.Deadline) +} + +func Test_LeaseStartReclaim(t *testing.T) { + _, _, suite := setupKeeper(t) + ctx := suite.Context() + keeper := suite.MarketKeeper() + + // Create order, bid, lease with reclamation + order, _ := createOrder(t, ctx, keeper) + provider := testutil.AccAddress(t) + price := testutil.ACTDecCoinRandom(t) + roffer := mvbeta.ResourceOfferFromRU(order.Spec.Resources) + bidID := mv1.MakeBidID(order.ID, provider) + window := 24 * time.Hour + + bid, err := keeper.CreateBid(ctx, bidID, price, roffer, &window) + require.NoError(t, err) + + err = keeper.CreateLease(ctx, bid) + require.NoError(t, err) + + // Store reclamation on lease + lease, found := keeper.GetLease(ctx, bid.ID.LeaseID()) + require.True(t, found) + lease.Reclamation = &mv1.Reclamation{Window: window} + err = keeper.SaveLease(ctx, lease) + require.NoError(t, err) + + // Set block time and height + blockTime := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + suite.SetBlockHeight(100) + ctx = suite.Context().WithBlockTime(blockTime) + + // Start reclamation + lease, found = keeper.GetLease(ctx, bid.ID.LeaseID()) + require.True(t, found) + lease.State = mv1.LeaseReclaiming + lease.Reclamation.StartedAt = ctx.BlockHeight() + lease.Reclamation.Deadline = blockTime.Add(window).Unix() + lease.Reclamation.Reason = mv1.LeaseClosedReason(10001) + err = keeper.SaveLease(ctx, lease) + require.NoError(t, err) + + // Verify state + lease, found = keeper.GetLease(ctx, bid.ID.LeaseID()) + require.True(t, found) + assert.Equal(t, mv1.LeaseReclaiming, lease.State) + assert.Equal(t, int64(100), lease.Reclamation.StartedAt) + assert.Equal(t, blockTime.Add(24*time.Hour).Unix(), lease.Reclamation.Deadline) + assert.Equal(t, mv1.LeaseClosedReason(10001), lease.Reclamation.Reason) +} + +func Test_OnLeaseClosedFromReclaiming(t *testing.T) { + _, _, suite := setupKeeper(t) + + leaseID := createLease(t, suite) + keeper := suite.MarketKeeper() + ctx := suite.Context() + + // Get lease and set it to reclaiming with reclamation data + lease, found := keeper.GetLease(ctx, leaseID) + require.True(t, found) + lease.State = mv1.LeaseReclaiming + lease.Reclamation = &mv1.Reclamation{ + Window: 24 * time.Hour, + StartedAt: 50, + Deadline: time.Now().Add(24 * time.Hour).Unix(), + Reason: mv1.LeaseClosedReason(10001), + } + err := keeper.SaveLease(ctx, lease) + require.NoError(t, err) + + // Close from reclaiming state + suite.SetBlockHeight(200) + ctx = suite.Context() + + lease, found = keeper.GetLease(ctx, leaseID) + require.True(t, found) + + err = keeper.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReason(10001)) + require.NoError(t, err) + + // Verify closed + lease, found = keeper.GetLease(ctx, leaseID) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) + assert.Equal(t, int64(200), lease.ClosedOn) +} + +func Test_OnLeaseClosedFromReclaimingIdempotent(t *testing.T) { + _, _, suite := setupKeeper(t) + + leaseID := createLease(t, suite) + keeper := suite.MarketKeeper() + ctx := suite.Context() + + // Set to reclaiming then close + lease, found := keeper.GetLease(ctx, leaseID) + require.True(t, found) + lease.State = mv1.LeaseReclaiming + lease.Reclamation = &mv1.Reclamation{Window: 1 * time.Hour} + err := keeper.SaveLease(ctx, lease) + require.NoError(t, err) + + suite.SetBlockHeight(100) + ctx = suite.Context() + lease, _ = keeper.GetLease(ctx, leaseID) + + err = keeper.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReason(10001)) + require.NoError(t, err) + + // Close again -- should be idempotent (skipped) + lease, _ = keeper.GetLease(ctx, leaseID) + err = keeper.OnLeaseClosed(ctx, lease, mv1.LeaseClosed, mv1.LeaseClosedReason(10001)) + require.NoError(t, err) + + lease, found = keeper.GetLease(ctx, leaseID) + require.True(t, found) + assert.Equal(t, mv1.LeaseClosed, lease.State) +} diff --git a/x/market/keeper/keeper_test.go b/x/market/keeper/keeper_test.go index bf8df6391..f81b8a1fc 100644 --- a/x/market/keeper/keeper_test.go +++ b/x/market/keeper/keeper_test.go @@ -24,7 +24,7 @@ func Test_CreateOrder(t *testing.T) { order, gspec := createOrder(t, ctx, keeper) // assert one active for group - _, err := keeper.CreateOrder(ctx, order.ID.GroupID(), gspec) + _, err := keeper.CreateOrder(ctx, order.ID.GroupID(), gspec, nil) require.Error(t, err) } @@ -469,7 +469,7 @@ func createBid(t testing.TB, suite *state.TestSuite) (mvbeta.Bid, mvbeta.Order) bidID := mv1.MakeBidID(order.ID, provider) - bid, err := suite.MarketKeeper().CreateBid(ctx, bidID, price, roffer) + bid, err := suite.MarketKeeper().CreateBid(ctx, bidID, price, roffer, nil) require.NoError(t, err) assert.Equal(t, order.ID, bid.ID.OrderID()) assert.Equal(t, price, bid.Price) @@ -500,7 +500,7 @@ func createOrder(t testing.TB, ctx sdk.Context, keeper keeper.IKeeper) (mvbeta.O t.Helper() group := testutil.DeploymentGroup(t, testutil.DeploymentID(t), 0) - order, err := keeper.CreateOrder(ctx, group.ID, group.GroupSpec) + order, err := keeper.CreateOrder(ctx, group.ID, group.GroupSpec, nil) require.NoError(t, err) require.Equal(t, group.ID, order.ID.GroupID()) diff --git a/x/market/module.go b/x/market/module.go index 0cb96d9d6..415ce1387 100644 --- a/x/market/module.go +++ b/x/market/module.go @@ -180,7 +180,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw // ConsensusVersion implements module.AppModule#ConsensusVersion func (am AppModule) ConsensusVersion() uint64 { - return 8 + return 9 } // AppModuleSimulation functions diff --git a/x/market/simulation/genesis.go b/x/market/simulation/genesis.go index 72bd102f1..51d7ed82e 100644 --- a/x/market/simulation/genesis.go +++ b/x/market/simulation/genesis.go @@ -14,8 +14,10 @@ var minDeposit, _ = dtypes.DefaultParams().MinDepositFor("uakt") func RandomizedGenState(simState *module.SimulationState) { marketGenesis := &mvbeta.GenesisState{ Params: mvbeta.Params{ - BidMinDeposit: minDeposit, - OrderMaxBids: 20, + BidMinDeposit: minDeposit, + OrderMaxBids: 20, + MinReclamationWindow: mvbeta.DefaultMinReclamationWindow, + MaxReclamationWindow: mvbeta.DefaultMaxReclamationWindow, }, } diff --git a/x/oracle/keeper/abci.go b/x/oracle/keeper/abci.go index 3aa31df61..fe8a9676b 100644 --- a/x/oracle/keeper/abci.go +++ b/x/oracle/keeper/abci.go @@ -153,10 +153,7 @@ func (k *keeper) EndBlocker(ctx context.Context) error { // Phase 3: aggregate from in-memory data aggregatedPrice, err := k.calculateAggregatedPricesFromHistory(sctx, did, latestPrices, sourcePrices) if err != nil { - sctx.Logger().Error( - "calculate aggregated price", - "reason", err.Error(), - ) + sctx.Logger().Error("calculate aggregated price", "error", err.Error()) } health := k.setPriceHealth(sctx, params, allSourceIDs, aggregatedPrice) @@ -164,10 +161,7 @@ func (k *keeper) EndBlocker(ctx context.Context) error { if health.IsHealthy && len(latestPrices) > 0 { err = k.aggregatedPrices.Set(sctx, did, aggregatedPrice) if err != nil { - sctx.Logger().Error( - "set aggregated price", - "reason", err.Error(), - ) + sctx.Logger().Error("set aggregated price", "reason", err.Error()) } evts = append(evts, &types.EventAggregatedPrice{Price: aggregatedPrice})