diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 00000000..8af44a9e --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,486 @@ +# Saga Security Chain (SSC) Release Notes + +## Version 1.0.0 + +### 🚀 Major Features + +This release introduces a comprehensive re-architecture of the Saga Security Chain with five core modules that work together to provide a robust, scalable blockchain infrastructure for chainlet management, billing, escrow services, and liquid staking capabilities. + +### 🆕 **New in This Release:** +- **x/escrow Module**: Multi-denomination escrow system with KV-based storage and predictable gas costs +- **x/chainlet Module**: Complete chainlet lifecycle management with stack-based architecture and auto-upgrade capabilities +- **x/billing Module**: Universal billing system with epoch-based billing and comprehensive history tracking +- **x/epochs Module**: Time-based event system with multiple epoch types and hook system for other modules +- **x/liquid Module**: Complete liquid staking system for tokenized delegation and reward management +- **Comprehensive Testing**: Full test suite for all module functionality +- **CLI Integration**: Complete command-line interface for all module operations +- **Tokenized Staking**: Advanced tokenization system for delegation shares and reward management +- **Multi-Denomination Support**: Flexible fee structures and token support across all modules +- **Storage Optimization**: KV-based architecture with deterministic gas costs + +--- + +## 📦 Core Modules + +The Saga Security Chain now includes five core modules that provide comprehensive blockchain infrastructure capabilities: + +### 🔐 **x/escrow** - Multi-Denomination Escrow System + +The escrow module provides a sophisticated fund management system with support for multiple denominations and predictable gas costs. + +#### **Key Features:** +- **Multi-Denomination Support**: Each chainlet can support multiple token denominations +- **KV-Based Storage**: Prevents state bloat from dust deposits with efficient per-funder storage +- **Deterministic Gas Costs**: Predictable transaction costs regardless of number of funders +- **Pool-Based Architecture**: Independent pools per `{chainId, denom}` combination +- **Share-Based System**: Proportional share tracking for fair fund distribution + +#### **CLI Commands:** +```bash +# Deposit funds into escrow +sscd tx escrow deposit --from --chain-id + +# Withdraw funds from escrow +sscd tx escrow withdraw --from --chain-id + +# Query escrow balance +sscd query escrow funder-balance
+ +# Query chainlet account information +sscd query escrow chainlet + +# List all funders for a specific pool +sscd query escrow funders + +# Query all pools for a chainlet +sscd query escrow pools + +# Query escrow parameters +sscd query escrow params +``` + +#### **Storage Architecture:** +- **Chainlet Keys**: `escrow/chainlet/{chainId}` +- **Pool Keys**: `escrow/pool/{chainId}/{denom}` +- **Funder Keys**: `escrow/funder/{chainId}/{denom}/{addr}` +- **By-Funder Keys**: `escrow/byFunder/{addr}/{chainId}/{denom}` + +--- + +### 🏗️ **x/chainlet** - Chainlet Lifecycle Management + +The chainlet module manages the complete lifecycle of blockchain chainlets, from creation to upgrades and maintenance. + +#### **Key Features:** +- **Chainlet Stack Management**: Create and manage reusable chainlet configurations +- **Multi-Fee Support**: Each stack can support multiple fee denominations +- **Version Management**: Sophisticated versioning system with compatibility checks +- **Auto-Upgrade Capability**: Automatic stack version upgrades +- **CCV Consumer Support**: Inter-Blockchain Communication (IBC) consumer chain support +- **Service Chainlet Support**: Special chainlets for system services +- **Genesis Validator Management**: Custom validator sets for chainlets + +#### **CLI Commands:** +```bash +# Create a new chainlet stack +sscd tx chainlet create-chainlet-stack --from --chain-id + +# Launch a new chainlet +sscd tx chainlet launch-chainlet --from --chain-id + +# Update chainlet stack +sscd tx chainlet update-chainlet-stack --from --chain-id + +# Update stack fees (NEW FEATURE) +sscd tx chainlet update-stack-fees "1000denom1,2000denom2" --from --chain-id + +# Upgrade chainlet +sscd tx chainlet upgrade-chainlet --from --chain-id + +# Disable chainlet stack version +sscd tx chainlet disable-chainlet-stack-version --from --chain-id + +# Query chainlet information +sscd query chainlet get-chainlet + +# Query chainlet stack +sscd query chainlet get-chainlet-stack + +# List all chainlets +sscd query chainlet list-chainlets + +# Query chainlet count +sscd query chainlet get-chainlet-count + +# Query chainlet parameters +sscd query chainlet params +``` + +#### **Chainlet Properties:** +- **Spawn Time**: When the chainlet was created +- **Launcher**: Account that launched the chainlet +- **Maintainers**: List of accounts with maintenance privileges +- **Stack Information**: Reference to the chainlet stack configuration +- **Chain Parameters**: Custom blockchain parameters (gas limits, block settings) +- **Status Tracking**: Online/offline status monitoring +- **Auto-Upgrade**: Automatic stack version upgrades +- **Genesis Validators**: Custom validator sets +- **Tags**: Categorization and metadata +- **Service Chainlet**: System service designation +- **CCV Consumer**: IBC consumer chain support + +--- + +### 💰 **x/billing** - Universal Billing System + +The billing module provides a centralized billing system with universal epoch configuration and comprehensive billing history tracking. + +#### **Key Features:** +- **Universal Epoch Configuration**: Centralized billing epoch management +- **Multi-Denomination Billing**: Support for billing from different token pools +- **Comprehensive History**: Detailed billing and payout history tracking +- **Validator Payouts**: Automated validator reward distribution +- **Epoch-Based Billing**: Automatic billing at epoch boundaries +- **Failed Billing Handling**: Graceful handling of insufficient funds + +#### **CLI Commands:** +```bash +# Query billing history for an account +sscd query billing get-billing-history
+ +# Query validator payout history +sscd query billing get-validator-payout-history + +# Query billing parameters +sscd query billing params +``` + +#### **Billing Process:** +1. **Epoch Trigger**: Billing occurs at the start of each epoch +2. **Multi-Fee Support**: Tries multiple fee denominations until one succeeds +3. **Automatic Stopping**: Chainlets are stopped if billing fails +4. **History Recording**: All billing events are recorded with timestamps +5. **Validator Payouts**: Automatic distribution to validators + +#### **Billing Parameters:** +- **Validator Payout Epoch**: Epoch identifier for validator payouts +- **Billing Epoch**: Universal epoch identifier for billing cycles + +--- + +### ⏰ **x/epochs** - Time-Based Event System + +The epochs module provides a generalized timing system for other modules to execute code at regular intervals. + +#### **Key Features:** +- **Multiple Epoch Types**: Support for different time intervals (minute, hour, day, week) +- **Hook System**: Other modules can register epoch hooks +- **Panic Isolation**: Failed epoch hooks don't affect other modules +- **Flexible Configuration**: Customizable epoch durations and start times +- **Genesis Initialization**: Pre-configured epoch types + +#### **CLI Commands:** +```bash +# Query all epoch information +sscd query epochs epoch-infos + +# Query current epoch for specific identifier +sscd query epochs current-epoch +``` + +#### **Default Epoch Types:** +- **Minute**: 1-minute intervals +- **Hour**: 1-hour intervals +- **Day**: 24-hour intervals +- **Week**: 7-day intervals + +#### **Epoch Information:** +- **Identifier**: Unique epoch type name +- **Duration**: Time interval between epochs +- **Current Epoch**: Current epoch number +- **Start Time**: When the current epoch began +- **Start Height**: Block height when epoch started +- **Counting Status**: Whether epoch counting has started + +--- + +### 💧 **x/liquid** - Liquid Staking System + +The liquid module provides a sophisticated liquid staking system that enables tokenization of delegation shares, allowing users to maintain liquidity while earning staking rewards. + +#### **Key Features:** +- **Tokenized Delegation**: Convert delegation shares into tradeable tokens +- **Liquid Staking**: Maintain liquidity while earning staking rewards +- **Reward Management**: Automated reward collection and distribution +- **Validator Support**: Support for multiple validators with liquid staking +- **Authorization System**: Controlled tokenization with governance oversight +- **Lock Management**: Time-based locking system for tokenized shares +- **Fee Management**: Configurable fees for tokenization operations + +#### **CLI Commands:** +```bash +# Tokenize delegation shares +sscd tx liquid tokenize-share --from --chain-id + +# Redeem tokenized shares back to delegation +sscd tx liquid redeem-tokens --from --chain-id + +# Enable tokenization for a validator +sscd tx liquid enable-tokenize-shares --from --chain-id + +# Disable tokenization for a validator +sscd tx liquid disable-tokenize-shares --from --chain-id + +# Transfer tokenize share record ownership +sscd tx liquid transfer-tokenize-share-record --from --chain-id + +# Withdraw rewards for a specific tokenize share record +sscd tx liquid withdraw-tokenize-share-rewards --from --chain-id + +# Withdraw all tokenize share record rewards +sscd tx liquid withdraw-all-tokenize-share-rewards --from --chain-id + +# Query liquid staking parameters +sscd query liquid params + +# Query all liquid validators +sscd query liquid liquid-validators + +# Query specific liquid validator +sscd query liquid liquid-validator + +# Query total liquid staked tokens +sscd query liquid total-liquid-staked + +# Query tokenize share records by owner +sscd query liquid tokenize-share-records-owned + +# Query tokenize share record by ID +sscd query liquid tokenize-share-record-by-id + +# Query tokenize share record by denom +sscd query liquid tokenize-share-record-by-denom + +# Query tokenize share record rewards +sscd query liquid tokenize-share-record-rewards + +# Query tokenize share lock information +sscd query liquid tokenize-share-lock-info + +# Query all tokenize share records +sscd query liquid all-tokenize-share-records + +# Query last tokenize share record ID +sscd query liquid last-tokenize-share-record-id + +# Query total tokenized share assets +sscd query liquid total-tokenize-share-assets +``` + +#### **Tokenization System:** +- **Share Tokenization**: Convert delegation shares into tradeable tokens +- **Denom Generation**: Automatic generation of unique token denominations +- **Record Management**: Comprehensive tracking of tokenized share records +- **Ownership Transfer**: Transfer ownership of tokenized share records +- **Authorization Control**: Governance-controlled tokenization permissions + +#### **Liquid Staking Features:** +- **Validator Support**: Support for multiple validators with liquid staking +- **Liquid Shares**: Track liquid shares per validator +- **Staking Caps**: Configurable caps on liquid staking per validator +- **Reward Distribution**: Automated reward collection and distribution +- **Lock Management**: Time-based locking system for tokenized shares + +#### **Reward Management:** +- **Automatic Collection**: Automated collection of staking rewards +- **Reward Withdrawal**: Manual withdrawal of accumulated rewards +- **Bulk Operations**: Withdraw rewards for all tokenized share records +- **Fee Distribution**: Distribution of fees to the community pool + +#### **Storage Architecture:** +- **Tokenize Share Records**: `liquid/tokenize-share-record/{id}` +- **Liquid Validators**: `liquid/liquid-validator/{validator-address}` +- **Total Liquid Staked**: `liquid/total-liquid-staked` +- **Tokenize Share Locks**: `liquid/tokenize-share-lock/{owner}` +- **Authorization Queue**: `liquid/tokenize-share-auth-queue/{timestamp}` + +#### **Default Parameters:** +- **Global Liquid Staking Cap**: Configurable global cap on liquid staking +- **Validator Liquid Staking Cap**: Per-validator caps on liquid staking +- **Tokenization Fees**: Configurable fees for tokenization operations +- **Lock Duration**: Time-based locking for tokenized shares + +#### **Governance Integration:** +- **Parameter Updates**: Governance-controlled parameter updates +- **Authorization Management**: Governance control over tokenization permissions +- **Fee Management**: Governance-controlled fee structures +- **Cap Management**: Governance-controlled staking caps + +#### **Integration Features:** +- **Staking Integration**: Seamless integration with the staking module +- **Distribution Integration**: Integration with the distribution module for rewards +- **Bank Integration**: Integration with the bank module for token operations +- **Event System**: Comprehensive event emission for all operations + +--- + +## 🔧 Technical Improvements + +### **Storage Optimization** +- **KV-Based Architecture**: Efficient storage patterns prevent state bloat +- **Compact Key Design**: Single-byte prefixes for optimal storage +- **Deterministic Gas Costs**: Predictable transaction costs + +### **Multi-Denomination Support** +- **Flexible Fee Structures**: Support for multiple token types +- **Pool Isolation**: Independent pools per denomination +- **Cross-Denomination Operations**: Seamless multi-token support + +### **Enhanced CLI Experience** +- **Comprehensive Commands**: Full CRUD operations for all modules +- **Rich Query Interface**: Detailed information retrieval +- **Parameter Management**: Easy configuration updates + +### **Robust Error Handling** +- **Graceful Degradation**: Failed operations don't crash the system +- **Detailed Logging**: Comprehensive event tracking +- **Recovery Mechanisms**: Automatic retry and fallback systems + +--- + +## 🚀 Getting Started + +### **Prerequisites** +- Go 1.21+ +- Cosmos SDK v0.50 +- CometBFT v0.38 + +### **Installation** +```bash +git clone https://github.com/sagaxyz/ssc.git +cd ssc +make build +``` + +### **Quick Start** +```bash +# Initialize the chain +./build/sscd init testchain --chain-id ssc + +# Create a chainlet stack +./build/sscd tx chainlet create-chainlet-stack "MyStack" "Test stack" --from alice --chain-id ssc + +# Launch a chainlet +./build/sscd tx chainlet launch-chainlet stack1 "MyChainlet" "chainlet-1" --from alice --chain-id ssc + +# Deposit funds +./build/sscd tx escrow deposit 1000stake stake --from alice --chain-id ssc + +# Tokenize delegation shares +./build/sscd tx liquid tokenize-share 1000000stake $(sscd keys show alice -a) --from alice --chain-id ssc + +# Query chainlet status +./build/sscd query chainlet get-chainlet chainlet-1 + +# Query liquid validators +./build/sscd query liquid liquid-validators +``` + +--- + +## 📊 Performance Characteristics + +### **Gas Efficiency** +- **Predictable Costs**: Gas costs don't scale with number of funders +- **Optimized Storage**: Efficient key-value storage patterns +- **Batch Operations**: Support for bulk operations + +### **Scalability** +- **Multi-Denomination**: Support for unlimited token types +- **Pool Isolation**: Independent scaling per denomination +- **Efficient Queries**: Fast data retrieval with indexed storage + +### **Reliability** +- **Fault Tolerance**: Graceful handling of failed operations +- **State Consistency**: Atomic operations ensure data integrity +- **Recovery Mechanisms**: Automatic retry and fallback systems + +--- + +## 🔄 Migration Notes + +### **From Previous Versions** +- **Escrow Re-architecture**: Complete rewrite of escrow storage system +- **Universal Billing**: Centralized epoch configuration +- **Multi-Fee Support**: Enhanced fee management capabilities +- **Liquid Module**: New liquid staking system for tokenized delegation and reward management +- **Script Updates**: New `escrow.sh` replaces `escrow-chainlet-restart.sh` + +### **Breaking Changes** +- **Storage Format**: New KV-based storage patterns +- **CLI Commands**: Updated command structure and parameters +- **Configuration**: New parameter structures for all modules + +--- + +## 🛠️ Development Tools + +### **Testing** +```bash +# Run all tests +go test ./... + +# Run specific module tests +go test ./x/escrow/... +go test ./x/chainlet/... +go test ./x/billing/... +go test ./x/epochs/... +go test ./x/liquid/... + +# Run liquid module keeper tests +go test ./x/liquid/keeper/... -v +``` + +### **Integration Testing** +```bash +# Run environment setup +./scripts/ci/prepare-env.sh + +# Run happypath tests +./scripts/happypath.sh + +# Run escrow tests +./scripts/escrow.sh +``` + +--- + +## 📚 Documentation + +- **Module Documentation**: Each module includes comprehensive README files +- **API Reference**: Full gRPC and REST API documentation +- **CLI Reference**: Complete command-line interface documentation +- **Architecture Guide**: Detailed system architecture documentation + +--- + +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to get started. + +--- + +## 📄 License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +--- + +## 🆘 Support + +- **Documentation**: [Validator/Node docs](https://nodedocs.saga.xyz/) +- **Issues**: [GitHub Issues](https://github.com/sagaxyz/ssc/issues) + +--- + +*For more information, visit [Saga Protocol](https://saga.xyz)* diff --git a/app/app.go b/app/app.go index 58408710..5854c597 100644 --- a/app/app.go +++ b/app/app.go @@ -95,8 +95,6 @@ import ( "github.com/cosmos/gogoproto/proto" ibcconnectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" - no_valupdates_genutil "github.com/cosmos/interchain-security/v7/x/ccv/no_valupdates_genutil" - no_valupdates_staking "github.com/cosmos/interchain-security/v7/x/ccv/no_valupdates_staking" "github.com/ignite/cli/ignite/pkg/openapiconsole" "github.com/spf13/cast" @@ -122,6 +120,8 @@ import ( ibctm "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" ibcmock "github.com/cosmos/ibc-go/v10/testing/mock" + no_valupdates_genutil "github.com/cosmos/interchain-security/v7/x/ccv/no_valupdates_genutil" + no_valupdates_staking "github.com/cosmos/interchain-security/v7/x/ccv/no_valupdates_staking" ccvprovider "github.com/cosmos/interchain-security/v7/x/ccv/provider" ccvproviderkeeper "github.com/cosmos/interchain-security/v7/x/ccv/provider/keeper" ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" @@ -156,7 +156,7 @@ import ( upgrade02 "github.com/sagaxyz/ssc/app/upgrades/0.2" upgrade03 "github.com/sagaxyz/ssc/app/upgrades/0.3" - upgrade05 "github.com/sagaxyz/ssc/app/upgrades/0.5" + upgrade1 "github.com/sagaxyz/ssc/app/upgrades/1.0" // this line is used by starport scaffolding # stargate/app/moduleImport @@ -552,7 +552,7 @@ func New( app.ICAHostKeeper = icahostkeeper.NewKeeper( appCodec, - runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), + runtime.NewKVStoreService(keys[icahosttypes.StoreKey]), app.GetSubspace(icahosttypes.SubModuleName), app.IBCKeeper.ChannelKeeper, app.IBCKeeper.ChannelKeeper, @@ -659,7 +659,7 @@ func New( app.BankKeeper, nil, nil, - app.DacKeeper, + nil, ) app.BillingKeeper = *billingmodulekeeper.NewKeeper( @@ -672,6 +672,7 @@ func New( app.StakingKeeper, nil, app.EpochsKeeper, + SagaAddress, ) // billingModule := billingmodule.NewAppModule(appCodec, app.BillingKeeper, app.AccountKeeper, app.BankKeeper) @@ -688,7 +689,6 @@ func New( keys[chainletmoduletypes.StoreKey], app.GetSubspace(chainletmoduletypes.ModuleName), ccvproviderkeeper.NewMsgServerImpl(&app.ProviderKeeper), - func() *ibckeeper.Keeper { return app.IBCKeeper }, app.StakingKeeper, app.IBCKeeper.ClientKeeper, app.IBCKeeper.ChannelKeeper, @@ -714,6 +714,7 @@ func New( app.EscrowKeeper.UpdateKeeper(app.BillingKeeper) app.EscrowKeeper.UpdateKeeper(app.ChainletKeeper) + app.EscrowKeeper.UpdateKeeper(app.DacKeeper) escrowModule := escrowmodule.NewAppModule(appCodec, app.EscrowKeeper, app.AccountKeeper, app.BankKeeper, app.ChainletKeeper) app.EpochsKeeper.SetHooks( @@ -757,8 +758,12 @@ func New( // this line is used by starport scaffolding # stargate/app/keeperDefinition + // Build transfer stack with middleware + // Order: transfer -> GMP -> packet-forward -> CCV provider var transferStack porttypes.IBCModule transferStack = transfer.NewIBCModule(app.TransferKeeper) + // GMP middleware wraps transfer to inspect and process ICS-20 packet memos + transferStack = gmpmodule.NewIBCModule(transferStack) transferStack = packetforward.NewIBCMiddleware( transferStack, app.PacketForwardKeeper, @@ -875,7 +880,7 @@ func New( escrowmoduletypes.ModuleName, billingmoduletypes.ModuleName, acltypes.ModuleName, - peerstypes.StoreKey, + peerstypes.ModuleName, consensusparamtypes.ModuleName, gmpmoduletypes.ModuleName, liquidmoduletypes.ModuleName, @@ -918,23 +923,23 @@ func New( // this line is used by starport scaffolding # stargate/app/endBlockers ) - // NOTE: The genutils module must occur after staking so that pools are - // properly initialized with tokens from genesis accounts. - // NOTE: The provider module must come after genutils and staking, since it relies on the + // NOTE: The staking module must occur before genutil so that staking pools are properly + // initialized with tokens from genesis accounts before genutil processes genesis transactions. + // NOTE: The provider module must come after genutil and staking, since it relies on the // information about the validators these modules provide to compute validator updates. + // With no_valupdates_genutil and no_valupdates_staking, only the provider module sets validator updates. genesisModuleOrder := []string{ authtypes.ModuleName, banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, + genutiltypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, ibcexported.ModuleName, packetforwardtypes.ModuleName, - genutiltypes.ModuleName, ibctransfertypes.ModuleName, - ccvprovidertypes.ModuleName, icatypes.ModuleName, ibcmock.ModuleName, evidencetypes.ModuleName, @@ -953,6 +958,8 @@ func New( consensusparamtypes.ModuleName, gmpmoduletypes.ModuleName, liquidmoduletypes.ModuleName, + // Provider module must be last to set validator updates after all other modules initialize + ccvprovidertypes.ModuleName, // this line is used by starport scaffolding # stargate/app/initGenesis } app.mm.SetOrderInitGenesis(genesisModuleOrder...) @@ -1173,6 +1180,8 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig nodeservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) // Register grpc-gateway routes for all modules. + app.bmm.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + docs.RegisterOpenAPIService(Name, apiSvr.Router) // // apiSvr.Router.Handle("/static/openapi.yml", http.FileServer(http.FS(docs.Static))) // register app's OpenAPI routes. @@ -1228,7 +1237,7 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino paramsKeeper.Subspace(epochstypes.ModuleName) paramsKeeper.Subspace(escrowmoduletypes.ModuleName) paramsKeeper.Subspace(billingmoduletypes.ModuleName) - paramsKeeper.Subspace(acltypes.ModuleName) + paramsKeeper.Subspace(acltypes.ModuleName).WithKeyTable(acltypes.ParamKeyTable()) paramsKeeper.Subspace(peerstypes.ModuleName) paramsKeeper.Subspace(ibctransfertypes.ModuleName).WithKeyTable(ibctransfertypes.ParamKeyTable()) paramsKeeper.Subspace(ibcexported.ModuleName).WithKeyTable(keyTable) @@ -1255,7 +1264,7 @@ func (app *App) RegisterUpgradeHandlers() { baseAppLegacySS := app.ParamsKeeper.Subspace(baseapp.Paramspace).WithKeyTable(paramstypes.ConsensusParamsKeyTable()) app.UpgradeKeeper.SetUpgradeHandler(upgrade02.Name, upgrade02.UpgradeHandler(app.mm, app.configurator, app.ParamsKeeper, &app.ConsensusParamsKeeper, baseAppLegacySS)) app.UpgradeKeeper.SetUpgradeHandler(upgrade03.Name, upgrade03.UpgradeHandler(app.mm, app.configurator)) - app.UpgradeKeeper.SetUpgradeHandler(upgrade05.Name, upgrade05.UpgradeHandler(app.mm, app.configurator, app.AccountKeeper, app.BankKeeper)) + app.UpgradeKeeper.SetUpgradeHandler(upgrade1.Name, upgrade1.UpgradeHandler(app.mm, app.configurator, app.AccountKeeper, app.BankKeeper, app.ProviderKeeper, app.DacKeeper, *app.ChainletKeeper, app.BillingKeeper)) upgradeInfo, err := app.UpgradeKeeper.ReadUpgradeInfoFromDisk() if err != nil { @@ -1274,6 +1283,19 @@ func (app *App) RegisterUpgradeHandlers() { packetforwardtypes.StoreKey, }, } + case upgrade1.Name: + storeUpgrades = &storetypes.StoreUpgrades{ + Added: []string{ + acltypes.StoreKey, + billingmoduletypes.StoreKey, + chainletmoduletypes.StoreKey, + ccvprovidertypes.StoreKey, + epochstypes.StoreKey, + escrowmoduletypes.StoreKey, + liquidmoduletypes.StoreKey, + peerstypes.StoreKey, + }, + } default: } if storeUpgrades != nil { diff --git a/app/upgrades/0.5/upgrades.go b/app/upgrades/0.5/upgrades.go deleted file mode 100644 index 0365964b..00000000 --- a/app/upgrades/0.5/upgrades.go +++ /dev/null @@ -1,96 +0,0 @@ -package v05 - -import ( - "context" - "fmt" - - sdkmath "cosmossdk.io/math" - upgradetypes "cosmossdk.io/x/upgrade/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/module" - - authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" -) - -const ( - Name = "0.5" - tempMinterName = "developer-credits" - baseDenom = "credit" - recipientBech32 = "saga1a8duyed73q8gmewuakdfgyge52rkdgklysgfam" -) - -var ( - // If "credit" has 6 decimals, this is 1,000,000.000000 CREDIT in base units. - mintAmount = sdkmath.NewInt(1_000_000).MulRaw(1_000_000) -) - -// ensureTempMinter creates the temp module account with a proper account number (idempotent). -func ensureTempMinter(ctx sdk.Context, ak authkeeper.AccountKeeper, name string, perms ...string) error { - addr := authtypes.NewModuleAddress(name) - if ak.GetAccount(ctx, addr) != nil { - return nil // already exists - } - ma := authtypes.NewEmptyModuleAccount(name, perms...) - acc := ak.NewAccount(ctx, ma) // assigns account number - mai, ok := acc.(sdk.ModuleAccountI) - if !ok { - return fmt.Errorf("expected ModuleAccountI, got %T", acc) - } - ak.SetModuleAccount(ctx, mai) - return nil -} - -func UpgradeHandler( - mm *module.Manager, - configurator module.Configurator, - ak authkeeper.AccountKeeper, - bk bankkeeper.Keeper, -) upgradetypes.UpgradeHandler { - return func(ctx context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { - sdkCtx := sdk.UnwrapSDKContext(ctx) - - // Parse recipient AFTER prefixes are configured. - recipientBz, err := ak.AddressCodec().StringToBytes(recipientBech32) - if err != nil { - return nil, fmt.Errorf("invalid recipient addr: %w", err) - } - recipient := sdk.AccAddress(recipientBz) - - // Run migrations first (determinism). - newVM, err := mm.RunMigrations(ctx, configurator, vm) - if err != nil { - return nil, err - } - - // Create ephemeral minter. - if err := ensureTempMinter(sdkCtx, ak, tempMinterName, authtypes.Minter); err != nil { - return nil, err - } - - coins := sdk.NewCoins(sdk.NewCoin(baseDenom, mintAmount)) - - // Mint and send. - if err := bk.MintCoins(ctx, tempMinterName, coins); err != nil { - return nil, err - } - if err := bk.SendCoinsFromModuleToAccount(ctx, tempMinterName, recipient, coins); err != nil { - return nil, err - } - - // Burn any dust left (should be zero). - if bal := bk.GetAllBalances(ctx, authtypes.NewModuleAddress(tempMinterName)); !bal.IsZero() { - if err := bk.BurnCoins(ctx, tempMinterName, bal); err != nil { - return nil, err - } - } - - // Remove the temporary minter so no Minter perms remain. - if acc := ak.GetAccount(sdkCtx, authtypes.NewModuleAddress(tempMinterName)); acc != nil { - ak.RemoveAccount(sdkCtx, acc) - } - - return newVM, nil - } -} diff --git a/app/upgrades/1.0/upgrades.go b/app/upgrades/1.0/upgrades.go new file mode 100644 index 00000000..7abb2a2c --- /dev/null +++ b/app/upgrades/1.0/upgrades.go @@ -0,0 +1,172 @@ +package v1 + +import ( + "context" + "fmt" + + sdkmath "cosmossdk.io/math" + upgradetypes "cosmossdk.io/x/upgrade/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + ccvprovider "github.com/cosmos/interchain-security/v7/x/ccv/provider" + ccvproviderkeeper "github.com/cosmos/interchain-security/v7/x/ccv/provider/keeper" + ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" + billingkeeper "github.com/sagaxyz/ssc/x/billing/keeper" + + aclkeeper "github.com/sagaxyz/saga-sdk/x/acl/keeper" + chainletkeeper "github.com/sagaxyz/ssc/x/chainlet/keeper" +) + +const ( + Name = "1.0" + tempMinterName = "developer-credits" + baseDenom = "credit" + recipientBech32 = "saga1a8duyed73q8gmewuakdfgyge52rkdgklysgfam" +) + +var ( + // 1,000,000 CREDIT with 6 decimals + mintAmount = sdkmath.NewInt(1_000_000).MulRaw(1_000_000) +) + +// ensureTempMinter creates the temp module account with a proper account number (idempotent). +func ensureTempMinter(ctx sdk.Context, ak authkeeper.AccountKeeper, name string, perms ...string) error { + addr := authtypes.NewModuleAddress(name) + if ak.GetAccount(ctx, addr) != nil { + return nil // already exists + } + ma := authtypes.NewEmptyModuleAccount(name, perms...) + acc := ak.NewAccount(ctx, ma) // assigns account number + mai, ok := acc.(sdk.ModuleAccountI) + if !ok { + return fmt.Errorf("expected ModuleAccountI, got %T", acc) + } + ak.SetModuleAccount(ctx, mai) + return nil +} + +func UpgradeHandler( + mm *module.Manager, + configurator module.Configurator, + ak authkeeper.AccountKeeper, + bk bankkeeper.Keeper, + providerKeeper ccvproviderkeeper.Keeper, + aclKeeper aclkeeper.Keeper, + chainletKeeper chainletkeeper.Keeper, + billingKeeper billingkeeper.Keeper, +) upgradetypes.UpgradeHandler { + return func(ctx context.Context, _ upgradetypes.Plan, vm module.VersionMap) (module.VersionMap, error) { + sdkCtx := sdk.UnwrapSDKContext(ctx) + + // ------------------------------------------------------------------ + // 1. Run module migrations, treating provider as existing + // ------------------------------------------------------------------ + + if _, exists := vm[ccvprovidertypes.ModuleName]; !exists { + vm[ccvprovidertypes.ModuleName] = ccvprovider.AppModule{}.ConsensusVersion() + } + + newVM, err := mm.RunMigrations(ctx, configurator, vm) + if err != nil { + return nil, err + } + + // ------------------------------------------------------------------ + // 2. Initialize provider store WITHOUT validator updates + // ------------------------------------------------------------------ + + genState := ccvprovidertypes.DefaultGenesisState() + providerKeeper.InitGenesis(sdkCtx, genState) + newVM[ccvprovidertypes.ModuleName] = ccvprovider.AppModule{}.ConsensusVersion() + + // ------------------------------------------------------------------ + // 3. Fix chainlet params (match SPC behavior) + // ------------------------------------------------------------------ + + chainletParams := chainletKeeper.GetParams(sdkCtx) // or ctx if your keeper uses context.Context + chainletParams.ChainletStackProtections = true + chainletParams.EnableCCV = false + chainletKeeper.SetParams(sdkCtx, chainletParams) + + // ------------------------------------------------------------------ + // 4. Patch ACL genesis: + // - enable = true + // - Admins = SPC allowed list + // - Allowed = SPC allowed list + // ------------------------------------------------------------------ + + aclParams := aclKeeper.GetParams(sdkCtx) + aclParams.Enable = true + aclKeeper.SetParams(sdkCtx, aclParams) + + aclGen := aclKeeper.ExportGenesis(sdkCtx) // returns acltypes.GenesisState + + addresses := []string{ + "saga1rdssl22ysxyendrkh2exw9zm7hvj8d2ju346g3", + "saga1rcs5sw5yy9r04xsultcqv6tj73408qnawmlxqw", + "saga1yuvju0cztlahsf6f37z9j83vwyzgj6pzhx090f", + "saga1gme3rzzddpf4hkdngpruz5e4739lqsyyakgu0j", + "saga1sz83y27774xwrahwmv5afutv86grc286hcf7w5", + "saga16p4cejpaqpuha65hqyj85k5lx4umw7qzku37eg", + "saga1u2a8ktctqhpx655ysw7ru27t6hqt9wlq4fn5ca", + "saga1uccxg0ud23424ssuddqnkgjlsz2f6rvqlgjf9t", + "saga17x049ugfafggn823dsnf32fhj5qlhlxrrzdz22", + "saga17gk4chqd0lrkyamrxdmu62czmu0dpnemmxlymn", + } + + aclGen.Allowed = append([]string(nil), addresses...) + aclGen.Admins = append([]string(nil), addresses...) + + aclKeeper.InitGenesis(sdkCtx, aclGen) + + // After this: + // spcd/sscd q acl list-allowed -> those 10 addresses + // spcd/sscd q acl params -> enable: true + + // ------------------------------------------------------------------ + // 5. One-shot dev credit mint + cleanup + // ------------------------------------------------------------------ + + if err := ensureTempMinter(sdkCtx, ak, tempMinterName, authtypes.Minter); err != nil { + return nil, err + } + + coins := sdk.NewCoins(sdk.NewCoin(baseDenom, mintAmount)) + + if err := bk.MintCoins(ctx, tempMinterName, coins); err != nil { + return nil, err + } + + recipientBz, err := ak.AddressCodec().StringToBytes(recipientBech32) + if err != nil { + return nil, fmt.Errorf("invalid recipient addr: %w", err) + } + recipient := sdk.AccAddress(recipientBz) + + if err := bk.SendCoinsFromModuleToAccount(ctx, tempMinterName, recipient, coins); err != nil { + return nil, err + } + + if bal := bk.GetAllBalances(ctx, authtypes.NewModuleAddress(tempMinterName)); !bal.IsZero() { + if err := bk.BurnCoins(ctx, tempMinterName, bal); err != nil { + return nil, err + } + } + + if acc := ak.GetAccount(sdkCtx, authtypes.NewModuleAddress(tempMinterName)); acc != nil { + ak.RemoveAccount(sdkCtx, acc) + } + + // Set platform validators + billingparams := billingKeeper.GetParams(sdkCtx) + billingparams.PlatformValidators = []string{} + billingKeeper.SetParams(sdkCtx, billingparams) + + // Done + return newVM, nil + } +} diff --git a/go.mod b/go.mod index 94066764..a5e0a3ba 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 github.com/ignite/cli v0.27.2 - github.com/sagaxyz/saga-sdk v0.11.0 + github.com/sagaxyz/saga-sdk v0.11.1 github.com/spf13/cast v1.9.2 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 @@ -211,14 +211,14 @@ require ( 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.uber.org/mock v0.5.2 // indirect + go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/net v0.40.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.25.0 // indirect diff --git a/go.sum b/go.sum index c14c0f76..c5796ed9 100644 --- a/go.sum +++ b/go.sum @@ -1484,8 +1484,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagaxyz/saga-sdk v0.11.0 h1:NdKuBLjOqMjhhhVA7xHe3lXREn9h1uMTUmnB2Ou+m9U= -github.com/sagaxyz/saga-sdk v0.11.0/go.mod h1:jccuO6o/seZpjBQvFHDmMq+tSoa/op/tZsR65+A/7/g= +github.com/sagaxyz/saga-sdk v0.11.1 h1:NNVAL3QHEaMiEpVIvvCKzbMj9kWE02QmQeU9kPWP1l4= +github.com/sagaxyz/saga-sdk v0.11.1/go.mod h1:XQEnR9jgvV9fYQeTRg/wPYCGMUyR5z2eKGuCoO+CYqc= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -1627,8 +1627,8 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= @@ -1853,8 +1853,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/proto/ssc/billing/billing_history.proto b/proto/ssc/billing/billing_history.proto index f4fc3ca9..8063b6c0 100644 --- a/proto/ssc/billing/billing_history.proto +++ b/proto/ssc/billing/billing_history.proto @@ -10,7 +10,7 @@ message BillingHistory { string chainletId = 3; string chainletStackName = 4; string epochIdentifier = 5; - int32 epochNumber = 6; + int64 epochNumber = 6; string epochStartTime = 7; string billedAmount = 8; } diff --git a/proto/ssc/billing/genesis.proto b/proto/ssc/billing/genesis.proto index f5eeae7f..7db31eb0 100644 --- a/proto/ssc/billing/genesis.proto +++ b/proto/ssc/billing/genesis.proto @@ -3,6 +3,8 @@ package ssc.billing; import "gogoproto/gogo.proto"; import "ssc/billing/params.proto"; +import "ssc/billing/save_billing_history.proto"; +import "ssc/billing/validator_payout_history.proto"; // this line is used by starport scaffolding # genesis/proto/import option go_package = "github.com/sagaxyz/ssc/x/billing/types"; @@ -10,5 +12,9 @@ option go_package = "github.com/sagaxyz/ssc/x/billing/types"; // GenesisState defines the billing module's genesis state. message GenesisState { Params params = 1 [ (gogoproto.nullable) = false ]; + // Billing history records + repeated SaveBillingHistory billing_history = 2 [ (gogoproto.nullable) = false ]; + // Validator payout history records + repeated ValidatorPayoutHistory validator_payout_history = 3 [ (gogoproto.nullable) = false ]; // this line is used by starport scaffolding # genesis/proto/state } diff --git a/proto/ssc/billing/params.proto b/proto/ssc/billing/params.proto index df5c2a92..bfb2c517 100644 --- a/proto/ssc/billing/params.proto +++ b/proto/ssc/billing/params.proto @@ -11,4 +11,5 @@ message Params { string validator_payout_epoch = 1; string billing_epoch = 2; + repeated string platform_validators = 3; } diff --git a/proto/ssc/billing/save_billing_history.proto b/proto/ssc/billing/save_billing_history.proto index 058c96ec..7b5ee316 100644 --- a/proto/ssc/billing/save_billing_history.proto +++ b/proto/ssc/billing/save_billing_history.proto @@ -7,6 +7,6 @@ message SaveBillingHistory { string chainletId = 1; string epochIdentifier = 2; - int32 epochNumber = 3; + int64 epochNumber = 3; string billedAmount = 4; } diff --git a/proto/ssc/billing/tx.proto b/proto/ssc/billing/tx.proto index 4cdb0415..600bdeb4 100644 --- a/proto/ssc/billing/tx.proto +++ b/proto/ssc/billing/tx.proto @@ -11,6 +11,16 @@ option go_package = "github.com/sagaxyz/ssc/x/billing/types"; service Msg { option (cosmos.msg.v1.service) = true; // this line is used by starport scaffolding # proto/tx/rpc + rpc SetPlatformValidators(MsgSetPlatformValidators) + returns (MsgSetPlatformValidatorsResponse); } // this line is used by starport scaffolding # proto/tx/message + +message MsgSetPlatformValidators { + option (cosmos.msg.v1.signer) = "creator"; + string creator = 1; + repeated string platform_validators = 2; +} + +message MsgSetPlatformValidatorsResponse {} \ No newline at end of file diff --git a/proto/ssc/billing/validator_payout_history.proto b/proto/ssc/billing/validator_payout_history.proto index 717fd62e..e87ca560 100644 --- a/proto/ssc/billing/validator_payout_history.proto +++ b/proto/ssc/billing/validator_payout_history.proto @@ -7,7 +7,7 @@ message ValidatorPayoutHistory { string validatorAddress = 1; string epochIdentifier = 2; - int32 epochNumber = 3; + int64 epochNumber = 3; string epochStartTime = 4; string rewardAmount = 5; } diff --git a/proto/ssc/chainlet/genesis.proto b/proto/ssc/chainlet/genesis.proto index 64871fa8..81a27fe8 100644 --- a/proto/ssc/chainlet/genesis.proto +++ b/proto/ssc/chainlet/genesis.proto @@ -4,6 +4,8 @@ package ssc.chainlet; import "gogoproto/gogo.proto"; import "ssc/chainlet/params.proto"; +import "ssc/chainlet/chainlet.proto"; +import "ssc/chainlet/chainlet_stack.proto"; // this line is used by starport scaffolding # genesis/proto/import @@ -12,5 +14,12 @@ option go_package = "github.com/sagaxyz/ssc/x/chainlet/types"; // GenesisState defines the chainlet module's genesis state. message GenesisState { Params params = 1 [ (gogoproto.nullable) = false ]; + // List of all chainlets + repeated Chainlet chainlets = 2 [ (gogoproto.nullable) = false ]; + // List of all chainlet stacks + repeated ChainletStack chainlet_stacks = 3 [ (gogoproto.nullable) = false ]; + // Chainlet count + uint64 chainlet_count = 4; + string port_id = 5; // this line is used by starport scaffolding # genesis/proto/state } diff --git a/proto/ssc/escrow/genesis.proto b/proto/ssc/escrow/genesis.proto index 9e00e2cb..e3dbd33d 100644 --- a/proto/ssc/escrow/genesis.proto +++ b/proto/ssc/escrow/genesis.proto @@ -3,6 +3,7 @@ package ssc.escrow; import "gogoproto/gogo.proto"; import "ssc/escrow/params.proto"; +import "ssc/escrow/escrow.proto"; // this line is used by starport scaffolding # genesis/proto/import option go_package = "github.com/sagaxyz/ssc/x/escrow/types"; @@ -10,5 +11,19 @@ option go_package = "github.com/sagaxyz/ssc/x/escrow/types"; // GenesisState defines the escrow module's genesis state. message GenesisState { Params params = 1 [ (gogoproto.nullable) = false ]; + // List of all chainlet accounts (headers) + repeated ChainletAccount chainlet_accounts = 2 [ (gogoproto.nullable) = false ]; + // List of all denomination pools + repeated DenomPool pools = 3 [ (gogoproto.nullable) = false ]; + // List of all funder positions with their identifiers + repeated GenesisFunder funders = 4 [ (gogoproto.nullable) = false ]; // this line is used by starport scaffolding # genesis/proto/state } + +// GenesisFunder wraps Funder with its composite key for genesis export/import +message GenesisFunder { + string chain_id = 1; + string denom = 2; + string address = 3; + Funder funder = 4 [ (gogoproto.nullable) = false ]; +} diff --git a/proto/ssc/peers/genesis.proto b/proto/ssc/peers/genesis.proto index ebb88f68..95473b57 100644 --- a/proto/ssc/peers/genesis.proto +++ b/proto/ssc/peers/genesis.proto @@ -3,6 +3,7 @@ package ssc.peers; import "gogoproto/gogo.proto"; import "ssc/peers/params.proto"; +import "ssc/peers/data.proto"; // this line is used by starport scaffolding # genesis/proto/import option go_package = "github.com/sagaxyz/ssc/x/peers/types"; @@ -10,5 +11,22 @@ option go_package = "github.com/sagaxyz/ssc/x/peers/types"; // GenesisState defines the peers module's genesis state. message GenesisState { Params params = 1 [ (gogoproto.nullable) = false ]; + // Peer data entries with their composite keys + repeated GenesisPeerData peer_data = 2 [ (gogoproto.nullable) = false ]; + // Chain counters + repeated GenesisChainCounter chain_counters = 3 [ (gogoproto.nullable) = false ]; // this line is used by starport scaffolding # genesis/proto/state } + +// GenesisPeerData wraps peer Data with its composite key (chainId, validator address) +message GenesisPeerData { + string chain_id = 1; + string validator_address = 2; + Data data = 3 [ (gogoproto.nullable) = false ]; +} + +// GenesisChainCounter stores the validator count for a chain +message GenesisChainCounter { + string chain_id = 1; + Counter counter = 2 [ (gogoproto.nullable) = false ]; +} diff --git a/testutil/keeper/billing.go b/testutil/keeper/billing.go index 6c7754b4..8a3736e6 100644 --- a/testutil/keeper/billing.go +++ b/testutil/keeper/billing.go @@ -3,18 +3,18 @@ package keeper import ( "testing" - tmdb "github.com/cosmos/cosmos-db" "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmdb "github.com/cosmos/cosmos-db" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "cosmossdk.io/store" - storetypes "cosmossdk.io/store/types" sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" - "cosmossdk.io/store/metrics" - + "github.com/sagaxyz/ssc/x/billing/keeper" "github.com/sagaxyz/ssc/x/billing/types" ) @@ -48,6 +48,7 @@ func BillingKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { nil, nil, nil, + "", ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/testutil/keeper/chainlet.go b/testutil/keeper/chainlet.go index cf0a6f0d..71f5438c 100644 --- a/testutil/keeper/chainlet.go +++ b/testutil/keeper/chainlet.go @@ -51,7 +51,6 @@ func ChainletKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { nil, nil, nil, - nil, ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/x/billing/genesis.go b/x/billing/genesis.go index 8462f781..e4eef5e5 100644 --- a/x/billing/genesis.go +++ b/x/billing/genesis.go @@ -8,8 +8,20 @@ import ( // InitGenesis initializes the module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // this line is used by starport scaffolding # genesis/module/init + // Set params k.SetParams(ctx, genState.Params) + + // Import billing history + for _, bh := range genState.BillingHistory { + k.ImportBillingHistory(ctx, bh) + } + + // Import validator payout history + for _, vph := range genState.ValidatorPayoutHistory { + k.ImportValidatorPayoutHistory(ctx, vph) + } + + // this line is used by starport scaffolding # genesis/module/init } // ExportGenesis returns the module's exported genesis @@ -17,6 +29,12 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() genesis.Params = k.GetParams(ctx) + // Export billing history + genesis.BillingHistory = k.ExportBillingHistory(ctx) + + // Export validator payout history + genesis.ValidatorPayoutHistory = k.ExportValidatorPayoutHistory(ctx) + // this line is used by starport scaffolding # genesis/module/export return genesis diff --git a/x/billing/keeper/billing.go b/x/billing/keeper/billing.go index 19b5ea54..4994cd23 100644 --- a/x/billing/keeper/billing.go +++ b/x/billing/keeper/billing.go @@ -15,7 +15,8 @@ func (k Keeper) BillAccount(ctx sdk.Context, amount sdk.Coin, chainlet chainlett err := k.escrowkeeper.BillAccount(ctx, amount, chainlet.ChainId, "billing") if err != nil { ctx.Logger().Info(fmt.Sprintf("failed to bill account %s for %s at epoch %s", chainlet.ChainId, amount.String(), memo)) - ctx.EventManager().EmitTypedEvent(&types.BillingEvent{ //nolint: errcheck + //nolint:errcheck // Event emission errors are non-critical + ctx.EventManager().EmitTypedEvent(&types.BillingEvent{ ChainId: chainlet.ChainId, Amount: amount.String(), Memo: memo, @@ -25,7 +26,8 @@ func (k Keeper) BillAccount(ctx sdk.Context, amount sdk.Coin, chainlet chainlett return err } ctx.Logger().Info(fmt.Sprintf("successfully billed account %s for %s at epoch %s", chainlet.ChainId, amount.String(), memo)) - ctx.EventManager().EmitTypedEvent(&types.BillingEvent{ //nolint: errcheck + //nolint:errcheck // Event emission errors are non-critical + ctx.EventManager().EmitTypedEvent(&types.BillingEvent{ ChainId: chainlet.ChainId, Amount: amount.String(), Memo: memo, @@ -43,7 +45,7 @@ func (k Keeper) BillAccount(ctx sdk.Context, amount sdk.Coin, chainlet chainlett ChainletName: chainlet.ChainletName, ChainletStackName: chainlet.ChainletStackName, EpochIdentifier: epochIdentifier, - EpochNumber: int32(epochInfo.CurrentEpoch), + EpochNumber: epochInfo.CurrentEpoch, EpochStartTime: epochEventStartTime, BilledAmount: amount.String(), }) @@ -104,6 +106,8 @@ func (k Keeper) GetChainletBillingHistory(ctx sdk.Context, chainId string) ([]*t // Get the store store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(fmt.Sprintf("%s-%s", types.BillingHistoryKey, chainId))) it := store.Iterator(nil, nil) + defer it.Close() + if !it.Valid() { return nil, cosmossdkerrors.Wrapf(types.ErrNoRecords, "no billing history found for chain %s", chainId) } @@ -116,12 +120,13 @@ func (k Keeper) GetChainletBillingHistory(ctx sdk.Context, chainId string) ([]*t return nil, cosmossdkerrors.Wrapf(types.ErrInternalFailure, "could not retrieve chainlet info for chain %s. Error: %v", chainId, err) } - for val := it.Value(); it.Valid(); it.Next() { + for ; it.Valid(); it.Next() { + val := it.Value() var sbhr types.SaveBillingHistory k.cdc.MustUnmarshal(val, &sbhr) // get epoch info epochInfo := k.epochskeeper.GetEpochInfo(ctx, sbhr.EpochIdentifier) - epochSince := (epochInfo.CurrentEpoch - int64(sbhr.EpochNumber)) + epochSince := (epochInfo.CurrentEpoch - sbhr.EpochNumber) epochEventStartTime := epochInfo.CurrentEpochStartTime.Add(-time.Duration(epochSince * int64(epochInfo.Duration))) bhr := types.BillingHistory{ ChainletId: sbhr.ChainletId, @@ -164,15 +169,17 @@ func (k Keeper) GetKprValidatorPayoutHistory(ctx sdk.Context, validatorAddress s // Get the store store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(fmt.Sprintf("%s-%s", types.ValidatorPayoutHistoryKey, validatorAddress))) it := store.Iterator(nil, nil) + defer it.Close() + if !it.Valid() { return nil, cosmossdkerrors.Wrapf(types.ErrNoRecords, "no validator payout history found for validator %s", validatorAddress) } - for val := it.Value(); it.Valid(); it.Next() { + for ; it.Valid(); it.Next() { + val := it.Value() var vphr types.ValidatorPayoutHistory k.cdc.MustUnmarshal(val, &vphr) vph = append(vph, &vphr) - } return vph, nil } diff --git a/x/billing/keeper/genesis.go b/x/billing/keeper/genesis.go new file mode 100644 index 00000000..9afe1319 --- /dev/null +++ b/x/billing/keeper/genesis.go @@ -0,0 +1,58 @@ +package keeper + +import ( + "fmt" + + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sagaxyz/ssc/x/billing/types" +) + +// ExportBillingHistory exports all billing history records from the store +func (k Keeper) ExportBillingHistory(ctx sdk.Context) []types.SaveBillingHistory { + // Use prefix store to efficiently iterate only over billing history keys + billingStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.BillingHistoryKey+"-")) + iterator := billingStore.Iterator(nil, nil) + defer iterator.Close() + + var records []types.SaveBillingHistory + for ; iterator.Valid(); iterator.Next() { + var record types.SaveBillingHistory + k.cdc.MustUnmarshal(iterator.Value(), &record) + records = append(records, record) + } + return records +} + +// ExportValidatorPayoutHistory exports all validator payout history records from the store +func (k Keeper) ExportValidatorPayoutHistory(ctx sdk.Context) []types.ValidatorPayoutHistory { + // Use prefix store to efficiently iterate only over validator payout history keys + payoutStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.ValidatorPayoutHistoryKey+"-")) + iterator := payoutStore.Iterator(nil, nil) + defer iterator.Close() + + var records []types.ValidatorPayoutHistory + for ; iterator.Valid(); iterator.Next() { + var record types.ValidatorPayoutHistory + k.cdc.MustUnmarshal(iterator.Value(), &record) + records = append(records, record) + } + return records +} + +// ImportBillingHistory imports a single billing history record into the store +func (k Keeper) ImportBillingHistory(ctx sdk.Context, record types.SaveBillingHistory) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(fmt.Sprintf("%s-%s", types.BillingHistoryKey, record.ChainletId))) + uniqueKey := fmt.Sprintf("%s-%d", record.EpochIdentifier, record.EpochNumber) + value := k.cdc.MustMarshal(&record) + store.Set([]byte(uniqueKey), value) +} + +// ImportValidatorPayoutHistory imports a single validator payout history record into the store +func (k Keeper) ImportValidatorPayoutHistory(ctx sdk.Context, record types.ValidatorPayoutHistory) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(fmt.Sprintf("%s-%s", types.ValidatorPayoutHistoryKey, record.ValidatorAddress))) + uniqueKey := fmt.Sprintf("%s-%d", record.EpochIdentifier, record.EpochNumber) + value := k.cdc.MustMarshal(&record) + store.Set([]byte(uniqueKey), value) +} diff --git a/x/billing/keeper/grpc_query_get_billing_history.go b/x/billing/keeper/grpc_query_get_billing_history.go index d1736d41..8a9660b9 100644 --- a/x/billing/keeper/grpc_query_get_billing_history.go +++ b/x/billing/keeper/grpc_query_get_billing_history.go @@ -36,7 +36,7 @@ func (k Keeper) GetBillingHistory(goCtx context.Context, req *types.QueryGetBill k.cdc.MustUnmarshal(value, &sbhr) // get epoch info epochInfo := k.epochskeeper.GetEpochInfo(ctx, sbhr.EpochIdentifier) - epochSince := (epochInfo.CurrentEpoch - int64(sbhr.EpochNumber)) + epochSince := (epochInfo.CurrentEpoch - sbhr.EpochNumber) epochEventStartTime := epochInfo.CurrentEpochStartTime.Add(-time.Duration(epochSince * int64(epochInfo.Duration))) bhr := types.BillingHistory{ ChainletId: sbhr.ChainletId, diff --git a/x/billing/keeper/hooks.go b/x/billing/keeper/hooks.go index 6f4d0b8b..36dddee8 100644 --- a/x/billing/keeper/hooks.go +++ b/x/billing/keeper/hooks.go @@ -15,6 +15,13 @@ import ( ) func (k Keeper) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) error { + params := k.GetParams(ctx) + + if epochIdentifier != params.BillingEpoch { + ctx.Logger().Info("skipping billing of chainlets as epoch identifier is " + epochIdentifier + " and we only process billing at epoch identifier " + params.BillingEpoch) + return nil + } + stacks, err := k.chainletkeeper.ListChainletStack(ctx, &chainlettypes.QueryListChainletStackRequest{}) if err != nil { @@ -124,11 +131,30 @@ func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumb return nil } - validators, err := k.stakingkeeper.GetValidators(ctx, 100) - if err != nil { - return err + platformValidators := k.GetPlatformValidators(ctx) + var validatorAddrs []string + if len(platformValidators) > 0 { + validatorAddrs = platformValidators + } else { + // Fall back to staking validator set if no platform validators configured + validators, err := k.stakingkeeper.GetValidators(ctx, 100) + if err != nil { + return err + } + for _, v := range validators { + valAddr, err := sdk.ValAddressFromBech32(v.OperatorAddress) + if err != nil { + ctx.Logger().Error("could not parse validator address: " + v.OperatorAddress) + continue + } + validatorAddrs = append(validatorAddrs, sdk.AccAddress(valAddr).String()) + } + } + numValidators := len(validatorAddrs) + if numValidators == 0 { + ctx.Logger().Info("no validators available for reward distribution") + return nil } - numValidators := len(validators) // number of validators moduleAccount := k.accountkeeper.GetModuleAccount(ctx, "billing") // module account address for the billing module moduleAccountBalance := k.bankkeeper.GetAllBalances(ctx, moduleAccount.GetAddress()) validatorDepositAmount := moduleAccountBalance.QuoInt(math.NewIntFromUint64(uint64(numValidators))) @@ -139,39 +165,51 @@ func (k Keeper) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumb epochInfo := k.epochskeeper.GetEpochInfo(ctx, epochIdentifier) epochEventStartTime := epochInfo.CurrentEpochStartTime.Format(time.RFC3339) - for _, v := range validators { - var valAddr sdk.ValAddress - var err error - - ctx.Logger().Debug("Validator being processed is " + v.OperatorAddress) - - if validatorDepositAmount.IsValid() && validatorDepositAmount.IsAllPositive() { - valAddr, err = sdk.ValAddressFromBech32(v.OperatorAddress) - if err != nil { - ctx.Logger().Error("could not get the validator address from operator address: " + v.OperatorAddress + ". Error: " + err.Error()) - continue - } + for _, v := range validatorAddrs { - ctx.Logger().Debug("Validator hex address is: " + valAddr.String()) - } else { - ctx.Logger().Error("funds in billing module with address " + moduleAccount.GetAddress().String() + " could not be validated, or no funds exist, for distribution to validators: " + v.OperatorAddress) + addr, err := sdk.AccAddressFromBech32(v) + if err != nil { + ctx.Logger().Error("could not parse validator address: " + v + ". Error: " + err.Error()) continue } - err = k.PayEpochFeeToValidator(ctx, validatorDepositAmount, "billing", sdk.AccAddress(valAddr), "epoch fee reward") + // Keep for post-ccv + // var valAddr sdk.ValAddress + // var err error + + // ctx.Logger().Debug("Validator being processed is " + v.OperatorAddress) + + // if validatorDepositAmount.IsValid() && validatorDepositAmount.IsAllPositive() { + // valAddr, err = sdk.ValAddressFromBech32(v.OperatorAddress) + // if err != nil { + // ctx.Logger().Error("could not get the validator address from operator address: " + v.OperatorAddress + ". Error: " + err.Error()) + // continue + // } + + // ctx.Logger().Debug("Validator hex address is: " + valAddr.String()) + // } else { + // ctx.Logger().Error("funds in billing module with address " + moduleAccount.GetAddress().String() + " could not be validated, or no funds exist, for distribution to validators: " + v.OperatorAddress) + // continue + // } + + // err = k.PayEpochFeeToValidator(ctx, validatorDepositAmount, "billing", sdk.AccAddress(valAddr), "epoch fee reward") + err = k.PayEpochFeeToValidator(ctx, validatorDepositAmount, "billing", addr, "epoch fee reward") if err != nil { - ctx.Logger().Error("could not pay epoch fee to validator " + v.OperatorAddress + ". Error: " + err.Error()) + // ctx.Logger().Error("could not pay epoch fee to validator " + v.OperatorAddress + ". Error: " + err.Error()) + ctx.Logger().Error("could not pay epoch fee to validator " + v + ". Error: " + err.Error()) continue } err = k.SaveValidatorPayoutHistory(ctx, types.ValidatorPayoutHistory{ - ValidatorAddress: sdk.AccAddress(valAddr).String(), - EpochIdentifier: epochIdentifier, - EpochNumber: int32(epochNumber), - EpochStartTime: epochEventStartTime, - RewardAmount: validatorDepositAmount.String(), + ValidatorAddress: v, + // ValidatorAddress: sdk.AccAddress(valAddr).String(), + EpochIdentifier: epochIdentifier, + EpochNumber: epochNumber, + EpochStartTime: epochEventStartTime, + RewardAmount: validatorDepositAmount.String(), }) if err != nil { - ctx.Logger().Error("could not save validator payout history for validator " + sdk.AccAddress(valAddr).String() + ". Error: " + err.Error()) + // ctx.Logger().Error("could not save validator payout history for validator " + sdk.AccAddress(valAddr).String() + ". Error: " + err.Error()) + ctx.Logger().Error("could not save validator payout history for validator " + v + ". Error: " + err.Error()) } } diff --git a/x/billing/keeper/hooks_test.go b/x/billing/keeper/hooks_test.go new file mode 100644 index 00000000..f401c5ca --- /dev/null +++ b/x/billing/keeper/hooks_test.go @@ -0,0 +1,347 @@ +package keeper_test + +import ( + "testing" + "time" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + storetypes "cosmossdk.io/store/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmdb "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + typesparams "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/sagaxyz/ssc/x/billing/keeper" + "github.com/sagaxyz/ssc/x/billing/testutil" + "github.com/sagaxyz/ssc/x/billing/types" + chainlettypes "github.com/sagaxyz/ssc/x/chainlet/types" + epochstypes "github.com/sagaxyz/ssc/x/epochs/types" +) + +// setupKeeperWithMocks creates a billing keeper with mocked dependencies +func setupKeeperWithMocks(t *testing.T) (*keeper.Keeper, sdk.Context, *testutil.MockChainletKeeper, *testutil.MockEpochsKeeper) { + storeKey := storetypes.NewKVStoreKey(types.StoreKey) + memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) + + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) + stateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) + require.NoError(t, stateStore.LoadLatestVersion()) + + registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + + paramsSubspace := typesparams.NewSubspace(cdc, + types.Amino, + storeKey, + memStoreKey, + "BillingParams", + ) + + ctrl := gomock.NewController(t) + mockChainletKeeper := testutil.NewMockChainletKeeper(ctrl) + mockEpochsKeeper := testutil.NewMockEpochsKeeper(ctrl) + + k := keeper.NewKeeper( + cdc, + storeKey, + paramsSubspace, + nil, // bankkeeper + nil, // escrowkeeper + nil, // accountkeeper + nil, // stakingkeeper + mockChainletKeeper, + mockEpochsKeeper, + "", + ) + + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + + // Initialize params with default billing epoch + k.SetParams(ctx, types.DefaultParams()) + + return k, ctx, mockChainletKeeper, mockEpochsKeeper +} + +// TestBeforeEpochStart_SkipsNonBillingEpoch verifies that BeforeEpochStart +// skips processing when the epoch identifier doesn't match BillingEpoch +func TestBeforeEpochStart_SkipsNonBillingEpoch(t *testing.T) { + k, ctx, mockChainletKeeper, mockEpochsKeeper := setupKeeperWithMocks(t) + + // Set billing epoch to "day" + params := k.GetParams(ctx) + params.BillingEpoch = "day" + k.SetParams(ctx, params) + + // Test with different epoch identifiers that should be skipped + epochIdentifiers := []string{"minute", "hour", "week"} + + for _, epochID := range epochIdentifiers { + t.Run("skips_"+epochID, func(t *testing.T) { + // Mock should NOT be called since we return early + // No expectations set means any call would fail the test + + // Call BeforeEpochStart with non-matching epoch identifier + err := k.BeforeEpochStart(ctx, epochID, 1) + + // Should return nil without error + require.NoError(t, err) + + // Verify no chainlet keeper methods were called + // (gomock will fail if any unexpected calls were made) + }) + } + + // Ensure mocks weren't called + mockChainletKeeper.EXPECT().ListChainletStack(gomock.Any(), gomock.Any()).Times(0) + mockEpochsKeeper.EXPECT().GetEpochInfo(gomock.Any(), gomock.Any()).Times(0) +} + +// TestBeforeEpochStart_ProcessesBillingEpoch verifies that BeforeEpochStart +// processes billing when the epoch identifier matches BillingEpoch +func TestBeforeEpochStart_ProcessesBillingEpoch(t *testing.T) { + k, ctx, mockChainletKeeper, mockEpochsKeeper := setupKeeperWithMocks(t) + + // Set billing epoch to "day" + params := k.GetParams(ctx) + params.BillingEpoch = "day" + k.SetParams(ctx, params) + + // Mock chainlet keeper GetParams (called before ListChainlets) + mockChainletKeeper.EXPECT(). + GetParams(gomock.Any()). + Return(chainlettypes.Params{MaxChainlets: 100}). + Times(1) + + // Mock chainlet stack list (empty to avoid further processing) + mockChainletKeeper.EXPECT(). + ListChainletStack(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletStackResponse{ + ChainletStacks: []*chainlettypes.ChainletStack{}, + }, nil). + Times(1) + + // Mock chainlet list (empty to avoid further processing) + mockChainletKeeper.EXPECT(). + ListChainlets(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletsResponse{ + Chainlets: []*chainlettypes.Chainlet{}, + }, nil). + Times(1) + + // Mock epoch info + mockEpochsKeeper.EXPECT(). + GetEpochInfo(gomock.Any(), "day"). + Return(epochstypes.EpochInfo{ + Identifier: "day", + CurrentEpoch: 1, + CurrentEpochStartTime: time.Now(), + CurrentEpochStartHeight: 1, + }). + Times(1) + + // Call BeforeEpochStart with matching epoch identifier + err := k.BeforeEpochStart(ctx, "day", 1) + + // Should return nil without error + require.NoError(t, err) +} + +// TestBeforeEpochStart_WithDifferentBillingEpochs tests that the hook +// correctly filters based on the configured BillingEpoch parameter +func TestBeforeEpochStart_WithDifferentBillingEpochs(t *testing.T) { + testCases := []struct { + name string + billingEpoch string + callEpochID string + shouldProcess bool + expectedCallCount int + }{ + { + name: "billing_day_called_with_day", + billingEpoch: "day", + callEpochID: "day", + shouldProcess: true, + expectedCallCount: 1, + }, + { + name: "billing_day_called_with_hour", + billingEpoch: "day", + callEpochID: "hour", + shouldProcess: false, + expectedCallCount: 0, + }, + { + name: "billing_hour_called_with_hour", + billingEpoch: "hour", + callEpochID: "hour", + shouldProcess: true, + expectedCallCount: 1, + }, + { + name: "billing_hour_called_with_minute", + billingEpoch: "hour", + callEpochID: "minute", + shouldProcess: false, + expectedCallCount: 0, + }, + { + name: "billing_week_called_with_week", + billingEpoch: "week", + callEpochID: "week", + shouldProcess: true, + expectedCallCount: 1, + }, + { + name: "billing_week_called_with_day", + billingEpoch: "week", + callEpochID: "day", + shouldProcess: false, + expectedCallCount: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + k, ctx, mockChainletKeeper, mockEpochsKeeper := setupKeeperWithMocks(t) + + // Set billing epoch + params := k.GetParams(ctx) + params.BillingEpoch = tc.billingEpoch + k.SetParams(ctx, params) + + if tc.shouldProcess { + // Mock chainlet keeper GetParams (called before ListChainlets) + mockChainletKeeper.EXPECT(). + GetParams(gomock.Any()). + Return(chainlettypes.Params{MaxChainlets: 100}). + Times(1) + + // Mock chainlet stack list + mockChainletKeeper.EXPECT(). + ListChainletStack(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletStackResponse{ + ChainletStacks: []*chainlettypes.ChainletStack{}, + }, nil). + Times(1) + + // Mock chainlet list + mockChainletKeeper.EXPECT(). + ListChainlets(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletsResponse{ + Chainlets: []*chainlettypes.Chainlet{}, + }, nil). + Times(1) + + // Mock epoch info + mockEpochsKeeper.EXPECT(). + GetEpochInfo(gomock.Any(), tc.callEpochID). + Return(epochstypes.EpochInfo{ + Identifier: tc.callEpochID, + CurrentEpoch: 1, + CurrentEpochStartTime: time.Now(), + CurrentEpochStartHeight: 1, + }). + Times(1) + } else { + // No mocks should be called when skipping + mockChainletKeeper.EXPECT(). + ListChainletStack(gomock.Any(), gomock.Any()). + Times(0) + mockChainletKeeper.EXPECT(). + ListChainlets(gomock.Any(), gomock.Any()). + Times(0) + mockEpochsKeeper.EXPECT(). + GetEpochInfo(gomock.Any(), gomock.Any()). + Times(0) + } + + // Call BeforeEpochStart + err := k.BeforeEpochStart(ctx, tc.callEpochID, 1) + require.NoError(t, err) + }) + } +} + +// TestBeforeEpochStart_DefaultBillingEpoch verifies that the default +// billing epoch (day) works correctly +func TestBeforeEpochStart_DefaultBillingEpoch(t *testing.T) { + k, ctx, mockChainletKeeper, mockEpochsKeeper := setupKeeperWithMocks(t) + + // Use default params (BillingEpoch should be "day") + params := k.GetParams(ctx) + require.Equal(t, types.SAGA_EPOCH_IDENTIFIER, params.BillingEpoch) + + // Mock chainlet keeper GetParams (called before ListChainlets) + mockChainletKeeper.EXPECT(). + GetParams(gomock.Any()). + Return(chainlettypes.Params{MaxChainlets: 100}). + Times(1) + + // Mock chainlet stack list + mockChainletKeeper.EXPECT(). + ListChainletStack(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletStackResponse{ + ChainletStacks: []*chainlettypes.ChainletStack{}, + }, nil). + Times(1) + + // Mock chainlet list + mockChainletKeeper.EXPECT(). + ListChainlets(gomock.Any(), gomock.Any()). + Return(&chainlettypes.QueryListChainletsResponse{ + Chainlets: []*chainlettypes.Chainlet{}, + }, nil). + Times(1) + + // Mock epoch info + mockEpochsKeeper.EXPECT(). + GetEpochInfo(gomock.Any(), "day"). + Return(epochstypes.EpochInfo{ + Identifier: "day", + CurrentEpoch: 1, + CurrentEpochStartTime: time.Now(), + CurrentEpochStartHeight: 1, + }). + Times(1) + + // Call with matching epoch identifier + err := k.BeforeEpochStart(ctx, "day", 1) + require.NoError(t, err) + + // Call with non-matching epoch identifier (should skip) + err = k.BeforeEpochStart(ctx, "hour", 1) + require.NoError(t, err) +} + +// TestBeforeEpochStart_MirrorsAfterEpochEndPattern verifies that +// BeforeEpochStart follows the same pattern as AfterEpochEnd +func TestBeforeEpochStart_MirrorsAfterEpochEndPattern(t *testing.T) { + k, ctx, _, _ := setupKeeperWithMocks(t) + + // Set billing epoch to "day" + params := k.GetParams(ctx) + params.BillingEpoch = "day" + k.SetParams(ctx, params) + + // Test that BeforeEpochStart returns early for non-matching epochs + // (similar to AfterEpochEnd's early return pattern) + err := k.BeforeEpochStart(ctx, "minute", 1) + require.NoError(t, err) + + err = k.BeforeEpochStart(ctx, "hour", 1) + require.NoError(t, err) + + err = k.BeforeEpochStart(ctx, "week", 1) + require.NoError(t, err) + + // All should return nil without processing +} + diff --git a/x/billing/keeper/keeper.go b/x/billing/keeper/keeper.go index 920ea365..d6373231 100644 --- a/x/billing/keeper/keeper.go +++ b/x/billing/keeper/keeper.go @@ -22,6 +22,7 @@ type ( stakingkeeper types.StakingKeeper chainletkeeper types.ChainletKeeper epochskeeper types.EpochsKeeper + authority string } ) @@ -35,6 +36,7 @@ func NewKeeper( stakingkeeper types.StakingKeeper, chainletkeeper types.ChainletKeeper, epochskeeper types.EpochsKeeper, + authority string, ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -51,6 +53,7 @@ func NewKeeper( stakingkeeper: stakingkeeper, chainletkeeper: chainletkeeper, epochskeeper: epochskeeper, + authority: authority, } } @@ -67,3 +70,19 @@ func (k *Keeper) UpdateKeeper(newKeeper interface{}) { k.epochskeeper = newk } } + +func (k Keeper) GetAuthority() string { + return k.authority +} + +func (k Keeper) SetPlatformValidators(ctx sdk.Context, vals []string) error { + params := k.GetParams(ctx) + params.PlatformValidators = vals + k.SetParams(ctx, params) + return nil +} + +func (k Keeper) GetPlatformValidators(ctx sdk.Context) []string { + params := k.GetParams(ctx) + return params.PlatformValidators +} diff --git a/x/billing/keeper/msg_server.go b/x/billing/keeper/msg_server.go index 689e5f4d..91192ff0 100644 --- a/x/billing/keeper/msg_server.go +++ b/x/billing/keeper/msg_server.go @@ -1,6 +1,9 @@ package keeper import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/sagaxyz/ssc/x/billing/types" ) @@ -15,3 +18,18 @@ func NewMsgServerImpl(keeper Keeper) types.MsgServer { } var _ types.MsgServer = msgServer{} + +func (m msgServer) SetPlatformValidators(goCtx context.Context, msg *types.MsgSetPlatformValidators) (*types.MsgSetPlatformValidatorsResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + if msg.Creator != m.GetAuthority() { + return nil, types.ErrUnauthorized + } + + err := m.Keeper.SetPlatformValidators(ctx, msg.PlatformValidators) + if err != nil { + return nil, err + } + + return &types.MsgSetPlatformValidatorsResponse{}, nil +} diff --git a/x/billing/types/billing_history.pb.go b/x/billing/types/billing_history.pb.go index 5a528386..b13867ad 100644 --- a/x/billing/types/billing_history.pb.go +++ b/x/billing/types/billing_history.pb.go @@ -28,7 +28,7 @@ type BillingHistory struct { ChainletId string `protobuf:"bytes,3,opt,name=chainletId,proto3" json:"chainletId,omitempty"` ChainletStackName string `protobuf:"bytes,4,opt,name=chainletStackName,proto3" json:"chainletStackName,omitempty"` EpochIdentifier string `protobuf:"bytes,5,opt,name=epochIdentifier,proto3" json:"epochIdentifier,omitempty"` - EpochNumber int32 `protobuf:"varint,6,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` + EpochNumber int64 `protobuf:"varint,6,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` EpochStartTime string `protobuf:"bytes,7,opt,name=epochStartTime,proto3" json:"epochStartTime,omitempty"` BilledAmount string `protobuf:"bytes,8,opt,name=billedAmount,proto3" json:"billedAmount,omitempty"` } @@ -101,7 +101,7 @@ func (m *BillingHistory) GetEpochIdentifier() string { return "" } -func (m *BillingHistory) GetEpochNumber() int32 { +func (m *BillingHistory) GetEpochNumber() int64 { if m != nil { return m.EpochNumber } @@ -131,7 +131,7 @@ func init() { proto.RegisterFile("ssc/billing/billing_history.proto", fileDescri var fileDescriptor_b2a9cabf2a680108 = []byte{ // 294 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xb1, 0x4e, 0xeb, 0x30, - 0x14, 0x86, 0xeb, 0xde, 0xdb, 0x02, 0x2e, 0x14, 0xe1, 0xc9, 0x93, 0x55, 0x2a, 0x54, 0x65, 0x40, + 0x14, 0x86, 0xeb, 0xf6, 0xde, 0x02, 0x2e, 0x14, 0xe1, 0xc9, 0x93, 0x55, 0x2a, 0x54, 0x65, 0x40, 0xc9, 0xc0, 0x0b, 0x40, 0x27, 0xba, 0x14, 0xa9, 0x65, 0x62, 0x41, 0x8e, 0x63, 0x12, 0x8b, 0x24, 0x8e, 0x6c, 0x47, 0x34, 0x3c, 0x05, 0x8f, 0xc4, 0xc8, 0xd8, 0x91, 0x11, 0x25, 0x2f, 0x82, 0xe2, 0x10, 0x94, 0x96, 0x29, 0xf9, 0xbf, 0xf3, 0xe9, 0x48, 0xc7, 0x3f, 0x3c, 0xd7, 0x9a, 0x79, 0xbe, @@ -139,16 +139,16 @@ var fileDescriptor_b2a9cabf2a680108 = []byte{ 0x91, 0xd6, 0xcc, 0xfd, 0x19, 0x4d, 0xdf, 0xfb, 0x70, 0x3c, 0x6f, 0xfe, 0x6f, 0x1b, 0x0b, 0x5d, 0xc0, 0x13, 0x16, 0x51, 0x91, 0xc6, 0xdc, 0xdc, 0xbd, 0xa4, 0x5c, 0x61, 0x30, 0x01, 0xce, 0xd1, 0x6a, 0x17, 0xa2, 0x29, 0x3c, 0x6e, 0xc1, 0x92, 0x26, 0x1c, 0xf7, 0xad, 0xb4, 0xc3, 0x10, 0x81, - 0xb0, 0xcd, 0x8b, 0x00, 0xff, 0xb3, 0x46, 0x87, 0xa0, 0x4b, 0x78, 0xd6, 0xa6, 0xb5, 0xa1, 0xec, - 0xd9, 0x2e, 0xfa, 0x6f, 0xb5, 0xbf, 0x03, 0xe4, 0xc0, 0x53, 0x9e, 0x49, 0x16, 0x2d, 0x02, 0x9e, - 0x1a, 0xf1, 0x24, 0xb8, 0xc2, 0x03, 0xeb, 0xee, 0x63, 0x34, 0x81, 0x23, 0x8b, 0x96, 0x79, 0xe2, + 0xb0, 0xcd, 0x8b, 0x00, 0x0f, 0xac, 0xd1, 0x21, 0xe8, 0x12, 0x9e, 0xb5, 0x69, 0x6d, 0x28, 0x7b, + 0xb6, 0x8b, 0xfe, 0x59, 0xed, 0xef, 0x00, 0x39, 0xf0, 0x94, 0x67, 0x92, 0x45, 0x8b, 0x80, 0xa7, + 0x46, 0x3c, 0x09, 0xae, 0xf0, 0x7f, 0xeb, 0xee, 0x63, 0x34, 0x81, 0x23, 0x8b, 0x96, 0x79, 0xe2, 0x73, 0x85, 0x87, 0x13, 0xe0, 0x0c, 0x56, 0x5d, 0x84, 0x66, 0x70, 0x6c, 0xe3, 0xda, 0x50, 0x65, 0xee, 0x45, 0xc2, 0xf1, 0x81, 0x5d, 0xb5, 0x47, 0xeb, 0x2b, 0xeb, 0x97, 0xe2, 0xc1, 0x4d, 0x22, 0xf3, 0xd4, 0xe0, 0xc3, 0xe6, 0xca, 0x2e, 0x9b, 0x5f, 0x7f, 0x94, 0x04, 0x6c, 0x4b, 0x02, 0xbe, 0x4a, 0x02, 0xde, 0x2a, 0xd2, 0xdb, 0x56, 0xa4, 0xf7, 0x59, 0x91, 0xde, 0xc3, 0x2c, 0x14, 0x26, 0xca, 0x7d, 0x97, 0xc9, 0xc4, 0xd3, 0x34, 0xa4, 0x9b, 0xe2, 0xd5, 0xab, 0xfb, 0xd9, 0xfc, 0x36, - 0x64, 0x8a, 0x8c, 0x6b, 0x7f, 0x68, 0x8b, 0xb9, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x8f, - 0xc5, 0xbb, 0xbd, 0x01, 0x00, 0x00, + 0x64, 0x8a, 0x8c, 0x6b, 0x7f, 0x68, 0x8b, 0xb9, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x2a, 0xcd, + 0xba, 0xee, 0xbd, 0x01, 0x00, 0x00, } func (m *BillingHistory) Marshal() (dAtA []byte, err error) { @@ -488,7 +488,7 @@ func (m *BillingHistory) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.EpochNumber |= int32(b&0x7F) << shift + m.EpochNumber |= int64(b&0x7F) << shift if b < 0x80 { break } diff --git a/x/billing/types/errors.go b/x/billing/types/errors.go index 9d1e25d6..a9ceeb9c 100644 --- a/x/billing/types/errors.go +++ b/x/billing/types/errors.go @@ -10,4 +10,5 @@ var ( ErrJSONMarhsal = cosmossdkerrors.Register(ModuleName, 7702, "failed to marshal json") ErrDuplicateRecord = cosmossdkerrors.Register(ModuleName, 7703, "duplicate record") ErrInternalBillingFailure = cosmossdkerrors.Register(ModuleName, 7704, "internal failure") + ErrUnauthorized = cosmossdkerrors.Register(ModuleName, 7705, "unauthorized") ) diff --git a/x/billing/types/expected_keepers.go b/x/billing/types/expected_keepers.go index c23473bb..6fed6788 100644 --- a/x/billing/types/expected_keepers.go +++ b/x/billing/types/expected_keepers.go @@ -63,3 +63,7 @@ type ChainletKeeper interface { GetChainletInfo(ctx sdk.Context, chainId string) (*chainlettypes.Chainlet, error) GetParams(ctx sdk.Context) chainlettypes.Params } + +type BillingKeeper interface { + GetPlatformValidators(ctx sdk.Context) []string +} diff --git a/x/billing/types/genesis.go b/x/billing/types/genesis.go index 2c15cd92..d8c361f2 100644 --- a/x/billing/types/genesis.go +++ b/x/billing/types/genesis.go @@ -1,5 +1,7 @@ package types +import "strconv" + // this line is used by starport scaffolding # genesis/types/import // DefaultIndex is the default global index @@ -9,7 +11,9 @@ const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ // this line is used by starport scaffolding # genesis/types/default - Params: DefaultParams(), + Params: DefaultParams(), + BillingHistory: []SaveBillingHistory{}, + ValidatorPayoutHistory: []ValidatorPayoutHistory{}, } } @@ -18,5 +22,25 @@ func DefaultGenesis() *GenesisState { func (gs GenesisState) Validate() error { // this line is used by starport scaffolding # genesis/types/validate + // Validate billing history records have unique {chainletId, epochIdentifier, epochNumber} tuples + billingKeys := make(map[string]bool) + for _, bh := range gs.BillingHistory { + key := bh.ChainletId + "/" + bh.EpochIdentifier + "/" + strconv.FormatInt(bh.EpochNumber, 10) + if billingKeys[key] { + return ErrDuplicateRecord + } + billingKeys[key] = true + } + + // Validate validator payout history records have unique {validatorAddress, epochIdentifier, epochNumber} tuples + payoutKeys := make(map[string]bool) + for _, vph := range gs.ValidatorPayoutHistory { + key := vph.ValidatorAddress + "/" + vph.EpochIdentifier + "/" + strconv.FormatInt(vph.EpochNumber, 10) + if payoutKeys[key] { + return ErrDuplicateRecord + } + payoutKeys[key] = true + } + return gs.Params.Validate() } diff --git a/x/billing/types/genesis.pb.go b/x/billing/types/genesis.pb.go index 9f64e160..25b02b7f 100644 --- a/x/billing/types/genesis.pb.go +++ b/x/billing/types/genesis.pb.go @@ -26,6 +26,10 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the billing module's genesis state. type GenesisState struct { Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + // Billing history records + BillingHistory []SaveBillingHistory `protobuf:"bytes,2,rep,name=billing_history,json=billingHistory,proto3" json:"billing_history"` + // Validator payout history records + ValidatorPayoutHistory []ValidatorPayoutHistory `protobuf:"bytes,3,rep,name=validator_payout_history,json=validatorPayoutHistory,proto3" json:"validator_payout_history"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -68,6 +72,20 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetBillingHistory() []SaveBillingHistory { + if m != nil { + return m.BillingHistory + } + return nil +} + +func (m *GenesisState) GetValidatorPayoutHistory() []ValidatorPayoutHistory { + if m != nil { + return m.ValidatorPayoutHistory + } + return nil +} + func init() { proto.RegisterType((*GenesisState)(nil), "ssc.billing.GenesisState") } @@ -75,19 +93,26 @@ func init() { func init() { proto.RegisterFile("ssc/billing/genesis.proto", fileDescriptor_02989b592da35a5b) } var fileDescriptor_02989b592da35a5b = []byte{ - // 189 bytes of a gzipped FileDescriptorProto + // 291 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2c, 0x2e, 0x4e, 0xd6, 0x4f, 0xca, 0xcc, 0xc9, 0xc9, 0xcc, 0x4b, 0xd7, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2e, 0x2e, 0x4e, 0xd6, 0x83, 0x4a, 0x49, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0xc5, 0xf5, 0x41, 0x2c, 0x88, 0x12, 0x29, 0x09, 0x64, 0xdd, 0x05, 0x89, - 0x45, 0x89, 0xb9, 0x50, 0xcd, 0x4a, 0x8e, 0x5c, 0x3c, 0xee, 0x10, 0xd3, 0x82, 0x4b, 0x12, 0x4b, - 0x52, 0x85, 0x0c, 0xb9, 0xd8, 0x20, 0xf2, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0xc2, 0x7a, - 0x48, 0xa6, 0xeb, 0x05, 0x80, 0xa5, 0x9c, 0x58, 0x4e, 0xdc, 0x93, 0x67, 0x08, 0x82, 0x2a, 0x74, - 0x72, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, - 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0xb5, 0xf4, 0xcc, 0x92, - 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0xfd, 0xe2, 0xc4, 0xf4, 0xc4, 0x8a, 0xca, 0x2a, 0x7d, - 0x90, 0x4b, 0x2a, 0xe0, 0x6e, 0x29, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0xbb, 0xc5, 0x18, - 0x10, 0x00, 0x00, 0xff, 0xff, 0x07, 0x90, 0x16, 0x12, 0xe5, 0x00, 0x00, 0x00, + 0x45, 0x89, 0xb9, 0x50, 0xcd, 0x52, 0x6a, 0xc8, 0x32, 0xc5, 0x89, 0x65, 0xa9, 0xf1, 0x50, 0x4e, + 0x7c, 0x46, 0x66, 0x71, 0x49, 0x7e, 0x51, 0x25, 0x54, 0x9d, 0x16, 0xb2, 0xba, 0xb2, 0xc4, 0x9c, + 0xcc, 0x94, 0xc4, 0x92, 0xfc, 0xa2, 0xf8, 0x82, 0xc4, 0xca, 0xfc, 0xd2, 0x12, 0x54, 0xb5, 0x4a, + 0xdf, 0x18, 0xb9, 0x78, 0xdc, 0x21, 0x4e, 0x0c, 0x2e, 0x49, 0x2c, 0x49, 0x15, 0x32, 0xe4, 0x62, + 0x83, 0x58, 0x2a, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xac, 0x87, 0xe4, 0x64, 0xbd, 0x00, + 0xb0, 0x94, 0x13, 0xcb, 0x89, 0x7b, 0xf2, 0x0c, 0x41, 0x50, 0x85, 0x42, 0x7e, 0x5c, 0xfc, 0x68, + 0x0e, 0x91, 0x60, 0x52, 0x60, 0xd6, 0xe0, 0x36, 0x92, 0x47, 0xd1, 0x1b, 0x9c, 0x58, 0x96, 0xea, + 0x04, 0x61, 0x7b, 0x40, 0x94, 0x41, 0xcd, 0xe1, 0x4b, 0x42, 0x11, 0x15, 0x4a, 0xe6, 0x92, 0xc0, + 0xe5, 0x6a, 0x09, 0x66, 0xb0, 0xc1, 0xca, 0x28, 0x06, 0x87, 0xc1, 0x14, 0x07, 0x80, 0xd5, 0xa2, + 0x1a, 0x2e, 0x56, 0x86, 0x5d, 0xd6, 0xe1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4, 0x18, 0x1f, + 0x3c, 0x92, 0x63, 0x9c, 0xf0, 0x58, 0x8e, 0xe1, 0xc2, 0x63, 0x39, 0x86, 0x1b, 0x8f, 0xe5, 0x18, + 0xa2, 0xd4, 0xd2, 0x33, 0x4b, 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0x8b, 0x13, 0xd3, + 0x13, 0x2b, 0x2a, 0xab, 0xf4, 0x41, 0x21, 0x5a, 0x01, 0x0f, 0xd3, 0x92, 0xca, 0x82, 0xd4, 0xe2, + 0x24, 0x36, 0x70, 0x08, 0x1a, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe6, 0xf0, 0x2f, 0x7a, 0xef, + 0x01, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -110,6 +135,34 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ValidatorPayoutHistory) > 0 { + for iNdEx := len(m.ValidatorPayoutHistory) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ValidatorPayoutHistory[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.BillingHistory) > 0 { + for iNdEx := len(m.BillingHistory) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.BillingHistory[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -142,6 +195,18 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.BillingHistory) > 0 { + for _, e := range m.BillingHistory { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.ValidatorPayoutHistory) > 0 { + for _, e := range m.ValidatorPayoutHistory { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } return n } @@ -213,6 +278,74 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field BillingHistory", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.BillingHistory = append(m.BillingHistory, SaveBillingHistory{}) + if err := m.BillingHistory[len(m.BillingHistory)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorPayoutHistory", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorPayoutHistory = append(m.ValidatorPayoutHistory, ValidatorPayoutHistory{}) + if err := m.ValidatorPayoutHistory[len(m.ValidatorPayoutHistory)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/billing/types/genesis_test.go b/x/billing/types/genesis_test.go index 9cf7a0bc..95f8c227 100644 --- a/x/billing/types/genesis_test.go +++ b/x/billing/types/genesis_test.go @@ -19,13 +19,53 @@ func TestGenesisState_Validate(t *testing.T) { valid: true, }, { - desc: "valid genesis state", + desc: "valid genesis state", genState: &types.GenesisState{ - + Params: types.DefaultParams(), + BillingHistory: []types.SaveBillingHistory{}, + ValidatorPayoutHistory: []types.ValidatorPayoutHistory{}, // this line is used by starport scaffolding # types/genesis/validField }, valid: true, }, + { + desc: "valid genesis state with data", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + BillingHistory: []types.SaveBillingHistory{ + {ChainletId: "chain-1", EpochIdentifier: "day", EpochNumber: 1, BilledAmount: "100usaga"}, + {ChainletId: "chain-1", EpochIdentifier: "day", EpochNumber: 2, BilledAmount: "100usaga"}, + }, + ValidatorPayoutHistory: []types.ValidatorPayoutHistory{ + {ValidatorAddress: "val1", EpochIdentifier: "day", EpochNumber: 1, RewardAmount: "50usaga"}, + }, + }, + valid: true, + }, + { + desc: "invalid - duplicate billing history", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + BillingHistory: []types.SaveBillingHistory{ + {ChainletId: "chain-1", EpochIdentifier: "day", EpochNumber: 1, BilledAmount: "100usaga"}, + {ChainletId: "chain-1", EpochIdentifier: "day", EpochNumber: 1, BilledAmount: "200usaga"}, + }, + ValidatorPayoutHistory: []types.ValidatorPayoutHistory{}, + }, + valid: false, + }, + { + desc: "invalid - duplicate validator payout history", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + BillingHistory: []types.SaveBillingHistory{}, + ValidatorPayoutHistory: []types.ValidatorPayoutHistory{ + {ValidatorAddress: "val1", EpochIdentifier: "day", EpochNumber: 1, RewardAmount: "50usaga"}, + {ValidatorAddress: "val1", EpochIdentifier: "day", EpochNumber: 1, RewardAmount: "100usaga"}, + }, + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } { t.Run(tc.desc, func(t *testing.T) { diff --git a/x/billing/types/params.go b/x/billing/types/params.go index 75b25b81..a35792d2 100644 --- a/x/billing/types/params.go +++ b/x/billing/types/params.go @@ -3,6 +3,8 @@ package types import ( fmt "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" ) @@ -19,6 +21,7 @@ func NewParams() Params { return Params{ ValidatorPayoutEpoch: SAGA_EPOCH_IDENTIFIER, BillingEpoch: SAGA_EPOCH_IDENTIFIER, + PlatformValidators: nil, } } @@ -33,6 +36,7 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { psp := paramtypes.ParamSetPairs{ paramtypes.NewParamSetPair([]byte("ValidatorPayoutEpoch"), &p.ValidatorPayoutEpoch, validateEpochParam), paramtypes.NewParamSetPair([]byte("BillingEpoch"), &p.BillingEpoch, validateEpochParam), + paramtypes.NewParamSetPair([]byte("PlatformValidators"), &p.PlatformValidators, validatePlatformValidatorsParam), } return psp @@ -56,3 +60,17 @@ func validateEpochParam(v interface{}) error { } return nil } + +func validatePlatformValidatorsParam(v interface{}) error { + vals, ok := v.([]string) + if !ok { + return fmt.Errorf("could not unmarshal platform-validators parm for validation") + } + for _, val := range vals { + _, err := sdk.AccAddressFromBech32(val) + if err != nil { + return fmt.Errorf("invalid platform validator address: %s", val) + } + } + return nil +} diff --git a/x/billing/types/params.pb.go b/x/billing/types/params.pb.go index 4516a3c3..bbb1c843 100644 --- a/x/billing/types/params.pb.go +++ b/x/billing/types/params.pb.go @@ -25,8 +25,9 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // Params defines the parameters for the module. type Params struct { - ValidatorPayoutEpoch string `protobuf:"bytes,1,opt,name=validator_payout_epoch,json=validatorPayoutEpoch,proto3" json:"validator_payout_epoch,omitempty"` - BillingEpoch string `protobuf:"bytes,2,opt,name=billing_epoch,json=billingEpoch,proto3" json:"billing_epoch,omitempty"` + ValidatorPayoutEpoch string `protobuf:"bytes,1,opt,name=validator_payout_epoch,json=validatorPayoutEpoch,proto3" json:"validator_payout_epoch,omitempty"` + BillingEpoch string `protobuf:"bytes,2,opt,name=billing_epoch,json=billingEpoch,proto3" json:"billing_epoch,omitempty"` + PlatformValidators []string `protobuf:"bytes,3,rep,name=platform_validators,json=platformValidators,proto3" json:"platform_validators,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -75,6 +76,13 @@ func (m *Params) GetBillingEpoch() string { return "" } +func (m *Params) GetPlatformValidators() []string { + if m != nil { + return m.PlatformValidators + } + return nil +} + func init() { proto.RegisterType((*Params)(nil), "ssc.billing.Params") } @@ -82,20 +90,22 @@ func init() { func init() { proto.RegisterFile("ssc/billing/params.proto", fileDescriptor_46fb5cb2ae268601) } var fileDescriptor_46fb5cb2ae268601 = []byte{ - // 206 bytes of a gzipped FileDescriptorProto + // 235 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x28, 0x2e, 0x4e, 0xd6, 0x4f, 0xca, 0xcc, 0xc9, 0xc9, 0xcc, 0x4b, 0xd7, 0x2f, 0x48, 0x2c, 0x4a, 0xcc, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2e, 0x2e, 0x4e, 0xd6, 0x83, 0xca, 0x48, 0x89, 0xa4, 0xe7, - 0xa7, 0xe7, 0x83, 0xc5, 0xf5, 0x41, 0x2c, 0x88, 0x12, 0xa5, 0x4c, 0x2e, 0xb6, 0x00, 0xb0, 0x16, - 0x21, 0x13, 0x2e, 0xb1, 0xb2, 0xc4, 0x9c, 0xcc, 0x94, 0xc4, 0x92, 0xfc, 0xa2, 0xf8, 0x82, 0xc4, - 0xca, 0xfc, 0xd2, 0x92, 0xf8, 0xd4, 0x82, 0xfc, 0xe4, 0x0c, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, - 0x20, 0x11, 0xb8, 0x6c, 0x00, 0x58, 0xd2, 0x15, 0x24, 0x27, 0xa4, 0xcc, 0xc5, 0x0b, 0xb5, 0x00, - 0xaa, 0x98, 0x09, 0xac, 0x98, 0x07, 0x2a, 0x08, 0x56, 0x64, 0xc5, 0x32, 0x63, 0x81, 0x3c, 0x83, - 0x93, 0xc3, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, - 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xa9, 0xa5, 0x67, 0x96, - 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x17, 0x27, 0xa6, 0x27, 0x56, 0x54, 0x56, 0xe9, - 0x83, 0x3c, 0x55, 0x01, 0xf7, 0x56, 0x49, 0x65, 0x41, 0x6a, 0x71, 0x12, 0x1b, 0xd8, 0xcd, 0xc6, - 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x13, 0x9f, 0xd7, 0x18, 0xf2, 0x00, 0x00, 0x00, + 0xa7, 0xe7, 0x83, 0xc5, 0xf5, 0x41, 0x2c, 0x88, 0x12, 0xa5, 0x59, 0x8c, 0x5c, 0x6c, 0x01, 0x60, + 0x3d, 0x42, 0x26, 0x5c, 0x62, 0x65, 0x89, 0x39, 0x99, 0x29, 0x89, 0x25, 0xf9, 0x45, 0xf1, 0x05, + 0x89, 0x95, 0xf9, 0xa5, 0x25, 0xf1, 0xa9, 0x05, 0xf9, 0xc9, 0x19, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, + 0x9c, 0x41, 0x22, 0x70, 0xd9, 0x00, 0xb0, 0xa4, 0x2b, 0x48, 0x4e, 0x48, 0x99, 0x8b, 0x17, 0x6a, + 0x03, 0x54, 0x31, 0x13, 0x58, 0x31, 0x0f, 0x54, 0x10, 0xa2, 0x48, 0x9f, 0x4b, 0xb8, 0x20, 0x27, + 0xb1, 0x24, 0x2d, 0xbf, 0x28, 0x37, 0x1e, 0x6e, 0x4a, 0xb1, 0x04, 0xb3, 0x02, 0xb3, 0x06, 0x67, + 0x90, 0x10, 0x4c, 0x2a, 0x0c, 0x2e, 0x63, 0xc5, 0x32, 0x63, 0x81, 0x3c, 0x83, 0x93, 0xc3, 0x89, + 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, 0xe1, 0xb1, 0x1c, 0xc3, + 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xa9, 0xa5, 0x67, 0x96, 0x64, 0x94, 0x26, + 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x17, 0x27, 0xa6, 0x27, 0x56, 0x54, 0x56, 0xe9, 0x83, 0x82, 0xa1, + 0x02, 0x1e, 0x10, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0x5f, 0x1a, 0x03, 0x02, 0x00, + 0x00, 0xff, 0xff, 0x77, 0x38, 0x88, 0xd8, 0x24, 0x01, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -118,6 +128,15 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.PlatformValidators) > 0 { + for iNdEx := len(m.PlatformValidators) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.PlatformValidators[iNdEx]) + copy(dAtA[i:], m.PlatformValidators[iNdEx]) + i = encodeVarintParams(dAtA, i, uint64(len(m.PlatformValidators[iNdEx]))) + i-- + dAtA[i] = 0x1a + } + } if len(m.BillingEpoch) > 0 { i -= len(m.BillingEpoch) copy(dAtA[i:], m.BillingEpoch) @@ -160,6 +179,12 @@ func (m *Params) Size() (n int) { if l > 0 { n += 1 + l + sovParams(uint64(l)) } + if len(m.PlatformValidators) > 0 { + for _, s := range m.PlatformValidators { + l = len(s) + n += 1 + l + sovParams(uint64(l)) + } + } return n } @@ -262,6 +287,38 @@ func (m *Params) Unmarshal(dAtA []byte) error { } m.BillingEpoch = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PlatformValidators", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PlatformValidators = append(m.PlatformValidators, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:]) diff --git a/x/billing/types/query.pb.go b/x/billing/types/query.pb.go index dbf78519..b6757d73 100644 --- a/x/billing/types/query.pb.go +++ b/x/billing/types/query.pb.go @@ -512,6 +512,7 @@ func _Query_GetValidatorPayoutHistory_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.billing.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/billing/types/save_billing_history.pb.go b/x/billing/types/save_billing_history.pb.go index 7de89705..b410cbdb 100644 --- a/x/billing/types/save_billing_history.pb.go +++ b/x/billing/types/save_billing_history.pb.go @@ -25,7 +25,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type SaveBillingHistory struct { ChainletId string `protobuf:"bytes,1,opt,name=chainletId,proto3" json:"chainletId,omitempty"` EpochIdentifier string `protobuf:"bytes,2,opt,name=epochIdentifier,proto3" json:"epochIdentifier,omitempty"` - EpochNumber int32 `protobuf:"varint,3,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` + EpochNumber int64 `protobuf:"varint,3,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` BilledAmount string `protobuf:"bytes,4,opt,name=billedAmount,proto3" json:"billedAmount,omitempty"` } @@ -76,7 +76,7 @@ func (m *SaveBillingHistory) GetEpochIdentifier() string { return "" } -func (m *SaveBillingHistory) GetEpochNumber() int32 { +func (m *SaveBillingHistory) GetEpochNumber() int64 { if m != nil { return m.EpochNumber } @@ -108,13 +108,13 @@ var fileDescriptor_26420f58771dffb0 = []byte{ 0xcf, 0x14, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x24, 0x11, 0x21, 0x0d, 0x2e, 0xfe, 0xd4, 0x82, 0xfc, 0xe4, 0x0c, 0xcf, 0x94, 0xd4, 0xbc, 0x92, 0xcc, 0xb4, 0xcc, 0xd4, 0x22, 0x09, 0x26, 0xb0, 0x22, 0x74, 0x61, 0x21, 0x05, 0x2e, 0x6e, 0xb0, 0x90, 0x5f, 0x69, 0x6e, 0x52, 0x6a, 0x91, - 0x04, 0xb3, 0x02, 0xa3, 0x06, 0x6b, 0x10, 0xb2, 0x90, 0x90, 0x12, 0x17, 0x0f, 0xc8, 0x35, 0xa9, + 0x04, 0xb3, 0x02, 0xa3, 0x06, 0x73, 0x10, 0xb2, 0x90, 0x90, 0x12, 0x17, 0x0f, 0xc8, 0x35, 0xa9, 0x29, 0x8e, 0xb9, 0xf9, 0xa5, 0x79, 0x25, 0x12, 0x2c, 0x60, 0x83, 0x50, 0xc4, 0x9c, 0x1c, 0x4e, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x2d, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0xbf, 0x38, 0x31, 0x3d, 0xb1, 0xa2, 0xb2, 0x4a, 0x1f, 0x14, 0x10, 0x15, 0xf0, 0xa0, 0x28, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0x7b, 0xde, 0x18, 0x10, 0x00, - 0x00, 0xff, 0xff, 0xc4, 0xbe, 0xaf, 0x0f, 0x26, 0x01, 0x00, 0x00, + 0x00, 0xff, 0xff, 0xfb, 0xa7, 0xc8, 0xec, 0x26, 0x01, 0x00, 0x00, } func (m *SaveBillingHistory) Marshal() (dAtA []byte, err error) { @@ -314,7 +314,7 @@ func (m *SaveBillingHistory) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.EpochNumber |= int32(b&0x7F) << shift + m.EpochNumber |= int64(b&0x7F) << shift if b < 0x80 { break } diff --git a/x/billing/types/tx.pb.go b/x/billing/types/tx.pb.go index 3b9eb4b9..fd846a8e 100644 --- a/x/billing/types/tx.pb.go +++ b/x/billing/types/tx.pb.go @@ -10,7 +10,11 @@ import ( grpc1 "github.com/cosmos/gogoproto/grpc" proto "github.com/cosmos/gogoproto/proto" grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" math "math" + math_bits "math/bits" ) // Reference imports to suppress errors if they are not otherwise used. @@ -24,20 +28,120 @@ var _ = math.Inf // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +type MsgSetPlatformValidators struct { + Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` + PlatformValidators []string `protobuf:"bytes,2,rep,name=platform_validators,json=platformValidators,proto3" json:"platform_validators,omitempty"` +} + +func (m *MsgSetPlatformValidators) Reset() { *m = MsgSetPlatformValidators{} } +func (m *MsgSetPlatformValidators) String() string { return proto.CompactTextString(m) } +func (*MsgSetPlatformValidators) ProtoMessage() {} +func (*MsgSetPlatformValidators) Descriptor() ([]byte, []int) { + return fileDescriptor_5648eef8735b4c01, []int{0} +} +func (m *MsgSetPlatformValidators) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSetPlatformValidators) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSetPlatformValidators.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSetPlatformValidators) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSetPlatformValidators.Merge(m, src) +} +func (m *MsgSetPlatformValidators) XXX_Size() int { + return m.Size() +} +func (m *MsgSetPlatformValidators) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSetPlatformValidators.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSetPlatformValidators proto.InternalMessageInfo + +func (m *MsgSetPlatformValidators) GetCreator() string { + if m != nil { + return m.Creator + } + return "" +} + +func (m *MsgSetPlatformValidators) GetPlatformValidators() []string { + if m != nil { + return m.PlatformValidators + } + return nil +} + +type MsgSetPlatformValidatorsResponse struct { +} + +func (m *MsgSetPlatformValidatorsResponse) Reset() { *m = MsgSetPlatformValidatorsResponse{} } +func (m *MsgSetPlatformValidatorsResponse) String() string { return proto.CompactTextString(m) } +func (*MsgSetPlatformValidatorsResponse) ProtoMessage() {} +func (*MsgSetPlatformValidatorsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_5648eef8735b4c01, []int{1} +} +func (m *MsgSetPlatformValidatorsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgSetPlatformValidatorsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgSetPlatformValidatorsResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgSetPlatformValidatorsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgSetPlatformValidatorsResponse.Merge(m, src) +} +func (m *MsgSetPlatformValidatorsResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgSetPlatformValidatorsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgSetPlatformValidatorsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgSetPlatformValidatorsResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*MsgSetPlatformValidators)(nil), "ssc.billing.MsgSetPlatformValidators") + proto.RegisterType((*MsgSetPlatformValidatorsResponse)(nil), "ssc.billing.MsgSetPlatformValidatorsResponse") +} + func init() { proto.RegisterFile("ssc/billing/tx.proto", fileDescriptor_5648eef8735b4c01) } var fileDescriptor_5648eef8735b4c01 = []byte{ - // 150 bytes of a gzipped FileDescriptorProto + // 262 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x29, 0x2e, 0x4e, 0xd6, 0x4f, 0xca, 0xcc, 0xc9, 0xc9, 0xcc, 0x4b, 0xd7, 0x2f, 0xa9, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2e, 0x2e, 0x4e, 0xd6, 0x83, 0x8a, 0x4a, 0x89, 0x27, 0xe7, 0x17, 0xe7, 0xe6, 0x17, - 0xeb, 0xe7, 0x16, 0xa7, 0xeb, 0x97, 0x19, 0x82, 0x28, 0x88, 0x2a, 0x23, 0x1e, 0x2e, 0x66, 0xdf, - 0xe2, 0x74, 0x29, 0xd6, 0x86, 0xe7, 0x1b, 0xb4, 0x18, 0x9d, 0x1c, 0x4e, 0x3c, 0x92, 0x63, 0xbc, - 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, 0x2e, 0x3c, 0x96, 0x63, - 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x2d, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, - 0x57, 0xbf, 0x38, 0x31, 0x3d, 0xb1, 0xa2, 0xb2, 0x4a, 0x1f, 0x64, 0x6d, 0x05, 0xc2, 0xe2, 0xca, - 0x82, 0xd4, 0xe2, 0x24, 0x36, 0xb0, 0xb1, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xaa, 0x3d, - 0x56, 0x64, 0x94, 0x00, 0x00, 0x00, + 0xeb, 0xe7, 0x16, 0xa7, 0xeb, 0x97, 0x19, 0x82, 0x28, 0x88, 0x2a, 0xa5, 0x62, 0x2e, 0x09, 0xdf, + 0xe2, 0xf4, 0xe0, 0xd4, 0x92, 0x80, 0x9c, 0xc4, 0x92, 0xb4, 0xfc, 0xa2, 0xdc, 0xb0, 0xc4, 0x9c, + 0xcc, 0x94, 0xc4, 0x92, 0xfc, 0xa2, 0x62, 0x21, 0x09, 0x2e, 0xf6, 0xe4, 0xa2, 0x54, 0x10, 0x5b, + 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0xc6, 0x15, 0xd2, 0xe7, 0x12, 0x2e, 0x80, 0xaa, 0x8f, + 0x2f, 0x83, 0x6b, 0x90, 0x60, 0x52, 0x60, 0xd6, 0xe0, 0x0c, 0x12, 0x2a, 0xc0, 0x30, 0xca, 0x8a, + 0xa7, 0xe9, 0xf9, 0x06, 0x2d, 0x98, 0x76, 0x25, 0x25, 0x2e, 0x05, 0x5c, 0x96, 0x06, 0xa5, 0x16, + 0x17, 0xe4, 0xe7, 0x15, 0xa7, 0x1a, 0x55, 0x73, 0x31, 0xfb, 0x16, 0xa7, 0x0b, 0xe5, 0x72, 0x89, + 0x62, 0x77, 0x9c, 0xaa, 0x1e, 0x92, 0xff, 0xf4, 0x70, 0x19, 0x27, 0xa5, 0x4b, 0x94, 0x32, 0x98, + 0xad, 0x52, 0xac, 0x0d, 0xcf, 0x37, 0x68, 0x31, 0x3a, 0x39, 0x9c, 0x78, 0x24, 0xc7, 0x78, 0xe1, + 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, + 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x5a, 0x7a, 0x66, 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, + 0x7e, 0x71, 0x62, 0x7a, 0x62, 0x45, 0x65, 0x95, 0x3e, 0x28, 0xf8, 0x2b, 0x10, 0x11, 0x50, 0x59, + 0x90, 0x5a, 0x9c, 0xc4, 0x06, 0x0e, 0x5e, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x14, 0xf5, + 0x99, 0x5e, 0x9c, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -52,6 +156,8 @@ const _ = grpc.SupportPackageIsVersion4 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type MsgClient interface { + // this line is used by starport scaffolding # proto/tx/rpc + SetPlatformValidators(ctx context.Context, in *MsgSetPlatformValidators, opts ...grpc.CallOption) (*MsgSetPlatformValidatorsResponse, error) } type msgClient struct { @@ -62,22 +168,417 @@ func NewMsgClient(cc grpc1.ClientConn) MsgClient { return &msgClient{cc} } +func (c *msgClient) SetPlatformValidators(ctx context.Context, in *MsgSetPlatformValidators, opts ...grpc.CallOption) (*MsgSetPlatformValidatorsResponse, error) { + out := new(MsgSetPlatformValidatorsResponse) + err := c.cc.Invoke(ctx, "/ssc.billing.Msg/SetPlatformValidators", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // MsgServer is the server API for Msg service. type MsgServer interface { + // this line is used by starport scaffolding # proto/tx/rpc + SetPlatformValidators(context.Context, *MsgSetPlatformValidators) (*MsgSetPlatformValidatorsResponse, error) } // UnimplementedMsgServer can be embedded to have forward compatible implementations. type UnimplementedMsgServer struct { } +func (*UnimplementedMsgServer) SetPlatformValidators(ctx context.Context, req *MsgSetPlatformValidators) (*MsgSetPlatformValidatorsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetPlatformValidators not implemented") +} + func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) } +func _Msg_SetPlatformValidators_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgSetPlatformValidators) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).SetPlatformValidators(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/ssc.billing.Msg/SetPlatformValidators", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).SetPlatformValidators(ctx, req.(*MsgSetPlatformValidators)) + } + return interceptor(ctx, in, info, handler) +} + +var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.billing.Msg", HandlerType: (*MsgServer)(nil), - Methods: []grpc.MethodDesc{}, - Streams: []grpc.StreamDesc{}, - Metadata: "ssc/billing/tx.proto", + Methods: []grpc.MethodDesc{ + { + MethodName: "SetPlatformValidators", + Handler: _Msg_SetPlatformValidators_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ssc/billing/tx.proto", +} + +func (m *MsgSetPlatformValidators) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSetPlatformValidators) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSetPlatformValidators) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.PlatformValidators) > 0 { + for iNdEx := len(m.PlatformValidators) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.PlatformValidators[iNdEx]) + copy(dAtA[i:], m.PlatformValidators[iNdEx]) + i = encodeVarintTx(dAtA, i, uint64(len(m.PlatformValidators[iNdEx]))) + i-- + dAtA[i] = 0x12 + } + } + if len(m.Creator) > 0 { + i -= len(m.Creator) + copy(dAtA[i:], m.Creator) + i = encodeVarintTx(dAtA, i, uint64(len(m.Creator))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil } + +func (m *MsgSetPlatformValidatorsResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgSetPlatformValidatorsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgSetPlatformValidatorsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgSetPlatformValidators) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Creator) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + if len(m.PlatformValidators) > 0 { + for _, s := range m.PlatformValidators { + l = len(s) + n += 1 + l + sovTx(uint64(l)) + } + } + return n +} + +func (m *MsgSetPlatformValidatorsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func sovTx(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgSetPlatformValidators) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSetPlatformValidators: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSetPlatformValidators: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Creator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Creator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PlatformValidators", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PlatformValidators = append(m.PlatformValidators, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgSetPlatformValidatorsResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgSetPlatformValidatorsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgSetPlatformValidatorsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTx(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTx + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTx + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTx + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTx = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTx = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTx = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/billing/types/validator_payout_history.pb.go b/x/billing/types/validator_payout_history.pb.go index 8d133303..6d58f8e7 100644 --- a/x/billing/types/validator_payout_history.pb.go +++ b/x/billing/types/validator_payout_history.pb.go @@ -25,7 +25,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type ValidatorPayoutHistory struct { ValidatorAddress string `protobuf:"bytes,1,opt,name=validatorAddress,proto3" json:"validatorAddress,omitempty"` EpochIdentifier string `protobuf:"bytes,2,opt,name=epochIdentifier,proto3" json:"epochIdentifier,omitempty"` - EpochNumber int32 `protobuf:"varint,3,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` + EpochNumber int64 `protobuf:"varint,3,opt,name=epochNumber,proto3" json:"epochNumber,omitempty"` EpochStartTime string `protobuf:"bytes,4,opt,name=epochStartTime,proto3" json:"epochStartTime,omitempty"` RewardAmount string `protobuf:"bytes,5,opt,name=rewardAmount,proto3" json:"rewardAmount,omitempty"` } @@ -77,7 +77,7 @@ func (m *ValidatorPayoutHistory) GetEpochIdentifier() string { return "" } -func (m *ValidatorPayoutHistory) GetEpochNumber() int32 { +func (m *ValidatorPayoutHistory) GetEpochNumber() int64 { if m != nil { return m.EpochNumber } @@ -109,7 +109,7 @@ func init() { var fileDescriptor_45af6150ff54cc1a = []byte{ // 265 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0xd0, 0x31, 0x4e, 0xc3, 0x30, - 0x14, 0xc6, 0xf1, 0x1a, 0x28, 0x12, 0x2e, 0x02, 0xe4, 0x01, 0x79, 0xb2, 0xa2, 0x0e, 0x55, 0xd4, + 0x14, 0xc6, 0xf1, 0x9a, 0x02, 0x12, 0x2e, 0x02, 0xe4, 0x01, 0x79, 0xb2, 0xa2, 0x0e, 0x55, 0xd4, 0x21, 0x19, 0xb8, 0x00, 0x65, 0x82, 0x05, 0xa1, 0x82, 0x18, 0x58, 0x2a, 0x27, 0x36, 0x89, 0xa5, 0x24, 0x8e, 0xfc, 0x5e, 0xa0, 0xe1, 0x14, 0x1c, 0x8b, 0xb1, 0x23, 0x03, 0x03, 0x4a, 0x2e, 0x82, 0x30, 0x50, 0x41, 0x19, 0xdf, 0x5f, 0x3f, 0xbd, 0xe1, 0xa3, 0x53, 0x80, 0x34, 0x4e, 0x4c, 0x51, @@ -118,13 +118,13 @@ var fileDescriptor_45af6150ff54cc1a = []byte{ 0xe3, 0xdb, 0x1f, 0x7f, 0xe5, 0xf9, 0xf9, 0x97, 0x66, 0x53, 0x7a, 0xb4, 0xfe, 0x34, 0x53, 0xca, 0x69, 0x00, 0x4e, 0x02, 0x12, 0xee, 0xcd, 0xff, 0x75, 0x16, 0xd2, 0x43, 0x5d, 0xdb, 0x34, 0xbf, 0x50, 0xba, 0x42, 0x73, 0x6f, 0xb4, 0xe3, 0x5b, 0x9e, 0x6e, 0x66, 0x16, 0xd0, 0x91, 0x4f, 0x97, - 0x4d, 0x99, 0x68, 0xc7, 0xb7, 0x03, 0x12, 0x0e, 0xe7, 0xbf, 0x13, 0x9b, 0xd0, 0x03, 0x7f, 0x5e, - 0xa3, 0x74, 0x78, 0x63, 0x4a, 0xcd, 0x77, 0xfc, 0xab, 0x8d, 0xca, 0xc6, 0x74, 0xdf, 0xe9, 0x47, - 0xe9, 0xd4, 0xac, 0xb4, 0x4d, 0x85, 0x7c, 0xe8, 0xd5, 0x9f, 0x76, 0x76, 0xfa, 0xd2, 0x09, 0xb2, + 0x4d, 0x99, 0x68, 0xc7, 0x87, 0x01, 0x09, 0x87, 0xf3, 0xdf, 0x89, 0x4d, 0xe8, 0x81, 0x3f, 0xaf, + 0x51, 0x3a, 0xbc, 0x31, 0xa5, 0xe6, 0xdb, 0xfe, 0xd5, 0x46, 0x65, 0x63, 0xba, 0xef, 0xf4, 0xa3, + 0x74, 0x6a, 0x56, 0xda, 0xa6, 0x42, 0xbe, 0xe3, 0xd5, 0x9f, 0x76, 0x76, 0xfa, 0xd2, 0x09, 0xb2, 0xea, 0x04, 0x79, 0xef, 0x04, 0x79, 0xee, 0xc5, 0x60, 0xd5, 0x8b, 0xc1, 0x6b, 0x2f, 0x06, 0x77, 0x93, 0xcc, 0x60, 0xde, 0x24, 0x51, 0x6a, 0xcb, 0x18, 0x64, 0x26, 0x97, 0xed, 0x53, 0xfc, 0x39, 0xe2, 0x72, 0x3d, 0x23, 0xb6, 0xb5, 0x86, 0x64, 0xd7, 0x8f, 0x76, 0xf2, 0x11, 0x00, 0x00, 0xff, - 0xff, 0x88, 0x67, 0x5c, 0xf6, 0x62, 0x01, 0x00, 0x00, + 0xff, 0x52, 0x25, 0x23, 0xa3, 0x62, 0x01, 0x00, 0x00, } func (m *ValidatorPayoutHistory) Marshal() (dAtA []byte, err error) { @@ -335,7 +335,7 @@ func (m *ValidatorPayoutHistory) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.EpochNumber |= int32(b&0x7F) << shift + m.EpochNumber |= int64(b&0x7F) << shift if b < 0x80 { break } diff --git a/x/chainlet/genesis.go b/x/chainlet/genesis.go index 9cd638be..a05204b6 100644 --- a/x/chainlet/genesis.go +++ b/x/chainlet/genesis.go @@ -9,16 +9,45 @@ import ( // InitGenesis initializes the module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k *keeper.Keeper, genState types.GenesisState) { - k.InitializeChainletCount(ctx) - // this line is used by starport scaffolding # genesis/module/init - + // Set params k.SetParams(ctx, genState.Params) + + // Set the port ID for the chainlet module + // In IBC v10, port binding is handled automatically when the module is registered in the router + k.SetPort(ctx, genState.PortId) + + // Set chainlet count + k.SetChainletCount(ctx, genState.ChainletCount) + + // Import chainlet stacks first (chainlets depend on stacks) + for _, stack := range genState.ChainletStacks { + if err := k.ImportChainletStack(ctx, stack); err != nil { + panic(err) + } + } + + // Import chainlets + for _, chainlet := range genState.Chainlets { + if err := k.ImportChainlet(ctx, chainlet); err != nil { + panic(err) + } + } + + // this line is used by starport scaffolding # genesis/module/init } // ExportGenesis returns the module's exported genesis func ExportGenesis(ctx sdk.Context, k *keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() genesis.Params = k.GetParams(ctx) + genesis.PortId = k.GetPort(ctx) + genesis.ChainletCount = k.GetChainletCount(ctx) + + // Export all chainlets + genesis.Chainlets = k.ExportChainlets(ctx) + + // Export all chainlet stacks + genesis.ChainletStacks = k.ExportChainletStacks(ctx) // this line is used by starport scaffolding # genesis/module/export diff --git a/x/chainlet/genesis_test.go b/x/chainlet/genesis_test.go index d10883ea..d5a586b5 100644 --- a/x/chainlet/genesis_test.go +++ b/x/chainlet/genesis_test.go @@ -15,6 +15,7 @@ import ( func TestGenesis(t *testing.T) { genesisState := types.GenesisState{ Params: types.DefaultParams(), + PortId: types.DefaultGenesis().PortId, // this line is used by starport scaffolding # genesis/test/state } @@ -27,5 +28,6 @@ func TestGenesis(t *testing.T) { nullify.Fill(got) require.Equal(t, genesisState.Params, got.Params) + require.Equal(t, genesisState.PortId, got.PortId) // this line is used by starport scaffolding # genesis/test/assert } diff --git a/x/chainlet/keeper/ccv_test.go b/x/chainlet/keeper/ccv_test.go index b6866ea7..416d2657 100644 --- a/x/chainlet/keeper/ccv_test.go +++ b/x/chainlet/keeper/ccv_test.go @@ -27,6 +27,10 @@ func (s *TestSuite) TestConsumerVSC() { BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil). AnyTimes() + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() // Set up expectations in order (only one round) gomock.InOrder( diff --git a/x/chainlet/keeper/chainlet.go b/x/chainlet/keeper/chainlet.go index 0f37c9cb..08dac714 100644 --- a/x/chainlet/keeper/chainlet.go +++ b/x/chainlet/keeper/chainlet.go @@ -18,7 +18,7 @@ func (k *Keeper) Chainlet(ctx sdk.Context, chainId string) (chainlet types.Chain store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletKey) if !store.Has(byteKey) { - err = fmt.Errorf("key %s not found", chainId) + err = fmt.Errorf("chainlet '%s' not found", chainId) return } @@ -89,7 +89,7 @@ func (k *Keeper) UpgradeChainletStackVersion(ctx sdk.Context, chainId, stackVers return cosmossdkerrors.Wrapf(types.ErrInvalidChainletStack, "cannot upgrade to stack %s version %s: %s", chainlet.ChainletStackName, stackVersion, err) } if !avail { - return cosmossdkerrors.Wrapf(types.ErrInvalidChainletStack, "stack %s version %s not available", chainlet.ChainletStackName, chainlet.ChainletStackVersion) + return cosmossdkerrors.Wrapf(types.ErrInvalidChainletStack, "stack %s version %s not available", chainlet.ChainletStackName, stackVersion) } chainlet.ChainletStackVersion = stackVersion @@ -236,6 +236,11 @@ func (k Keeper) incrementChainletCount(ctx sdk.Context) { func (k *Keeper) AutoUpgradeChainlets(ctx sdk.Context) error { iter := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletKey).Iterator(nil, nil) + defer func() { + if err := iter.Close(); err != nil { + ctx.Logger().Error(fmt.Sprintf("failed to close iterator: %v", err)) + } + }() for ; iter.Valid(); iter.Next() { var chainlet types.Chainlet k.cdc.MustUnmarshal(iter.Value(), &chainlet) @@ -247,31 +252,23 @@ func (k *Keeper) AutoUpgradeChainlets(ctx sdk.Context) error { latestVersion, err := k.LatestVersion(ctx, chainlet.ChainletStackName, chainlet.ChainletStackVersion) if err != nil { - iter.Close() return err } if latestVersion == chainlet.ChainletStackVersion { - iter.Close() - return nil + ctx.Logger().Debug(fmt.Sprintf("chainlet %s: %s is at its latest available version\n", chainlet.ChainId, chainlet.ChainletStackVersion)) + continue } available, err := k.chainletStackVersionAvailable(ctx, chainlet.ChainletStackName, latestVersion) if err != nil || !available { - iter.Close() //TODO change to panic in the future, should never happen if the loaded versions are consistent with the state return fmt.Errorf("chainlet stack %s has unavailable version %s loaded", chainlet.ChainletStackName, latestVersion) } - if chainlet.ChainletStackVersion == latestVersion { - ctx.Logger().Debug(fmt.Sprintf("chainlet %s: %s is at its latest available version\n", chainlet.ChainId, chainlet.ChainletStackVersion)) - continue - } - ctx.Logger().Info(fmt.Sprintf("upgrading chainlet %s: %s to %s\n", chainlet.ChainId, chainlet.ChainletStackVersion, latestVersion)) chainlet.ChainletStackVersion = latestVersion - defer k.setChainletInfo(ctx, &chainlet) + k.setChainletInfo(ctx, &chainlet) } - iter.Close() return nil } diff --git a/x/chainlet/keeper/chainlet_stack.go b/x/chainlet/keeper/chainlet_stack.go index ba2ee473..4647c043 100644 --- a/x/chainlet/keeper/chainlet_stack.go +++ b/x/chainlet/keeper/chainlet_stack.go @@ -25,21 +25,38 @@ func (k *Keeper) NewChainletStack(ctx sdk.Context, cs types.ChainletStack) error return fmt.Errorf("cannot add chainlet stack %v as it already exists", cs.DisplayName) } + // Validate versions first (before any cache updates) for _, version := range cs.Versions { if !versions.Check(version.Version) { return fmt.Errorf("version string '%s' invalid", version.Version) } + } + + // Write to KV FIRST (before updating caches) + value := k.cdc.MustMarshal(&cs) + store.Set(byteKey, value) + + // Update caches AFTER successful KV write + // Track only newly added versions for rollback on error + addedVersions := make([]string, 0, len(cs.Versions)) + for _, version := range cs.Versions { if version.Enabled { - err := k.AddVersion(ctx, cs.DisplayName, version) - if err != nil { + // Check if version already exists in cache before adding + versionExisted := k.VersionExistsInCache(ctx, cs.DisplayName, version.Version) + if err := k.AddVersion(ctx, cs.DisplayName, version); err != nil { + // Rollback: remove only versions that were newly added to cache + for _, addedVersion := range addedVersions { + _ = k.RemoveVersion(ctx, cs.DisplayName, addedVersion) + } return err } + // Only track versions that were actually newly added + if !versionExisted { + addedVersions = append(addedVersions, version.Version) + } } } - value := k.cdc.MustMarshal(&cs) - store.Set(byteKey, value) - return nil } @@ -58,15 +75,20 @@ func (k *Keeper) AddChainletStackVersion(ctx sdk.Context, stackName string, vers return fmt.Errorf("cannot update chainlet stack %s: %w", stackName, err) } - // Upsert the version + // Upsert the version in KV FIRST (before updating caches) stack.Versions = append(stack.Versions, version) updatedValue := k.cdc.MustMarshal(&stack) store.Set([]byte(stackName), updatedValue) - // Store in the version tree for automatic updates + // Update caches AFTER successful KV write if version.Enabled { - err = k.AddVersion(ctx, stack.DisplayName, version) - if err != nil { + // Check if version already exists in cache before adding + versionExisted := k.VersionExistsInCache(ctx, stack.DisplayName, version.Version) + if err = k.AddVersion(ctx, stack.DisplayName, version); err != nil { + // Rollback: remove the version only if it was newly added to cache + if !versionExisted { + _ = k.RemoveVersion(ctx, stack.DisplayName, version.Version) + } return err } } diff --git a/x/chainlet/keeper/genesis.go b/x/chainlet/keeper/genesis.go new file mode 100644 index 00000000..3daf96c7 --- /dev/null +++ b/x/chainlet/keeper/genesis.go @@ -0,0 +1,65 @@ +package keeper + +import ( + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sagaxyz/ssc/x/chainlet/types" +) + +// ExportChainlets exports all chainlets from the store +func (k *Keeper) ExportChainlets(ctx sdk.Context) []types.Chainlet { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletKey) + iterator := store.Iterator(nil, nil) + defer iterator.Close() + + var chainlets []types.Chainlet + for ; iterator.Valid(); iterator.Next() { + var chainlet types.Chainlet + k.cdc.MustUnmarshal(iterator.Value(), &chainlet) + chainlets = append(chainlets, chainlet) + } + return chainlets +} + +// ExportChainletStacks exports all chainlet stacks from the store +func (k *Keeper) ExportChainletStacks(ctx sdk.Context) []types.ChainletStack { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletStackKey) + iterator := store.Iterator(nil, nil) + defer iterator.Close() + + var stacks []types.ChainletStack + for ; iterator.Valid(); iterator.Next() { + var stack types.ChainletStack + k.cdc.MustUnmarshal(iterator.Value(), &stack) + stacks = append(stacks, stack) + } + return stacks +} + +// ImportChainlet imports a single chainlet into the store (without validation, for genesis import) +func (k *Keeper) ImportChainlet(ctx sdk.Context, chainlet types.Chainlet) error { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletKey) + key := []byte(chainlet.ChainId) + value := k.cdc.MustMarshal(&chainlet) + store.Set(key, value) + return nil +} + +// ImportChainletStack imports a single chainlet stack into the store (without validation, for genesis import) +func (k *Keeper) ImportChainletStack(ctx sdk.Context, stack types.ChainletStack) error { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletStackKey) + key := []byte(stack.DisplayName) + value := k.cdc.MustMarshal(&stack) + store.Set(key, value) + + // Also add enabled versions to the version tree + for _, version := range stack.Versions { + if version.Enabled { + if err := k.AddVersion(ctx, stack.DisplayName, version); err != nil { + return err + } + } + } + return nil +} diff --git a/x/chainlet/keeper/ibc.go b/x/chainlet/keeper/ibc.go index 4e01cf0e..24efe130 100644 --- a/x/chainlet/keeper/ibc.go +++ b/x/chainlet/keeper/ibc.go @@ -28,7 +28,7 @@ func (k Keeper) TransmitCreateUpgradePacket( return 0, errorsmod.Wrapf(sdkerrors.ErrJSONMarshal, "cannot marshal the packet: %s", err) } - return k.ibcKeeperFn().ChannelKeeper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, packetBytes) + return k.channelKeeper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, packetBytes) } // OnAcknowledgementCreateUpgradePacket responds to the success or failure of a packet @@ -44,7 +44,7 @@ func (k Keeper) OnAcknowledgementCreateUpgradePacket(ctx sdk.Context, packet cha return nil } - // Verify channel maches chain ID + // Verify channel matches chain ID clientID, consumerRegistered := k.providerKeeper.GetConsumerClientId(ctx, chainlet.ConsumerId) if !consumerRegistered { return errors.New("consumer not registered yet") @@ -55,7 +55,7 @@ func (k Keeper) OnAcknowledgementCreateUpgradePacket(ctx sdk.Context, packet cha } // Cancel if the upgrade plan matches the current upgrade - planName, err := upgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + planName, err := UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) if err != nil { return err } @@ -102,7 +102,7 @@ func (k Keeper) OnTimeoutCreateUpgradePacket(ctx sdk.Context, packet channeltype } // Cancel if the upgrade plan matches the current upgrade - planName, err := upgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + planName, err := UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) if err != nil { return err } @@ -127,7 +127,7 @@ func (k Keeper) TransmitCancelUpgradePacket( return 0, errorsmod.Wrapf(sdkerrors.ErrJSONMarshal, "cannot marshal the packet: %s", err) } - return k.ibcKeeperFn().ChannelKeeper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, packetBytes) + return k.channelKeeper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, packetBytes) } // OnAcknowledgementCancelUpgradePacket responds to the success or failure of a packet @@ -166,7 +166,7 @@ func (k Keeper) OnAcknowledgementCancelUpgradePacket(ctx sdk.Context, packet cha } // Cancel if the upgrade plan matches the current upgrade - planName, err := upgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + planName, err := UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) if err != nil { return err } @@ -211,7 +211,8 @@ func (k Keeper) OnRecvConfirmUpgradePacket(ctx sdk.Context, packet channeltypes. err = errors.New("consumer not registered yet") return } - err = k.verifyChannel(ctx, clientID, packet.SourceChannel) + // Use DestinationChannel when receiving packets (provider is destination) + err = k.verifyChannel(ctx, clientID, packet.DestinationChannel) if err != nil { return } diff --git a/x/chainlet/keeper/ibc_test.go b/x/chainlet/keeper/ibc_test.go new file mode 100644 index 00000000..03c4709f --- /dev/null +++ b/x/chainlet/keeper/ibc_test.go @@ -0,0 +1,667 @@ +package keeper_test + +import ( + "errors" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" + "github.com/golang/mock/gomock" + chainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + sdkchainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + + "github.com/sagaxyz/ssc/x/chainlet/keeper" + "github.com/sagaxyz/ssc/x/chainlet/types" +) + +func (s *TestSuite) ibcSetup(chainID, consumerID, channelID string) { + // Calls we don't care about in these tests + s.escrowKeeper.EXPECT(). + NewChainletAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + s.billingKeeper.EXPECT(). + BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)). + AnyTimes() + + // Create stacks + ver := "1.2.3" + _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( + creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, true, + )) + s.Require().NoError(err) + _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( + creator.String(), "test", "test/test:2.0.0", "2.0.0", "xyz", true, + )) + s.Require().NoError(err) + + // Setup mocks with the correct chain ID and consumer ID + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: consumerID, + }, nil) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq(consumerID), gomock.Any()). + AnyTimes() + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()). + AnyTimes() + s.providerKeeper.EXPECT(). + GetConsumerIdToChannelId(gomock.Any(), gomock.Eq(consumerID)). + Return(channelID, true). + AnyTimes() + s.providerKeeper.EXPECT(). + SendVSCPacketsToChain(gomock.Any(), gomock.Eq(consumerID), gomock.Eq(channelID)). + AnyTimes() + s.providerKeeper.EXPECT(). + GetConsumerPhase(gomock.Any(), gomock.Eq(consumerID)). + Return(ccvprovidertypes.CONSUMER_PHASE_LAUNCHED). + AnyTimes() + + // Launch a chainlet + _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( + creator.String(), + []string{creator.String()}, + "test", + ver, + "test_chainlet", + chainID, + "asaga", + types.ChainletParams{}, + nil, false, "", + )) + s.Require().NoError(err) + s.chainletKeeper.InitConsumers(s.ctx) +} +func (s *TestSuite) breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID string) { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return(clientID, true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + s.clientKeeper.EXPECT(). + GetClientLatestHeight(gomock.Any(), gomock.Eq(clientID)). + Return(ibcclienttypes.Height{}), + s.channelKeeper.EXPECT(). + SendPacket( + gomock.Any(), + gomock.Eq(sdkchainlettypes.PortID), + gomock.Eq(channelID), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + Return(uint64(1337), nil), + ) + _, err := s.msgServer.UpgradeChainlet(s.ctx, &types.MsgUpgradeChainlet{ + Creator: creator.String(), + ChainId: chainID, + StackVersion: "2.0.0", + HeightDelta: 100, + ChannelId: channelID, + }) + s.Require().NoError(err) + + // Check if upgrade is correctly set in the chainlet + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) +} + +func (s *TestSuite) packetVerificationMocks(consumerID, consumerClientID, clientID, connectionID, channelID string) { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return(consumerClientID, true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + ) +} + +func (s *TestSuite) TestCreateUpgradePacket() { + tests := []struct { + name string + fn func(chainID, consumerID, clientID, connectionID, channelID string) + }{ + { + name: "success ack", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + packet := channeltypes.Packet{} + data := chainlettypes.CreateUpgradePacketData{ + ChainId: chainID, + Name: "xxx", + Height: 123, + Info: "xyz", + } + + // Success ack without an upgrade in progress + packetAck := chainlettypes.CreateUpgradePacketAck{} + packetAckBytes, err := types.ModuleCdc.MarshalJSON(&packetAck) + s.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) + err = s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Error ack without an upgrade in progress + ack = channeltypes.NewErrorAcknowledgement(errors.New("error")) + err = s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Check chainlet is unaffected + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Nil(chainlet.Upgrade) + s.Require().Equal("1.2.3", chainlet.ChainletStackVersion) + + // Upgrade + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Check if upgrade is correctly set/unset in the chainlet + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + + // Success ack + packetAck = chainlettypes.CreateUpgradePacketAck{} + packetAckBytes, err = types.ModuleCdc.MarshalJSON(&packetAck) + s.Require().NoError(err) + ack = channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) + err = s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + }, + }, { + name: "valid error ack", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + // Error ack + gomock.InOrder( + // Verification for the source of the packet + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return(clientID, true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + ) + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CreateUpgradePacketData{ + ChainId: chainID, + Name: planName, + Height: 123, + Info: "xyz", + } + ack := channeltypes.NewErrorAcknowledgement(errors.New("error")) + err = s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Upgrade removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Nil(chainlet.Upgrade) + }, + }, { + name: "valid timeout", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CreateUpgradePacketData{ + ChainId: chainID, + Name: planName, + Height: 123, + Info: "xyz", + } + + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + err = s.chainletKeeper.OnTimeoutCreateUpgradePacket(s.ctx, packet, data) + s.Require().NoError(err) + + // Upgrade removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Nil(chainlet.Upgrade) + }, + }, { + name: "error ack or timeout for different upgrade plan", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CreateUpgradePacketData{ + ChainId: chainID, + Name: "xxx", // incorrect + Height: 123, + Info: "xyz", + } + + // Error ack + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + ack := channeltypes.NewErrorAcknowledgement(errors.New("error")) + err := s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Upgrade NOT removed + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + + // Timeout + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + err = s.chainletKeeper.OnTimeoutCreateUpgradePacket(s.ctx, packet, data) + s.Require().NoError(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, { + name: "error ack or timeout from incorrect client ID", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + packet := channeltypes.Packet{ + SourceChannel: "channel-42", + } + data := chainlettypes.CreateUpgradePacketData{ + ChainId: chainID, + Name: planName, + Height: 123, + Info: "xyz", + } + + // Error ack + s.packetVerificationMocks(consumerID, clientID, "bad-client", "bad-connection", "channel-42") + ack := channeltypes.NewErrorAcknowledgement(errors.New("error")) + err = s.chainletKeeper.OnAcknowledgementCreateUpgradePacket(s.ctx, packet, data, ack) + s.Require().Error(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + + // Timeout + s.packetVerificationMocks(consumerID, clientID, "bad-client", "bad-connection", "channel-42") + err = s.chainletKeeper.OnTimeoutCreateUpgradePacket(s.ctx, packet, data) + s.Require().Error(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, + } + for i, tt := range tests { + s.Run(tt.name, func() { + s.SetupTest() + + chainID := fmt.Sprintf("chain_%d-1", i+1) + consumerID := fmt.Sprintf("%d", i) + clientID := fmt.Sprintf("client-%d", i) + connectionID := fmt.Sprintf("connection-%d", i) + channelID := fmt.Sprintf("channel-%d", i) + + s.ibcSetup(chainID, consumerID, channelID) + + tt.fn(chainID, consumerID, clientID, connectionID, channelID) + }) + } +} + +func (s *TestSuite) TestCancelUpgradePacket() { + tests := []struct { + name string + fn func(chainID, consumerID, clientID, connectionID, channelID string) + }{ + { + name: "success ack", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CancelUpgradePacketData{ + ChainId: chainID, + Plan: planName, + } + + // Success ack + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + packetAck := chainlettypes.CancelUpgradePacketAck{} + packetAckBytes, err := types.ModuleCdc.MarshalJSON(&packetAck) + s.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) + err = s.chainletKeeper.OnAcknowledgementCancelUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Check chainlet upgrade is removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Nil(chainlet.Upgrade) + }, + }, { + name: "valid error ack", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + // Error ack + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CancelUpgradePacketData{ + ChainId: chainID, + Plan: planName, + } + ack := channeltypes.NewErrorAcknowledgement(errors.New("error")) + err = s.chainletKeeper.OnAcknowledgementCancelUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, { + name: "valid timeout", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CancelUpgradePacketData{ + ChainId: chainID, + Plan: planName, + } + + err = s.chainletKeeper.OnTimeoutCancelUpgradePacket(s.ctx, packet, data) + s.Require().NoError(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, { + name: "ack for different upgrade plan", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + packet := channeltypes.Packet{ + SourceChannel: channelID, + } + data := chainlettypes.CancelUpgradePacketData{ + ChainId: chainID, + Plan: "xxx", // incorrect + } + + // Success ack + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + packetAck := chainlettypes.CancelUpgradePacketAck{} + packetAckBytes, err := types.ModuleCdc.MarshalJSON(&packetAck) + s.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) + err = s.chainletKeeper.OnAcknowledgementCancelUpgradePacket(s.ctx, packet, data, ack) + s.Require().NoError(err) + + // Upgrade NOT removed + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, { + name: "ack from incorrect client ID", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + // Upgrade it + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + // Get correct upgrade plan name + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + planName, err := keeper.UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + s.Require().NoError(err) + + packet := channeltypes.Packet{ + SourceChannel: "channel-42", + } + data := chainlettypes.CancelUpgradePacketData{ + ChainId: chainID, + Plan: planName, + } + + // Success ack + s.packetVerificationMocks(consumerID, clientID, "bad-client", "bad-connection", "channel-42") + packetAck := chainlettypes.CancelUpgradePacketAck{} + packetAckBytes, err := types.ModuleCdc.MarshalJSON(&packetAck) + s.Require().NoError(err) + ack := channeltypes.NewResultAcknowledgement(sdk.MustSortJSON(packetAckBytes)) + err = s.chainletKeeper.OnAcknowledgementCancelUpgradePacket(s.ctx, packet, data, ack) + s.Require().Error(err) + + // Upgrade NOT removed + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, + } + for i, tt := range tests { + s.Run(tt.name, func() { + s.SetupTest() + + chainID := fmt.Sprintf("chain_%d-1", i+1) + consumerID := fmt.Sprintf("%d", i) + clientID := fmt.Sprintf("client-%d", i) + connectionID := fmt.Sprintf("connection-%d", i) + channelID := fmt.Sprintf("channel-%d", i) + + s.ibcSetup(chainID, consumerID, channelID) + + tt.fn(chainID, consumerID, clientID, connectionID, channelID) + }) + } +} + +func (s *TestSuite) TestConfirmUpgradePacket() { + tests := []struct { + name string + fn func(chainID, consumerID, clientID, connectionID, channelID string) + }{ + { + name: "ok", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + packet := channeltypes.Packet{ + DestinationChannel: channelID, + } + data := chainlettypes.ConfirmUpgradePacketData{ + ChainId: chainID, + Height: 123, + Plan: "xyz", + } + + // Upgrade confirmation + s.packetVerificationMocks(consumerID, clientID, clientID, connectionID, channelID) + _, err := s.chainletKeeper.OnRecvConfirmUpgradePacket(s.ctx, packet, data) + s.Require().NoError(err) + + // Check if the upgrade is finished + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Nil(chainlet.Upgrade) // not upgrading anymore + s.Require().Equal("2.0.0", chainlet.ChainletStackVersion) // new version set + }, + }, { + name: "incorrect client ID", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + packet := channeltypes.Packet{ + DestinationChannel: "channel-42", + } + data := chainlettypes.ConfirmUpgradePacketData{ + ChainId: chainID, + Height: 123, + Plan: "xyz", + } + + // Upgrade confirmation + s.packetVerificationMocks(consumerID, clientID, "bad-client", "bad-connection", "channel-42") + _, err := s.chainletKeeper.OnRecvConfirmUpgradePacket(s.ctx, packet, data) + s.Require().Error(err) + + // Check if the upgrade is not removed + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, { + name: "incorrect chain ID", + fn: func(chainID, consumerID, clientID, connectionID, channelID string) { + s.breakingUpgrade(chainID, consumerID, clientID, connectionID, channelID) + + packet := channeltypes.Packet{ + DestinationChannel: channelID, + } + data := chainlettypes.ConfirmUpgradePacketData{ + ChainId: "abcd", // incorrect + Height: 123, + Plan: "xyz", + } + _, err := s.chainletKeeper.OnRecvConfirmUpgradePacket(s.ctx, packet, data) + s.Require().Error(err) + + // Check if the upgrade is not removed + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + s.Require().NotNil(chainlet.Upgrade) + }, + }, + } + for i, tt := range tests { + s.Run(tt.name, func() { + s.SetupTest() + + chainID := fmt.Sprintf("chain_%d-1", i+1) + consumerID := fmt.Sprintf("%d", i) + clientID := fmt.Sprintf("client-%d", i) + connectionID := fmt.Sprintf("connection-%d", i) + channelID := fmt.Sprintf("channel-%d", i) + + s.ibcSetup(chainID, consumerID, channelID) + + tt.fn(chainID, consumerID, clientID, connectionID, channelID) + }) + } +} diff --git a/x/chainlet/keeper/keeper.go b/x/chainlet/keeper/keeper.go index 4b41d6ed..5be45ca1 100644 --- a/x/chainlet/keeper/keeper.go +++ b/x/chainlet/keeper/keeper.go @@ -8,7 +8,6 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - ibckeeper "github.com/cosmos/ibc-go/v10/modules/core/keeper" "github.com/sagaxyz/ssc/x/chainlet/types" "github.com/sagaxyz/ssc/x/chainlet/types/versions" @@ -28,8 +27,6 @@ type Keeper struct { escrowKeeper types.EscrowKeeper aclKeeper types.AclKeeper - ibcKeeperFn func() *ibckeeper.Keeper - stackVersions map[string]*versions.Versions // display name => version tree stackVersionParams map[string]map[string]types.ChainletStackParams } @@ -39,7 +36,6 @@ func NewKeeper( storeKey storetypes.StoreKey, ps paramtypes.Subspace, providerMsgServer types.ProviderMsgServer, - ibcKeeperFn func() *ibckeeper.Keeper, stakingKeeper types.StakingKeeper, clientKeeper types.ClientKeeper, channelKeeper types.ChannelKeeper, @@ -59,7 +55,6 @@ func NewKeeper( storeKey: storeKey, paramstore: ps, providerMsgServer: providerMsgServer, - ibcKeeperFn: ibcKeeperFn, stakingKeeper: stakingKeeper, clientKeeper: clientKeeper, channelKeeper: channelKeeper, @@ -78,3 +73,19 @@ func (k *Keeper) Logger(ctx sdk.Context) log.Logger { func (k *Keeper) StackVersions(stackName string) *versions.Versions { return k.stackVersions[stackName] } + +// GetPort returns the portID for the IBC app module. Used in ExportGenesis +func (k *Keeper) GetPort(ctx sdk.Context) string { + store := ctx.KVStore(k.storeKey) + portBytes := store.Get(types.PortKey) + if portBytes == nil { + return "" + } + return string(portBytes) +} + +// SetPort sets the portID for the IBC app module. Used in InitGenesis +func (k *Keeper) SetPort(ctx sdk.Context, portID string) { + store := ctx.KVStore(k.storeKey) + store.Set(types.PortKey, []byte(portID)) +} diff --git a/x/chainlet/keeper/keeper_test.go b/x/chainlet/keeper/keeper_test.go index 77c4275c..a941ab73 100644 --- a/x/chainlet/keeper/keeper_test.go +++ b/x/chainlet/keeper/keeper_test.go @@ -30,8 +30,11 @@ var ( addrs = []sdk.AccAddress{ sdk.AccAddress("test1"), sdk.AccAddress("test2"), + sdk.AccAddress("test3"), } - creator = addrs[0] + creator = addrs[0] + maintainer = addrs[1] + admin = addrs[2] ) type TestSuite struct { @@ -83,11 +86,6 @@ func (s *TestSuite) SetupTest() { s.escrowKeeper = chainlettestutil.NewMockEscrowKeeper(ctrl) s.providerMsgServer = chainlettestutil.NewMockProviderMsgServer(ctrl) - s.aclKeeper.EXPECT(). - IsAdmin(gomock.Any(), gomock.Any()). - Return(true). - AnyTimes() - // Set up Staking keeper expectations for GetAllValidators since it's used in msg_server_launch_chainlet.go s.stakingKeeper.EXPECT(). GetAllValidators(gomock.Any()). @@ -107,7 +105,6 @@ func (s *TestSuite) SetupTest() { s.chainletKeeper = keeper.NewKeeper( encCfg.Codec, key, sub, s.providerMsgServer, - nil, s.stakingKeeper, s.clientKeeper, s.channelKeeper, diff --git a/x/chainlet/keeper/msg_server_cancel_upgrade.go b/x/chainlet/keeper/msg_server_cancel_upgrade.go index b6c01a54..30f6cbd9 100644 --- a/x/chainlet/keeper/msg_server_cancel_upgrade.go +++ b/x/chainlet/keeper/msg_server_cancel_upgrade.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + cosmossdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" "golang.org/x/exp/slices" @@ -33,11 +34,15 @@ func (k msgServer) CancelChainletUpgrade(goCtx context.Context, msg *types.MsgCa return nil, fmt.Errorf("not supported for chainlet %s (not a consumer)", chainlet.ChainId) } + // Check if upgrade is in progress before attempting to cancel + if chainlet.Upgrade == nil { + return nil, cosmossdkerrors.Wrapf(types.ErrNoUpgradeInProgress, "chainlet %s has no upgrade in progress", msg.ChainId) + } + err = k.sendCancelUpgradePlan(ctx, &chainlet, msg.ChannelId) if err != nil { return nil, fmt.Errorf("error sending cancel upgrade: %s", err) } - return &types.MsgCancelChainletUpgradeResponse{ - }, nil + return &types.MsgCancelChainletUpgradeResponse{}, nil } diff --git a/x/chainlet/keeper/msg_server_disable_stack_version_test.go b/x/chainlet/keeper/msg_server_disable_stack_version_test.go index cff1767d..5e4b3580 100644 --- a/x/chainlet/keeper/msg_server_disable_stack_version_test.go +++ b/x/chainlet/keeper/msg_server_disable_stack_version_test.go @@ -3,7 +3,9 @@ package keeper_test import ( "fmt" + ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" "github.com/golang/mock/gomock" + "github.com/sagaxyz/ssc/x/chainlet/types" ) @@ -18,17 +20,33 @@ func (s *TestSuite) TestDisabledVersionsLaunch() { BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil). AnyTimes() + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: "0", + }, nil) + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq("0"), gomock.Any()) + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()) // Create a stack ver := "1.2.3" _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( - creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, false, + creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, true, )) s.Require().NoError(err) // Launch a chainlet _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( - creator.String(), nil, "test", ver, "test_chainlet", "test_12345-1", "asaga", types.ChainletParams{}, nil, false, "", + creator.String(), []string{creator.String()}, "test", ver, "test_chainlet", "test_12345-1", "asaga", types.ChainletParams{}, nil, false, "", )) s.Require().NoError(err) @@ -38,7 +56,7 @@ func (s *TestSuite) TestDisabledVersionsLaunch() { // Try and fail to launch another chainlet _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( - creator.String(), nil, "test", ver, "test_chainlet", "test_12346-1", "asaga", types.ChainletParams{}, nil, false, "", + creator.String(), []string{creator.String()}, "test", ver, "test_chainlet", "test_12346-1", "asaga", types.ChainletParams{}, nil, false, "", )) s.Require().Error(err) } @@ -49,7 +67,7 @@ func (s *TestSuite) TestDisabledVersionsUpgrade() { // Create a stack ver := "1.2.3" _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( - creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, false, + creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, true, )) s.Require().NoError(err) @@ -60,15 +78,32 @@ func (s *TestSuite) TestDisabledVersionsUpgrade() { s.billingKeeper.EXPECT(). BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: "0", + }, nil) + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq("0"), gomock.Any()) + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()) + _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( - creator.String(), nil, "test", ver, "test_chainlet", "test_12345-1", "asaga", types.ChainletParams{}, nil, false, "", + creator.String(), []string{creator.String()}, "test", ver, "test_chainlet", "test_12345-1", "asaga", types.ChainletParams{}, nil, false, "", )) s.Require().NoError(err) // Create a newer but disabled stack version ver2 := "1.2.4" _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( - creator.String(), "test", "test/test:"+ver2, ver2, "abcd"+ver2, false, + creator.String(), "test", "test/test:"+ver2, ver2, "abcd"+ver2, true, )) s.Require().NoError(err) _, err = s.msgServer.DisableChainletStackVersion(s.ctx, types.NewMsgDisableChainletStackVersion(creator.String(), "test", ver2)) @@ -120,12 +155,12 @@ func (s *TestSuite) TestDisabledVersionAutoUpgrade() { for j, ver := range tt.addedVersions { if j == 0 { _, err = s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( - creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, false, + creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, true, )) s.Require().NoError(err) } else { _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( - creator.String(), "test", "test/test:"+ver, ver, "abcd"+ver, false, + creator.String(), "test", "test/test:"+ver, ver, "abcd"+ver, true, )) s.Require().NoError(err) } @@ -137,11 +172,30 @@ func (s *TestSuite) TestDisabledVersionAutoUpgrade() { s.billingKeeper.EXPECT(). BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) - chainId := "test_12345-42" + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: "0", + }, nil) + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq("0"), gomock.Any()) + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()) + + chainId := "test_12345-1" _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( - creator.String(), nil, "test", tt.current, "test_chainlet", chainId, "asaga", types.ChainletParams{}, nil, false, "", + creator.String(), []string{creator.String()}, "test", tt.current, "test_chainlet", chainId, "asaga", types.ChainletParams{}, nil, false, "", )) s.Require().NoError(err) + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainId) + s.Require().NoError(err) // Disable specified stack versions for _, ver := range tt.disabledVersions { @@ -157,7 +211,7 @@ func (s *TestSuite) TestDisabledVersionAutoUpgrade() { // Check it with a chainlet auto-upgrade err = s.chainletKeeper.AutoUpgradeChainlets(s.ctx) s.Require().NoError(err) - chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainId) + chainlet, err = s.chainletKeeper.Chainlet(s.ctx, chainId) s.Require().NoError(err) s.Require().Equal(tt.expectedLatest, chainlet.ChainletStackVersion) }) diff --git a/x/chainlet/keeper/msg_server_launch_chainlet.go b/x/chainlet/keeper/msg_server_launch_chainlet.go index 8220105d..0575cc8f 100644 --- a/x/chainlet/keeper/msg_server_launch_chainlet.go +++ b/x/chainlet/keeper/msg_server_launch_chainlet.go @@ -22,9 +22,12 @@ func (k msgServer) LaunchChainlet(goCtx context.Context, msg *types.MsgLaunchCha admin := k.aclKeeper.IsAdmin(ctx, msg.GetSigners()[0]) if !admin { ok, err := types.ValidateNonAdminChainId(msg.ChainId) - if !ok { + if err != nil { return &types.MsgLaunchChainletResponse{}, err } + if !ok { + return &types.MsgLaunchChainletResponse{}, types.ErrInvalidChainId + } } // get total number of chainlets @@ -100,68 +103,53 @@ func (k msgServer) LaunchChainlet(goCtx context.Context, msg *types.MsgLaunchCha } chainlet.Tags = msg.Tags - - err = k.NewChainlet(ctx, chainlet) - if err != nil { - return &types.MsgLaunchChainletResponse{}, err - } - - return &types.MsgLaunchChainletResponse{}, ctx.EventManager().EmitTypedEvent(&types.EventLaunchChainlet{ - ChainName: chainlet.ChainletName, - Launcher: chainlet.Launcher, - ChainId: chainlet.ChainId, - Stack: chainlet.ChainletStackName, - StackVersion: chainlet.ChainletStackVersion, - }) - } - - if len(stack.Fees) == 0 { - return &types.MsgLaunchChainletResponse{}, cosmossdkerrors.Wrapf(types.ErrBillingFailure, "chainlet stack '%s' has no fees configured", stack.DisplayName) - } - - billed := false - for _, feeOption := range stack.Fees { - - // logic to launch non-service chainlets - epochfee, err := sdk.ParseCoinNormalized(feeOption.EpochFee) - if err != nil { - return &types.MsgLaunchChainletResponse{}, types.ErrInvalidCoin - } - setupfee, err := sdk.ParseCoinNormalized(feeOption.SetupFee) - if err != nil { - return &types.MsgLaunchChainletResponse{}, types.ErrInvalidCoin - } - owner, err := sdk.AccAddressFromBech32(msg.Creator) - if err != nil { - return &types.MsgLaunchChainletResponse{}, err - } - - multiplier, ok := math.NewIntFromString(k.GetParams(ctx).NEpochDeposit) - if !ok { - return &types.MsgLaunchChainletResponse{}, fmt.Errorf("bad multiplier") - } - - deposit := sdk.Coin{ - Amount: epochfee.Amount.Mul(multiplier), - Denom: epochfee.Denom, - } - deposit.Add(setupfee) - err = k.escrowKeeper.NewChainletAccount(ctx, owner, msg.ChainId, deposit) - if err != nil { - return &types.MsgLaunchChainletResponse{}, err + } else { + if len(stack.Fees) == 0 { + return &types.MsgLaunchChainletResponse{}, cosmossdkerrors.Wrapf(types.ErrBillingFailure, "chainlet stack '%s' has no fees configured", stack.DisplayName) + } + + billed := false + for _, feeOption := range stack.Fees { + // logic to launch non-service chainlets + epochfee, err := sdk.ParseCoinNormalized(feeOption.EpochFee) + if err != nil { + return &types.MsgLaunchChainletResponse{}, types.ErrInvalidCoin + } + setupfee, err := sdk.ParseCoinNormalized(feeOption.SetupFee) + if err != nil { + return &types.MsgLaunchChainletResponse{}, types.ErrInvalidCoin + } + owner, err := sdk.AccAddressFromBech32(msg.Creator) + if err != nil { + return &types.MsgLaunchChainletResponse{}, err + } + + multiplier, ok := math.NewIntFromString(k.GetParams(ctx).NEpochDeposit) + if !ok { + return &types.MsgLaunchChainletResponse{}, fmt.Errorf("bad multiplier") + } + + deposit := sdk.Coin{ + Amount: epochfee.Amount.Mul(multiplier), + Denom: epochfee.Denom, + } + deposit = deposit.Add(setupfee) + err = k.escrowKeeper.NewChainletAccount(ctx, owner, msg.ChainId, deposit) + if err != nil { + return &types.MsgLaunchChainletResponse{}, err + } + + // Bill for the chainlet just after it is launched + totalFee := epochfee.Add(setupfee) + err = k.billingKeeper.BillAccount(ctx, totalFee, chainlet, "launching chainlet") + if err == nil { + billed = true + break + } + } + if !billed { + return &types.MsgLaunchChainletResponse{}, cosmossdkerrors.Wrapf(types.ErrBillingFailure, "failed to bill new account %s", err.Error()) } - - // Bill for the chainlet just after it is launched - totalFee := epochfee.Add(setupfee) - err = k.billingKeeper.BillAccount(ctx, totalFee, chainlet, "launching chainlet") - if err == nil { - billed = true - break - } - } - - if !billed { - return &types.MsgLaunchChainletResponse{}, cosmossdkerrors.Wrapf(types.ErrBillingFailure, "failed to bill new account %s", err.Error()) } // Add as a CCV consumer if enabled @@ -180,11 +168,11 @@ func (k msgServer) LaunchChainlet(goCtx context.Context, msg *types.MsgLaunchCha } return &types.MsgLaunchChainletResponse{}, ctx.EventManager().EmitTypedEvent(&types.EventLaunchChainlet{ - ChainName: msg.ChainletName, - Launcher: msg.Creator, - ChainId: msg.ChainId, - Stack: msg.ChainletStackName, - StackVersion: msg.ChainletStackVersion, + ChainName: chainlet.ChainletName, + Launcher: chainlet.Launcher, + ChainId: chainlet.ChainId, + Stack: chainlet.ChainletStackName, + StackVersion: chainlet.ChainletStackVersion, }) } diff --git a/x/chainlet/keeper/msg_server_test.go b/x/chainlet/keeper/msg_server_test.go index 17482690..a46552b9 100644 --- a/x/chainlet/keeper/msg_server_test.go +++ b/x/chainlet/keeper/msg_server_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" keepertest "github.com/sagaxyz/ssc/testutil/keeper" "github.com/sagaxyz/ssc/x/chainlet/keeper" @@ -15,3 +17,85 @@ func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { k, ctx := keepertest.ChainletKeeper(t) return keeper.NewMsgServerImpl(k), ctx } + +// TestLaunchChainlet_CustomLauncherEvent verifies that when a custom launcher is set, +// the event emission correctly uses the custom launcher instead of the message creator. +func (s *TestSuite) TestLaunchChainlet_CustomLauncherEvent() { + // Setup: Create a chainlet stack with fees + stackName := "test-stack-custom" + stackVersion := "1.0.0" + + _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( + creator.String(), stackName, "test description", "test/test:"+stackVersion, stackVersion, "abcd"+stackVersion, fees, false, + )) + s.Require().NoError(err) + + // Setup: Configure mocks for billing and escrow + customLauncher := sdk.AccAddress("custom_launcher").String() + chainID := "test_12345-1" // Valid chain ID format: lowercase_letters_numbers-numbers + + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(true). + AnyTimes() + + s.billingKeeper.EXPECT(). + BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + + s.escrowKeeper.EXPECT(). + NewChainletAccount(gomock.Any(), gomock.Any(), gomock.Eq(chainID), gomock.Any()). + Return(nil). + AnyTimes() + + // Clear any existing events + s.ctx = s.ctx.WithEventManager(sdk.NewEventManager()) + + // Test: Launch chainlet with custom launcher + msg := types.NewMsgLaunchChainlet( + creator.String(), + []string{creator.String()}, + stackName, + stackVersion, + "test-chainlet", + chainID, + "utsaga", + types.ChainletParams{}, + nil, + false, + customLauncher, // Custom launcher + ) + + _, err = s.msgServer.LaunchChainlet(s.ctx, msg) + s.Require().NoError(err) + + // Verify: Check that the event was emitted with the custom launcher + events := s.ctx.EventManager().Events() + var foundLauncherEvent bool + for _, event := range events { + if event.Type == "ssc.chainlet.EventLaunchChainlet" { + // Find the launcher attribute + for _, attr := range event.Attributes { + if string(attr.Key) == "launcher" { + launcherValue := string(attr.Value) + // Remove JSON quotes if present + if len(launcherValue) > 0 && launcherValue[0] == '"' && launcherValue[len(launcherValue)-1] == '"' { + launcherValue = launcherValue[1 : len(launcherValue)-1] + } + s.Require().Equal(customLauncher, launcherValue, + "Event should contain custom launcher (%s), not message creator (%s). Got: %s", customLauncher, creator.String(), launcherValue) + foundLauncherEvent = true + break + } + } + } + } + s.Require().True(foundLauncherEvent, "LaunchChainlet event should be emitted with launcher attribute") + + // Verify: Also check the chainlet was created with correct launcher + chainlet, err := s.chainletKeeper.GetChainletInfo(s.ctx, chainID) + s.Require().NoError(err) + s.Require().Equal(customLauncher, chainlet.Launcher, + "Chainlet should be stored with custom launcher (%s), got %s", customLauncher, chainlet.Launcher) +} diff --git a/x/chainlet/keeper/msg_server_upgrade_chainlet.go b/x/chainlet/keeper/msg_server_upgrade_chainlet.go index dffb0de2..335e79b7 100644 --- a/x/chainlet/keeper/msg_server_upgrade_chainlet.go +++ b/x/chainlet/keeper/msg_server_upgrade_chainlet.go @@ -2,6 +2,7 @@ package keeper import ( "context" + "errors" "fmt" "time" @@ -25,19 +26,34 @@ func (k msgServer) UpgradeChainlet(goCtx context.Context, msg *types.MsgUpgradeC return &types.MsgUpgradeChainletResponse{}, err } - if !slices.Contains(ogChainlet.Maintainers, msg.Creator) { - return nil, fmt.Errorf("address %s is not a chainlet maintainer", msg.Creator) + creator, err := sdk.AccAddressFromBech32(msg.Creator) + if err != nil { + return &types.MsgUpgradeChainletResponse{}, err + } + + isAdmin := k.aclKeeper.IsAdmin(ctx, creator) + isMaintainer := slices.Contains(ogChainlet.Maintainers, msg.Creator) + canUpgrade := isMaintainer || (!ogChainlet.IsCCVConsumer && isAdmin) // Non-CCV chainlets have to be manually upgraded by Saga + if !canUpgrade { + if ogChainlet.IsCCVConsumer { + return nil, fmt.Errorf("address %s is not a chainlet maintainer", msg.Creator) + } + return nil, fmt.Errorf("address %s is not allowed to upgrade this chainlet (must be maintainer or admin)", msg.Creator) } - majorUpgrade, err := versions.CheckUpgrade(ogChainlet.ChainletStackVersion, msg.StackVersion) + + newStack, err := k.getChainletStackVersion(ctx, ogChainlet.ChainletStackName, msg.StackVersion) if err != nil { return nil, err } - if majorUpgrade { - currentStack, err := k.getChainletStackVersion(ctx, ogChainlet.ChainletStackName, ogChainlet.ChainletStackVersion) - if err != nil { - return nil, err - } - if currentStack.CcvConsumer { + breakingUpgrade, err := versions.CheckUpgrade(ogChainlet.ChainletStackVersion, msg.StackVersion) + if err != nil { + return nil, err + } + if breakingUpgrade { + if ogChainlet.IsCCVConsumer { + if !newStack.CcvConsumer { + return &types.MsgUpgradeChainletResponse{}, errors.New("CCV cannot be disabled") + } p := k.GetParams(ctx) upgradeDelta := p.UpgradeMinimumHeightDelta + msg.HeightDelta height, err := k.sendUpgradePlan(ctx, &ogChainlet, msg.StackVersion, upgradeDelta, msg.ChannelId) @@ -49,11 +65,12 @@ func (k msgServer) UpgradeChainlet(goCtx context.Context, msg *types.MsgUpgradeC Height: height, }, nil } else { - // Add as a consumer if upgrade enables CCV - newStack, err := k.getChainletStackVersion(ctx, ogChainlet.ChainletStackName, msg.StackVersion) - if err != nil { - return nil, err + //NOTE: Non-CCV chainlets have to be manually upgraded by Saga + if !isAdmin { + return nil, fmt.Errorf("address %s is not allowed to upgrade this chainlet", msg.Creator) } + + // Add as a consumer if upgrade enables CCV if newStack.CcvConsumer { p := k.GetParams(ctx) @@ -69,11 +86,15 @@ func (k msgServer) UpgradeChainlet(goCtx context.Context, msg *types.MsgUpgradeC } } } + } else { + if newStack.CcvConsumer != ogChainlet.IsCCVConsumer { + return &types.MsgUpgradeChainletResponse{}, errors.New("changing CCV requires a breaking upgrade") + } } err = k.UpgradeChainletStackVersion(ctx, msg.ChainId, msg.StackVersion) if err != nil { - return nil, fmt.Errorf("error while updating chainlet: %s", err) + return &types.MsgUpgradeChainletResponse{}, fmt.Errorf("error while updating chainlet: %s", err) } return &types.MsgUpgradeChainletResponse{}, ctx.EventManager().EmitTypedEvent(&types.EventUpdateChainlet{ diff --git a/x/chainlet/keeper/msg_server_upgrade_chainlet_test.go b/x/chainlet/keeper/msg_server_upgrade_chainlet_test.go new file mode 100644 index 00000000..16c45496 --- /dev/null +++ b/x/chainlet/keeper/msg_server_upgrade_chainlet_test.go @@ -0,0 +1,239 @@ +package keeper_test + +import ( + "fmt" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + ibcclienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" + "github.com/golang/mock/gomock" + sdkchainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + + "github.com/sagaxyz/ssc/x/chainlet/types" +) + +func (s *TestSuite) TestUpgradeChainlet() { + var ( + clientID = "client-123" + channelID = "channel-42" + connectionID = "connection-0" + consumerID = "0" + ) + + testCases := []struct { + name string + fromVersion string + fromCCV bool + toVersion string + toCCV bool + sender sdk.AccAddress + mocks func(s *TestSuite) + expErr string + }{ + { + "ok - legacy-to-legacy", + "1.2.3", false, + "2.0.0", false, + admin, + nil, + "", + }, + { + "ok - legacy-to-CCV", + "1.2.3", false, + "2.0.0", true, + admin, + func(s *TestSuite) { + // Added to consumers during upgrade + gomock.InOrder( + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: consumerID, + }, nil), + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)), + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq(consumerID), gomock.Any()), + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()), + ) + }, + "", + }, + { + "ok - CCV-to-CCV", + "1.2.3", true, + "2.0.0", true, + maintainer, + func(s *TestSuite) { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return(clientID, true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + s.clientKeeper.EXPECT(). + GetClientLatestHeight(gomock.Any(), gomock.Eq(clientID)). + Return(ibcclienttypes.Height{}), + s.channelKeeper.EXPECT(). + SendPacket( + gomock.Any(), + gomock.Eq(sdkchainlettypes.PortID), + gomock.Eq(channelID), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + Return(uint64(1337), nil), + ) + }, + "", + }, + { + "fail - CCV-to-legacy", + "1.2.3", true, + "2.0.0", false, + maintainer, + nil, + "cannot be disabled", + }, + { + "fail - skip upgrade", + "1.2.3", false, + "3.0.0", false, + admin, + nil, + "increments of", + }, + { + "fail - legacy upgrade as maintainer", + "1.2.3", false, + "2.0.0", false, + maintainer, + nil, + "not allowed", + }, + { + "fail - legacy upgrade as maintainer", + "1.2.3", false, + "2.0.0", true, + maintainer, + nil, + "not allowed", + }, + { + "fail - not maintainer", + "1.2.3", true, + "2.0.0", true, + creator, + nil, + "not a chainlet maintainer", + }, + { + "fail - enable CCV with non-breaking upgrade", + "1.0.2", false, + "1.1.0", true, + admin, + nil, + "requires a breaking upgrade", + }, + { + "fail - disable CCV with non-breaking upgrade", + "1.0.2", true, + "1.1.0", false, + maintainer, + nil, + "requires a breaking upgrade", + }, + } + for i, tc := range testCases { + s.Run(fmt.Sprintf("%d: %s", i, tc.name), func() { + s.SetupTest() + + // Mocks we do not care about + s.escrowKeeper.EXPECT(). + NewChainletAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + s.billingKeeper.EXPECT(). + BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + + // Mock IsAdmin so our admin address is the (only) admin + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Eq(tc.sender)). + Return(tc.sender.String() == admin.String()). + AnyTimes() + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Eq(creator)). + Return(false). + AnyTimes() + + // Create stack versions + _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( + creator.String(), "test", "test", "test/test:"+tc.fromVersion, tc.fromVersion, "abcd"+tc.fromVersion, fees, tc.fromCCV, + )) + s.Require().NoError(err) + _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( + creator.String(), "test", "test/test:"+tc.toVersion, tc.toVersion, "abcd"+tc.toVersion, tc.toCCV, + )) + s.Require().NoError(err) + + // Launch a chainlet + if tc.fromCCV { + // Implies adding to consumers when launching + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: consumerID, + }, nil) + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq(consumerID), gomock.Any()) + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()) + } + chainID := fmt.Sprintf("test_%d-1", i+1) + _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( + creator.String(), []string{maintainer.String()}, "test", tc.fromVersion, "test_chainlet", chainID, "asaga", types.ChainletParams{}, nil, false, "", + )) + s.Require().NoError(err) + + // Upgrade the chainlet + if tc.mocks != nil { + tc.mocks(s) + } + _, err = s.msgServer.UpgradeChainlet(s.ctx, types.NewMsgUpgradeChainlet( + tc.sender.String(), chainID, tc.toVersion, 0, channelID, nil, + )) + if tc.expErr == "" { + s.Require().NoError(err) + } else { + s.Require().Error(err) + if !strings.Contains(err.Error(), tc.expErr) { + s.Require().Fail(fmt.Sprintf("err '%s' does not contain '%s'", err.Error(), tc.expErr)) + } + } + }) + } +} diff --git a/x/chainlet/keeper/port_test.go b/x/chainlet/keeper/port_test.go new file mode 100644 index 00000000..32f421c0 --- /dev/null +++ b/x/chainlet/keeper/port_test.go @@ -0,0 +1,68 @@ +package keeper_test + +import ( + "testing" + + chainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + keepertest "github.com/sagaxyz/ssc/testutil/keeper" + + "github.com/stretchr/testify/require" +) + +func TestKeeper_SetPort(t *testing.T) { + k, ctx := keepertest.ChainletKeeper(t) + + // Test setting a port + portID := chainlettypes.PortID + k.SetPort(ctx, portID) + + // Verify the port was set + retrievedPort := k.GetPort(ctx) + require.Equal(t, portID, retrievedPort) +} + +func TestKeeper_GetPort(t *testing.T) { + k, ctx := keepertest.ChainletKeeper(t) + + // Test getting port when not set (should return empty string) + port := k.GetPort(ctx) + require.Empty(t, port) + + // Set a port + portID := chainlettypes.PortID + k.SetPort(ctx, portID) + + // Verify we can retrieve it + retrievedPort := k.GetPort(ctx) + require.Equal(t, portID, retrievedPort) +} + +func TestKeeper_PortPersistence(t *testing.T) { + k, ctx := keepertest.ChainletKeeper(t) + + // Set a port + portID := chainlettypes.PortID + k.SetPort(ctx, portID) + + // Create a new context (simulating a new block) + ctx2 := ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // Verify port persists across contexts + retrievedPort := k.GetPort(ctx2) + require.Equal(t, portID, retrievedPort) +} + +func TestKeeper_PortUpdate(t *testing.T) { + k, ctx := keepertest.ChainletKeeper(t) + + // Set initial port + initialPort := "chainlet" + k.SetPort(ctx, initialPort) + require.Equal(t, initialPort, k.GetPort(ctx)) + + // Update to a different port + newPort := chainlettypes.PortID + k.SetPort(ctx, newPort) + require.Equal(t, newPort, k.GetPort(ctx)) +} + diff --git a/x/chainlet/keeper/upgrade.go b/x/chainlet/keeper/upgrade.go index 281387d3..e685afb0 100644 --- a/x/chainlet/keeper/upgrade.go +++ b/x/chainlet/keeper/upgrade.go @@ -8,44 +8,13 @@ import ( "cosmossdk.io/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" - ccvtypes "github.com/cosmos/interchain-security/v7/x/ccv/types" sdkchainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" "github.com/sagaxyz/ssc/x/chainlet/types" "github.com/sagaxyz/ssc/x/chainlet/types/versions" ) -//nolint:unused -func (k *Keeper) getConsumerConnectionIDs(ctx sdk.Context, consumerID string) (controllerConnectionID, hostConnectionID string, err error) { - // Get controller/local connection ID - ccvChannelID, found := k.providerKeeper.GetConsumerIdToChannelId(ctx, consumerID) - if !found { - err = fmt.Errorf("channel ID for consumer ID %s not found", consumerID) - return - } - ccvChannel, found := k.channelKeeper.GetChannel(ctx, ccvtypes.ProviderPortID, ccvChannelID) - if !found { - err = fmt.Errorf("channel %s for consumer %s not found", ccvChannelID, consumerID) - return - } - if len(ccvChannel.ConnectionHops) == 0 { - err = fmt.Errorf("no connections for channel %s", ccvChannelID) - return - } - controllerConnectionID = ccvChannel.ConnectionHops[0] - - // Get host/counterparty connection ID - connection, found := k.connectionKeeper.GetConnection(ctx, controllerConnectionID) - if !found { - err = fmt.Errorf("connection %s for consumer ID %s not found", controllerConnectionID, consumerID) - return - } - hostConnectionID = connection.Counterparty.ConnectionId - return -} - -//nolint:unused -func upgradePlanName(from, to string) (plan string, err error) { +func UpgradePlanName(from, to string) (plan string, err error) { major, minor, _, _, err := versions.Parse(from) if err != nil { return @@ -97,12 +66,13 @@ func (k Keeper) sendUpgradePlan(ctx sdk.Context, chainlet *types.Chainlet, newVe return } - clientRevisionHeight := k.clientKeeper.GetClientLatestHeight(ctx, clientID).GetRevisionHeight() - clientRevisionNumber := k.clientKeeper.GetClientLatestHeight(ctx, clientID).GetRevisionNumber() + lh := k.clientKeeper.GetClientLatestHeight(ctx, clientID) + clientRevisionHeight := lh.GetRevisionHeight() + clientRevisionNumber := lh.GetRevisionNumber() // Create the IBC packet upgradeHeight := clientRevisionHeight + heightDelta - planName, err := upgradePlanName(chainlet.ChainletStackVersion, newVersion) + planName, err := UpgradePlanName(chainlet.ChainletStackVersion, newVersion) if err != nil { return } @@ -189,11 +159,8 @@ func (k Keeper) sendCancelUpgradePlan(ctx sdk.Context, chainlet *types.Chainlet, return } - clientRevisionHeight := k.clientKeeper.GetClientLatestHeight(ctx, clientID).GetRevisionHeight() - clientRevisionNumber := k.clientKeeper.GetClientLatestHeight(ctx, clientID).GetRevisionNumber() - // Create the IBC packet - planName, err := upgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) + planName, err := UpgradePlanName(chainlet.ChainletStackVersion, chainlet.Upgrade.Version) if err != nil { return } @@ -218,10 +185,11 @@ func (k Keeper) sendCancelUpgradePlan(ctx sdk.Context, chainlet *types.Chainlet, timeoutTimestamp = uint64(un) } var timeoutHeight clienttypes.Height + lh := k.clientKeeper.GetClientLatestHeight(ctx, clientID) if p.UpgradeTimeoutHeight > 0 { timeoutHeight = clienttypes.Height{ - RevisionNumber: clientRevisionNumber, - RevisionHeight: clientRevisionHeight + p.UpgradeTimeoutHeight, + RevisionNumber: lh.GetRevisionNumber(), + RevisionHeight: lh.GetRevisionHeight() + p.UpgradeTimeoutHeight, } } @@ -234,7 +202,6 @@ func (k Keeper) sendCancelUpgradePlan(ctx sdk.Context, chainlet *types.Chainlet, return } -//nolint:unused func (k *Keeper) setUpgrading(ctx sdk.Context, chainlet *types.Chainlet, version string, height uint64) error { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainletKey) @@ -264,7 +231,6 @@ func (k *Keeper) setUpgrading(ctx sdk.Context, chainlet *types.Chainlet, version return nil } -//nolint:unused func (k *Keeper) finishUpgrading(ctx sdk.Context, chainlet *types.Chainlet) error { if chainlet.Upgrade == nil { return fmt.Errorf("chainlet %s is not being upgraded", chainlet.ChainId) @@ -279,7 +245,6 @@ func (k *Keeper) finishUpgrading(ctx sdk.Context, chainlet *types.Chainlet) erro return nil } -//nolint:unused func (k *Keeper) cancelUpgrading(ctx sdk.Context, chainlet *types.Chainlet) { chainlet.Upgrade = nil diff --git a/x/chainlet/keeper/upgrade_test.go b/x/chainlet/keeper/upgrade_test.go new file mode 100644 index 00000000..19c063ed --- /dev/null +++ b/x/chainlet/keeper/upgrade_test.go @@ -0,0 +1,196 @@ +package keeper_test + +import ( + "fmt" + + ibcclienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + ibcconnectiontypes "github.com/cosmos/ibc-go/v10/modules/core/03-connection/types" + ibcchanneltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ccvprovidertypes "github.com/cosmos/interchain-security/v7/x/ccv/provider/types" + "github.com/golang/mock/gomock" + sdkchainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + + "github.com/sagaxyz/ssc/x/chainlet/types" +) + +func (s *TestSuite) TestUpgrade() { + tests := []struct { + name string + expErr bool + fn func(chainID, consumerID, clientID, connectionID, channelID string) error + }{ + { + name: "ok", + expErr: false, + fn: func(chainID, consumerID, clientID, connectionID, channelID string) error { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return(clientID, true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + s.clientKeeper.EXPECT(). + GetClientLatestHeight(gomock.Any(), gomock.Eq(clientID)). + Return(ibcclienttypes.Height{}), + s.channelKeeper.EXPECT(). + SendPacket( + gomock.Any(), + gomock.Eq(sdkchainlettypes.PortID), + gomock.Eq(channelID), + gomock.Any(), // timeout height + gomock.Any(), // timeout timestamp + gomock.Any(), // data + //TODO check any values + ). + Return(uint64(1337), nil), //TODO + ) + return nil + }, + }, { + name: "consumer not registered yet", + expErr: true, + fn: func(chainID, consumerID, clientID, connectionID, channelID string) error { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return("", false), + ) + return nil + }, + }, { + name: "incorrect client id for the provided channel", + expErr: true, + fn: func(chainID, consumerID, clientID, connectionID, channelID string) error { + gomock.InOrder( + s.providerKeeper.EXPECT(). + GetConsumerClientId(gomock.Any(), gomock.Eq(consumerID)). + Return("client-123", true), + s.channelKeeper.EXPECT(). + GetChannel(gomock.Any(), sdkchainlettypes.PortID, gomock.Eq(channelID)). + Return(ibcchanneltypes.Channel{ + ConnectionHops: []string{connectionID}, + }, true), + s.connectionKeeper.EXPECT(). + GetConnection(gomock.Any(), gomock.Eq(connectionID)). + Return(ibcconnectiontypes.ConnectionEnd{ + ClientId: clientID, + Versions: []*ibcconnectiontypes.Version{}, + State: 0, + Counterparty: ibcconnectiontypes.Counterparty{}, + DelayPeriod: 0, + }, true), + ) + return nil + }, + }, + } + for i, tt := range tests { + s.Run(tt.name, func() { + s.SetupTest() + + // Calls we don't care about in this test + s.escrowKeeper.EXPECT(). + NewChainletAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + s.billingKeeper.EXPECT(). + BillAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + s.providerKeeper.EXPECT(). + GetValidatorSetUpdateId(gomock.Any()). + Return(uint64(1)). + AnyTimes() + s.aclKeeper.EXPECT(). + IsAdmin(gomock.Any(), gomock.Any()). + Return(false). + AnyTimes() + + // Create stacks + ver := "1.2.3" + _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( + creator.String(), "test", "test", "test/test:"+ver, ver, "abcd"+ver, fees, true, + )) + s.Require().NoError(err) + _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( + creator.String(), "test", "test/test:2.0.0", "2.0.0", "xyz", true, + )) + s.Require().NoError(err) + chainID := fmt.Sprintf("chain_%d-1", i+1) + consumerID := fmt.Sprintf("%d", i) + clientID := fmt.Sprintf("client-%d", i) + connectionID := fmt.Sprintf("connection-%d", i) + channelID := fmt.Sprintf("channel-%d", i) + + // Setup mocks with the correct chain ID and consumer ID + s.providerMsgServer.EXPECT(). + CreateConsumer(gomock.Any(), gomock.Any()). + Return(&ccvprovidertypes.MsgCreateConsumerResponse{ + ConsumerId: consumerID, + }, nil) + s.providerKeeper.EXPECT(). + AppendPendingVSCPackets(gomock.Any(), gomock.Eq(consumerID), gomock.Any()). + AnyTimes() + s.providerKeeper.EXPECT(). + IncrementValidatorSetUpdateId(gomock.Any()). + AnyTimes() + s.providerKeeper.EXPECT(). + GetConsumerIdToChannelId(gomock.Any(), gomock.Eq(consumerID)). + Return(channelID, true). + AnyTimes() + s.providerKeeper.EXPECT(). + SendVSCPacketsToChain(gomock.Any(), gomock.Eq(consumerID), gomock.Eq(channelID)). + AnyTimes() + s.providerKeeper.EXPECT(). + GetConsumerPhase(gomock.Any(), gomock.Eq(consumerID)). + Return(ccvprovidertypes.CONSUMER_PHASE_LAUNCHED). + AnyTimes() + + _ = tt.fn(chainID, consumerID, clientID, connectionID, channelID) //TODO remove return value + + // Launch a chainlet + _, err = s.msgServer.LaunchChainlet(s.ctx, types.NewMsgLaunchChainlet( + creator.String(), []string{creator.String()}, "test", ver, "test_chainlet", chainID, "asaga", types.ChainletParams{}, nil, false, "", + )) + s.Require().NoError(err) + s.chainletKeeper.InitConsumers(s.ctx) + + // Breaking upgrade + resp, err := s.msgServer.UpgradeChainlet(s.ctx, &types.MsgUpgradeChainlet{ + Creator: creator.String(), + ChainId: chainID, + StackVersion: "2.0.0", + HeightDelta: 100, + ChannelId: channelID, + }) + if tt.expErr { + s.Require().Error(err) + } else { + s.Require().NoError(err) + s.Require().Equal(uint64(0xc8), resp.Height) //TODO calculate correct value + } + + // Check if upgrade is correctly set/unset in the chainlet + chainlet, err := s.chainletKeeper.Chainlet(s.ctx, chainID) + s.Require().NoError(err) + if tt.expErr { + s.Require().Nil(chainlet.Upgrade) + } else { + s.Require().NotNil(chainlet.Upgrade) + s.Require().Equal("2.0.0", chainlet.Upgrade.Version) + } + }) + } +} diff --git a/x/chainlet/keeper/versions.go b/x/chainlet/keeper/versions.go index 9539a7b5..dec8d59c 100644 --- a/x/chainlet/keeper/versions.go +++ b/x/chainlet/keeper/versions.go @@ -52,6 +52,27 @@ func normalizeVer(v string) string { return v } +// VersionExistsInCache checks if a version already exists in the cache. +// Loads the cache if it's not initialized to ensure accurate results. +// Note: This function may be called independently (not just before AddVersion), +// so it must load the cache if needed for correctness. +func (k *Keeper) VersionExistsInCache(ctx sdk.Context, stackName, version string) bool { + // Ensure caches are loaded for accurate results + if k.stackVersionParams == nil || k.stackVersions == nil { + if err := k.loadVersions(ctx); err != nil { + // If loading fails, assume version doesn't exist (safe default) + return false + } + } + verKey := normalizeVer(version) + pmap := k.stackVersionParams[stackName] + if pmap == nil { + return false + } + _, exists := pmap[verKey] + return exists +} + func (k *Keeper) AddVersion(ctx sdk.Context, stackName string, params types.ChainletStackParams) error { version := params.Version if k.stackVersions == nil || k.stackVersionParams == nil { diff --git a/x/chainlet/keeper/versions_test.go b/x/chainlet/keeper/versions_test.go index e8510dfe..e04048b9 100644 --- a/x/chainlet/keeper/versions_test.go +++ b/x/chainlet/keeper/versions_test.go @@ -89,3 +89,69 @@ func (s *TestSuite) TestVersionsLoading() { s.Require().Equal(tt.expectedState, versions) } } + +func (s *TestSuite) TestVersionExistsInCache() { + s.SetupTest() + + stackName := "test" + version1 := "1.2.3" + version2 := "2.0.0" + nonExistentVersion := "3.0.0" + + // Create a chainlet stack with version 1.2.3 + _, err := s.msgServer.CreateChainletStack(s.ctx, types.NewMsgCreateChainletStack( + creator.String(), stackName, "test", "test/test:"+version1, version1, "abcd"+version1, fees, true, + )) + s.Require().NoError(err) + + // Add version 2.0.0 + _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( + creator.String(), stackName, "test/test:"+version2, version2, "abcd"+version2, true, + )) + s.Require().NoError(err) + + // Test 1: Version exists in cache (1.2.3) + exists := s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, version1) + s.Require().True(exists, "version 1.2.3 should exist in cache") + + // Test 2: Version exists in cache (2.0.0) + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, version2) + s.Require().True(exists, "version 2.0.0 should exist in cache") + + // Test 3: Version doesn't exist + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, nonExistentVersion) + s.Require().False(exists, "version 3.0.0 should not exist in cache") + + // Test 4: Stack doesn't exist + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, "nonexistent", version1) + s.Require().False(exists, "version should not exist for nonexistent stack") + + // Test 5: Version with 'v' prefix normalization + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, "v"+version1) + s.Require().True(exists, "version with 'v' prefix should be normalized and found") + + // Test 6: Version with 'V' prefix normalization + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, "V"+version2) + s.Require().True(exists, "version with 'V' prefix should be normalized and found") + + // Test 7: Cache is nil - should load and check + s.chainletKeeper.DeleteVersions() + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, version1) + s.Require().True(exists, "should load cache and find version after DeleteVersions") + + // Test 8: Disabled version should not exist in cache + // First add an enabled version, then disable it + enabledVersion := "4.0.0" + _, err = s.msgServer.UpdateChainletStack(s.ctx, types.NewMsgUpdateChainletStack( + creator.String(), stackName, "test/test:"+enabledVersion, enabledVersion, "abcd"+enabledVersion, true, // enabled + )) + s.Require().NoError(err) + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, enabledVersion) + s.Require().True(exists, "enabled version should exist in cache") + + // Now disable it - this removes it from cache + _, err = s.msgServer.DisableChainletStackVersion(s.ctx, types.NewMsgDisableChainletStackVersion(creator.String(), stackName, enabledVersion)) + s.Require().NoError(err) + exists = s.chainletKeeper.VersionExistsInCache(s.ctx, stackName, enabledVersion) + s.Require().False(exists, "disabled version should not exist in cache after disabling") +} diff --git a/x/chainlet/module.go b/x/chainlet/module.go index 74d2f72f..8b347128 100644 --- a/x/chainlet/module.go +++ b/x/chainlet/module.go @@ -73,7 +73,8 @@ func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncod // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck + //nolint:errcheck // gRPC gateway registration errors are non-critical at startup + types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) } // GetTxCmd returns the root Tx command for the module. The subcommands of this root command are used by end-users to generate new transactions containing messages defined in the module diff --git a/x/chainlet/testutil/expected_keepers_mocks.go b/x/chainlet/testutil/expected_keepers_mocks.go index 7548ff98..a6440f68 100644 --- a/x/chainlet/testutil/expected_keepers_mocks.go +++ b/x/chainlet/testutil/expected_keepers_mocks.go @@ -368,6 +368,21 @@ func (mr *MockChannelKeeperMockRecorder) GetChannel(arg0, arg1, arg2 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannel", reflect.TypeOf((*MockChannelKeeper)(nil).GetChannel), arg0, arg1, arg2) } +// SendPacket mocks base method. +func (m *MockChannelKeeper) SendPacket(ctx types.Context, sourcePort, sourceChannel string, timeoutHeight types1.Height, timeoutTimestamp uint64, data []byte) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendPacket", ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendPacket indicates an expected call of SendPacket. +func (mr *MockChannelKeeperMockRecorder) SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendPacket", reflect.TypeOf((*MockChannelKeeper)(nil).SendPacket), ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + // MockConnectionKeeper is a mock of ConnectionKeeper interface. type MockConnectionKeeper struct { ctrl *gomock.Controller @@ -480,20 +495,6 @@ func (m *MockEscrowKeeper) EXPECT() *MockEscrowKeeperMockRecorder { return m.recorder } -// NewChainletAccount mocks base method. -func (m *MockEscrowKeeper) NewChainletAccount(ctx types.Context, address types.AccAddress, chainId string, depositAmount types.Coin) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewChainletAccount", ctx, address, chainId, depositAmount) - ret0, _ := ret[0].(error) - return ret0 -} - -// NewChainletAccount indicates an expected call of NewChainletAccount. -func (mr *MockEscrowKeeperMockRecorder) NewChainletAccount(ctx, address, chainId, depositAmount interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewChainletAccount", reflect.TypeOf((*MockEscrowKeeper)(nil).NewChainletAccount), ctx, address, chainId, depositAmount) -} - // GetSupportedDenoms mocks base method. func (m *MockEscrowKeeper) GetSupportedDenoms(ctx types.Context) []string { m.ctrl.T.Helper() @@ -508,6 +509,20 @@ func (mr *MockEscrowKeeperMockRecorder) GetSupportedDenoms(ctx interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupportedDenoms", reflect.TypeOf((*MockEscrowKeeper)(nil).GetSupportedDenoms), ctx) } +// NewChainletAccount mocks base method. +func (m *MockEscrowKeeper) NewChainletAccount(ctx types.Context, address types.AccAddress, chainId string, depositAmount types.Coin) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewChainletAccount", ctx, address, chainId, depositAmount) + ret0, _ := ret[0].(error) + return ret0 +} + +// NewChainletAccount indicates an expected call of NewChainletAccount. +func (mr *MockEscrowKeeperMockRecorder) NewChainletAccount(ctx, address, chainId, depositAmount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewChainletAccount", reflect.TypeOf((*MockEscrowKeeper)(nil).NewChainletAccount), ctx, address, chainId, depositAmount) +} + // MockAclKeeper is a mock of AclKeeper interface. type MockAclKeeper struct { ctrl *gomock.Controller diff --git a/x/chainlet/types/errors.go b/x/chainlet/types/errors.go index 6d78b1c2..20303cdf 100644 --- a/x/chainlet/types/errors.go +++ b/x/chainlet/types/errors.go @@ -23,4 +23,5 @@ var ( ErrInvalidVersion = sdkerrors.Register(ModuleName, 6912, "invalid version") ErrInvalidFees = sdkerrors.Register(ModuleName, 6913, "invalid fees") ErrDuplicateDenom = sdkerrors.Register(ModuleName, 6914, "duplicate denom in fees") + ErrNoUpgradeInProgress = sdkerrors.Register(ModuleName, 6915, "no upgrade in progress") ) diff --git a/x/chainlet/types/expected_keepers.go b/x/chainlet/types/expected_keepers.go index 6a77624b..827eb4b5 100644 --- a/x/chainlet/types/expected_keepers.go +++ b/x/chainlet/types/expected_keepers.go @@ -45,6 +45,14 @@ type ClientKeeper interface { } type ChannelKeeper interface { GetChannel(sdk.Context, string, string) (ibcchanneltypes.Channel, bool) + SendPacket( + ctx sdk.Context, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, + ) (uint64, error) } type ConnectionKeeper interface { GetConnection(sdk.Context, string) (ibcconnectiontypes.ConnectionEnd, bool) diff --git a/x/chainlet/types/genesis.go b/x/chainlet/types/genesis.go index da0d7899..2121d99d 100644 --- a/x/chainlet/types/genesis.go +++ b/x/chainlet/types/genesis.go @@ -1,5 +1,10 @@ package types +import ( + chainlettypes "github.com/sagaxyz/saga-sdk/x/chainlet/types" + host "github.com/cosmos/ibc-go/v10/modules/core/24-host" +) + // this line is used by starport scaffolding # genesis/types/import // DefaultIndex is the default global index @@ -9,7 +14,11 @@ const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { df := DefaultParams() return &GenesisState{ - Params: df, + Params: df, + PortId: chainlettypes.PortID, + Chainlets: []Chainlet{}, + ChainletStacks: []ChainletStack{}, + ChainletCount: 0, } } @@ -18,5 +27,27 @@ func DefaultGenesis() *GenesisState { func (gs GenesisState) Validate() error { // this line is used by starport scaffolding # genesis/types/validate + if err := host.PortIdentifierValidator(gs.PortId); err != nil { + return err + } + + // Validate chainlets have unique chain IDs + chainletIDs := make(map[string]bool) + for _, chainlet := range gs.Chainlets { + if chainletIDs[chainlet.ChainId] { + return ErrChainletExists + } + chainletIDs[chainlet.ChainId] = true + } + + // Validate chainlet stacks have unique display names + stackNames := make(map[string]bool) + for _, stack := range gs.ChainletStacks { + if stackNames[stack.DisplayName] { + return ErrInvalidChainletStack + } + stackNames[stack.DisplayName] = true + } + return gs.Params.Validate() } diff --git a/x/chainlet/types/genesis.pb.go b/x/chainlet/types/genesis.pb.go index 6ea8b765..0e804992 100644 --- a/x/chainlet/types/genesis.pb.go +++ b/x/chainlet/types/genesis.pb.go @@ -26,6 +26,13 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the chainlet module's genesis state. type GenesisState struct { Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + // List of all chainlets + Chainlets []Chainlet `protobuf:"bytes,2,rep,name=chainlets,proto3" json:"chainlets"` + // List of all chainlet stacks + ChainletStacks []ChainletStack `protobuf:"bytes,3,rep,name=chainlet_stacks,json=chainletStacks,proto3" json:"chainlet_stacks"` + // Chainlet count + ChainletCount uint64 `protobuf:"varint,4,opt,name=chainlet_count,json=chainletCount,proto3" json:"chainlet_count,omitempty"` + PortId string `protobuf:"bytes,5,opt,name=port_id,json=portId,proto3" json:"port_id,omitempty"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -68,6 +75,34 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetChainlets() []Chainlet { + if m != nil { + return m.Chainlets + } + return nil +} + +func (m *GenesisState) GetChainletStacks() []ChainletStack { + if m != nil { + return m.ChainletStacks + } + return nil +} + +func (m *GenesisState) GetChainletCount() uint64 { + if m != nil { + return m.ChainletCount + } + return 0 +} + +func (m *GenesisState) GetPortId() string { + if m != nil { + return m.PortId + } + return "" +} + func init() { proto.RegisterType((*GenesisState)(nil), "ssc.chainlet.GenesisState") } @@ -75,19 +110,27 @@ func init() { func init() { proto.RegisterFile("ssc/chainlet/genesis.proto", fileDescriptor_d094dfce36c926a5) } var fileDescriptor_d094dfce36c926a5 = []byte{ - // 190 bytes of a gzipped FileDescriptorProto + // 306 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2a, 0x2e, 0x4e, 0xd6, 0x4f, 0xce, 0x48, 0xcc, 0xcc, 0xcb, 0x49, 0x2d, 0xd1, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x29, 0x2e, 0x4e, 0xd6, 0x83, 0xc9, 0x49, 0x89, 0xa4, 0xe7, 0xa7, 0xe7, 0x83, 0x25, 0xf4, 0x41, 0x2c, 0x88, 0x1a, 0x29, 0x49, 0x14, 0xfd, 0x05, - 0x89, 0x45, 0x89, 0xb9, 0x50, 0xed, 0x4a, 0x4e, 0x5c, 0x3c, 0xee, 0x10, 0xf3, 0x82, 0x4b, 0x12, - 0x4b, 0x52, 0x85, 0x8c, 0xb8, 0xd8, 0x20, 0xf2, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0x22, - 0x7a, 0xc8, 0xe6, 0xeb, 0x05, 0x80, 0xe5, 0x9c, 0x58, 0x4e, 0xdc, 0x93, 0x67, 0x08, 0x82, 0xaa, - 0x74, 0x72, 0x3c, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, - 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0xf5, 0xf4, 0xcc, - 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0xfd, 0xe2, 0xc4, 0xf4, 0xc4, 0x8a, 0xca, 0x2a, - 0x7d, 0x90, 0x5b, 0x2a, 0x10, 0xae, 0x29, 0xa9, 0x2c, 0x48, 0x2d, 0x4e, 0x62, 0x03, 0xbb, 0xc6, - 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x0d, 0xd6, 0xa5, 0x1f, 0xea, 0x00, 0x00, 0x00, + 0x89, 0x45, 0x89, 0xb9, 0x50, 0xed, 0x52, 0xd2, 0x28, 0x52, 0x30, 0x06, 0x54, 0x52, 0x11, 0xab, + 0x64, 0x7c, 0x71, 0x49, 0x62, 0x72, 0x36, 0x44, 0x89, 0x52, 0x17, 0x13, 0x17, 0x8f, 0x3b, 0xc4, + 0x41, 0xc1, 0x25, 0x89, 0x25, 0xa9, 0x42, 0x46, 0x5c, 0x6c, 0x10, 0x0b, 0x24, 0x18, 0x15, 0x18, + 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0x1d, 0xa8, 0x17, 0x00, 0x96, 0x73, 0x62, 0x39, 0x71, 0x4f, + 0x9e, 0x21, 0x08, 0xaa, 0x52, 0xc8, 0x8a, 0x8b, 0x13, 0xa6, 0xa0, 0x58, 0x82, 0x49, 0x81, 0x59, + 0x83, 0xdb, 0x48, 0x0c, 0x55, 0x9b, 0x33, 0x94, 0x01, 0xd5, 0x88, 0x50, 0x2e, 0xe4, 0xc5, 0xc5, + 0x8f, 0xea, 0xb0, 0x62, 0x09, 0x66, 0xb0, 0x09, 0xd2, 0xd8, 0x4d, 0x08, 0x06, 0xa9, 0x81, 0x1a, + 0xc3, 0x97, 0x8c, 0x2c, 0x58, 0x2c, 0xa4, 0xca, 0x05, 0x17, 0x89, 0x4f, 0xce, 0x2f, 0xcd, 0x2b, + 0x91, 0x60, 0x51, 0x60, 0xd4, 0x60, 0x09, 0xe2, 0x85, 0x89, 0x3a, 0x83, 0x04, 0x85, 0xc4, 0xb9, + 0xd8, 0x0b, 0xf2, 0x8b, 0x4a, 0xe2, 0x33, 0x53, 0x24, 0x58, 0x15, 0x18, 0x35, 0x38, 0x83, 0xd8, + 0x40, 0x5c, 0xcf, 0x14, 0x27, 0xc7, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, + 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x88, + 0x52, 0x4f, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x2f, 0x4e, 0x4c, 0x4f, + 0xac, 0xa8, 0xac, 0xd2, 0x07, 0x05, 0x6e, 0x05, 0x22, 0x78, 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, + 0xd8, 0xc0, 0xc1, 0x6a, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xcf, 0x72, 0xbb, 0x60, 0xf3, 0x01, + 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -110,6 +153,46 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.PortId) > 0 { + i -= len(m.PortId) + copy(dAtA[i:], m.PortId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PortId))) + i-- + dAtA[i] = 0x2a + } + if m.ChainletCount != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.ChainletCount)) + i-- + dAtA[i] = 0x20 + } + if len(m.ChainletStacks) > 0 { + for iNdEx := len(m.ChainletStacks) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ChainletStacks[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.Chainlets) > 0 { + for iNdEx := len(m.Chainlets) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Chainlets[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -142,6 +225,25 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.Chainlets) > 0 { + for _, e := range m.Chainlets { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.ChainletStacks) > 0 { + for _, e := range m.ChainletStacks { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if m.ChainletCount != 0 { + n += 1 + sovGenesis(uint64(m.ChainletCount)) + } + l = len(m.PortId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } return n } @@ -213,6 +315,125 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Chainlets", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Chainlets = append(m.Chainlets, Chainlet{}) + if err := m.Chainlets[len(m.Chainlets)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainletStacks", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainletStacks = append(m.ChainletStacks, ChainletStack{}) + if err := m.ChainletStacks[len(m.ChainletStacks)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainletCount", wireType) + } + m.ChainletCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ChainletCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PortId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PortId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/chainlet/types/genesis_test.go b/x/chainlet/types/genesis_test.go index 5585988f..ed4da35a 100644 --- a/x/chainlet/types/genesis_test.go +++ b/x/chainlet/types/genesis_test.go @@ -22,15 +22,78 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ - types.Params{ + Params: types.Params{ ChainletStackProtections: false, NEpochDeposit: "30", AutomaticChainletUpgrades: true, AutomaticChainletUpgradeInterval: 100, }, + PortId: "chainlet", + Chainlets: []types.Chainlet{}, + ChainletStacks: []types.ChainletStack{}, + ChainletCount: 0, }, valid: true, }, + { + desc: "valid genesis state with chainlets", + genState: &types.GenesisState{ + Params: types.Params{ + ChainletStackProtections: false, + NEpochDeposit: "30", + AutomaticChainletUpgrades: true, + AutomaticChainletUpgradeInterval: 100, + }, + PortId: "chainlet", + Chainlets: []types.Chainlet{ + {ChainId: "chain-1"}, + {ChainId: "chain-2"}, + }, + ChainletStacks: []types.ChainletStack{ + {DisplayName: "stack-1"}, + }, + ChainletCount: 2, + }, + valid: true, + }, + { + desc: "invalid genesis state - duplicate chainlet IDs", + genState: &types.GenesisState{ + Params: types.Params{ + ChainletStackProtections: false, + NEpochDeposit: "30", + AutomaticChainletUpgrades: true, + AutomaticChainletUpgradeInterval: 100, + }, + PortId: "chainlet", + Chainlets: []types.Chainlet{ + {ChainId: "chain-1"}, + {ChainId: "chain-1"}, // duplicate + }, + ChainletStacks: []types.ChainletStack{}, + ChainletCount: 2, + }, + valid: false, + }, + { + desc: "invalid genesis state - duplicate stack names", + genState: &types.GenesisState{ + Params: types.Params{ + ChainletStackProtections: false, + NEpochDeposit: "30", + AutomaticChainletUpgrades: true, + AutomaticChainletUpgradeInterval: 100, + }, + PortId: "chainlet", + Chainlets: []types.Chainlet{}, + ChainletStacks: []types.ChainletStack{ + {DisplayName: "stack-1"}, + {DisplayName: "stack-1"}, // duplicate + }, + ChainletCount: 0, + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } { t.Run(tc.desc, func(t *testing.T) { diff --git a/x/chainlet/types/keys.go b/x/chainlet/types/keys.go index 222de39b..5458b60c 100644 --- a/x/chainlet/types/keys.go +++ b/x/chainlet/types/keys.go @@ -20,6 +20,7 @@ var ( ChainletInit = []byte{0x03} ChainletCountKey = []byte{0x04} UpgradingChainletsKey = []byte{0x05} + PortKey = []byte{0x06} ) func KeyPrefix(p string) []byte { diff --git a/x/chainlet/types/query.pb.go b/x/chainlet/types/query.pb.go index 963ba1e9..bb9ac475 100644 --- a/x/chainlet/types/query.pb.go +++ b/x/chainlet/types/query.pb.go @@ -867,6 +867,7 @@ func _Query_ChainletCount_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.chainlet.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/chainlet/types/tx.pb.go b/x/chainlet/types/tx.pb.go index f16f4155..781e702f 100644 --- a/x/chainlet/types/tx.pb.go +++ b/x/chainlet/types/tx.pb.go @@ -6,11 +6,6 @@ package types import ( context "context" fmt "fmt" - io "io" - math "math" - math_bits "math/bits" - time "time" - _ "github.com/cosmos/cosmos-sdk/types/msgservice" _ "github.com/cosmos/gogoproto/gogoproto" grpc1 "github.com/cosmos/gogoproto/grpc" @@ -20,6 +15,10 @@ import ( grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. @@ -1227,6 +1226,7 @@ func _Msg_CancelChainletUpgrade_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.chainlet.Msg", HandlerType: (*MsgServer)(nil), diff --git a/x/chainlet/types/versions/parse.go b/x/chainlet/types/versions/parse.go index fc1dfba0..7d15cca7 100644 --- a/x/chainlet/types/versions/parse.go +++ b/x/chainlet/types/versions/parse.go @@ -44,16 +44,11 @@ func CheckUpgrade(old, new string) (major bool, err error) { } func convertUint16(str string) (num uint16, err error) { - i, err := strconv.Atoi(str) + val, err := strconv.ParseUint(str, 10, 16) // Base 10, 16 bits if err != nil { return } - if i > int(MaxUint16) { - err = errors.New("uint16 overflow") - return - } - - num = uint16(i) + num = uint16(val) // Safe conversion - ParseUint ensures it fits in 16 bits return } diff --git a/x/epochs/keeper/abci.go b/x/epochs/keeper/abci.go index 7133ba5d..4e5daa3b 100644 --- a/x/epochs/keeper/abci.go +++ b/x/epochs/keeper/abci.go @@ -14,18 +14,21 @@ import ( func (k Keeper) BeginBlocker(ctx sdk.Context) error { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) + var hookErr error k.IterateEpochInfo(ctx, func(index int64, epochInfo types.EpochInfo) (stop bool) { logger := k.Logger(ctx) // If blocktime < initial epoch start time, return if ctx.BlockTime().Before(epochInfo.StartTime) { - return + return false } // if epoch counting hasn't started, signal we need to start. shouldInitialEpochStart := !epochInfo.EpochCountingStarted epochEndTime := epochInfo.CurrentEpochStartTime.Add(epochInfo.Duration) - shouldEpochStart := (ctx.BlockTime().After(epochEndTime)) || shouldInitialEpochStart + // When BlockTime() == epochEndTime, that's the start of the new epoch + // (epochEndTime is both the end of current epoch and start of next epoch) + shouldEpochStart := (!ctx.BlockTime().Before(epochEndTime)) || shouldInitialEpochStart if !shouldEpochStart { return false @@ -44,7 +47,11 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) error { sdk.NewAttribute(types.AttributeEpochNumber, fmt.Sprintf("%d", epochInfo.CurrentEpoch)), ), ) - k.AfterEpochEnd(ctx, epochInfo.Identifier, epochInfo.CurrentEpoch) + err := k.AfterEpochEnd(ctx, epochInfo.Identifier, epochInfo.CurrentEpoch) + if err != nil { + hookErr = err + return true // Stop iteration on error + } epochInfo.CurrentEpoch += 1 epochInfo.CurrentEpochStartTime = epochInfo.CurrentEpochStartTime.Add(epochInfo.Duration) logger.Info(fmt.Sprintf("Starting epoch with identifier %s epoch number %d", epochInfo.Identifier, epochInfo.CurrentEpoch)) @@ -59,10 +66,14 @@ func (k Keeper) BeginBlocker(ctx sdk.Context) error { ), ) k.setEpochInfo(ctx, epochInfo) - k.BeforeEpochStart(ctx, epochInfo.Identifier, epochInfo.CurrentEpoch) + err := k.BeforeEpochStart(ctx, epochInfo.Identifier, epochInfo.CurrentEpoch) + if err != nil { + hookErr = err + return true // Stop iteration on error + } return false }) - return nil + return hookErr } diff --git a/x/epochs/keeper/abci_test.go b/x/epochs/keeper/abci_test.go index 1d77ea08..4ad22e60 100644 --- a/x/epochs/keeper/abci_test.go +++ b/x/epochs/keeper/abci_test.go @@ -55,14 +55,16 @@ func TestEpochInfoChangesBeginBlockerAndInitGenesis(t *testing.T) { }, }, { - expCurrentEpochStartHeight: 2, - expCurrentEpochStartTime: now, - expCurrentEpoch: 1, + // When BlockTime == epochEndTime, the epoch should start (this is the fix) + expCurrentEpochStartHeight: 3, + expCurrentEpochStartTime: now.Add(time.Hour * 24 * 31), + expCurrentEpoch: 2, expInitialEpochStartTime: now, fn: func() { ctx = ctx.WithBlockHeight(2).WithBlockTime(now.Add(time.Second)) err := epochsKeeper.BeginBlocker(ctx) require.NoError(t, err) + // BlockTime == epochEndTime should trigger epoch start ctx = ctx.WithBlockHeight(3).WithBlockTime(now.Add(time.Hour * 24 * 31)) err = epochsKeeper.BeginBlocker(ctx) require.NoError(t, err) diff --git a/x/epochs/keeper/hooks.go b/x/epochs/keeper/hooks.go index 77cc6308..5c6f704c 100644 --- a/x/epochs/keeper/hooks.go +++ b/x/epochs/keeper/hooks.go @@ -5,13 +5,11 @@ import ( ) // AfterEpochEnd gets called at the end of the epoch, end of epoch is the timestamp of first block produced after epoch duration. -func (k Keeper) AfterEpochEnd(ctx sdk.Context, identifier string, epochNumber int64) { - // Error is not handled as AfterEpochEnd Hooks use osmoutils.ApplyFuncIfNoError() - _ = k.hooks.AfterEpochEnd(ctx, identifier, epochNumber) +func (k Keeper) AfterEpochEnd(ctx sdk.Context, identifier string, epochNumber int64) error { + return k.hooks.AfterEpochEnd(ctx, identifier, epochNumber) } // BeforeEpochStart new epoch is next block of epoch end block -func (k Keeper) BeforeEpochStart(ctx sdk.Context, identifier string, epochNumber int64) { - // Error is not handled as BeforeEpochStart Hooks use osmoutils.ApplyFuncIfNoError() - _ = k.hooks.BeforeEpochStart(ctx, identifier, epochNumber) +func (k Keeper) BeforeEpochStart(ctx sdk.Context, identifier string, epochNumber int64) error { + return k.hooks.BeforeEpochStart(ctx, identifier, epochNumber) } diff --git a/x/epochs/module.go b/x/epochs/module.go index d95ec9e4..02d87868 100644 --- a/x/epochs/module.go +++ b/x/epochs/module.go @@ -79,7 +79,8 @@ func (AppModuleBasic) RegisterRESTRoutes(clientCtx client.Context, rtr *mux.Rout // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module. func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck + //nolint:errcheck // gRPC gateway registration errors are non-critical at startup + types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) } // GetTxCmd returns the capability module's root tx command. diff --git a/x/epochs/types/hooks.go b/x/epochs/types/hooks.go index 97976edb..9ec7aa4a 100644 --- a/x/epochs/types/hooks.go +++ b/x/epochs/types/hooks.go @@ -28,18 +28,30 @@ func NewMultiEpochHooks(hooks ...EpochHooks) MultiEpochHooks { // AfterEpochEnd is called when epoch is going to be ended, epochNumber is the number of epoch that is ending. func (h MultiEpochHooks) AfterEpochEnd(ctx sdk.Context, epochIdentifier string, epochNumber int64) error { + var firstErr error for i := range h { - panicCatchingEpochHook(ctx, h[i].AfterEpochEnd, epochIdentifier, epochNumber) + if err := panicCatchingEpochHook(ctx, h[i].AfterEpochEnd, epochIdentifier, epochNumber); err != nil { + if firstErr == nil { + firstErr = err + } + // Continue processing other hooks for panic isolation, but track the first error + } } - return nil + return firstErr } // BeforeEpochStart is called when epoch is going to be started, epochNumber is the number of epoch that is starting. func (h MultiEpochHooks) BeforeEpochStart(ctx sdk.Context, epochIdentifier string, epochNumber int64) error { + var firstErr error for i := range h { - panicCatchingEpochHook(ctx, h[i].BeforeEpochStart, epochIdentifier, epochNumber) + if err := panicCatchingEpochHook(ctx, h[i].BeforeEpochStart, epochIdentifier, epochNumber); err != nil { + if firstErr == nil { + firstErr = err + } + // Continue processing other hooks for panic isolation, but track the first error + } } - return nil + return firstErr } func panicCatchingEpochHook( @@ -47,7 +59,7 @@ func panicCatchingEpochHook( hookFn func(ctx sdk.Context, epochIdentifier string, epochNumber int64) error, epochIdentifier string, epochNumber int64, -) { +) error { wrappedHookFn := func(ctx sdk.Context) error { return hookFn(ctx, epochIdentifier, epochNumber) } @@ -55,7 +67,9 @@ func panicCatchingEpochHook( err := applyFuncIfNoError(ctx, wrappedHookFn) if err != nil { ctx.Logger().Error(fmt.Sprintf("error in epoch hook %v", err)) + return err } + return nil } func applyFuncIfNoError(ctx sdk.Context, f func(ctx sdk.Context) error) (err error) { diff --git a/x/epochs/types/query.pb.go b/x/epochs/types/query.pb.go index f82217d9..75fb5b01 100644 --- a/x/epochs/types/query.pb.go +++ b/x/epochs/types/query.pb.go @@ -339,6 +339,7 @@ func _Query_CurrentEpoch_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.epochs.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/escrow/genesis.go b/x/escrow/genesis.go index 9d1a6747..0c79d28a 100644 --- a/x/escrow/genesis.go +++ b/x/escrow/genesis.go @@ -8,8 +8,25 @@ import ( // InitGenesis initializes the module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // this line is used by starport scaffolding # genesis/module/init + // Set params k.SetParams(ctx, genState.Params) + + // Import chainlet accounts + for _, acc := range genState.ChainletAccounts { + k.ImportChainletAccount(ctx, acc) + } + + // Import pools + for _, pool := range genState.Pools { + k.ImportPool(ctx, pool) + } + + // Import funders + for _, gf := range genState.Funders { + k.ImportFunder(ctx, gf.ChainId, gf.Denom, gf.Address, gf.Funder) + } + + // this line is used by starport scaffolding # genesis/module/init } // ExportGenesis returns the module's exported genesis @@ -17,6 +34,15 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() genesis.Params = k.GetParams(ctx) + // Export chainlet accounts + genesis.ChainletAccounts = k.ExportChainletAccounts(ctx) + + // Export pools + genesis.Pools = k.ExportPools(ctx) + + // Export funders + genesis.Funders = k.ExportFunders(ctx) + // this line is used by starport scaffolding # genesis/module/export return genesis diff --git a/x/escrow/keeper/escrow.go b/x/escrow/keeper/escrow.go index 79878ae7..027afdeb 100644 --- a/x/escrow/keeper/escrow.go +++ b/x/escrow/keeper/escrow.go @@ -68,6 +68,28 @@ func (k Keeper) deleteFunder(ctx sdk.Context, chainID, denom, addr string) { store.Delete(types.ByFunderKey(addr, chainID, denom)) } +// clearPoolFunders removes all funder records for a specific (chainID, denom) pool. +// This is called when the pool balance is fully drained to ensure share accounting +// is reset properly for future deposits. +func (k Keeper) clearPoolFunders(ctx sdk.Context, chainID, denom string) { + store := ctx.KVStore(k.storeKey) + pfx := prefix.NewStore(store, types.FunderPrefix(chainID, denom)) + + // Collect all addresses first to avoid iterator invalidation during deletion + var addrs []string + it := pfx.Iterator(nil, nil) + for ; it.Valid(); it.Next() { + addrs = append(addrs, string(it.Key())) + } + it.Close() + + // Delete all funder records and their reverse indexes + for _, addr := range addrs { + store.Delete(types.FunderKey(chainID, denom, addr)) + store.Delete(types.ByFunderKey(addr, chainID, denom)) + } +} + // ---------- params / validation ---------- func (k Keeper) assertSupportedDenom(ctx sdk.Context, denom string) error { @@ -176,10 +198,12 @@ func (k Keeper) deposit(ctx sdk.Context, addr sdk.AccAddress, chainID string, am } else { // S_j = S * T_j / T var newShares math.LegacyDec - if pool.Balance.IsPositive() { + if pool.Balance.IsPositive() && pool.Shares.IsPositive() { newShares = pool.Shares.MulInt(amount.Amount).QuoInt(pool.Balance.Amount) } else { - newShares = math.LegacyNewDecFromInt(amount.Amount) // bootstrap 1:1 shares + // Bootstrap 1:1 shares when pool is empty or in an invalid state + // (e.g., Balance > 0 but Shares = 0 due to migration/genesis issues) + newShares = math.LegacyNewDecFromInt(amount.Amount) } pool.Shares = pool.Shares.Add(newShares) pool.Balance = pool.Balance.Add(amount) @@ -324,7 +348,7 @@ func (k Keeper) withdrawOne( } // Default: tokens = floor(f.Shares / sf) == floor(f.Shares * T / S) - tokensDec := f.Shares.Quo(sf) + tokensDec := f.Shares.QuoTruncate(sf) amt := tokensDec.TruncateInt() // ---- Dust flush for the last withdrawer ---- @@ -396,6 +420,16 @@ func (k Keeper) BillAccount(ctx sdk.Context, amount sdk.Coin, chainID, toModule // update pool pool, _ := k.getPool(ctx, chainID, amount.Denom) pool.Balance = nb + + // If pool is fully drained, clear all funders and reset shares. + // This prevents the share pricing vulnerability where new depositors + // would get 1:1 shares while existing shareholders (with worthless shares + // backed by zero balance) could claim a portion of new deposits. + if pool.Balance.IsZero() { + k.clearPoolFunders(ctx, chainID, amount.Denom) + pool.Shares = math.LegacyZeroDec() + } + k.setPool(ctx, pool) return nil } @@ -412,9 +446,6 @@ func ScalingFactor(pool types.DenomPool) math.LegacyDec { return totalPoolShares.Quo(totalDeposit) } -func InverseScalingFactor(pool types.DenomPool) math.LegacyDec { - return math.LegacyNewDec(int64(1)).Quo(ScalingFactor(pool)) -} // GetChainletWithPools returns the ChainletAccount head and all DenomPool rows // for the given chainID. If the chainlet doesn't exist, returns NotFound. diff --git a/x/escrow/keeper/escrow_test.go b/x/escrow/keeper/escrow_test.go new file mode 100644 index 00000000..43164b77 --- /dev/null +++ b/x/escrow/keeper/escrow_test.go @@ -0,0 +1,605 @@ +package keeper + +import ( + "testing" + + "cosmossdk.io/log" + "cosmossdk.io/math" + "cosmossdk.io/store" + "cosmossdk.io/store/metrics" + "cosmossdk.io/store/prefix" + storetypes "cosmossdk.io/store/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmdb "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + typesparams "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/stretchr/testify/require" + + "github.com/sagaxyz/ssc/x/escrow/types" +) + +// testKeeper creates a minimal keeper for unit tests without external dependencies +func testKeeper(t *testing.T) (*Keeper, sdk.Context) { + storeKey := storetypes.NewKVStoreKey(types.StoreKey) + memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) + + db := tmdb.NewMemDB() + stateStore := store.NewCommitMultiStore(db, log.NewNopLogger(), metrics.NewNoOpMetrics()) + stateStore.MountStoreWithDB(storeKey, storetypes.StoreTypeIAVL, db) + stateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) + require.NoError(t, stateStore.LoadLatestVersion()) + + registry := codectypes.NewInterfaceRegistry() + cdc := codec.NewProtoCodec(registry) + + paramsSubspace := typesparams.NewSubspace(cdc, + types.Amino, + storeKey, + memStoreKey, + "EscrowParams", + ) + + k := NewKeeper(cdc, storeKey, paramsSubspace, nil, nil, nil, nil) + ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) + k.SetParams(ctx, types.DefaultParams()) + + return k, ctx +} + +func TestScalingFactor(t *testing.T) { + tests := []struct { + name string + pool types.DenomPool + expected math.LegacyDec + }{ + { + name: "1:1 ratio", + pool: types.DenomPool{ + ChainId: "test-chain", + Denom: "utoken", + Balance: sdk.NewCoin("utoken", math.NewInt(1000)), + Shares: math.LegacyNewDec(1000), + }, + expected: math.LegacyOneDec(), + }, + { + name: "2:1 shares to tokens", + pool: types.DenomPool{ + ChainId: "test-chain", + Denom: "utoken", + Balance: sdk.NewCoin("utoken", math.NewInt(500)), + Shares: math.LegacyNewDec(1000), + }, + expected: math.LegacyNewDec(2), + }, + { + name: "zero balance returns 1", + pool: types.DenomPool{ + ChainId: "test-chain", + Denom: "utoken", + Balance: sdk.NewCoin("utoken", math.NewInt(0)), + Shares: math.LegacyNewDec(0), + }, + expected: math.LegacyOneDec(), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sf := ScalingFactor(tc.pool) + require.True(t, tc.expected.Equal(sf), "expected %s, got %s", tc.expected, sf) + }) + } +} + +func TestWithdrawTokenCalculation_UsesFloor(t *testing.T) { + // This test verifies that token calculation uses floor (truncation) behavior, + // not rounding. This is critical for security: users should never receive + // more tokens than their proportional share. + + tests := []struct { + name string + poolShares math.LegacyDec + poolBalance math.Int + funderShares math.LegacyDec + expectedTokens math.Int + }{ + { + name: "exact division - no rounding needed", + poolShares: math.LegacyNewDec(100), + poolBalance: math.NewInt(100), + funderShares: math.LegacyNewDec(50), + expectedTokens: math.NewInt(50), + }, + { + name: "should floor, not round up", + poolShares: math.LegacyNewDec(100), + poolBalance: math.NewInt(100), + funderShares: math.LegacyMustNewDecFromStr("33.6"), // Would round to 34 with banker's rounding + expectedTokens: math.NewInt(33), // Should be 33 (floor) + }, + { + name: "edge case - 0.5 remainder should floor to 0", + poolShares: math.LegacyNewDec(100), + poolBalance: math.NewInt(100), + funderShares: math.LegacyMustNewDecFromStr("0.5"), + expectedTokens: math.NewInt(0), // Should be 0 (floor), not 1 (round) + }, + { + name: "complex ratio with truncation", + poolShares: math.LegacyNewDec(300), + poolBalance: math.NewInt(100), + funderShares: math.LegacyNewDec(100), // 100 shares = 100/300 * 100 = 33.33... tokens + expectedTokens: math.NewInt(33), // Should floor to 33 + }, + { + name: "high precision shares", + poolShares: math.LegacyMustNewDecFromStr("1000000000000000000"), + poolBalance: math.NewInt(1000000000), + funderShares: math.LegacyMustNewDecFromStr("999999999999999999"), + expectedTokens: math.NewInt(999999999), // Should floor + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + pool := types.DenomPool{ + ChainId: "test-chain", + Denom: "utoken", + Balance: sdk.NewCoin("utoken", tc.poolBalance), + Shares: tc.poolShares, + } + + sf := ScalingFactor(pool) + require.False(t, sf.IsZero(), "scaling factor should not be zero") + + // This mirrors the actual calculation in withdrawOne(): + // tokensDec := f.Shares.QuoTruncate(sf) + // amt := tokensDec.TruncateInt() + tokensDec := tc.funderShares.QuoTruncate(sf) + amt := tokensDec.TruncateInt() + + require.True(t, tc.expectedTokens.Equal(amt), + "expected %s tokens, got %s (tokensDec: %s)", + tc.expectedTokens, amt, tokensDec) + + // Verify that using Quo (which rounds) could give different results + // This demonstrates why QuoTruncate is necessary + tokenWithRounding := tc.funderShares.Quo(sf).TruncateInt() + if !tc.expectedTokens.Equal(tokenWithRounding) { + t.Logf("Note: Quo would have given %s instead of %s (QuoTruncate)", + tokenWithRounding, amt) + } + }) + } +} + +func TestWithdrawTokenCalculation_NeverExceedsEntitlement(t *testing.T) { + // Property test: withdrawn tokens should never exceed the proportional share + // tokens_out <= funder_shares / total_shares * total_balance + + testCases := []struct { + poolShares string + poolBalance int64 + funderShares string + }{ + {"100", 100, "33"}, + {"100", 100, "33.5"}, + {"100", 100, "33.9"}, + {"100", 100, "34"}, + {"1000", 333, "500"}, + {"777", 999, "123.456789"}, + } + + for _, tc := range testCases { + pool := types.DenomPool{ + ChainId: "test-chain", + Denom: "utoken", + Balance: sdk.NewCoin("utoken", math.NewInt(tc.poolBalance)), + Shares: math.LegacyMustNewDecFromStr(tc.poolShares), + } + funderShares := math.LegacyMustNewDecFromStr(tc.funderShares) + + sf := ScalingFactor(pool) + tokensDec := funderShares.QuoTruncate(sf) + amt := tokensDec.TruncateInt() + + // Calculate exact entitlement: funderShares / poolShares * poolBalance + exactEntitlement := funderShares.MulInt(pool.Balance.Amount).Quo(pool.Shares) + + // amt should be <= floor(exactEntitlement) + require.True(t, math.LegacyNewDecFromInt(amt).LTE(exactEntitlement), + "withdrawn amount %s exceeds entitlement %s for shares %s/%s on balance %d", + amt, exactEntitlement, funderShares, tc.poolShares, tc.poolBalance) + } +} + +func TestClearPoolFunders(t *testing.T) { + k, ctx := testKeeper(t) + + chainID := "test-chain" + denom := "utoken" + + // Set up multiple funders for the pool + funders := []string{ + "saga1abc123", + "saga1def456", + "saga1ghi789", + } + + for _, addr := range funders { + k.setFunder(ctx, chainID, denom, addr, types.Funder{ + Shares: math.LegacyNewDec(100), + }) + } + + // Verify funders exist + for _, addr := range funders { + _, exists := k.getFunder(ctx, chainID, denom, addr) + require.True(t, exists, "funder %s should exist before clearing", addr) + } + + // Clear all funders + k.clearPoolFunders(ctx, chainID, denom) + + // Verify all funders are removed + for _, addr := range funders { + _, exists := k.getFunder(ctx, chainID, denom, addr) + require.False(t, exists, "funder %s should not exist after clearing", addr) + } + + // Verify reverse index is also cleared + store := ctx.KVStore(k.storeKey) + for _, addr := range funders { + key := types.ByFunderKey(addr, chainID, denom) + require.False(t, store.Has(key), "reverse index for %s should be cleared", addr) + } +} + +func TestClearPoolFunders_OnlyAffectsSpecificPool(t *testing.T) { + k, ctx := testKeeper(t) + + chainID1 := "chain-1" + chainID2 := "chain-2" + denom1 := "utoken" + denom2 := "usaga" + + // Set up funders across different pools + k.setFunder(ctx, chainID1, denom1, "addr1", types.Funder{Shares: math.LegacyNewDec(100)}) + k.setFunder(ctx, chainID1, denom2, "addr2", types.Funder{Shares: math.LegacyNewDec(100)}) + k.setFunder(ctx, chainID2, denom1, "addr3", types.Funder{Shares: math.LegacyNewDec(100)}) + + // Clear only chainID1/denom1 pool + k.clearPoolFunders(ctx, chainID1, denom1) + + // Verify only the targeted pool's funders are removed + _, exists := k.getFunder(ctx, chainID1, denom1, "addr1") + require.False(t, exists, "addr1 in chain-1/utoken should be cleared") + + _, exists = k.getFunder(ctx, chainID1, denom2, "addr2") + require.True(t, exists, "addr2 in chain-1/usaga should NOT be cleared") + + _, exists = k.getFunder(ctx, chainID2, denom1, "addr3") + require.True(t, exists, "addr3 in chain-2/utoken should NOT be cleared") +} + +func TestClearPoolFunders_EmptyPool(t *testing.T) { + k, ctx := testKeeper(t) + + // Clearing an empty pool should not panic + k.clearPoolFunders(ctx, "nonexistent-chain", "utoken") + + // Verify no funders exist (nothing to verify, just ensure no panic) + store := ctx.KVStore(k.storeKey) + pfx := prefix.NewStore(store, types.FunderPrefix("nonexistent-chain", "utoken")) + it := pfx.Iterator(nil, nil) + defer it.Close() + require.False(t, it.Valid(), "should have no funders") +} + +func TestDepositIntoDrainedPool_SharePricingVulnerability(t *testing.T) { + // This test documents the vulnerability that was fixed: + // When a pool's balance is drained to zero but shares remain, + // new deposits would get 1:1 shares, allowing existing shareholders + // to claim a portion of new deposits. + // + // The fix ensures that when balance hits zero, all funders are + // cleared and shares are reset to zero. + + k, ctx := testKeeper(t) + + chainID := "test-chain" + denom := "utoken" + + // Test case 1: Balance exactly zero - should clear + t.Run("balance_zero_clears_funders", func(t *testing.T) { + drainedPool := types.DenomPool{ + ChainId: chainID, + Denom: denom, + Balance: sdk.NewCoin(denom, math.ZeroInt()), + Shares: math.LegacyNewDec(1000), + } + k.setFunder(ctx, chainID, denom, "old-funder", types.Funder{ + Shares: math.LegacyNewDec(1000), + }) + k.setPool(ctx, drainedPool) + + // Simulate the check in BillAccount + pool, _ := k.getPool(ctx, chainID, denom) + if pool.Balance.IsZero() { + k.clearPoolFunders(ctx, chainID, denom) + pool.Shares = math.LegacyZeroDec() + k.setPool(ctx, pool) + } + + pool, _ = k.getPool(ctx, chainID, denom) + require.True(t, pool.Shares.IsZero(), "pool shares should be zero") + _, exists := k.getFunder(ctx, chainID, denom, "old-funder") + require.False(t, exists, "old funder should be cleared") + }) + + // Test case 2: Balance > 0 - should NOT clear (proportional math works) + t.Run("positive_balance_preserves_funders", func(t *testing.T) { + chainID2 := "test-chain-2" + lowBalancePool := types.DenomPool{ + ChainId: chainID2, + Denom: denom, + Balance: sdk.NewCoin(denom, math.NewInt(1)), + Shares: math.LegacyNewDec(132323124), + } + k.setFunder(ctx, chainID2, denom, "old-funder-2", types.Funder{ + Shares: math.LegacyNewDec(132323124), + }) + k.setPool(ctx, lowBalancePool) + + pool, _ := k.getPool(ctx, chainID2, denom) + if pool.Balance.IsZero() { + k.clearPoolFunders(ctx, chainID2, denom) + pool.Shares = math.LegacyZeroDec() + k.setPool(ctx, pool) + } + + pool, _ = k.getPool(ctx, chainID2, denom) + require.False(t, pool.Shares.IsZero(), "pool shares should NOT be zero when balance > 0") + require.Equal(t, int64(1), pool.Balance.Amount.Int64(), "pool balance should remain") + _, exists := k.getFunder(ctx, chainID2, denom, "old-funder-2") + require.True(t, exists, "funder should NOT be cleared when balance > 0") + }) +} + +func TestVulnerabilityScenario_EndToEnd(t *testing.T) { + // End-to-end test of the vulnerability fix: + // 1. Create initial funders with deposits + // 2. Simulate BillAccount draining pool to zero (verify funders cleared) + // 3. New depositor adds funds + // 4. Verify new depositor receives 100% of shares (not diluted) + + k, ctx := testKeeper(t) + + chainID := "vuln-test-chain" + denom := "utoken" + + // Step 1: Create initial pool with multiple funders + initialPool := types.DenomPool{ + ChainId: chainID, + Denom: denom, + Balance: sdk.NewCoin(denom, math.NewInt(10000)), + Shares: math.LegacyNewDec(10000), + } + k.setPool(ctx, initialPool) + k.setChainlet(ctx, types.ChainletAccount{ChainId: chainID}) + + // Add multiple funders + k.setFunder(ctx, chainID, denom, "funder-alice", types.Funder{ + Shares: math.LegacyNewDec(6000), + }) + k.setFunder(ctx, chainID, denom, "funder-bob", types.Funder{ + Shares: math.LegacyNewDec(4000), + }) + + // Verify initial state + pool, _ := k.getPool(ctx, chainID, denom) + require.Equal(t, int64(10000), pool.Balance.Amount.Int64()) + require.Equal(t, "10000.000000000000000000", pool.Shares.String()) + + _, aliceExists := k.getFunder(ctx, chainID, denom, "funder-alice") + _, bobExists := k.getFunder(ctx, chainID, denom, "funder-bob") + require.True(t, aliceExists, "alice should exist initially") + require.True(t, bobExists, "bob should exist initially") + + // Step 2: Simulate billing draining the pool to zero + // This mimics what BillAccount does when it drains the pool + pool.Balance = sdk.NewCoin(denom, math.ZeroInt()) + + // Apply the fix: when balance is zero, clear funders and reset shares + if pool.Balance.IsZero() { + k.clearPoolFunders(ctx, chainID, denom) + pool.Shares = math.LegacyZeroDec() + } + k.setPool(ctx, pool) + + // Verify funders were cleared + pool, _ = k.getPool(ctx, chainID, denom) + require.True(t, pool.Balance.IsZero(), "pool balance should be zero after drain") + require.True(t, pool.Shares.IsZero(), "pool shares should be zero after drain") + + _, aliceExists = k.getFunder(ctx, chainID, denom, "funder-alice") + _, bobExists = k.getFunder(ctx, chainID, denom, "funder-bob") + require.False(t, aliceExists, "alice should be cleared after drain") + require.False(t, bobExists, "bob should be cleared after drain") + + // Step 3: New depositor adds funds + // Simulate deposit logic (without bank transfer since we don't have mock) + newDepositAmount := math.NewInt(50000) + var newShares math.LegacyDec + + pool, _ = k.getPool(ctx, chainID, denom) + if pool.Balance.IsPositive() && pool.Shares.IsPositive() { + // Proportional shares (won't execute since pool is empty) + newShares = pool.Shares.MulInt(newDepositAmount).QuoInt(pool.Balance.Amount) + } else { + // Bootstrap 1:1 shares + newShares = math.LegacyNewDecFromInt(newDepositAmount) + } + + pool.Shares = pool.Shares.Add(newShares) + pool.Balance = pool.Balance.Add(sdk.NewCoin(denom, newDepositAmount)) + k.setFunder(ctx, chainID, denom, "funder-new", types.Funder{Shares: newShares}) + k.setPool(ctx, pool) + + // Step 4: Verify new depositor receives 100% of shares + pool, _ = k.getPool(ctx, chainID, denom) + newFunder, newFunderExists := k.getFunder(ctx, chainID, denom, "funder-new") + + require.True(t, newFunderExists, "new funder should exist") + require.Equal(t, int64(50000), pool.Balance.Amount.Int64(), "pool balance should be new deposit") + require.Equal(t, "50000.000000000000000000", pool.Shares.String(), "pool shares should equal new deposit (1:1)") + require.Equal(t, "50000.000000000000000000", newFunder.Shares.String(), "new funder should have 100% of shares") + + // Verify new funder owns 100% of the pool + ownershipRatio := newFunder.Shares.Quo(pool.Shares) + require.Equal(t, "1.000000000000000000", ownershipRatio.String(), "new funder should own 100% of pool") + + // Verify old funders cannot claim anything (they don't exist) + _, aliceExists = k.getFunder(ctx, chainID, denom, "funder-alice") + _, bobExists = k.getFunder(ctx, chainID, denom, "funder-bob") + require.False(t, aliceExists, "alice should not exist after fix") + require.False(t, bobExists, "bob should not exist after fix") +} + +func TestDepositIntoPoolWithZeroShares(t *testing.T) { + // Test edge case: Balance > 0 but Shares = 0 + // This shouldn't happen through normal operations but could occur + // due to migration/genesis issues. Without the defensive check, + // new depositors would get 0 shares (stolen deposit). + + k, ctx := testKeeper(t) + + chainID := "zero-shares-chain" + denom := "utoken" + + // Setup: Invalid state where balance exists but no shares + invalidPool := types.DenomPool{ + ChainId: chainID, + Denom: denom, + Balance: sdk.NewCoin(denom, math.NewInt(1000)), // Has balance + Shares: math.LegacyZeroDec(), // But no shares! + } + k.setPool(ctx, invalidPool) + k.setChainlet(ctx, types.ChainletAccount{ChainId: chainID}) + + // New depositor adds funds using the FIXED deposit logic + newDepositAmount := math.NewInt(50000) + var newShares math.LegacyDec + + pool, _ := k.getPool(ctx, chainID, denom) + + // FIXED LOGIC: checks both balance AND shares + if pool.Balance.IsPositive() && pool.Shares.IsPositive() { + newShares = pool.Shares.MulInt(newDepositAmount).QuoInt(pool.Balance.Amount) + } else { + // Bootstrap 1:1 because shares is zero + newShares = math.LegacyNewDecFromInt(newDepositAmount) + } + + pool.Shares = pool.Shares.Add(newShares) + pool.Balance = pool.Balance.Add(sdk.NewCoin(denom, newDepositAmount)) + k.setFunder(ctx, chainID, denom, "new-funder", types.Funder{Shares: newShares}) + k.setPool(ctx, pool) + + // Verify new depositor gets proper shares (not zero!) + pool, _ = k.getPool(ctx, chainID, denom) + newFunder, _ := k.getFunder(ctx, chainID, denom, "new-funder") + + require.Equal(t, "50000.000000000000000000", newShares.String(), "new funder should get 1:1 shares") + require.Equal(t, int64(51000), pool.Balance.Amount.Int64(), "pool balance should include both") + require.Equal(t, "50000.000000000000000000", pool.Shares.String(), "pool shares should be new deposit only") + + // New funder owns 100% of shares + ownershipRatio := newFunder.Shares.Quo(pool.Shares) + require.Equal(t, "1.000000000000000000", ownershipRatio.String(), "new funder should own 100% of shares") + + t.Logf("Balance > 0, Shares = 0: New depositor correctly gets 1:1 shares") + t.Logf(" Pool balance: %d (includes 1000 orphaned + 50000 new)", pool.Balance.Amount.Int64()) + t.Logf(" Pool shares: %s", pool.Shares.String()) + t.Logf(" New funder shares: %s (100%% ownership)", newFunder.Shares.String()) +} + +func TestDepositIntoPoolWithZeroBalance(t *testing.T) { + // Test edge case: Balance = 0 but Shares > 0 + // This is the main vulnerability scenario. Without the fix in BillAccount, + // this state would persist and new depositors would be diluted. + + k, ctx := testKeeper(t) + + chainID := "zero-balance-chain" + denom := "utoken" + + // Setup: Vulnerable state where shares exist but balance is zero + vulnerablePool := types.DenomPool{ + ChainId: chainID, + Denom: denom, + Balance: sdk.NewCoin(denom, math.ZeroInt()), // Drained to zero + Shares: math.LegacyNewDec(10000), // But shares still exist! + } + k.setPool(ctx, vulnerablePool) + k.setChainlet(ctx, types.ChainletAccount{ChainId: chainID}) + + // Old funder still has shares in this vulnerable state + k.setFunder(ctx, chainID, denom, "old-funder", types.Funder{ + Shares: math.LegacyNewDec(10000), + }) + + // Simulate the FIX: BillAccount clears funders when balance hits zero + pool, _ := k.getPool(ctx, chainID, denom) + if pool.Balance.IsZero() { + k.clearPoolFunders(ctx, chainID, denom) + pool.Shares = math.LegacyZeroDec() + k.setPool(ctx, pool) + } + + // Verify old funder was cleared + _, oldFunderExists := k.getFunder(ctx, chainID, denom, "old-funder") + require.False(t, oldFunderExists, "old funder should be cleared") + + // Now new depositor adds funds + newDepositAmount := math.NewInt(50000) + var newShares math.LegacyDec + + pool, _ = k.getPool(ctx, chainID, denom) + + if pool.Balance.IsPositive() && pool.Shares.IsPositive() { + newShares = pool.Shares.MulInt(newDepositAmount).QuoInt(pool.Balance.Amount) + } else { + // Bootstrap 1:1 because pool was reset + newShares = math.LegacyNewDecFromInt(newDepositAmount) + } + + pool.Shares = pool.Shares.Add(newShares) + pool.Balance = pool.Balance.Add(sdk.NewCoin(denom, newDepositAmount)) + k.setFunder(ctx, chainID, denom, "new-funder", types.Funder{Shares: newShares}) + k.setPool(ctx, pool) + + // Verify new depositor gets 100% ownership + pool, _ = k.getPool(ctx, chainID, denom) + newFunder, _ := k.getFunder(ctx, chainID, denom, "new-funder") + + require.Equal(t, "50000.000000000000000000", newShares.String(), "new funder should get 1:1 shares") + require.Equal(t, int64(50000), pool.Balance.Amount.Int64(), "pool balance should be new deposit") + require.Equal(t, "50000.000000000000000000", pool.Shares.String(), "pool shares should equal new deposit") + + ownershipRatio := newFunder.Shares.Quo(pool.Shares) + require.Equal(t, "1.000000000000000000", ownershipRatio.String(), "new funder should own 100% of pool") + + // Old funder cannot claim anything + _, oldFunderExists = k.getFunder(ctx, chainID, denom, "old-funder") + require.False(t, oldFunderExists, "old funder should still not exist") + + t.Logf("Balance = 0, Shares > 0: Fix clears old funders, new depositor gets 100%%") + t.Logf(" Pool balance: %d", pool.Balance.Amount.Int64()) + t.Logf(" Pool shares: %s", pool.Shares.String()) + t.Logf(" New funder shares: %s (100%% ownership)", newFunder.Shares.String()) +} + diff --git a/x/escrow/keeper/genesis.go b/x/escrow/keeper/genesis.go new file mode 100644 index 00000000..f2f89904 --- /dev/null +++ b/x/escrow/keeper/genesis.go @@ -0,0 +1,84 @@ +package keeper + +import ( + "bytes" + + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sagaxyz/ssc/x/escrow/types" +) + +// ExportChainletAccounts exports all chainlet accounts from the store +func (k Keeper) ExportChainletAccounts(ctx sdk.Context) []types.ChainletAccount { + store := ctx.KVStore(k.storeKey) + iterator := prefix.NewStore(store, types.KeyChainletPrefix).Iterator(nil, nil) + defer iterator.Close() + + var accounts []types.ChainletAccount + for ; iterator.Valid(); iterator.Next() { + var acc types.ChainletAccount + k.cdc.MustUnmarshal(iterator.Value(), &acc) + accounts = append(accounts, acc) + } + return accounts +} + +// ExportPools exports all denomination pools from the store +func (k Keeper) ExportPools(ctx sdk.Context) []types.DenomPool { + store := ctx.KVStore(k.storeKey) + iterator := prefix.NewStore(store, types.KeyPoolPrefix).Iterator(nil, nil) + defer iterator.Close() + + var pools []types.DenomPool + for ; iterator.Valid(); iterator.Next() { + var pool types.DenomPool + k.cdc.MustUnmarshal(iterator.Value(), &pool) + pools = append(pools, pool) + } + return pools +} + +// ExportFunders exports all funder positions from the store +func (k Keeper) ExportFunders(ctx sdk.Context) []types.GenesisFunder { + store := ctx.KVStore(k.storeKey) + iterator := prefix.NewStore(store, types.KeyFunderPrefix).Iterator(nil, nil) + defer iterator.Close() + + var funders []types.GenesisFunder + for ; iterator.Valid(); iterator.Next() { + var funder types.Funder + k.cdc.MustUnmarshal(iterator.Value(), &funder) + + // Parse the key to extract chainId, denom, and address + // Key format: {chainId}/{denom}/{addr} + key := iterator.Key() + parts := bytes.SplitN(key, []byte{'/'}, 3) + if len(parts) != 3 { + continue + } + + funders = append(funders, types.GenesisFunder{ + ChainId: string(parts[0]), + Denom: string(parts[1]), + Address: string(parts[2]), + Funder: funder, + }) + } + return funders +} + +// ImportChainletAccount imports a single chainlet account into the store +func (k Keeper) ImportChainletAccount(ctx sdk.Context, acc types.ChainletAccount) { + k.setChainlet(ctx, acc) +} + +// ImportPool imports a single pool into the store +func (k Keeper) ImportPool(ctx sdk.Context, pool types.DenomPool) { + k.setPool(ctx, pool) +} + +// ImportFunder imports a single funder into the store (includes reverse index) +func (k Keeper) ImportFunder(ctx sdk.Context, chainID, denom, addr string, funder types.Funder) { + k.setFunder(ctx, chainID, denom, addr, funder) +} diff --git a/x/escrow/module.go b/x/escrow/module.go index a86f01f6..8aab29ea 100644 --- a/x/escrow/module.go +++ b/x/escrow/module.go @@ -72,7 +72,8 @@ func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncod // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck + //nolint:errcheck // gRPC gateway registration errors are non-critical at startup + types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) } // GetTxCmd returns the root Tx command for the module. The subcommands of this root command are used by end-users to generate new transactions containing messages defined in the module diff --git a/x/escrow/testutil/expected_keepers_mocks.go b/x/escrow/testutil/expected_keepers_mocks.go index 526868da..a7462f30 100644 --- a/x/escrow/testutil/expected_keepers_mocks.go +++ b/x/escrow/testutil/expected_keepers_mocks.go @@ -13,6 +13,57 @@ import ( types0 "github.com/sagaxyz/ssc/x/chainlet/types" ) +// MockAclKeeper is a mock of AclKeeper interface. +type MockAclKeeper struct { + ctrl *gomock.Controller + recorder *MockAclKeeperMockRecorder +} + +// MockAclKeeperMockRecorder is the mock recorder for MockAclKeeper. +type MockAclKeeperMockRecorder struct { + mock *MockAclKeeper +} + +// NewMockAclKeeper creates a new mock instance. +func NewMockAclKeeper(ctrl *gomock.Controller) *MockAclKeeper { + mock := &MockAclKeeper{ctrl: ctrl} + mock.recorder = &MockAclKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAclKeeper) EXPECT() *MockAclKeeperMockRecorder { + return m.recorder +} + +// Allowed mocks base method. +func (m *MockAclKeeper) Allowed(ctx types.Context, addr types.AccAddress) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Allowed", ctx, addr) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Allowed indicates an expected call of Allowed. +func (mr *MockAclKeeperMockRecorder) Allowed(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Allowed", reflect.TypeOf((*MockAclKeeper)(nil).Allowed), ctx, addr) +} + +// IsAdmin mocks base method. +func (m *MockAclKeeper) IsAdmin(ctx types.Context, addr types.AccAddress) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAdmin", ctx, addr) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsAdmin indicates an expected call of IsAdmin. +func (mr *MockAclKeeperMockRecorder) IsAdmin(ctx, addr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAdmin", reflect.TypeOf((*MockAclKeeper)(nil).IsAdmin), ctx, addr) +} + // MockAccountKeeper is a mock of AccountKeeper interface. type MockAccountKeeper struct { ctrl *gomock.Controller diff --git a/x/escrow/types/genesis.go b/x/escrow/types/genesis.go index 4f9eac49..4c813700 100644 --- a/x/escrow/types/genesis.go +++ b/x/escrow/types/genesis.go @@ -9,7 +9,10 @@ const DefaultIndex uint64 = 1 func DefaultGenesis() *GenesisState { return &GenesisState{ // this line is used by starport scaffolding # genesis/types/default - Params: DefaultParams(), + Params: DefaultParams(), + ChainletAccounts: []ChainletAccount{}, + Pools: []DenomPool{}, + Funders: []GenesisFunder{}, } } @@ -17,5 +20,35 @@ func DefaultGenesis() *GenesisState { // failure. func (gs GenesisState) Validate() error { // this line is used by starport scaffolding # genesis/types/validate + + // Validate chainlet accounts have unique chain IDs + chainletIDs := make(map[string]bool) + for _, acc := range gs.ChainletAccounts { + if chainletIDs[acc.ChainId] { + return ErrChainletAccountNotFound // reuse existing error type + } + chainletIDs[acc.ChainId] = true + } + + // Validate pools have unique {chainId, denom} pairs + poolKeys := make(map[string]bool) + for _, pool := range gs.Pools { + key := pool.ChainId + "/" + pool.Denom + if poolKeys[key] { + return ErrChainletAccountNotFound + } + poolKeys[key] = true + } + + // Validate funders have unique {chainId, denom, address} tuples + funderKeys := make(map[string]bool) + for _, f := range gs.Funders { + key := f.ChainId + "/" + f.Denom + "/" + f.Address + if funderKeys[key] { + return ErrFunderNotFound + } + funderKeys[key] = true + } + return gs.Params.Validate() } diff --git a/x/escrow/types/genesis.pb.go b/x/escrow/types/genesis.pb.go index 82a81f09..2b42acbc 100644 --- a/x/escrow/types/genesis.pb.go +++ b/x/escrow/types/genesis.pb.go @@ -26,6 +26,12 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the escrow module's genesis state. type GenesisState struct { Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + // List of all chainlet accounts (headers) + ChainletAccounts []ChainletAccount `protobuf:"bytes,2,rep,name=chainlet_accounts,json=chainletAccounts,proto3" json:"chainlet_accounts"` + // List of all denomination pools + Pools []DenomPool `protobuf:"bytes,3,rep,name=pools,proto3" json:"pools"` + // List of all funder positions with their identifiers + Funders []GenesisFunder `protobuf:"bytes,4,rep,name=funders,proto3" json:"funders"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -68,26 +74,128 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetChainletAccounts() []ChainletAccount { + if m != nil { + return m.ChainletAccounts + } + return nil +} + +func (m *GenesisState) GetPools() []DenomPool { + if m != nil { + return m.Pools + } + return nil +} + +func (m *GenesisState) GetFunders() []GenesisFunder { + if m != nil { + return m.Funders + } + return nil +} + +// GenesisFunder wraps Funder with its composite key for genesis export/import +type GenesisFunder struct { + ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + Denom string `protobuf:"bytes,2,opt,name=denom,proto3" json:"denom,omitempty"` + Address string `protobuf:"bytes,3,opt,name=address,proto3" json:"address,omitempty"` + Funder Funder `protobuf:"bytes,4,opt,name=funder,proto3" json:"funder"` +} + +func (m *GenesisFunder) Reset() { *m = GenesisFunder{} } +func (m *GenesisFunder) String() string { return proto.CompactTextString(m) } +func (*GenesisFunder) ProtoMessage() {} +func (*GenesisFunder) Descriptor() ([]byte, []int) { + return fileDescriptor_d20be0fd550c3abf, []int{1} +} +func (m *GenesisFunder) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisFunder) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisFunder.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisFunder) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisFunder.Merge(m, src) +} +func (m *GenesisFunder) XXX_Size() int { + return m.Size() +} +func (m *GenesisFunder) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisFunder.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisFunder proto.InternalMessageInfo + +func (m *GenesisFunder) GetChainId() string { + if m != nil { + return m.ChainId + } + return "" +} + +func (m *GenesisFunder) GetDenom() string { + if m != nil { + return m.Denom + } + return "" +} + +func (m *GenesisFunder) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *GenesisFunder) GetFunder() Funder { + if m != nil { + return m.Funder + } + return Funder{} +} + func init() { proto.RegisterType((*GenesisState)(nil), "ssc.escrow.GenesisState") + proto.RegisterType((*GenesisFunder)(nil), "ssc.escrow.GenesisFunder") } func init() { proto.RegisterFile("ssc/escrow/genesis.proto", fileDescriptor_d20be0fd550c3abf) } var fileDescriptor_d20be0fd550c3abf = []byte{ - // 188 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x28, 0x2e, 0x4e, 0xd6, - 0x4f, 0x2d, 0x4e, 0x2e, 0xca, 0x2f, 0xd7, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, - 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2a, 0x2e, 0x4e, 0xd6, 0x83, 0xc8, 0x48, 0x89, 0xa4, 0xe7, - 0xa7, 0xe7, 0x83, 0x85, 0xf5, 0x41, 0x2c, 0x88, 0x0a, 0x29, 0x71, 0x24, 0xbd, 0x05, 0x89, 0x45, - 0x89, 0xb9, 0x50, 0xad, 0x4a, 0x0e, 0x5c, 0x3c, 0xee, 0x10, 0xb3, 0x82, 0x4b, 0x12, 0x4b, 0x52, - 0x85, 0x0c, 0xb8, 0xd8, 0x20, 0xf2, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0x42, 0x7a, 0x08, - 0xb3, 0xf5, 0x02, 0xc0, 0x32, 0x4e, 0x2c, 0x27, 0xee, 0xc9, 0x33, 0x04, 0x41, 0xd5, 0x39, 0xd9, - 0x9f, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, 0x1e, 0xcb, - 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x6a, 0x7a, 0x66, 0x49, 0x46, - 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x7e, 0x71, 0x62, 0x7a, 0x62, 0x45, 0x65, 0x95, 0x3e, 0xc8, - 0x1d, 0x15, 0x30, 0x97, 0x94, 0x54, 0x16, 0xa4, 0x16, 0x27, 0xb1, 0x81, 0x5d, 0x62, 0x0c, 0x08, - 0x00, 0x00, 0xff, 0xff, 0x3f, 0xa2, 0xfd, 0xde, 0xe0, 0x00, 0x00, 0x00, + // 355 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x91, 0xcf, 0x4a, 0xc3, 0x40, + 0x10, 0xc6, 0x93, 0xfe, 0xd5, 0xad, 0x82, 0x2e, 0x15, 0xb7, 0x15, 0x62, 0x29, 0x08, 0x3d, 0x25, + 0x5a, 0x4f, 0x9e, 0xc4, 0x2a, 0x8a, 0x17, 0x29, 0xf5, 0xe6, 0xa5, 0x6c, 0x37, 0x6b, 0x1a, 0x68, + 0xb3, 0x21, 0xb3, 0xc5, 0xd6, 0x67, 0xf0, 0xe0, 0x63, 0xf5, 0xd8, 0xa3, 0x27, 0x91, 0xf6, 0x3d, + 0x44, 0xb2, 0xbb, 0xc5, 0x14, 0x3d, 0x25, 0x33, 0xf3, 0xfb, 0x66, 0xbe, 0x9d, 0x41, 0x04, 0x80, + 0x79, 0x1c, 0x58, 0x22, 0x5e, 0xbc, 0x80, 0x47, 0x1c, 0x42, 0x70, 0xe3, 0x44, 0x48, 0x81, 0x11, + 0x00, 0x73, 0x75, 0xa5, 0x5e, 0x0d, 0x44, 0x20, 0x54, 0xda, 0x4b, 0xff, 0x34, 0x51, 0x3f, 0xcc, + 0x68, 0x63, 0x9a, 0xd0, 0x31, 0xfc, 0x53, 0xd0, 0x1f, 0x5d, 0x68, 0x7e, 0xdb, 0x68, 0xe7, 0x4e, + 0x4f, 0x79, 0x94, 0x54, 0x72, 0x7c, 0x8a, 0x4a, 0x5a, 0x49, 0xec, 0x86, 0xdd, 0xaa, 0xb4, 0xb1, + 0xfb, 0x3b, 0xd5, 0xed, 0xaa, 0x4a, 0xa7, 0x30, 0xff, 0x3c, 0xb6, 0x7a, 0x86, 0xc3, 0x0f, 0x68, + 0x9f, 0x0d, 0x69, 0x18, 0x8d, 0xb8, 0xec, 0x53, 0xc6, 0xc4, 0x24, 0x92, 0x40, 0x72, 0x8d, 0x7c, + 0xab, 0xd2, 0x3e, 0xca, 0x8a, 0xaf, 0x0d, 0x74, 0xa5, 0x19, 0xd3, 0x65, 0x8f, 0x6d, 0xa6, 0x01, + 0x9f, 0xa1, 0x62, 0x2c, 0xc4, 0x08, 0x48, 0x5e, 0xf5, 0x38, 0xc8, 0xf6, 0xb8, 0xe1, 0x91, 0x18, + 0x77, 0x85, 0x18, 0x19, 0xb5, 0x26, 0xf1, 0x05, 0x2a, 0x3f, 0x4f, 0x22, 0x9f, 0x27, 0x40, 0x0a, + 0x4a, 0x54, 0xcb, 0x8a, 0xcc, 0xfb, 0x6e, 0x15, 0x61, 0x84, 0x6b, 0xbe, 0xf9, 0x66, 0xa3, 0xdd, + 0x0d, 0x00, 0xd7, 0xd0, 0x96, 0xf2, 0xd4, 0x0f, 0x7d, 0xb5, 0x83, 0xed, 0x5e, 0x59, 0xc5, 0xf7, + 0x3e, 0xae, 0xa2, 0xa2, 0x9f, 0x3a, 0x20, 0x39, 0x95, 0xd7, 0x01, 0x26, 0xa8, 0x4c, 0x7d, 0x3f, + 0xe1, 0x90, 0x5a, 0x56, 0xbc, 0x09, 0xd3, 0x65, 0xea, 0x39, 0xa4, 0xf0, 0x77, 0x99, 0x1b, 0x7e, + 0x0c, 0xd7, 0xb9, 0x9c, 0x2f, 0x1d, 0x7b, 0xb1, 0x74, 0xec, 0xaf, 0xa5, 0x63, 0xbf, 0xaf, 0x1c, + 0x6b, 0xb1, 0x72, 0xac, 0x8f, 0x95, 0x63, 0x3d, 0x9d, 0x04, 0xa1, 0x1c, 0x4e, 0x06, 0x2e, 0x13, + 0x63, 0x0f, 0x68, 0x40, 0xa7, 0xb3, 0x57, 0x2f, 0xbd, 0xea, 0x74, 0x7d, 0x57, 0x39, 0x8b, 0x39, + 0x0c, 0x4a, 0xea, 0xae, 0xe7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x5c, 0x69, 0xe3, 0xa7, 0x47, + 0x02, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -110,6 +218,48 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.Funders) > 0 { + for iNdEx := len(m.Funders) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Funders[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } + if len(m.Pools) > 0 { + for iNdEx := len(m.Pools) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Pools[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.ChainletAccounts) > 0 { + for iNdEx := len(m.ChainletAccounts) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ChainletAccounts[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -123,6 +273,60 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *GenesisFunder) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisFunder) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisFunder) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Funder.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + if len(m.Address) > 0 { + i -= len(m.Address) + copy(dAtA[i:], m.Address) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.Address))) + i-- + dAtA[i] = 0x1a + } + if len(m.Denom) > 0 { + i -= len(m.Denom) + copy(dAtA[i:], m.Denom) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.Denom))) + i-- + dAtA[i] = 0x12 + } + if len(m.ChainId) > 0 { + i -= len(m.ChainId) + copy(dAtA[i:], m.ChainId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.ChainId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { offset -= sovGenesis(v) base := offset @@ -142,6 +346,47 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.ChainletAccounts) > 0 { + for _, e := range m.ChainletAccounts { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.Pools) > 0 { + for _, e := range m.Pools { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.Funders) > 0 { + for _, e := range m.Funders { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *GenesisFunder) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ChainId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.Denom) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.Address) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = m.Funder.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -213,6 +458,287 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainletAccounts", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainletAccounts = append(m.ChainletAccounts, ChainletAccount{}) + if err := m.ChainletAccounts[len(m.ChainletAccounts)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pools", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Pools = append(m.Pools, DenomPool{}) + if err := m.Pools[len(m.Pools)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Funders", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Funders = append(m.Funders, GenesisFunder{}) + if err := m.Funders[len(m.Funders)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GenesisFunder) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisFunder: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisFunder: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Denom", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Denom = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Address = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Funder", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Funder.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/escrow/types/genesis_test.go b/x/escrow/types/genesis_test.go index 5ee6cfd0..78be955d 100644 --- a/x/escrow/types/genesis_test.go +++ b/x/escrow/types/genesis_test.go @@ -3,6 +3,8 @@ package types_test import ( "testing" + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/sagaxyz/ssc/x/escrow/types" "github.com/stretchr/testify/require" ) @@ -19,13 +21,72 @@ func TestGenesisState_Validate(t *testing.T) { valid: true, }, { - desc: "valid genesis state", + desc: "valid genesis state", genState: &types.GenesisState{ - + Params: types.DefaultParams(), + ChainletAccounts: []types.ChainletAccount{}, + Pools: []types.DenomPool{}, + Funders: []types.GenesisFunder{}, // this line is used by starport scaffolding # types/genesis/validField }, valid: true, }, + { + desc: "valid genesis state with data", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + ChainletAccounts: []types.ChainletAccount{ + {ChainId: "chain-1"}, + {ChainId: "chain-2"}, + }, + Pools: []types.DenomPool{ + {ChainId: "chain-1", Denom: "usaga", Balance: sdk.NewCoin("usaga", math.NewInt(1000)), Shares: math.LegacyNewDec(1000)}, + }, + Funders: []types.GenesisFunder{ + {ChainId: "chain-1", Denom: "usaga", Address: "saga1abc", Funder: types.Funder{Shares: math.LegacyNewDec(500)}}, + }, + }, + valid: true, + }, + { + desc: "invalid - duplicate chainlet accounts", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + ChainletAccounts: []types.ChainletAccount{ + {ChainId: "chain-1"}, + {ChainId: "chain-1"}, + }, + Pools: []types.DenomPool{}, + Funders: []types.GenesisFunder{}, + }, + valid: false, + }, + { + desc: "invalid - duplicate pools", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + ChainletAccounts: []types.ChainletAccount{}, + Pools: []types.DenomPool{ + {ChainId: "chain-1", Denom: "usaga", Balance: sdk.NewCoin("usaga", math.NewInt(1000)), Shares: math.LegacyNewDec(1000)}, + {ChainId: "chain-1", Denom: "usaga", Balance: sdk.NewCoin("usaga", math.NewInt(500)), Shares: math.LegacyNewDec(500)}, + }, + Funders: []types.GenesisFunder{}, + }, + valid: false, + }, + { + desc: "invalid - duplicate funders", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + ChainletAccounts: []types.ChainletAccount{}, + Pools: []types.DenomPool{}, + Funders: []types.GenesisFunder{ + {ChainId: "chain-1", Denom: "usaga", Address: "saga1abc", Funder: types.Funder{Shares: math.LegacyNewDec(500)}}, + {ChainId: "chain-1", Denom: "usaga", Address: "saga1abc", Funder: types.Funder{Shares: math.LegacyNewDec(300)}}, + }, + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } { t.Run(tc.desc, func(t *testing.T) { diff --git a/x/escrow/types/query.pb.go b/x/escrow/types/query.pb.go index 85051d2a..c8257a0d 100644 --- a/x/escrow/types/query.pb.go +++ b/x/escrow/types/query.pb.go @@ -1067,6 +1067,7 @@ func _Query_GetFunderBalance_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.escrow.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/escrow/types/tx.pb.go b/x/escrow/types/tx.pb.go index 547c6959..7b1722a2 100644 --- a/x/escrow/types/tx.pb.go +++ b/x/escrow/types/tx.pb.go @@ -471,6 +471,7 @@ func _Msg_UpdateParams_Handler(srv interface{}, ctx context.Context, dec func(in return interceptor(ctx, in, info, handler) } +var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.escrow.Msg", HandlerType: (*MsgServer)(nil), diff --git a/x/gmp/keeper/params.go b/x/gmp/keeper/params.go index cad61624..e72b5292 100644 --- a/x/gmp/keeper/params.go +++ b/x/gmp/keeper/params.go @@ -7,6 +7,13 @@ import ( // GetParams get all parameters as types.Params func (k Keeper) GetParams(ctx sdk.Context) types.Params { + var p types.Params + k.paramstore.GetParamSetIfExists(ctx, &p) + // If params were loaded from store, return them; otherwise return defaults + // Note: For empty Params struct, Size() will be 0, so we return defaults + if p.Size() > 0 { + return p + } return types.NewParams() } diff --git a/x/gmp/module_ibc_integration_test.go b/x/gmp/module_ibc_integration_test.go new file mode 100644 index 00000000..a1349245 --- /dev/null +++ b/x/gmp/module_ibc_integration_test.go @@ -0,0 +1,143 @@ +package gmp_test + +import ( + "encoding/json" + "testing" + + "cosmossdk.io/log" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/sagaxyz/ssc/x/gmp" + "github.com/sagaxyz/ssc/x/gmp/types" + "github.com/stretchr/testify/require" + + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" +) + +// TestGMPInTransferStack verifies that GMP middleware properly wraps the transfer module +// and processes packets before forwarding them to the underlying module +func TestGMPInTransferStack(t *testing.T) { + relayer := sdk.AccAddress(address.Module("relayer")) + ctx := sdk.Context{}.WithLogger(log.NewNopLogger()) + + // Create a mock transfer module that tracks if it was called + mockTransfer := &mockIBCModule{ + lastCalled: "", + } + + // Build the stack: transfer -> GMP + transferStack := mockTransfer + gmpStack := gmp.NewIBCModule(transferStack) + + t.Run("GMP processes TypeGeneralMessageWithToken and forwards to transfer", func(t *testing.T) { + // Reset mock state + mockTransfer.lastCalled = "" + + // Create a GMP message with TypeGeneralMessageWithToken + payloadType, err := abi.NewType("string", "", nil) + require.NoError(t, err) + args := abi.Arguments{{Type: payloadType}} + encoded, err := args.Pack("forward-memo") + require.NoError(t, err) + + msg := gmp.Message{ + SourceChain: "chainA", + SourceAddress: "addr", + Payload: encoded, + Type: gmp.TypeGeneralMessageWithToken, + } + memo, err := json.Marshal(msg) + require.NoError(t, err) + + // Create packet with GMP memo + data := transfertypes.FungibleTokenPacketData{ + Denom: "foo", + Amount: "1", + Sender: "sender", + Receiver: "receiver", + Memo: string(memo), + } + bz, err := types.ModuleCdc.MarshalJSON(&data) + require.NoError(t, err) + packet := channeltypes.Packet{Data: bz} + + // Process packet through GMP stack + ack := gmpStack.OnRecvPacket(ctx, types.Version, packet, relayer) + + // Verify GMP processed the packet and forwarded to transfer + require.Equal(t, "OnRecvPacket", mockTransfer.lastCalled) + require.NotNil(t, ack) + + // Verify the packet data was modified (memo should be replaced with unpacked payload) + require.NotNil(t, mockTransfer.lastPacket) + var modifiedData transfertypes.FungibleTokenPacketData + err = types.ModuleCdc.UnmarshalJSON(mockTransfer.lastPacket.Data, &modifiedData) + require.NoError(t, err) + // The memo should be replaced with the unpacked ABI payload + require.Equal(t, "forward-memo", modifiedData.Memo) + }) + + t.Run("GMP forwards non-GMP packets directly to transfer", func(t *testing.T) { + // Reset mock state + mockTransfer.lastCalled = "" + + // Create a regular transfer packet without GMP memo + data := transfertypes.FungibleTokenPacketData{ + Denom: "foo", + Amount: "1", + Sender: "sender", + Receiver: "receiver", + Memo: "regular-memo", + } + bz, err := types.ModuleCdc.MarshalJSON(&data) + require.NoError(t, err) + packet := channeltypes.Packet{Data: bz} + + // Process packet through GMP stack + ack := gmpStack.OnRecvPacket(ctx, types.Version, packet, relayer) + + // Verify packet was forwarded to transfer + require.Equal(t, "OnRecvPacket", mockTransfer.lastCalled) + require.NotNil(t, ack) + + // Verify packet data was not modified + var forwardedData transfertypes.FungibleTokenPacketData + err = types.ModuleCdc.UnmarshalJSON(mockTransfer.lastPacket.Data, &forwardedData) + require.NoError(t, err) + require.Equal(t, "regular-memo", forwardedData.Memo) + }) + + t.Run("GMP processes TypeGeneralMessage and forwards to transfer", func(t *testing.T) { + // Reset mock state + mockTransfer.lastCalled = "" + + msg := gmp.Message{ + SourceChain: "chainA", + SourceAddress: "addr", + Payload: []byte("payload"), + Type: gmp.TypeGeneralMessage, + } + memo, err := json.Marshal(msg) + require.NoError(t, err) + + data := transfertypes.FungibleTokenPacketData{ + Denom: "foo", + Amount: "1", + Sender: "sender", + Receiver: "receiver", + Memo: string(memo), + } + bz, err := types.ModuleCdc.MarshalJSON(&data) + require.NoError(t, err) + packet := channeltypes.Packet{Data: bz} + + ack := gmpStack.OnRecvPacket(ctx, types.Version, packet, relayer) + + // Verify GMP processed and forwarded + require.Equal(t, "OnRecvPacket", mockTransfer.lastCalled) + require.NotNil(t, ack) + }) +} + diff --git a/x/gmp/types/query.pb.go b/x/gmp/types/query.pb.go index 8942b216..9a5d1004 100644 --- a/x/gmp/types/query.pb.go +++ b/x/gmp/types/query.pb.go @@ -212,6 +212,7 @@ func _Query_Params_Handler(srv interface{}, ctx context.Context, dec func(interf return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.gmp.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/gmp/types/tx.pb.go b/x/gmp/types/tx.pb.go index e2ba400f..bd2e7657 100644 --- a/x/gmp/types/tx.pb.go +++ b/x/gmp/types/tx.pb.go @@ -74,6 +74,7 @@ func RegisterMsgServer(s grpc1.Server, srv MsgServer) { s.RegisterService(&_Msg_serviceDesc, srv) } +var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.gmp.Msg", HandlerType: (*MsgServer)(nil), diff --git a/x/peers/genesis.go b/x/peers/genesis.go index 0f6c9bd9..69bd0d43 100644 --- a/x/peers/genesis.go +++ b/x/peers/genesis.go @@ -9,8 +9,20 @@ import ( // InitGenesis initializes the module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // this line is used by starport scaffolding # genesis/module/init + // Set params k.SetParams(ctx, genState.Params) + + // Import peer data + for _, pd := range genState.PeerData { + k.ImportPeerData(ctx, pd.ChainId, pd.ValidatorAddress, pd.Data) + } + + // Import chain counters + for _, cc := range genState.ChainCounters { + k.ImportChainCounter(ctx, cc.ChainId, cc.Counter) + } + + // this line is used by starport scaffolding # genesis/module/init } // ExportGenesis returns the module's exported genesis @@ -18,6 +30,12 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesis() genesis.Params = k.GetParams(ctx) + // Export peer data + genesis.PeerData = k.ExportPeerData(ctx) + + // Export chain counters + genesis.ChainCounters = k.ExportChainCounters(ctx) + // this line is used by starport scaffolding # genesis/module/export return genesis diff --git a/x/peers/keeper/data_test.go b/x/peers/keeper/data_test.go index 94cf2404..23e5fbff 100644 --- a/x/peers/keeper/data_test.go +++ b/x/peers/keeper/data_test.go @@ -7,7 +7,7 @@ import ( types "github.com/sagaxyz/ssc/x/peers/types" ) -func (s *KeeperTestSuite) TestDataStorage() { +func (s *TestSuite) TestDataStorage() { _, _, addrA := testdata.KeyTestPubAddr() valAddrA := sdk.ValAddress(addrA) _, _, addrB := testdata.KeyTestPubAddr() diff --git a/x/peers/keeper/genesis.go b/x/peers/keeper/genesis.go new file mode 100644 index 00000000..e5495f4e --- /dev/null +++ b/x/peers/keeper/genesis.go @@ -0,0 +1,78 @@ +package keeper + +import ( + "cosmossdk.io/store/prefix" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sagaxyz/ssc/x/peers/types" +) + +// ExportPeerData exports all peer data from the store +func (k Keeper) ExportPeerData(ctx sdk.Context) []types.GenesisPeerData { + dataStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.DataKey) + chainStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainsKey) + + var peerData []types.GenesisPeerData + + // Iterate over all chain IDs + chainIterator := chainStore.Iterator(nil, nil) + defer chainIterator.Close() + + for ; chainIterator.Valid(); chainIterator.Next() { + chainID := string(chainIterator.Key()) + + // For each chain, iterate over all validators + chainDataStore := prefix.NewStore(dataStore, types.KeyPrefix(chainID)) + dataIterator := chainDataStore.Iterator(nil, nil) + + for ; dataIterator.Valid(); dataIterator.Next() { + validatorAddr := string(dataIterator.Key()) + var data types.Data + k.cdc.MustUnmarshal(dataIterator.Value(), &data) + + peerData = append(peerData, types.GenesisPeerData{ + ChainId: chainID, + ValidatorAddress: validatorAddr, + Data: data, + }) + } + _ = dataIterator.Close() + } + + return peerData +} + +// ExportChainCounters exports all chain counters from the store +func (k Keeper) ExportChainCounters(ctx sdk.Context) []types.GenesisChainCounter { + chainStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainsKey) + + var counters []types.GenesisChainCounter + + iterator := chainStore.Iterator(nil, nil) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + var counter types.Counter + k.cdc.MustUnmarshal(iterator.Value(), &counter) + + counters = append(counters, types.GenesisChainCounter{ + ChainId: string(iterator.Key()), + Counter: counter, + }) + } + + return counters +} + +// ImportPeerData imports a single peer data entry into the store +func (k Keeper) ImportPeerData(ctx sdk.Context, chainID, validatorAddr string, data types.Data) { + dataStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.DataKey) + chainDataStore := prefix.NewStore(dataStore, types.KeyPrefix(chainID)) + chainDataStore.Set([]byte(validatorAddr), k.cdc.MustMarshal(&data)) +} + +// ImportChainCounter imports a single chain counter into the store +func (k Keeper) ImportChainCounter(ctx sdk.Context, chainID string, counter types.Counter) { + chainStore := prefix.NewStore(ctx.KVStore(k.storeKey), types.ChainsKey) + chainStore.Set([]byte(chainID), k.cdc.MustMarshal(&counter)) +} diff --git a/x/peers/keeper/hooks_test.go b/x/peers/keeper/hooks_test.go index 186a6211..dbcdb703 100644 --- a/x/peers/keeper/hooks_test.go +++ b/x/peers/keeper/hooks_test.go @@ -9,7 +9,7 @@ import ( types "github.com/sagaxyz/ssc/x/peers/types" ) -func (s *KeeperTestSuite) TestAfterValidatorRemoved() { +func (s *TestSuite) TestAfterValidatorRemoved() { require := s.Require() _, _, addr := testdata.KeyTestPubAddr() @@ -23,13 +23,13 @@ func (s *KeeperTestSuite) TestAfterValidatorRemoved() { _, err := s.msgServer.SetPeers(s.ctx, &types.MsgSetPeers{ Validator: accAddr.String(), ChainId: chainIDs[0], - Peers: []string{"a", "b"}, + Peers: addrs[chainIDs[0]], }) require.NoError(err) _, err = s.msgServer.SetPeers(s.ctx, &types.MsgSetPeers{ Validator: accAddr.String(), ChainId: chainIDs[1], - Peers: []string{"c", "d"}, + Peers: addrs[chainIDs[1]], }) require.NoError(err) @@ -37,7 +37,7 @@ func (s *KeeperTestSuite) TestAfterValidatorRemoved() { ChainId: chainIDs[0], }) require.NoError(err) - require.Equal([]string{"a", "b"}, resp.Peers) + require.Equal(addrs[chainIDs[0]], resp.Peers) err = s.peersKeeper.Hooks().AfterValidatorRemoved(s.ctx, consAddr, valAddr) require.NoError(err) diff --git a/x/peers/keeper/keeper_test.go b/x/peers/keeper/keeper_test.go index 2018ef85..24968d72 100644 --- a/x/peers/keeper/keeper_test.go +++ b/x/peers/keeper/keeper_test.go @@ -4,8 +4,8 @@ import ( "testing" storetypes "cosmossdk.io/store/types" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - cmttime "github.com/cometbft/cometbft/types/time" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + tmtime "github.com/cometbft/cometbft/types/time" "github.com/cosmos/cosmos-sdk/baseapp" sdktestutil "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" @@ -15,16 +15,25 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/suite" + "github.com/sagaxyz/ssc/x/peers/keeper" testutil "github.com/sagaxyz/ssc/x/peers/testutil" "github.com/sagaxyz/ssc/x/peers/types" - "github.com/sagaxyz/ssc/x/peers/keeper" ) var ( chainIDs = []string{"chain_1-1", "chain_2-1", "chain_3-1"} + addrs = map[string][]string{ + "chain_1-1": {"aa@123.123.123.123:1234", "bb@111.111.111.111:1234"}, + "chain_2-1": {"cc@100.100.100.100:1234", "dd@example.com:1234"}, + "chain_3-1": {"ee@google.com:1234"}, + } + accounts = []sdk.AccAddress{ + sdk.AccAddress("test1"), + sdk.AccAddress("test2"), + } ) -type KeeperTestSuite struct { +type TestSuite struct { suite.Suite ctx sdk.Context @@ -34,11 +43,21 @@ type KeeperTestSuite struct { msgServer types.MsgServer } -func (s *KeeperTestSuite) SetupTest() { +func (s *TestSuite) SetupTest() { storeKey := storetypes.NewKVStoreKey(types.StoreKey) - storeTKey := storetypes.NewTransientStoreKey("transient_test") - testCtx := sdktestutil.DefaultContextWithDB(s.T(), storeKey, storeTKey) - ctx := testCtx.Ctx.WithBlockHeader(cmtproto.Header{Time: cmttime.Now()}) + paramsKey := storetypes.NewKVStoreKey(paramstypes.StoreKey) + paramsTKey := storetypes.NewTransientStoreKey(paramstypes.TStoreKey) + ctx := sdktestutil.DefaultContextWithKeys( + map[string]*storetypes.KVStoreKey{ + types.StoreKey: storeKey, + paramstypes.StoreKey: paramsKey, + }, + map[string]*storetypes.TransientStoreKey{ + paramstypes.TStoreKey: paramsTKey, + }, + nil, + ) + s.ctx = ctx.WithBlockHeader(tmproto.Header{Time: tmtime.Now()}) encCfg := moduletestutil.MakeTestEncodingConfig() // gomock initializations @@ -46,8 +65,6 @@ func (s *KeeperTestSuite) SetupTest() { s.chainletKeeper = testutil.NewMockChainletKeeper(ctrl) //nolint:staticcheck - paramsKey := storetypes.NewKVStoreKey(paramstypes.StoreKey) - paramsTKey := storetypes.NewTransientStoreKey(paramstypes.TStoreKey) paramsKeeper := paramskeeper.NewKeeper(encCfg.Codec, encCfg.Amino, paramsKey, paramsTKey) //nolint:staticcheck paramsKeeper.Subspace(paramstypes.ModuleName) paramsKeeper.Subspace(types.ModuleName) @@ -69,6 +86,6 @@ func (s *KeeperTestSuite) SetupTest() { s.msgServer = keeper.NewMsgServerImpl(s.peersKeeper) } -func TestKeeperTestSuite(t *testing.T) { - suite.Run(t, new(KeeperTestSuite)) +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) } diff --git a/x/peers/keeper/msg_server_set_peers.go b/x/peers/keeper/msg_server_set_peers.go index 49d1aef4..c5020920 100644 --- a/x/peers/keeper/msg_server_set_peers.go +++ b/x/peers/keeper/msg_server_set_peers.go @@ -2,7 +2,14 @@ package keeper import ( "context" + "encoding/hex" "errors" + "fmt" + "math" + "net" + "regexp" + "strconv" + "strings" cosmossdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -11,6 +18,43 @@ import ( "github.com/sagaxyz/ssc/x/peers/types" ) +var hostAllowed = regexp.MustCompile(`^[A-Za-z0-9\.\-\[\]:]+$`) // hostname or IP + +// Basic validation of the ID@addr:port format. +// Only needs the bare minimum check for safety. +func validateAddress(addr string) error { + parts := strings.Split(addr, "@") + if len(parts) != 2 { + return errors.New("missing @") + } + id := parts[0] + hostPort := parts[1] + + // Check ID is a hex string + if len(id) == 0 { + return errors.New("missing ID") + } + _, err := hex.DecodeString(id) + if err != nil { + return err + } + + // Check valid host:port + host, port, err := net.SplitHostPort(hostPort) + if err != nil { + return err + } + _, err = strconv.Atoi(port) // port is a number + if err != nil { + return err + } + if !hostAllowed.MatchString(host) { // only allowed characters + return errors.New("invalid characters in host") + } + + return nil +} + func (k msgServer) SetPeers(goCtx context.Context, msg *types.MsgSetPeers) (resp *types.MsgSetPeersResponse, err error) { err = msg.ValidateBasic() if err != nil { @@ -24,6 +68,29 @@ func (k msgServer) SetPeers(goCtx context.Context, msg *types.MsgSetPeers) (resp return } + if len(msg.Peers) == 0 { + err = errors.New("no peers provided") + return + } + p := k.GetParams(ctx) + var dataSize uint32 + for _, addr := range msg.Peers { + if len(addr) > math.MaxUint32 { + err = errors.New("data size exceeds uint32") + return + } + dataSize += uint32(len(addr)) + if dataSize > p.MaxData { + err = fmt.Errorf("exceeded maximum size (%d) of peers", p.MaxData) + return + } + err = validateAddress(addr) + if err != nil { + err = fmt.Errorf("invalid addr '%s' in peers: %w", addr, err) + return + } + } + accAddr, err := sdk.AccAddressFromBech32(msg.Validator) if err != nil { err = cosmossdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address (%s)", err) diff --git a/x/peers/keeper/msg_server_set_peers_test.go b/x/peers/keeper/msg_server_set_peers_test.go new file mode 100644 index 00000000..f99446a3 --- /dev/null +++ b/x/peers/keeper/msg_server_set_peers_test.go @@ -0,0 +1,78 @@ +package keeper_test + +import ( + "errors" + "strings" + + "github.com/golang/mock/gomock" + chainlettypes "github.com/sagaxyz/ssc/x/chainlet/types" + "github.com/sagaxyz/ssc/x/peers/types" +) + +func (s *TestSuite) TestChainIDValidation() { + s.SetupTest() + + // Chainlet exists + s.chainletKeeper.EXPECT().Chainlet(gomock.Any(), chainIDs[0]).Return(chainlettypes.Chainlet{ChainId: chainIDs[0]}, nil) + _, err := s.msgServer.SetPeers(s.ctx, types.NewMsgSetPeers(accounts[0].String(), chainIDs[0], addrs[chainIDs[0]]...)) + s.Require().NoError(err) + + // Chainlet does not exists + s.chainletKeeper.EXPECT().Chainlet(gomock.Any(), "something").Return(chainlettypes.Chainlet{}, errors.New("nope")) + _, err = s.msgServer.SetPeers(s.ctx, types.NewMsgSetPeers(accounts[0].String(), "something", addrs[chainIDs[0]]...)) + s.Require().Error(err) +} +func (s *TestSuite) TestPeersValidation() { + tests := []struct { + peers []string + expErr bool + }{ + {addrs[chainIDs[0]], false}, + {addrs[chainIDs[1]], false}, + {addrs[chainIDs[2]], false}, + {[]string{"aa@127.0.0.1:1234"}, false}, + {[]string{"aa@example.com:1234"}, false}, + {[]string{"abcd"}, true}, + {[]string{""}, true}, + {[]string{}, true}, + {[]string{"127.0.0.1:1234"}, true}, + {[]string{"@"}, true}, + {[]string{"aa@b"}, true}, + {[]string{"aa@"}, true}, + {[]string{"@127.0.0.1"}, true}, + {[]string{"@127.0.0.1:1234"}, true}, // missing ID + {[]string{"aa@127.0.0.1:y"}, true}, // invalid port + {[]string{"a@127.0.0.1:1234"}, true}, // invalid hex ID + {[]string{"xx@127.0.0.1:1234"}, true}, // invalid hex ID + {[]string{"aa'@127.0.0.1:1234"}, true}, // invalid character + {[]string{"aa@'127.0.0.1:1234"}, true}, // invalid character + {[]string{"aa@127.0.0.1:1234'"}, true}, // invalid character + {[]string{"aa@127.0.0.1:1234 "}, true}, // invalid character + {[]string{"aa\"@127.0.0.1:1234"}, true}, // invalid character + {[]string{"aa@\"127.0.0.1:1234"}, true}, // invalid character + {[]string{"aa@127.0.0.1:1234\""}, true}, // invalid character + // Test size limit + {[]string{strings.Repeat("aa", 1000) + "@example.com:1234"}, true}, + {[]string{strings.Repeat("aa", 300) + "@example.com:1234"}, false}, + {[]string{strings.Repeat("bb", 300) + "@example2.com:1234"}, false}, + {[]string{ + strings.Repeat("aa", 300) + "@example.com:1234", + strings.Repeat("bb", 300) + "@example2.com:1234", + }, true}, + } + + for _, tt := range tests { + s.Run(strings.Join(tt.peers, ","), func() { + s.SetupTest() + + s.chainletKeeper.EXPECT().Chainlet(gomock.Any(), chainIDs[0]).Return(chainlettypes.Chainlet{ChainId: chainIDs[0]}, nil).AnyTimes() + + _, err := s.msgServer.SetPeers(s.ctx, types.NewMsgSetPeers(accounts[0].String(), chainIDs[0], tt.peers...)) + if tt.expErr { + s.Require().Error(err) + } else { + s.Require().NoError(err) + } + }) + } +} diff --git a/x/peers/module.go b/x/peers/module.go index c004ebea..1ef5c33d 100644 --- a/x/peers/module.go +++ b/x/peers/module.go @@ -71,7 +71,8 @@ func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncod // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { - types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) //nolint:errcheck + //nolint:errcheck // gRPC gateway registration errors are non-critical at startup + types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)) } // GetTxCmd returns the root Tx command for the module. The subcommands of this root command are used by end-users to generate new transactions containing messages defined in the module diff --git a/x/peers/types/genesis.go b/x/peers/types/genesis.go index 6dec591f..0c4640f4 100644 --- a/x/peers/types/genesis.go +++ b/x/peers/types/genesis.go @@ -1,5 +1,7 @@ package types +import "fmt" + // this line is used by starport scaffolding # genesis/types/import // DefaultIndex is the default global index @@ -10,7 +12,9 @@ func DefaultGenesis() *GenesisState { df := DefaultParams() return &GenesisState{ // this line is used by starport scaffolding # genesis/types/default - Params: df, + Params: df, + PeerData: []GenesisPeerData{}, + ChainCounters: []GenesisChainCounter{}, } } @@ -19,5 +23,24 @@ func DefaultGenesis() *GenesisState { func (gs GenesisState) Validate() error { // this line is used by starport scaffolding # genesis/types/validate + // Validate peer data entries have unique {chainId, validatorAddress} pairs + peerDataKeys := make(map[string]bool) + for _, pd := range gs.PeerData { + key := pd.ChainId + "/" + pd.ValidatorAddress + if peerDataKeys[key] { + return fmt.Errorf("duplicate peer data for chain %s and validator %s", pd.ChainId, pd.ValidatorAddress) + } + peerDataKeys[key] = true + } + + // Validate chain counters have unique chain IDs + chainCounterKeys := make(map[string]bool) + for _, cc := range gs.ChainCounters { + if chainCounterKeys[cc.ChainId] { + return fmt.Errorf("duplicate chain counter for chain %s", cc.ChainId) + } + chainCounterKeys[cc.ChainId] = true + } + return gs.Params.Validate() } diff --git a/x/peers/types/genesis.pb.go b/x/peers/types/genesis.pb.go index 2ff1fe75..4f4d5288 100644 --- a/x/peers/types/genesis.pb.go +++ b/x/peers/types/genesis.pb.go @@ -26,6 +26,10 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // GenesisState defines the peers module's genesis state. type GenesisState struct { Params Params `protobuf:"bytes,1,opt,name=params,proto3" json:"params"` + // Peer data entries with their composite keys + PeerData []GenesisPeerData `protobuf:"bytes,2,rep,name=peer_data,json=peerData,proto3" json:"peer_data"` + // Chain counters + ChainCounters []GenesisChainCounter `protobuf:"bytes,3,rep,name=chain_counters,json=chainCounters,proto3" json:"chain_counters"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -68,26 +72,167 @@ func (m *GenesisState) GetParams() Params { return Params{} } +func (m *GenesisState) GetPeerData() []GenesisPeerData { + if m != nil { + return m.PeerData + } + return nil +} + +func (m *GenesisState) GetChainCounters() []GenesisChainCounter { + if m != nil { + return m.ChainCounters + } + return nil +} + +// GenesisPeerData wraps peer Data with its composite key (chainId, validator address) +type GenesisPeerData struct { + ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + ValidatorAddress string `protobuf:"bytes,2,opt,name=validator_address,json=validatorAddress,proto3" json:"validator_address,omitempty"` + Data Data `protobuf:"bytes,3,opt,name=data,proto3" json:"data"` +} + +func (m *GenesisPeerData) Reset() { *m = GenesisPeerData{} } +func (m *GenesisPeerData) String() string { return proto.CompactTextString(m) } +func (*GenesisPeerData) ProtoMessage() {} +func (*GenesisPeerData) Descriptor() ([]byte, []int) { + return fileDescriptor_88809c013959c1db, []int{1} +} +func (m *GenesisPeerData) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisPeerData) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisPeerData.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisPeerData) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisPeerData.Merge(m, src) +} +func (m *GenesisPeerData) XXX_Size() int { + return m.Size() +} +func (m *GenesisPeerData) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisPeerData.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisPeerData proto.InternalMessageInfo + +func (m *GenesisPeerData) GetChainId() string { + if m != nil { + return m.ChainId + } + return "" +} + +func (m *GenesisPeerData) GetValidatorAddress() string { + if m != nil { + return m.ValidatorAddress + } + return "" +} + +func (m *GenesisPeerData) GetData() Data { + if m != nil { + return m.Data + } + return Data{} +} + +// GenesisChainCounter stores the validator count for a chain +type GenesisChainCounter struct { + ChainId string `protobuf:"bytes,1,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + Counter Counter `protobuf:"bytes,2,opt,name=counter,proto3" json:"counter"` +} + +func (m *GenesisChainCounter) Reset() { *m = GenesisChainCounter{} } +func (m *GenesisChainCounter) String() string { return proto.CompactTextString(m) } +func (*GenesisChainCounter) ProtoMessage() {} +func (*GenesisChainCounter) Descriptor() ([]byte, []int) { + return fileDescriptor_88809c013959c1db, []int{2} +} +func (m *GenesisChainCounter) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisChainCounter) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisChainCounter.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisChainCounter) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisChainCounter.Merge(m, src) +} +func (m *GenesisChainCounter) XXX_Size() int { + return m.Size() +} +func (m *GenesisChainCounter) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisChainCounter.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisChainCounter proto.InternalMessageInfo + +func (m *GenesisChainCounter) GetChainId() string { + if m != nil { + return m.ChainId + } + return "" +} + +func (m *GenesisChainCounter) GetCounter() Counter { + if m != nil { + return m.Counter + } + return Counter{} +} + func init() { proto.RegisterType((*GenesisState)(nil), "ssc.peers.GenesisState") + proto.RegisterType((*GenesisPeerData)(nil), "ssc.peers.GenesisPeerData") + proto.RegisterType((*GenesisChainCounter)(nil), "ssc.peers.GenesisChainCounter") } func init() { proto.RegisterFile("ssc/peers/genesis.proto", fileDescriptor_88809c013959c1db) } var fileDescriptor_88809c013959c1db = []byte{ - // 187 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2f, 0x2e, 0x4e, 0xd6, - 0x2f, 0x48, 0x4d, 0x2d, 0x2a, 0xd6, 0x4f, 0x4f, 0xcd, 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, - 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x2c, 0x2e, 0x4e, 0xd6, 0x03, 0x4b, 0x48, 0x89, 0xa4, 0xe7, 0xa7, - 0xe7, 0x83, 0x45, 0xf5, 0x41, 0x2c, 0x88, 0x02, 0x29, 0x31, 0x84, 0xce, 0x82, 0xc4, 0xa2, 0xc4, - 0x5c, 0xa8, 0x46, 0x25, 0x7b, 0x2e, 0x1e, 0x77, 0x88, 0x49, 0xc1, 0x25, 0x89, 0x25, 0xa9, 0x42, - 0xfa, 0x5c, 0x6c, 0x10, 0x79, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x6e, 0x23, 0x41, 0x3d, 0xb8, 0xc9, - 0x7a, 0x01, 0x60, 0x09, 0x27, 0x96, 0x13, 0xf7, 0xe4, 0x19, 0x82, 0xa0, 0xca, 0x9c, 0xec, 0x4e, - 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e, 0xf1, 0xc1, 0x23, 0x39, 0xc6, 0x09, 0x8f, 0xe5, 0x18, - 0x2e, 0x3c, 0x96, 0x63, 0xb8, 0xf1, 0x58, 0x8e, 0x21, 0x4a, 0x25, 0x3d, 0xb3, 0x24, 0xa3, 0x34, - 0x49, 0x2f, 0x39, 0x3f, 0x57, 0xbf, 0x38, 0x31, 0x3d, 0xb1, 0xa2, 0xb2, 0x4a, 0x1f, 0xe4, 0x8a, - 0x0a, 0xa8, 0x3b, 0x4a, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0xee, 0x30, 0x06, 0x04, 0x00, - 0x00, 0xff, 0xff, 0xa5, 0x9f, 0x4b, 0xa0, 0xdb, 0x00, 0x00, 0x00, + // 365 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0x4d, 0x4f, 0xf2, 0x30, + 0x1c, 0xdf, 0x80, 0xf0, 0x52, 0x9e, 0xe7, 0xe1, 0xa1, 0x12, 0x9d, 0x3b, 0x54, 0x42, 0x3c, 0x60, + 0x4c, 0xb6, 0x64, 0x9e, 0x35, 0x11, 0x4c, 0x8c, 0xf1, 0x42, 0xf0, 0xe6, 0x85, 0x94, 0xb5, 0x19, + 0x4b, 0x84, 0x2e, 0x6d, 0x31, 0xe0, 0xd9, 0x0f, 0xe0, 0xc7, 0x22, 0xf1, 0xc2, 0xd1, 0x93, 0x31, + 0xf0, 0x45, 0xcc, 0xda, 0x0a, 0xf3, 0x25, 0xde, 0xd6, 0xdf, 0xff, 0xf7, 0xf2, 0xff, 0xad, 0x05, + 0x7b, 0x42, 0x84, 0x7e, 0x42, 0x29, 0x17, 0x7e, 0x44, 0x27, 0x54, 0xc4, 0xc2, 0x4b, 0x38, 0x93, + 0x0c, 0x56, 0x84, 0x08, 0x3d, 0x35, 0x70, 0x1b, 0x11, 0x8b, 0x98, 0x42, 0xfd, 0xf4, 0x4b, 0x13, + 0xdc, 0xdd, 0xad, 0x32, 0xc1, 0x1c, 0x8f, 0x8d, 0xd0, 0x6d, 0x6c, 0x71, 0x82, 0x25, 0xd6, 0x68, + 0xeb, 0xd9, 0x06, 0x7f, 0x2e, 0x75, 0xc0, 0x8d, 0xc4, 0x92, 0x42, 0x1f, 0x14, 0xb5, 0xcc, 0xb1, + 0x9b, 0x76, 0xbb, 0x1a, 0xd4, 0xbd, 0x4d, 0xa0, 0xd7, 0x53, 0x83, 0x4e, 0x61, 0xf1, 0x7a, 0x60, + 0xf5, 0x0d, 0x0d, 0x9e, 0x82, 0x4a, 0x3a, 0x1d, 0xa4, 0xa6, 0x4e, 0xae, 0x99, 0x6f, 0x57, 0x03, + 0x37, 0xa3, 0x31, 0xe6, 0x3d, 0x4a, 0xf9, 0x05, 0x96, 0xd8, 0x88, 0xcb, 0x89, 0x39, 0xc3, 0x6b, + 0xf0, 0x2f, 0x1c, 0xe1, 0x78, 0x32, 0x08, 0xd9, 0x74, 0x22, 0x29, 0x17, 0x4e, 0x5e, 0x79, 0xa0, + 0xef, 0x1e, 0xdd, 0x94, 0xd7, 0xd5, 0x34, 0xe3, 0xf3, 0x37, 0xcc, 0x60, 0xa2, 0xf5, 0x68, 0x83, + 0xda, 0x97, 0x40, 0xb8, 0x0f, 0xca, 0x3a, 0x20, 0x26, 0xaa, 0x52, 0xa5, 0x5f, 0x52, 0xe7, 0x2b, + 0x02, 0x8f, 0x41, 0xfd, 0x1e, 0xdf, 0xc5, 0x04, 0x4b, 0xc6, 0x07, 0x98, 0x10, 0x4e, 0x85, 0x70, + 0x72, 0x8a, 0xf3, 0x7f, 0x33, 0x38, 0xd7, 0x38, 0x3c, 0x02, 0x05, 0x55, 0x31, 0xaf, 0x7e, 0x4b, + 0x2d, 0xb3, 0x5e, 0xa6, 0x97, 0xa2, 0xb4, 0x08, 0xd8, 0xf9, 0x61, 0xe5, 0xdf, 0x36, 0x09, 0x40, + 0xc9, 0xf4, 0x57, 0xf9, 0xd5, 0x00, 0x66, 0xfc, 0x3f, 0x57, 0xfe, 0x20, 0x76, 0xce, 0x16, 0x2b, + 0x64, 0x2f, 0x57, 0xc8, 0x7e, 0x5b, 0x21, 0xfb, 0x69, 0x8d, 0xac, 0xe5, 0x1a, 0x59, 0x2f, 0x6b, + 0x64, 0xdd, 0x1e, 0x46, 0xb1, 0x1c, 0x4d, 0x87, 0x5e, 0xc8, 0xc6, 0xbe, 0xc0, 0x11, 0x9e, 0xcd, + 0x1f, 0xfc, 0xf4, 0xf6, 0x67, 0xe6, 0xfe, 0xe5, 0x3c, 0xa1, 0x62, 0x58, 0x54, 0x2f, 0xe0, 0xe4, + 0x3d, 0x00, 0x00, 0xff, 0xff, 0xe2, 0xd1, 0x77, 0xe1, 0x6b, 0x02, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -110,6 +255,34 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ChainCounters) > 0 { + for iNdEx := len(m.ChainCounters) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.ChainCounters[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + } + } + if len(m.PeerData) > 0 { + for iNdEx := len(m.PeerData) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.PeerData[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + } { size, err := m.Params.MarshalToSizedBuffer(dAtA[:i]) if err != nil { @@ -123,6 +296,93 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *GenesisPeerData) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisPeerData) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisPeerData) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Data.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + if len(m.ValidatorAddress) > 0 { + i -= len(m.ValidatorAddress) + copy(dAtA[i:], m.ValidatorAddress) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.ValidatorAddress))) + i-- + dAtA[i] = 0x12 + } + if len(m.ChainId) > 0 { + i -= len(m.ChainId) + copy(dAtA[i:], m.ChainId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.ChainId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *GenesisChainCounter) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisChainCounter) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisChainCounter) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Counter.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + if len(m.ChainId) > 0 { + i -= len(m.ChainId) + copy(dAtA[i:], m.ChainId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.ChainId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { offset -= sovGenesis(v) base := offset @@ -142,6 +402,52 @@ func (m *GenesisState) Size() (n int) { _ = l l = m.Params.Size() n += 1 + l + sovGenesis(uint64(l)) + if len(m.PeerData) > 0 { + for _, e := range m.PeerData { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + if len(m.ChainCounters) > 0 { + for _, e := range m.ChainCounters { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func (m *GenesisPeerData) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ChainId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.ValidatorAddress) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = m.Data.Size() + n += 1 + l + sovGenesis(uint64(l)) + return n +} + +func (m *GenesisChainCounter) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.ChainId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = m.Counter.Size() + n += 1 + l + sovGenesis(uint64(l)) return n } @@ -213,6 +519,336 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PeerData", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PeerData = append(m.PeerData, GenesisPeerData{}) + if err := m.PeerData[len(m.PeerData)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainCounters", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainCounters = append(m.ChainCounters, GenesisChainCounter{}) + if err := m.ChainCounters[len(m.ChainCounters)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GenesisPeerData) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisPeerData: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisPeerData: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Data.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GenesisChainCounter) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisChainCounter: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisChainCounter: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ChainId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ChainId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Counter", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Counter.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/peers/types/genesis_test.go b/x/peers/types/genesis_test.go index 241d2ce8..7d210317 100644 --- a/x/peers/types/genesis_test.go +++ b/x/peers/types/genesis_test.go @@ -2,6 +2,7 @@ package types_test import ( "testing" + "time" "github.com/sagaxyz/ssc/x/peers/types" @@ -20,12 +21,53 @@ func TestGenesisState_Validate(t *testing.T) { valid: true, }, { - desc: "valid genesis state", + desc: "valid genesis state", genState: &types.GenesisState{ + Params: types.DefaultParams(), + PeerData: []types.GenesisPeerData{}, + ChainCounters: []types.GenesisChainCounter{}, // this line is used by starport scaffolding # types/genesis/validField }, valid: true, }, + { + desc: "valid genesis state with data", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + PeerData: []types.GenesisPeerData{ + {ChainId: "chain-1", ValidatorAddress: "val1", Data: types.Data{Updated: time.Now(), Addresses: []string{"peer1"}}}, + {ChainId: "chain-1", ValidatorAddress: "val2", Data: types.Data{Updated: time.Now(), Addresses: []string{"peer2"}}}, + }, + ChainCounters: []types.GenesisChainCounter{ + {ChainId: "chain-1", Counter: types.Counter{Number: 2}}, + }, + }, + valid: true, + }, + { + desc: "invalid - duplicate peer data", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + PeerData: []types.GenesisPeerData{ + {ChainId: "chain-1", ValidatorAddress: "val1", Data: types.Data{Updated: time.Now(), Addresses: []string{"peer1"}}}, + {ChainId: "chain-1", ValidatorAddress: "val1", Data: types.Data{Updated: time.Now(), Addresses: []string{"peer2"}}}, + }, + ChainCounters: []types.GenesisChainCounter{}, + }, + valid: false, + }, + { + desc: "invalid - duplicate chain counters", + genState: &types.GenesisState{ + Params: types.DefaultParams(), + PeerData: []types.GenesisPeerData{}, + ChainCounters: []types.GenesisChainCounter{ + {ChainId: "chain-1", Counter: types.Counter{Number: 2}}, + {ChainId: "chain-1", Counter: types.Counter{Number: 3}}, + }, + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } { t.Run(tc.desc, func(t *testing.T) { diff --git a/x/peers/types/params.go b/x/peers/types/params.go index 47f07621..92368521 100644 --- a/x/peers/types/params.go +++ b/x/peers/types/params.go @@ -16,7 +16,7 @@ func ParamKeyTable() paramtypes.KeyTable { // NewParams creates a new Params instance func NewParams() Params { return Params{ - MaxData: 100, + MaxData: 1024, } } diff --git a/x/peers/types/query.pb.go b/x/peers/types/query.pb.go index 6b24a742..9ca4f484 100644 --- a/x/peers/types/query.pb.go +++ b/x/peers/types/query.pb.go @@ -359,6 +359,7 @@ func _Query_Peers_Handler(srv interface{}, ctx context.Context, dec func(interfa return interceptor(ctx, in, info, handler) } +var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.peers.Query", HandlerType: (*QueryServer)(nil), diff --git a/x/peers/types/tx.pb.go b/x/peers/types/tx.pb.go index efcc6561..0d3065fa 100644 --- a/x/peers/types/tx.pb.go +++ b/x/peers/types/tx.pb.go @@ -220,6 +220,7 @@ func _Msg_SetPeers_Handler(srv interface{}, ctx context.Context, dec func(interf return interceptor(ctx, in, info, handler) } +var Msg_serviceDesc = _Msg_serviceDesc var _Msg_serviceDesc = grpc.ServiceDesc{ ServiceName: "ssc.peers.Msg", HandlerType: (*MsgServer)(nil),