diff --git a/CHANGELOG.md b/CHANGELOG.md index d841f3b44..8d2d0ef19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased ### Changes +#### Upgrade Module +- ([\#467](https://github.com/forbole/bdjuno/pull/467)) Store software upgrade plan and refresh data at upgrade height + + #### Staking Module - ([\#443](https://github.com/forbole/bdjuno/pull/443)) Remove tombstone status from staking module(already stored in slashing module) - ([\#455](https://github.com/forbole/bdjuno/pull/455)) Added `unbonding_tokens` and `staked_not_bonded_tokens` values to staking pool table diff --git a/cmd/parse/staking/validators.go b/cmd/parse/staking/validators.go index ffe67febe..2497bd786 100644 --- a/cmd/parse/staking/validators.go +++ b/cmd/parse/staking/validators.go @@ -41,18 +41,9 @@ func validatorsCmd(parseConfig *parsecmdtypes.Config) *cobra.Command { return fmt.Errorf("error while getting latest block height: %s", err) } - // Get all validators - validators, err := sources.StakingSource.GetValidatorsWithStatus(height, "") + err = stakingModule.RefreshAllValidatorInfos(height) if err != nil { - return fmt.Errorf("error while getting validators: %s", err) - } - - // Refresh each validator - for _, validator := range validators { - err = stakingModule.RefreshValidatorInfos(height, validator.OperatorAddress) - if err != nil { - return fmt.Errorf("error while refreshing validator: %s", err) - } + return fmt.Errorf("error while refreshing all validators infos: %s", err) } return nil diff --git a/database/gov.go b/database/gov.go index 3577e27c7..673bf4f68 100644 --- a/database/gov.go +++ b/database/gov.go @@ -7,6 +7,7 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" "github.com/gogo/protobuf/proto" "github.com/forbole/bdjuno/v3/types" @@ -397,3 +398,62 @@ WHERE proposal_validator_status_snapshot.height <= excluded.height` return nil } + +// SaveSoftwareUpgradePlan allows to save the given software upgrade plan with its proposal id +func (db *Db) SaveSoftwareUpgradePlan(proposalID uint64, plan upgradetypes.Plan, height int64) error { + + stmt := ` +INSERT INTO software_upgrade_plan(proposal_id, plan_name, upgrade_height, info, height) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (proposal_id) DO UPDATE SET + plan_name = excluded.plan_name, + upgrade_height = excluded.upgrade_height, + info = excluded.info, + height = excluded.height +WHERE software_upgrade_plan.height <= excluded.height` + + _, err := db.Sql.Exec(stmt, + proposalID, plan.Name, plan.Height, plan.Info, height) + if err != nil { + return fmt.Errorf("error while storing software upgrade plan: %s", err) + } + + return nil +} + +// DeleteSoftwareUpgradePlan allows to delete a SoftwareUpgradePlan with proposal ID +func (db *Db) DeleteSoftwareUpgradePlan(proposalID uint64) error { + stmt := `DELETE FROM software_upgrade_plan WHERE proposal_id = $1` + + _, err := db.Sql.Exec(stmt, proposalID) + if err != nil { + return fmt.Errorf("error while deleting software upgrade plan: %s", err) + } + + return nil +} + +// CheckSoftwareUpgradePlan returns true if an upgrade is scheduled at the given height +func (db *Db) CheckSoftwareUpgradePlan(upgradeHeight int64) (bool, error) { + var exist bool + + stmt := `SELECT EXISTS (SELECT 1 FROM software_upgrade_plan WHERE upgrade_height=$1)` + err := db.Sql.QueryRow(stmt, upgradeHeight).Scan(&exist) + if err != nil { + return exist, fmt.Errorf("error while checking software upgrade plan existence: %s", err) + } + + return exist, nil +} + +// TruncateSoftwareUpgradePlan delete software upgrade plans once the upgrade height passed +func (db *Db) TruncateSoftwareUpgradePlan(height int64) error { + stmt := `DELETE FROM software_upgrade_plan WHERE upgrade_height <= $1` + + _, err := db.Sql.Exec(stmt, height) + if err != nil { + return fmt.Errorf("error while deleting software upgrade plan: %s", err) + } + + return nil +} diff --git a/database/gov_test.go b/database/gov_test.go index 6bf8f5912..04143efe6 100644 --- a/database/gov_test.go +++ b/database/gov_test.go @@ -5,6 +5,7 @@ import ( "time" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/gogo/protobuf/proto" @@ -825,3 +826,132 @@ func (suite *DbTestSuite) TestBigDipperDb_SaveProposalValidatorsStatusesSnapshot ), }) } + +func (suite *DbTestSuite) TestBigDipperDb_SaveSoftwareUpgradePlan() { + _ = suite.getProposalRow(1) + + // ---------------------------------------------------------------------------------------------------------------- + // Save software upgrade plan at height 10 with upgrade height at 100 + var plan = upgradetypes.Plan{ + Name: "name", + Height: 100, + Info: "info", + } + + err := suite.database.SaveSoftwareUpgradePlan(1, plan, 10) + suite.Require().NoError(err) + + var rows []dbtypes.SoftwareUpgradePlanRow + err = suite.database.Sqlx.Select(&rows, `SELECT * FROM software_upgrade_plan`) + suite.Require().NoError(err) + suite.Require().Len(rows, 1) + suite.Require().Equal(rows, []dbtypes.SoftwareUpgradePlanRow{ + dbtypes.NewSoftwareUpgradePlanRow(1, plan.Name, plan.Height, plan.Info, 10), + }) + + // ---------------------------------------------------------------------------------------------------------------- + // Update software upgrade plan with lower height + planEdit1 := upgradetypes.Plan{ + Name: "name_edit_1", + Height: 101, + Info: "info_edit_1", + } + + err = suite.database.SaveSoftwareUpgradePlan(1, planEdit1, 9) + suite.Require().NoError(err) + + rows = []dbtypes.SoftwareUpgradePlanRow{} + err = suite.database.Sqlx.Select(&rows, `SELECT * FROM software_upgrade_plan`) + suite.Require().NoError(err) + suite.Require().Len(rows, 1) + suite.Require().Equal(rows, []dbtypes.SoftwareUpgradePlanRow{ + dbtypes.NewSoftwareUpgradePlanRow(1, plan.Name, plan.Height, plan.Info, 10), + }) + + // ---------------------------------------------------------------------------------------------------------------- + // Update software upgrade plan with same height + planEdit2 := upgradetypes.Plan{ + Name: "name_edit_2", + Height: 102, + Info: "info_edit_2", + } + + err = suite.database.SaveSoftwareUpgradePlan(1, planEdit2, 10) + suite.Require().NoError(err) + + rows = []dbtypes.SoftwareUpgradePlanRow{} + err = suite.database.Sqlx.Select(&rows, `SELECT * FROM software_upgrade_plan`) + suite.Require().NoError(err) + suite.Require().Len(rows, 1) + suite.Require().Equal(rows, []dbtypes.SoftwareUpgradePlanRow{ + dbtypes.NewSoftwareUpgradePlanRow(1, planEdit2.Name, planEdit2.Height, planEdit2.Info, 10), + }) + + // ---------------------------------------------------------------------------------------------------------------- + // Update software upgrade plan with higher height + planEdit3 := upgradetypes.Plan{ + Name: "name_edit_3", + Height: 103, + Info: "info_edit_3", + } + + err = suite.database.SaveSoftwareUpgradePlan(1, planEdit3, 11) + suite.Require().NoError(err) + + rows = []dbtypes.SoftwareUpgradePlanRow{} + err = suite.database.Sqlx.Select(&rows, `SELECT * FROM software_upgrade_plan`) + suite.Require().NoError(err) + suite.Require().Len(rows, 1) + suite.Require().Equal(rows, []dbtypes.SoftwareUpgradePlanRow{ + dbtypes.NewSoftwareUpgradePlanRow(1, planEdit3.Name, planEdit3.Height, planEdit3.Info, 11), + }) +} + +func (suite *DbTestSuite) TestBigDipperDb_DeleteSoftwareUpgradePlan() { + _ = suite.getProposalRow(1) + + // Save software upgrade plan at height 10 with upgrade height at 100 + var plan = upgradetypes.Plan{ + Name: "name", + Height: 100, + Info: "info", + } + + err := suite.database.SaveSoftwareUpgradePlan(1, plan, 10) + suite.Require().NoError(err) + + // Delete software upgrade plan + err = suite.database.DeleteSoftwareUpgradePlan(1) + suite.Require().NoError(err) + + var rows []dbtypes.SoftwareUpgradePlanRow + err = suite.database.Sqlx.Select(&rows, `SELECT * FROM software_upgrade_plan`) + suite.Require().NoError(err) + suite.Require().Len(rows, 0) + +} + +func (suite *DbTestSuite) TestBigDipperDb_CheckSoftwareUpgradePlan() { + _ = suite.getProposalRow(1) + + // Save software upgrade plan at height 10 with upgrade height at 100 + var plan = upgradetypes.Plan{ + Name: "name", + // the Height here is the upgrade height + Height: 100, + Info: "info", + } + + err := suite.database.SaveSoftwareUpgradePlan(1, plan, 10) + suite.Require().NoError(err) + + // Check software upgrade plan at existing height + exist, err := suite.database.CheckSoftwareUpgradePlan(100) + suite.Require().NoError(err) + suite.Require().Equal(true, exist) + + // Check software upgrade plan at non-existing height + exist, err = suite.database.CheckSoftwareUpgradePlan(11) + suite.Require().NoError(err) + suite.Require().Equal(false, exist) +} diff --git a/database/schema/12-upgrade.sql b/database/schema/12-upgrade.sql new file mode 100644 index 000000000..982be6a2f --- /dev/null +++ b/database/schema/12-upgrade.sql @@ -0,0 +1,10 @@ +CREATE TABLE software_upgrade_plan +( + proposal_id INTEGER REFERENCES proposal (id) UNIQUE, + plan_name TEXT NOT NULL, + upgrade_height BIGINT NOT NULL, + info TEXT NOT NULL, + height BIGINT NOT NULL +); +CREATE INDEX software_upgrade_plan_proposal_id_index ON software_upgrade_plan (proposal_id); +CREATE INDEX software_upgrade_plan_height_index ON software_upgrade_plan (height); diff --git a/database/types/upgrade.go b/database/types/upgrade.go new file mode 100644 index 000000000..c3abc7407 --- /dev/null +++ b/database/types/upgrade.go @@ -0,0 +1,21 @@ +package types + +type SoftwareUpgradePlanRow struct { + ProposalID uint64 `db:"proposal_id"` + PlanName string `db:"plan_name"` + UpgradeHeight int64 `db:"upgrade_height"` + Info string `db:"info"` + Height int64 `db:"height"` +} + +func NewSoftwareUpgradePlanRow( + proposalID uint64, planName string, upgradeHeight int64, info string, height int64, +) SoftwareUpgradePlanRow { + return SoftwareUpgradePlanRow{ + ProposalID: proposalID, + PlanName: planName, + UpgradeHeight: upgradeHeight, + Info: info, + Height: height, + } +} diff --git a/hasura/metadata/databases/bdjuno/tables/public_software_upgrade_plan.yaml b/hasura/metadata/databases/bdjuno/tables/public_software_upgrade_plan.yaml new file mode 100644 index 000000000..d7ab38f50 --- /dev/null +++ b/hasura/metadata/databases/bdjuno/tables/public_software_upgrade_plan.yaml @@ -0,0 +1,18 @@ +table: + name: software_upgrade_plan + schema: public +object_relationships: +- name: proposal + using: + foreign_key_constraint_on: proposal_id +select_permissions: +- permission: + allow_aggregations: true + columns: + - proposal_id + - plan_name + - upgrade_height + - info + - height + filter: {} + role: anonymous diff --git a/hasura/metadata/databases/bdjuno/tables/tables.yaml b/hasura/metadata/databases/bdjuno/tables/tables.yaml index 919fa44cd..920a84a31 100644 --- a/hasura/metadata/databases/bdjuno/tables/tables.yaml +++ b/hasura/metadata/databases/bdjuno/tables/tables.yaml @@ -23,6 +23,7 @@ - "!include public_proposal_validator_status_snapshot.yaml" - "!include public_proposal_vote.yaml" - "!include public_slashing_params.yaml" +- "!include public_software_upgrade_plan.yaml" - "!include public_staking_params.yaml" - "!include public_staking_pool.yaml" - "!include public_supply.yaml" diff --git a/modules/gov/handle_msg.go b/modules/gov/handle_msg.go index 3f2d920bd..1c50e17c7 100644 --- a/modules/gov/handle_msg.go +++ b/modules/gov/handle_msg.go @@ -56,13 +56,6 @@ func (m *Module) handleMsgSubmitProposal(tx *juno.Tx, index int, msg *govtypes.M return fmt.Errorf("error while getting proposal: %s", err) } - // Unpack the content - var content govtypes.Content - err = m.cdc.UnpackAny(proposal.Content, &content) - if err != nil { - return fmt.Errorf("error while unpacking proposal content: %s", err) - } - // Store the proposal proposalObj := types.NewProposal( proposal.ProposalId, diff --git a/modules/gov/utils_proposal.go b/modules/gov/utils_proposal.go index b560b266b..be57cb3d0 100644 --- a/modules/gov/utils_proposal.go +++ b/modules/gov/utils_proposal.go @@ -8,6 +8,7 @@ import ( proposaltypes "github.com/cosmos/cosmos-sdk/x/params/types/proposal" slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" tmctypes "github.com/tendermint/tendermint/rpc/core/types" "google.golang.org/grpc/codes" @@ -31,11 +32,6 @@ func (m *Module) UpdateProposal(height int64, id uint64) error { return fmt.Errorf("error while getting proposal: %s", err) } - err = m.handleParamChangeProposal(height, proposal) - if err != nil { - return fmt.Errorf("error while updating params from ParamChangeProposal: %s", err) - } - err = m.updateProposalStatus(proposal) if err != nil { return fmt.Errorf("error while updating proposal status: %s", err) @@ -50,6 +46,12 @@ func (m *Module) UpdateProposal(height int64, id uint64) error { if err != nil { return fmt.Errorf("error while updating account: %s", err) } + + err = m.handlePassedProposal(proposal, height) + if err != nil { + return fmt.Errorf("error while handling passed proposals: %s", err) + } + return nil } @@ -86,23 +88,7 @@ func (m *Module) updateDeletedProposalStatus(id uint64) error { } // handleParamChangeProposal updates params to the corresponding modules if a ParamChangeProposal has passed -func (m *Module) handleParamChangeProposal(height int64, proposal govtypes.Proposal) error { - if proposal.Status != govtypes.StatusPassed { - // If the status of ParamChangeProposal is not passed, do nothing - return nil - } - - var content govtypes.Content - err := m.db.EncodingConfig.Marshaler.UnpackAny(proposal.Content, &content) - if err != nil { - return fmt.Errorf("error while handling ParamChangeProposal: %s", err) - } - - paramChangeProposal, ok := content.(*proposaltypes.ParameterChangeProposal) - if !ok { - return nil - } - +func (m *Module) handleParamChangeProposal(height int64, paramChangeProposal *proposaltypes.ParameterChangeProposal) (err error) { for _, change := range paramChangeProposal.Changes { // Update the params for corresponding modules switch change.Subspace { @@ -279,3 +265,41 @@ func findStatus(consAddr string, statuses []types.ValidatorStatus) (types.Valida } return types.ValidatorStatus{}, fmt.Errorf("cannot find status for validator with consensus address %s", consAddr) } + +func (m *Module) handlePassedProposal(proposal govtypes.Proposal, height int64) error { + if proposal.Status != govtypes.StatusPassed { + // If proposal status is not passed, do nothing + return nil + } + + // Unpack proposal + var content govtypes.Content + err := m.db.EncodingConfig.Marshaler.UnpackAny(proposal.Content, &content) + if err != nil { + return fmt.Errorf("error while handling ParamChangeProposal: %s", err) + } + + switch p := content.(type) { + case *proposaltypes.ParameterChangeProposal: + // Update params while ParameterChangeProposal passed + err = m.handleParamChangeProposal(height, p) + if err != nil { + return fmt.Errorf("error while updating params from ParamChangeProposal: %s", err) + } + + case *upgradetypes.SoftwareUpgradeProposal: + // Store software upgrade plan while SoftwareUpgradeProposal passed + err = m.db.SaveSoftwareUpgradePlan(proposal.ProposalId, p.Plan, height) + if err != nil { + return fmt.Errorf("error while storing software upgrade plan: %s", err) + } + + case *upgradetypes.CancelSoftwareUpgradeProposal: + // Delete software upgrade plan while CancelSoftwareUpgradeProposal passed + err = m.db.DeleteSoftwareUpgradePlan(proposal.ProposalId) + if err != nil { + return fmt.Errorf("error while deleting software upgrade plan: %s", err) + } + } + return nil +} diff --git a/modules/registrar.go b/modules/registrar.go index c1d753f78..1b4b1f33e 100644 --- a/modules/registrar.go +++ b/modules/registrar.go @@ -30,6 +30,7 @@ import ( "github.com/forbole/bdjuno/v3/modules/modules" "github.com/forbole/bdjuno/v3/modules/pricefeed" "github.com/forbole/bdjuno/v3/modules/staking" + "github.com/forbole/bdjuno/v3/modules/upgrade" ) // UniqueAddressesParser returns a wrapper around the given parser that removes all duplicated addresses @@ -83,6 +84,7 @@ func (r *Registrar) BuildModules(ctx registrar.Context) jmodules.Modules { slashingModule := slashing.NewModule(sources.SlashingSource, cdc, db) stakingModule := staking.NewModule(sources.StakingSource, cdc, db) govModule := gov.NewModule(sources.GovSource, authModule, distrModule, mintModule, slashingModule, stakingModule, cdc, db) + upgradeModule := upgrade.NewModule(db, stakingModule) return []jmodules.Module{ messages.NewModule(r.parser, cdc, ctx.Database), @@ -102,5 +104,6 @@ func (r *Registrar) BuildModules(ctx registrar.Context) jmodules.Modules { pricefeed.NewModule(ctx.JunoConfig, cdc, db), slashingModule, stakingModule, + upgradeModule, } } diff --git a/modules/staking/utils_validators.go b/modules/staking/utils_validators.go index 4c5ee63d9..19f9176f9 100644 --- a/modules/staking/utils_validators.go +++ b/modules/staking/utils_validators.go @@ -80,6 +80,25 @@ func (m *Module) convertValidatorDescription( // -------------------------------------------------------------------------------------------------------------------- +// RefreshAllValidatorInfos refreshes the info of all the validators at the given height +func (m *Module) RefreshAllValidatorInfos(height int64) error { + // Get all validators + validators, err := m.source.GetValidatorsWithStatus(height, "") + if err != nil { + return fmt.Errorf("error while getting validators: %s", err) + } + + // Refresh each validator + for _, validator := range validators { + err = m.RefreshValidatorInfos(height, validator.OperatorAddress) + if err != nil { + return fmt.Errorf("error while refreshing validator: %s", err) + } + } + + return nil +} + // RefreshValidatorInfos refreshes the info for the validator with the given operator address at the provided height func (m *Module) RefreshValidatorInfos(height int64, valOper string) error { stakingValidator, err := m.source.GetValidator(height, valOper) diff --git a/modules/upgrade/expected_modules.go b/modules/upgrade/expected_modules.go new file mode 100644 index 000000000..1ee349204 --- /dev/null +++ b/modules/upgrade/expected_modules.go @@ -0,0 +1,5 @@ +package upgrade + +type StakingModule interface { + RefreshAllValidatorInfos(height int64) error +} diff --git a/modules/upgrade/handle_block.go b/modules/upgrade/handle_block.go new file mode 100644 index 000000000..9a588b71f --- /dev/null +++ b/modules/upgrade/handle_block.go @@ -0,0 +1,45 @@ +package upgrade + +import ( + "fmt" + + "github.com/forbole/juno/v3/types" + + tmctypes "github.com/tendermint/tendermint/rpc/core/types" +) + +// HandleBlock implements modules.Module +func (m *Module) HandleBlock( + b *tmctypes.ResultBlock, _ *tmctypes.ResultBlockResults, _ []*types.Tx, _ *tmctypes.ResultValidators, +) error { + err := m.refreshDataUponSoftwareUpgrade(b.Block.Height) + if err != nil { + return fmt.Errorf("error while refreshing data upon software upgrade: %s", err) + } + + return nil +} + +func (m *Module) refreshDataUponSoftwareUpgrade(height int64) error { + exist, err := m.db.CheckSoftwareUpgradePlan(height) + if err != nil { + return fmt.Errorf("error while checking software upgrade plan existence: %s", err) + } + if !exist { + return nil + } + + // Refresh validator infos + err = m.stakingModule.RefreshAllValidatorInfos(height) + if err != nil { + return fmt.Errorf("error while refreshing validator infos upon software upgrade: %s", err) + } + + // Delete plan after refreshing data + err = m.db.TruncateSoftwareUpgradePlan(height) + if err != nil { + return fmt.Errorf("error while truncating software upgrade plan: %s", err) + } + + return nil +} diff --git a/modules/upgrade/module.go b/modules/upgrade/module.go new file mode 100644 index 000000000..6576bff4b --- /dev/null +++ b/modules/upgrade/module.go @@ -0,0 +1,31 @@ +package upgrade + +import ( + "github.com/forbole/bdjuno/v3/database" + + "github.com/forbole/juno/v3/modules" +) + +var ( + _ modules.Module = &Module{} + _ modules.BlockModule = &Module{} +) + +// Module represents the x/upgrade module +type Module struct { + db *database.Db + stakingModule StakingModule +} + +// NewModule builds a new Module instance +func NewModule(db *database.Db, stakingModule StakingModule) *Module { + return &Module{ + stakingModule: stakingModule, + db: db, + } +} + +// Name implements modules.Module +func (m *Module) Name() string { + return "upgrade" +}