Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/network/governance: Parameter change proposal support #162

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
@@ -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",
@@ -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",
@@ -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
@@ -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"
}
149 changes: 130 additions & 19 deletions cmd/network/governance/show.go
Original file line number Diff line number Diff line change
@@ -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",
@@ -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(&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 {
cobra.CheckErr(fmt.Errorf("failed to add votes: %w", err))
}
derivedResults[vote] = currentVotes
}
}

// Display the high-level summary of the proposal status.
@@ -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()
}
}
},
}
19 changes: 10 additions & 9 deletions examples/network-governance/list.out
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=