Skip to content

Commit b5560e2

Browse files
zivkovicmilosmoul
andauthored
feat: add valset injection through r/sys/validators (#2229)
## 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>
1 parent b1d778c commit b5560e2

File tree

25 files changed

+881
-143
lines changed

25 files changed

+881
-143
lines changed

contribs/gnodev/pkg/dev/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ func buildNode(logger *slog.Logger, emitter emitter.Emitter, cfg *gnoland.InMemo
527527
func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig {
528528
// Create Mocked Identity
529529
pv := gnoland.NewMockedPrivValidator()
530-
genesis := gnoland.NewDefaultGenesisConfig(pv.GetPubKey(), chainid)
530+
genesis := gnoland.NewDefaultGenesisConfig(chainid)
531531
genesis.AppState = appstate
532532

533533
// Add self as validator

examples/gno.land/r/gov/dao/prop1_filetest.gno

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func main() {
9191
// Status: succeeded
9292
// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
9393
// --
94-
// Valset changes to apply:
95-
// - g12345678 (10)
96-
// - g000000000 (10)
97-
// - g000000000 (0)
94+
// Valset changes:
95+
// - #123: g12345678 (10)
96+
// - #123: g000000000 (10)
97+
// - #123: g000000000 (0)

examples/gno.land/r/sys/validators/gno.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
module gno.land/r/sys/validators
22

33
require (
4+
gno.land/p/demo/avl v0.0.0-latest
5+
gno.land/p/demo/seqid v0.0.0-latest
6+
gno.land/p/demo/testutils v0.0.0-latest
7+
gno.land/p/demo/uassert v0.0.0-latest
8+
gno.land/p/demo/ufmt v0.0.0-latest
49
gno.land/p/gov/proposal v0.0.0-latest
510
gno.land/p/nt/poa v0.0.0-latest
611
gno.land/p/sys/validators v0.0.0-latest
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
package validators
22

3-
import "gno.land/p/sys/validators"
3+
import (
4+
"gno.land/p/sys/validators"
5+
)
46

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

12-
// Reset the changes set
13-
changes = changes[:0]
12+
// Gather the changes from the specified block
13+
changes.Iterate(getBlockID(from), "", func(_ string, value interface{}) bool {
14+
chs := value.([]change)
15+
16+
for _, ch := range chs {
17+
valsetChanges = append(valsetChanges, ch.validator)
18+
}
19+
20+
return false
21+
})
1422

1523
return valsetChanges
1624
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package validators
22

33
import (
4+
"gno.land/p/demo/avl"
45
"gno.land/p/nt/poa"
5-
"gno.land/p/sys/validators"
66
)
77

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

1212
// No changes to apply initially
13-
changes = make([]validators.Validator, 0)
13+
changes = avl.NewTree()
1414
}

examples/gno.land/r/sys/validators/validators.gno

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@ package validators
22

33
import (
44
"std"
5-
"strconv"
65

6+
"gno.land/p/demo/avl"
7+
"gno.land/p/demo/seqid"
8+
"gno.land/p/demo/ufmt"
79
"gno.land/p/sys/validators"
810
)
911

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

17+
// change represents a single valset change, tied to a specific block number
18+
type change struct {
19+
blockNum int64 // the block number associated with the valset change
20+
validator validators.Validator // the validator update
21+
}
22+
1523
// addValidator adds a new validator to the validator set.
1624
// If the validator is already present, the method errors out
1725
func addValidator(validator validators.Validator) {
@@ -21,7 +29,12 @@ func addValidator(validator validators.Validator) {
2129
}
2230

2331
// Validator added, note the change
24-
changes = append(changes, val)
32+
ch := change{
33+
blockNum: std.GetHeight(),
34+
validator: val,
35+
}
36+
37+
saveChange(ch)
2538

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

3851
// Validator removed, note the change
39-
changes = append(changes, validators.Validator{
40-
Address: val.Address,
41-
PubKey: val.PubKey,
42-
VotingPower: 0, // nullified the voting power indicates removal
43-
})
52+
ch := change{
53+
blockNum: std.GetHeight(),
54+
validator: validators.Validator{
55+
Address: val.Address,
56+
PubKey: val.PubKey,
57+
VotingPower: 0, // nullified the voting power indicates removal
58+
},
59+
}
60+
61+
saveChange(ch)
4462

4563
// Emit the validator set change
4664
std.Emit(validators.ValidatorRemovedEvent)
4765
}
4866

67+
// saveChange saves the valset change
68+
func saveChange(ch change) {
69+
id := getBlockID(ch.blockNum)
70+
71+
setRaw, exists := changes.Get(id)
72+
if !exists {
73+
changes.Set(id, []change{ch})
74+
75+
return
76+
}
77+
78+
// Save the change
79+
set := setRaw.([]change)
80+
set = append(set, ch)
81+
82+
changes.Set(id, set)
83+
}
84+
85+
// getBlockID converts the block number to a sequential ID
86+
func getBlockID(blockNum int64) string {
87+
return seqid.ID(uint64(blockNum)).String()
88+
}
89+
4990
func Render(_ string) string {
50-
if len(changes) == 0 {
91+
var (
92+
size = changes.Size()
93+
maxDisplay = 10
94+
)
95+
96+
if size == 0 {
5197
return "No valset changes to apply."
5298
}
5399

54-
output := "Valset changes to apply:\n"
55-
for _, change := range changes {
56-
output += "- " + string(change.Address) + " (" + strconv.FormatUint(change.VotingPower, 10) + ")\n"
57-
}
100+
output := "Valset changes:\n"
101+
changes.ReverseIterateByOffset(size-maxDisplay, maxDisplay, func(_ string, value interface{}) bool {
102+
chs := value.([]change)
103+
104+
for _, ch := range chs {
105+
output += ufmt.Sprintf(
106+
"- #%d: %s (%d)\n",
107+
ch.blockNum,
108+
ch.validator.Address.String(),
109+
ch.validator.VotingPower,
110+
)
111+
}
112+
113+
return false
114+
})
58115

59116
return output
60117
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package validators
2+
3+
import (
4+
"testing"
5+
6+
"std"
7+
8+
"gno.land/p/demo/avl"
9+
"gno.land/p/demo/testutils"
10+
"gno.land/p/demo/uassert"
11+
"gno.land/p/demo/ufmt"
12+
"gno.land/p/sys/validators"
13+
)
14+
15+
// generateTestValidators generates a dummy validator set
16+
func generateTestValidators(count int) []validators.Validator {
17+
vals := make([]validators.Validator, 0, count)
18+
19+
for i := 0; i < count; i++ {
20+
val := validators.Validator{
21+
Address: testutils.TestAddress(ufmt.Sprintf("%d", i)),
22+
PubKey: "public-key",
23+
VotingPower: 10,
24+
}
25+
26+
vals = append(vals, val)
27+
}
28+
29+
return vals
30+
}
31+
32+
func TestValidators_AddRemove(t *testing.T) {
33+
// Clear any changes
34+
changes = avl.NewTree()
35+
36+
var (
37+
vals = generateTestValidators(100)
38+
initialHeight = int64(123)
39+
)
40+
41+
// Add in the validators
42+
for _, val := range vals {
43+
addValidator(val)
44+
45+
// Make sure the validator is added
46+
uassert.True(t, vp.IsValidator(val.Address))
47+
48+
std.TestSkipHeights(1)
49+
}
50+
51+
for i := initialHeight; i < initialHeight+int64(len(vals)); i++ {
52+
// Make sure the changes are saved
53+
chs := GetChanges(i)
54+
55+
// We use the funky index calculation to make sure
56+
// changes are properly handled for each block span
57+
uassert.Equal(t, initialHeight+int64(len(vals))-i, int64(len(chs)))
58+
59+
for index, val := range vals[i-initialHeight:] {
60+
// Make sure the changes are equal to the additions
61+
ch := chs[index]
62+
63+
uassert.Equal(t, val.Address, ch.Address)
64+
uassert.Equal(t, val.PubKey, ch.PubKey)
65+
uassert.Equal(t, val.VotingPower, ch.VotingPower)
66+
}
67+
}
68+
69+
// Save the beginning height for the removal
70+
initialRemoveHeight := std.GetHeight()
71+
72+
// Clear any changes
73+
changes = avl.NewTree()
74+
75+
// Remove the validators
76+
for _, val := range vals {
77+
removeValidator(val.Address)
78+
79+
// Make sure the validator is removed
80+
uassert.False(t, vp.IsValidator(val.Address))
81+
82+
std.TestSkipHeights(1)
83+
}
84+
85+
for i := initialRemoveHeight; i < initialRemoveHeight+int64(len(vals)); i++ {
86+
// Make sure the changes are saved
87+
chs := GetChanges(i)
88+
89+
// We use the funky index calculation to make sure
90+
// changes are properly handled for each block span
91+
uassert.Equal(t, initialRemoveHeight+int64(len(vals))-i, int64(len(chs)))
92+
93+
for index, val := range vals[i-initialRemoveHeight:] {
94+
// Make sure the changes are equal to the additions
95+
ch := chs[index]
96+
97+
uassert.Equal(t, val.Address, ch.Address)
98+
uassert.Equal(t, val.PubKey, ch.PubKey)
99+
uassert.Equal(t, uint64(0), ch.VotingPower)
100+
}
101+
}
102+
}

gno.land/cmd/gnoland/start.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
2424
"github.com/gnolang/gno/tm2/pkg/commands"
2525
"github.com/gnolang/gno/tm2/pkg/crypto"
26+
"github.com/gnolang/gno/tm2/pkg/events"
2627
osm "github.com/gnolang/gno/tm2/pkg/os"
2728
"github.com/gnolang/gno/tm2/pkg/telemetry"
2829
"go.uber.org/zap"
@@ -239,14 +240,17 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
239240
io.Println(startGraphic)
240241
}
241242

243+
// Create a top-level shared event switch
244+
evsw := events.NewEventSwitch()
245+
242246
// Create application and node
243-
cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles)
247+
cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger)
244248
if err != nil {
245249
return fmt.Errorf("unable to create the Gnoland app, %w", err)
246250
}
247251

248252
// Create a default node, with the given setup
249-
gnoNode, err := node.DefaultNewNode(cfg, genesisPath, logger)
253+
gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger)
250254
if err != nil {
251255
return fmt.Errorf("unable to create the Gnoland node, %w", err)
252256
}

0 commit comments

Comments
 (0)