From 456d98dfbb7fe43e3fe19d6698baf5c81f6de4ea Mon Sep 17 00:00:00 2001 From: ptrus Date: Fri, 24 Nov 2023 17:18:03 +0100 Subject: [PATCH] cmd/network/governance: Parameter change proposal support --- cmd/network/governance/create.go | 96 +++++++++++++++++ cmd/network/governance/list.go | 2 + cmd/network/governance/show.go | 149 +++++++++++++++++++++++---- examples/network-governance/list.out | 19 ++-- go.mod | 2 +- go.sum | 4 +- 6 files changed, 241 insertions(+), 31 deletions(-) diff --git a/cmd/network/governance/create.go b/cmd/network/governance/create.go index d6d34dbd..8bcc5633 100644 --- a/cmd/network/governance/create.go +++ b/cmd/network/governance/create.go @@ -1,6 +1,7 @@ package governance import ( + "bytes" "context" "encoding/json" "fmt" @@ -9,7 +10,13 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/oasis-core/go/common/cbor" governance "github.com/oasisprotocol/oasis-core/go/governance/api" + keymanager "github.com/oasisprotocol/oasis-core/go/keymanager/api" + registry "github.com/oasisprotocol/oasis-core/go/registry/api" + roothash "github.com/oasisprotocol/oasis-core/go/roothash/api" + scheduler "github.com/oasisprotocol/oasis-core/go/scheduler/api" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" upgrade "github.com/oasisprotocol/oasis-core/go/upgrade/api" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" @@ -17,6 +24,36 @@ import ( cliConfig "github.com/oasisprotocol/cli/config" ) +func parseChange[A any](raw []byte, dst *A, module string) (cbor.RawMessage, error) { + dec := json.NewDecoder(bytes.NewReader(raw)) + // Fail on unknown fields, to ensure that the parameters change is valid for the module. + dec.DisallowUnknownFields() + + if err := dec.Decode(dst); err != nil { + return nil, fmt.Errorf("%s: %w", module, err) + } + return cbor.Marshal(dst), nil +} + +func parseConsensusParameterChange(module string, raw []byte) (cbor.RawMessage, error) { + switch module { + case governance.ModuleName: + return parseChange(raw, &governance.ConsensusParameterChanges{}, module) + case keymanager.ModuleName: + return parseChange(raw, &keymanager.ConsensusParameterChanges{}, module) + case registry.ModuleName: + return parseChange(raw, ®istry.ConsensusParameterChanges{}, module) + case roothash.ModuleName: + return parseChange(raw, &roothash.ConsensusParameterChanges{}, module) + case scheduler.ModuleName: + return parseChange(raw, &scheduler.ConsensusParameterChanges{}, module) + case staking.ModuleName: + return parseChange(raw, &staking.ConsensusParameterChanges{}, module) + default: + return nil, fmt.Errorf("unknown module: %s", module) + } +} + var ( govCreateProposalCmd = &cobra.Command{ Use: "create-proposal", @@ -74,6 +111,61 @@ var ( }, } + govCreateProposalParameterChangeCmd = &cobra.Command{ + Use: "parameter-change ", + Short: "Create a parameter change governance proposal", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + txCfg := common.GetTransactionConfig() + + module := args[0] + changesFile := args[1] + + if npa.Account == nil { + cobra.CheckErr("no accounts configured in your wallet") + } + + // When not in offline mode, connect to the given network endpoint. + ctx := context.Background() + var conn connection.Connection + if !txCfg.Offline { + var err error + conn, err = connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + } + + // Load and parse changes json. + rawChanges, err := os.ReadFile(changesFile) + cobra.CheckErr(err) + + changes, err := parseConsensusParameterChange(module, rawChanges) + if err != nil { + cobra.CheckErr(fmt.Errorf("malformed parameter upgrade proposal: %w", err)) + } + + content := &governance.ChangeParametersProposal{ + Module: module, + Changes: changes, + } + if err = content.ValidateBasic(); err != nil { + cobra.CheckErr(fmt.Errorf("invalid parameter upgrade proposal: %w", err)) + } + + // Prepare transaction. + tx := governance.NewSubmitProposalTx(0, nil, &governance.ProposalContent{ + ChangeParameters: content, + }) + + acc := common.LoadAccount(cfg, npa.AccountName) + sigTx, err := common.SignConsensusTransaction(ctx, npa, acc, conn, tx) + cobra.CheckErr(err) + + common.BroadcastOrExportTransaction(ctx, npa.ParaTime, conn, sigTx, nil, nil) + }, + } + govCreateProposalCancelUpgradeCmd = &cobra.Command{ Use: "cancel-upgrade ", Short: "Create a cancel upgrade governance proposal", @@ -121,9 +213,13 @@ func init() { govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) + govCreateProposalParameterChangeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) + govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.TxFlags) govCreateProposalCmd.AddCommand(govCreateProposalUpgradeCmd) + govCreateProposalCmd.AddCommand(govCreateProposalParameterChangeCmd) govCreateProposalCmd.AddCommand(govCreateProposalCancelUpgradeCmd) } diff --git a/cmd/network/governance/list.go b/cmd/network/governance/list.go index ba3e3c90..37b76bde 100644 --- a/cmd/network/governance/list.go +++ b/cmd/network/governance/list.go @@ -43,6 +43,8 @@ var govListCmd = &cobra.Command{ kind = "upgrade" case proposal.Content.CancelUpgrade != nil: kind = fmt.Sprintf("cancel upgrade %d", proposal.Content.CancelUpgrade.ProposalID) + case proposal.Content.ChangeParameters != nil: + kind = fmt.Sprintf("change parameters (%s)", proposal.Content.ChangeParameters.Module) default: kind = "unknown" } diff --git a/cmd/network/governance/show.go b/cmd/network/governance/show.go index f75e7f12..0efda254 100644 --- a/cmd/network/governance/show.go +++ b/cmd/network/governance/show.go @@ -22,6 +22,26 @@ import ( "github.com/oasisprotocol/cli/metadata" ) +func addShares(validatorVoteShares map[governance.Vote]quantity.Quantity, vote governance.Vote, amount quantity.Quantity) error { + amt := amount.Clone() + currShares := validatorVoteShares[vote] + if err := amt.Add(&currShares); err != nil { + return fmt.Errorf("failed to add votes: %w", err) + } + validatorVoteShares[vote] = *amt + return nil +} + +func subShares(validatorVoteShares map[governance.Vote]quantity.Quantity, vote governance.Vote, amount quantity.Quantity) error { + amt := amount.Clone() + currShares := validatorVoteShares[vote] + if err := currShares.Sub(amt); err != nil { + return fmt.Errorf("failed to sub votes: %w", err) + } + validatorVoteShares[vote] = currShares + return nil +} + var govShowCmd = &cobra.Command{ Use: "show ", Short: "Show proposal status by ID", @@ -98,9 +118,11 @@ var govShowCmd = &cobra.Command{ // as the actual votes are examined. totalVotingStake := quantity.NewQuantity() - validatorEntitiesEscrow := make(map[staking.Address]*quantity.Quantity) - voters := make(map[staking.Address]quantity.Quantity) - nonVoters := make(map[staking.Address]quantity.Quantity) + validatorVotes := make(map[staking.Address]*governance.Vote) + validatorVoteShares := make(map[staking.Address]map[governance.Vote]quantity.Quantity) + validatorEntitiesShares := make(map[staking.Address]*staking.SharePool) + validatorVoters := make(map[staking.Address]quantity.Quantity) + validatorNonVoters := make(map[staking.Address]quantity.Quantity) validators, err := schedulerConn.GetValidators(ctx, height) cobra.CheckErr(err) @@ -113,7 +135,7 @@ var govShowCmd = &cobra.Command{ // If there are multiple nodes in the validator set belonging // to the same entity, only count the entity escrow once. entityAddr := staking.NewAddress(node.EntityID) - if validatorEntitiesEscrow[entityAddr] != nil { + if validatorEntitiesShares[entityAddr] != nil { continue } @@ -127,32 +149,109 @@ var govShowCmd = &cobra.Command{ ) cobra.CheckErr(err) - validatorEntitiesEscrow[entityAddr] = &account.Escrow.Active.Balance + validatorEntitiesShares[entityAddr] = &account.Escrow.Active err = totalVotingStake.Add(&account.Escrow.Active.Balance) cobra.CheckErr(err) - nonVoters[entityAddr] = account.Escrow.Active.Balance + validatorNonVoters[entityAddr] = account.Escrow.Active.Balance + validatorVoteShares[entityAddr] = make(map[governance.Vote]quantity.Quantity) } - // Tally the votes. + // Tally the validator votes. - derivedResults := make(map[governance.Vote]quantity.Quantity) var invalidVotes uint64 for _, vote := range votes { - escrow, ok := validatorEntitiesEscrow[vote.Voter] + escrow, ok := validatorEntitiesShares[vote.Voter] if !ok { - // Voter not in current validator set - invalid vote. - invalidVotes++ + // Non validator votes handled later. continue } - currentVotes := derivedResults[vote.Vote] - newVotes := escrow.Clone() - err = newVotes.Add(¤tVotes) + validatorVotes[vote.Voter] = &vote.Vote + if err = addShares(validatorVoteShares[vote.Voter], vote.Vote, escrow.TotalShares); err != nil { + cobra.CheckErr(fmt.Errorf("failed to add shares: %w", err)) + } + delete(validatorNonVoters, vote.Voter) + validatorVoters[vote.Voter] = escrow.Balance + } + + // Tally the delegator (non-validator) votes. + type override struct { + vote governance.Vote + shares quantity.Quantity + sharePercent *big.Float + } + validatorVoteOverrides := make(map[staking.Address]map[staking.Address]override) + for _, vote := range votes { + // Fetch outgoing delegations. + var delegations map[staking.Address]*staking.Delegation + delegations, err = stakingConn.DelegationsFor(ctx, &staking.OwnerQuery{Height: height, Owner: vote.Voter}) cobra.CheckErr(err) - derivedResults[vote.Vote] = *newVotes - delete(nonVoters, vote.Voter) - voters[vote.Voter] = *escrow.Clone() + var delegatesToValidator bool + for to, delegation := range delegations { + // Skip delegations to non-validators. + if _, ok := validatorEntitiesShares[to]; !ok { + continue + } + delegatesToValidator = true + + validatorVote := validatorVotes[to] + // Nothing to do if vote matches the validator's vote. + if validatorVote != nil && vote.Vote == *validatorVote { + continue + } + + // Vote doesn't match. Deduct shares from the validator's vote and + // add shares to the delegator's vote. + if validatorVote != nil { + if err = subShares(validatorVoteShares[to], *validatorVote, delegation.Shares); err != nil { + cobra.CheckErr(fmt.Errorf("failed to sub shares: %w", err)) + } + } + if err = addShares(validatorVoteShares[to], vote.Vote, delegation.Shares); err != nil { + cobra.CheckErr(fmt.Errorf("failed to add shares: %w", err)) + } + + // Remember the validator vote overrides, so that we can display them later. + if validatorVoteOverrides[to] == nil { + validatorVoteOverrides[to] = make(map[staking.Address]override) + } + sharePercent := new(big.Float).SetInt(delegation.Shares.Clone().ToBigInt()) + sharePercent = sharePercent.Mul(sharePercent, new(big.Float).SetInt64(100)) + sharePercent = sharePercent.Quo(sharePercent, new(big.Float).SetInt(validatorEntitiesShares[to].TotalShares.ToBigInt())) + validatorVoteOverrides[to][vote.Voter] = override{ + vote: vote.Vote, + shares: delegation.Shares, + sharePercent: sharePercent, + } + } + + if !delegatesToValidator { + // Invalid vote if delegator doesn't delegate to a validator. + invalidVotes++ + } + } + + // Finalize the voting results - convert votes in shares into results in stake. + + derivedResults := make(map[governance.Vote]quantity.Quantity) + for validator, votes := range validatorVoteShares { + sharePool := validatorEntitiesShares[validator] + for vote, shares := range votes { + // Compute stake from shares. + var escrow *quantity.Quantity + escrow, err = sharePool.StakeForShares(shares.Clone()) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to compute stake from shares: %w", err)) + } + + // Add stake to results. + currentVotes := derivedResults[vote] + if err = currentVotes.Add(escrow); err != nil { + cobra.CheckErr(fmt.Errorf("failed to add votes: %w", err)) + } + derivedResults[vote] = currentVotes + } } // Display the high-level summary of the proposal status. @@ -267,7 +366,7 @@ var govShowCmd = &cobra.Command{ fmt.Println() fmt.Println("=== VALIDATORS VOTED ===") - votersList := entitiesByDescendingStake(voters) + votersList := entitiesByDescendingStake(validatorVoters) for i, val := range votersList { name := getName(val.Address) stakePercentage := new(big.Float).SetInt(val.Stake.Clone().ToBigInt()) @@ -275,11 +374,17 @@ var govShowCmd = &cobra.Command{ stakePercentage = stakePercentage.Quo(stakePercentage, new(big.Float).SetInt(totalVotingStake.ToBigInt())) fmt.Printf(" %d. %s,%s,%s (%.2f%%)", i+1, val.Address, name, val.Stake, stakePercentage) fmt.Println() + // Display delegators that voted differently. + for voter, override := range validatorVoteOverrides[val.Address] { + voterName := getName(voter) + fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s", voter, voterName, override.shares, override.sharePercent, override.vote) + fmt.Println() + } } fmt.Println() fmt.Println("=== VALIDATORS NOT VOTED ===") - nonVotersList := entitiesByDescendingStake(nonVoters) + nonVotersList := entitiesByDescendingStake(validatorNonVoters) for i, val := range nonVotersList { name := getName(val.Address) stakePercentage := new(big.Float).SetInt(val.Stake.Clone().ToBigInt()) @@ -287,6 +392,12 @@ var govShowCmd = &cobra.Command{ stakePercentage = stakePercentage.Quo(stakePercentage, new(big.Float).SetInt(totalVotingStake.ToBigInt())) fmt.Printf(" %d. %s,%s,%s (%.2f%%)", i+1, val.Address, name, val.Stake, stakePercentage) fmt.Println() + // Display delegators that voted differently. + for voter, override := range validatorVoteOverrides[val.Address] { + voterName := getName(voter) + fmt.Printf(" - %s,%s,%s (%.2f%%) -> %s", voter, voterName, override.shares, override.sharePercent, override.vote) + fmt.Println() + } } }, } diff --git a/examples/network-governance/list.out b/examples/network-governance/list.out index 04b1ddfb..df89e0fe 100644 --- a/examples/network-governance/list.out +++ b/examples/network-governance/list.out @@ -1,9 +1,10 @@ -ID KIND SUBMITTER CREATED AT CLOSES AT STATE -1 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 5633 5645 passed -2 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 7525 7537 passed -3 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 8817 8829 passed -4 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14183 14195 passed -5 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14869 14881 passed -6 cancel upgrade 5 oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14895 14907 passed -7 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14982 14994 passed -8 upgrade oasis1qpwaggvmhwq5uk40clase3knt655nn2tdy39nz2f 29493 29505 passed +ID KIND SUBMITTER CREATED AT CLOSES AT STATE +1 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 5633 5645 passed +2 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 7525 7537 passed +3 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 8817 8829 passed +4 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14183 14195 passed +5 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14869 14881 passed +6 cancel upgrade 5 oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14895 14907 passed +7 upgrade oasis1qrs2dl6nz6fcxxr3tq37laxlz6hxk6kuscnr6rxj 14982 14994 passed +8 upgrade oasis1qpwaggvmhwq5uk40clase3knt655nn2tdy39nz2f 29493 29505 passed +9 change parameters (governance) oasis1qrx85mv85k708ylww597rd42enlzhdmeu56wqj72 30693 30705 passed diff --git a/go.mod b/go.mod index a39c69ea..d37b0f08 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991920 - github.com/oasisprotocol/oasis-core/go v0.2300.5 + github.com/oasisprotocol/oasis-core/go v0.2300.6 github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.7.0 github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.0 diff --git a/go.sum b/go.sum index a5b54818..b773a30c 100644 --- a/go.sum +++ b/go.sum @@ -596,8 +596,8 @@ github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2 github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991920 h1:rugJRYKamNl6WGBaU+b0wLQFkYcsnBr0ycX5QmB+AYU= github.com/oasisprotocol/metadata-registry-tools v0.0.0-20220406100644-7e9a2b991920/go.mod h1:MKr/giwakLyCCjSWh0W9Pbaf7rDD1K96Wr57OhNoUK0= -github.com/oasisprotocol/oasis-core/go v0.2300.5 h1:qkwhIFQX++HI7Du/l50Wq9Wkw5P09Us/6i05i9ThxJE= -github.com/oasisprotocol/oasis-core/go v0.2300.5/go.mod h1:3ub+3LT8GEjuzeAXrve9pZEDkM/goL1HKBXVp4queNM= +github.com/oasisprotocol/oasis-core/go v0.2300.6 h1:LveIyqsIof+WhLVIAQRTMNlhuiQGqZaztVeu/gBCyE0= +github.com/oasisprotocol/oasis-core/go v0.2300.6/go.mod h1:3ub+3LT8GEjuzeAXrve9pZEDkM/goL1HKBXVp4queNM= github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.7.0 h1:2/SNCfzxTVq+wGSYtzrBQr9beEw9xa30UDhPCL+c2Lc= github.com/oasisprotocol/oasis-sdk/client-sdk/go v0.7.0/go.mod h1:oRZDB09CU4KeKup7ZQVA6uENK4OdzKYfSQfRF0c/JEg= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=