Skip to content

Commit

Permalink
feat: add valset injection through r/sys/validators (#2229)
Browse files Browse the repository at this point in the history
## Description

This PR introduces an `EndBlocker` system for applying validator set
changes protocol level, based on on-chain events (from the `/r/sys/vals`
realm).

I've utilized an already existing system:
- validator set changes still stay managed protocol-level in
`ConsensusState` -> refactoring this is not a small feat, and saying
this is an understatement
- event switch utilized by the node that dumps new block / new
transaction events

The way this flow essentially works is the following:
1. An on-chain event happens that indicates a change in the validator
set (added / removed)
2. These events (ABCI events) are parsed as soon as they end up in a
transaction result (are pushed to the event system of the SDK). The
top-level ABCI event type needs to be`EventTx` (indicating it's a new TX
result). The underlying tx GnoVM events (`GnoEvent`) need to be from the
`/r/sys/validators` Realm, and be a validator addition / removal (type
defined in the Realm)
4. Events are parsed down into `abci.ValidatorUpdates`, which are
returned as a result of `EndBlocker`
5. This `EndBlocker` result is later read by the `ConsensusState`, and
the validator set changes are applied for the upcoming block in a series
of existing callbacks. This also keeps proposer priority logic in check.

Blocked by #2130 
Closes #1823 

```mermaid
---
title: on-chain validator set injection flow
---
flowchart TD
    subgraph app.go
        nesvw(["Node event switch"]) -. pass all block events .-> collector(["event collector"])
        collector -. subscribes to .-> nesvw

        collector -. filter new events .-> collector

        EB["func EndBlocker(...)"] == 1: fetch relevant events ==> collector
        collector -. 2: return events, if any .-> EB
    end

    subgraph gno.land/r/sys/validators.gno
        GC["func GetChanges(from int64) []validators.Validator"]

        addVal["func addValidator(...)"]
        removeVal["func removeValidator(...)"]
        PE["func NewPropExecutor(changesFn) proposal.Executor"]

        PE -. calls internally .-> addVal
        PE -. calls internally .-> removeVal

        addVal -. std emits ValidatorAdded .-> nesvw
        removeVal -. std emits ValidatorRemoved .-> nesvw
    end

    subgraph gno.land/r/gov/dao.gno
        EP["func ExecuteProposal(...)"]
        

        EP == executes on-chain ==>PE
    end

    subgraph user_proposal.gno
        main("func main() {...}")
        PR["govdao.Propose(...)"]

        main -. contains .-> CB
        main -. contains .-> PR

        CB("changesFn func() []validators.Validator {...}")

        CB== creates ==>PE
        CB("changesFn func() []validators.Validator {...}")

        PE == passed into ==> PR
    end

    A[/fa:fa-user User\] == gnokey maketx run ==> main
    GDV[/fa:fa-people-group GOVDAO members\] == manually call ==> EP

    EB == 3: execute VM call to get changes since last block ==> GC
    GC -. 4: return changes .-> EB

    EB -. 5: return response with valset changes .-> EBR([abci.ResponseEndBlock]) -- applied in --> AB
    subgraph Cosmos SDK
        AB["func ApplyBlock(...) {...}"]
    end
```

Related: #1945

<details><summary>Contributors' checklist...</summary>

- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
zivkovicmilos and moul committed Jul 7, 2024
1 parent b1d778c commit b5560e2
Show file tree
Hide file tree
Showing 25 changed files with 881 additions and 143 deletions.
2 changes: 1 addition & 1 deletion contribs/gnodev/pkg/dev/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ func buildNode(logger *slog.Logger, emitter emitter.Emitter, cfg *gnoland.InMemo
func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig {
// Create Mocked Identity
pv := gnoland.NewMockedPrivValidator()
genesis := gnoland.NewDefaultGenesisConfig(pv.GetPubKey(), chainid)
genesis := gnoland.NewDefaultGenesisConfig(chainid)
genesis.AppState = appstate

// Add self as validator
Expand Down
8 changes: 4 additions & 4 deletions examples/gno.land/r/gov/dao/prop1_filetest.gno
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func main() {
// Status: succeeded
// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
// --
// Valset changes to apply:
// - g12345678 (10)
// - g000000000 (10)
// - g000000000 (0)
// Valset changes:
// - #123: g12345678 (10)
// - #123: g000000000 (10)
// - #123: g000000000 (0)
5 changes: 5 additions & 0 deletions examples/gno.land/r/sys/validators/gno.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
module gno.land/r/sys/validators

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/seqid v0.0.0-latest
gno.land/p/demo/testutils v0.0.0-latest
gno.land/p/demo/uassert v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/gov/proposal v0.0.0-latest
gno.land/p/nt/poa v0.0.0-latest
gno.land/p/sys/validators v0.0.0-latest
Expand Down
26 changes: 17 additions & 9 deletions examples/gno.land/r/sys/validators/gnosdk.gno
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package validators

import "gno.land/p/sys/validators"
import (
"gno.land/p/sys/validators"
)

// getChanges returns the validator changes stored on the realm.
// This function is unexported and intended to be called by gno.land through the GnoSDK
func getChanges() []validators.Validator {
// Construct the changes
valsetChanges := make([]validators.Validator, len(changes))
copy(valsetChanges, changes)
// GetChanges returns the validator changes stored on the realm, since the given block number.
// This function is intended to be called by gno.land through the GnoSDK
func GetChanges(from int64) []validators.Validator {
valsetChanges := make([]validators.Validator, 0)

// Reset the changes set
changes = changes[:0]
// Gather the changes from the specified block
changes.Iterate(getBlockID(from), "", func(_ string, value interface{}) bool {
chs := value.([]change)

for _, ch := range chs {
valsetChanges = append(valsetChanges, ch.validator)
}

return false
})

return valsetChanges
}
4 changes: 2 additions & 2 deletions examples/gno.land/r/sys/validators/init.gno
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package validators

import (
"gno.land/p/demo/avl"
"gno.land/p/nt/poa"
"gno.land/p/sys/validators"
)

func init() {
// The default valset protocol is PoA
vp = poa.NewPoA()

// No changes to apply initially
changes = make([]validators.Validator, 0)
changes = avl.NewTree()
}
83 changes: 70 additions & 13 deletions examples/gno.land/r/sys/validators/validators.gno
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ package validators

import (
"std"
"strconv"

"gno.land/p/demo/avl"
"gno.land/p/demo/seqid"
"gno.land/p/demo/ufmt"
"gno.land/p/sys/validators"
)

var (
vp validators.ValsetProtocol // p is the underlying validator set protocol
changes []validators.Validator // changes are the set changes that happened between scrapes
changes *avl.Tree // changes holds any valset changes; seqid(block number) -> []change
)

// change represents a single valset change, tied to a specific block number
type change struct {
blockNum int64 // the block number associated with the valset change
validator validators.Validator // the validator update
}

// addValidator adds a new validator to the validator set.
// If the validator is already present, the method errors out
func addValidator(validator validators.Validator) {
Expand All @@ -21,7 +29,12 @@ func addValidator(validator validators.Validator) {
}

// Validator added, note the change
changes = append(changes, val)
ch := change{
blockNum: std.GetHeight(),
validator: val,
}

saveChange(ch)

// Emit the validator set change
std.Emit(validators.ValidatorAddedEvent)
Expand All @@ -36,25 +49,69 @@ func removeValidator(address std.Address) {
}

// Validator removed, note the change
changes = append(changes, validators.Validator{
Address: val.Address,
PubKey: val.PubKey,
VotingPower: 0, // nullified the voting power indicates removal
})
ch := change{
blockNum: std.GetHeight(),
validator: validators.Validator{
Address: val.Address,
PubKey: val.PubKey,
VotingPower: 0, // nullified the voting power indicates removal
},
}

saveChange(ch)

// Emit the validator set change
std.Emit(validators.ValidatorRemovedEvent)
}

// saveChange saves the valset change
func saveChange(ch change) {
id := getBlockID(ch.blockNum)

setRaw, exists := changes.Get(id)
if !exists {
changes.Set(id, []change{ch})

return
}

// Save the change
set := setRaw.([]change)
set = append(set, ch)

changes.Set(id, set)
}

// getBlockID converts the block number to a sequential ID
func getBlockID(blockNum int64) string {
return seqid.ID(uint64(blockNum)).String()
}

func Render(_ string) string {
if len(changes) == 0 {
var (
size = changes.Size()
maxDisplay = 10
)

if size == 0 {
return "No valset changes to apply."
}

output := "Valset changes to apply:\n"
for _, change := range changes {
output += "- " + string(change.Address) + " (" + strconv.FormatUint(change.VotingPower, 10) + ")\n"
}
output := "Valset changes:\n"
changes.ReverseIterateByOffset(size-maxDisplay, maxDisplay, func(_ string, value interface{}) bool {
chs := value.([]change)

for _, ch := range chs {
output += ufmt.Sprintf(
"- #%d: %s (%d)\n",
ch.blockNum,
ch.validator.Address.String(),
ch.validator.VotingPower,
)
}

return false
})

return output
}
102 changes: 102 additions & 0 deletions examples/gno.land/r/sys/validators/validators_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package validators

import (
"testing"

"std"

"gno.land/p/demo/avl"
"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
"gno.land/p/demo/ufmt"
"gno.land/p/sys/validators"
)

// generateTestValidators generates a dummy validator set
func generateTestValidators(count int) []validators.Validator {
vals := make([]validators.Validator, 0, count)

for i := 0; i < count; i++ {
val := validators.Validator{
Address: testutils.TestAddress(ufmt.Sprintf("%d", i)),
PubKey: "public-key",
VotingPower: 10,
}

vals = append(vals, val)
}

return vals
}

func TestValidators_AddRemove(t *testing.T) {
// Clear any changes
changes = avl.NewTree()

var (
vals = generateTestValidators(100)
initialHeight = int64(123)
)

// Add in the validators
for _, val := range vals {
addValidator(val)

// Make sure the validator is added
uassert.True(t, vp.IsValidator(val.Address))

std.TestSkipHeights(1)
}

for i := initialHeight; i < initialHeight+int64(len(vals)); i++ {
// Make sure the changes are saved
chs := GetChanges(i)

// We use the funky index calculation to make sure
// changes are properly handled for each block span
uassert.Equal(t, initialHeight+int64(len(vals))-i, int64(len(chs)))

for index, val := range vals[i-initialHeight:] {
// Make sure the changes are equal to the additions
ch := chs[index]

uassert.Equal(t, val.Address, ch.Address)
uassert.Equal(t, val.PubKey, ch.PubKey)
uassert.Equal(t, val.VotingPower, ch.VotingPower)
}
}

// Save the beginning height for the removal
initialRemoveHeight := std.GetHeight()

// Clear any changes
changes = avl.NewTree()

// Remove the validators
for _, val := range vals {
removeValidator(val.Address)

// Make sure the validator is removed
uassert.False(t, vp.IsValidator(val.Address))

std.TestSkipHeights(1)
}

for i := initialRemoveHeight; i < initialRemoveHeight+int64(len(vals)); i++ {
// Make sure the changes are saved
chs := GetChanges(i)

// We use the funky index calculation to make sure
// changes are properly handled for each block span
uassert.Equal(t, initialRemoveHeight+int64(len(vals))-i, int64(len(chs)))

for index, val := range vals[i-initialRemoveHeight:] {
// Make sure the changes are equal to the additions
ch := chs[index]

uassert.Equal(t, val.Address, ch.Address)
uassert.Equal(t, val.PubKey, ch.PubKey)
uassert.Equal(t, uint64(0), ch.VotingPower)
}
}
}
8 changes: 6 additions & 2 deletions gno.land/cmd/gnoland/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/events"
osm "github.com/gnolang/gno/tm2/pkg/os"
"github.com/gnolang/gno/tm2/pkg/telemetry"
"go.uber.org/zap"
Expand Down Expand Up @@ -239,14 +240,17 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
io.Println(startGraphic)
}

// Create a top-level shared event switch
evsw := events.NewEventSwitch()

// Create application and node
cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles)
cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger)
if err != nil {
return fmt.Errorf("unable to create the Gnoland app, %w", err)
}

// Create a default node, with the given setup
gnoNode, err := node.DefaultNewNode(cfg, genesisPath, logger)
gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger)
if err != nil {
return fmt.Errorf("unable to create the Gnoland node, %w", err)
}
Expand Down
Loading

0 comments on commit b5560e2

Please sign in to comment.