Skip to content

Commit

Permalink
cmd/network/governance: Parameter change proposal support
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrus committed Nov 27, 2023
1 parent a63db76 commit e782a47
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 22 deletions.
96 changes: 96 additions & 0 deletions cmd/network/governance/create.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package governance

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand All @@ -9,14 +10,50 @@ 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"

"github.com/oasisprotocol/cli/cmd/common"
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, &registry.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",
Expand Down Expand Up @@ -74,6 +111,61 @@ var (
},
}

govCreateProposalParameterChangeCmd = &cobra.Command{
Use: "parameter-change <module> <changes.json>",
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 <proposal-id>",
Short: "Create a cancel upgrade governance proposal",
Expand Down Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions cmd/network/governance/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
149 changes: 130 additions & 19 deletions cmd/network/governance/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <proposal-id>",
Short: "Show proposal status by ID",
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand All @@ -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(&currentVotes)
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 {

Check failure on line 250 in cmd/network/governance/show.go

View workflow job for this annotation

GitHub Actions / lint

shadow: declaration of "err" shadows declaration at line 54 (govet)
cobra.CheckErr(fmt.Errorf("failed to add votes: %w", err))
}
derivedResults[vote] = currentVotes
}
}

// Display the high-level summary of the proposal status.
Expand Down Expand Up @@ -267,26 +366,38 @@ 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())
stakePercentage = stakePercentage.Mul(stakePercentage, new(big.Float).SetInt64(100))
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())
stakePercentage = stakePercentage.Mul(stakePercentage, new(big.Float).SetInt64(100))
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()
}
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit e782a47

Please sign in to comment.