Skip to content

Commit

Permalink
[P2P] KISS 4 - Basic nodes bootstrapping [Merge me after #522] - (Iss…
Browse files Browse the repository at this point in the history
…ue: #498) (#523)

## Description

This PR has been extracted from #491 and is, hopefully, more digestible
from a code-review and scope point of view.

This simply adds entrypoints and basic logic for node bootstrapping
leveraging the finite state machine

## Issue

Fixes #498 

## Type of change

Please mark the relevant option(s):

- [x] New feature, functionality or library
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Major breaking change
- [ ] Documentation
- [ ] Other <!-- add details here if it a different type of change -->

## List of changes

- Ability to fetch an addressbook from a bootstrap node(s) (by
convention the first validator in LocalNet)
- It's possible to override bootstrap nodes via CLI flag

## Testing

- [x] `make develop_test`
- [x]
[LocalNet](https://github.com/pokt-network/pocket/blob/main/docs/development/README.md)
w/ all of the steps outlined in the `README`

## Required Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have tested my changes using the available tooling
- [x] I have updated the corresponding CHANGELOG

### If Applicable Checklist

- [ ] I have updated the corresponding README(s); local and/or global
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added, or updated,
[mermaid.js](https://mermaid-js.github.io) diagrams in the corresponding
README(s)
- [ ] I have added, or updated, documentation and
[mermaid.js](https://mermaid-js.github.io) diagrams in `shared/docs/*`
if I updated `shared/*`README(s)

---------

Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
Co-authored-by: Dmitry K <okdas@pm.me>
Co-authored-by: Dmitry Knyazev <okdas@users.noreply.github.com>
Co-authored-by: Daniel Olshansky <olshansky@pokt.network>
Co-authored-by: Daniel Olshansky <olshansky.daniel@gmail.com>
  • Loading branch information
5 people authored Feb 20, 2023
1 parent 6da420c commit b3ed290
Show file tree
Hide file tree
Showing 16 changed files with 318 additions and 12 deletions.
4 changes: 4 additions & 0 deletions app/pocket/doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.0.6] - 2023-02-20

- Added the ability to override the default `BootstrapNodes` via flag

## [0.0.0.5] - 2023-02-17

- Removed unnecessary server mode enabling call via `EnableServerMode()` function
Expand Down
8 changes: 7 additions & 1 deletion app/pocket/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
func main() {
configFilename := flag.String("config", "", "Relative or absolute path to the config file.")
genesisFilename := flag.String("genesis", "", "Relative or absolute path to the genesis file.")
bootstrapNodes := flag.String("bootstrap-nodes", "", "Comma separated list of bootstrap nodes.")

v := flag.Bool("version", false, "")
flag.Parse()
Expand All @@ -21,7 +22,12 @@ func main() {
return
}

runtimeMgr := runtime.NewManagerFromFiles(*configFilename, *genesisFilename)
options := []func(*runtime.Manager){}
if bootstrapNodes != nil && *bootstrapNodes != "" {
options = append(options, runtime.WithBootstrapNodes(*bootstrapNodes))
}

runtimeMgr := runtime.NewManagerFromFiles(*configFilename, *genesisFilename, options...)
bus, err := runtime.CreateBus(runtimeMgr)
if err != nil {
logger.Global.Fatal().Err(err).Msg("Failed to create bus")
Expand Down
5 changes: 5 additions & 0 deletions p2p/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.0.28] - 2023-02-20

- Added basic `bootstrap` nodes support
- Reacting to `ConsensusNewHeightEventType` and `StateMachineTransitionEventType` to update the address book and current height and determine if a bootstrap is needed

## [0.0.0.27] - 2023-02-17

- Deprecated `debugAddressBookProvider`
Expand Down
105 changes: 105 additions & 0 deletions p2p/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package p2p

import (
"context"
"encoding/csv"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"

rpcABP "github.com/pokt-network/pocket/p2p/providers/addrbook_provider/rpc"
rpcCHP "github.com/pokt-network/pocket/p2p/providers/current_height_provider/rpc"
typesP2P "github.com/pokt-network/pocket/p2p/types"
"github.com/pokt-network/pocket/rpc"
"github.com/pokt-network/pocket/runtime/defaults"
)

// configureBootstrapNodes parses the bootstrap nodes from the config and validates them
func (m *p2pModule) configureBootstrapNodes() error {
p2pCfg := m.GetBus().GetRuntimeMgr().GetConfig().P2P

bootstrapNodesCsv := strings.Trim(p2pCfg.BootstrapNodesCsv, " ")
if bootstrapNodesCsv == "" {
bootstrapNodesCsv = defaults.DefaultP2PBootstrapNodesCsv
}
csvReader := csv.NewReader(strings.NewReader(bootstrapNodesCsv))
bootStrapNodes, err := csvReader.Read()
if err != nil {
return fmt.Errorf("error parsing bootstrap nodes: %w", err)
}

// validate the bootstrap nodes
for i, node := range bootStrapNodes {
bootStrapNodes[i] = strings.Trim(node, " ")
if !isValidHostnamePort(bootStrapNodes[i]) {
return fmt.Errorf("invalid bootstrap node: %s", bootStrapNodes[i])
}
}
m.bootstrapNodes = bootStrapNodes
return nil
}

// bootstrap attempts to bootstrap from a bootstrap node
func (m *p2pModule) bootstrap() error {
var addrBook typesP2P.AddrBook

for _, bootstrapNode := range m.bootstrapNodes {
m.logger.Info().Str("endpoint", bootstrapNode).Msg("Attempting to bootstrap from bootstrap node")

client, err := rpc.NewClientWithResponses(bootstrapNode)
if err != nil {
continue
}
healthCheck, err := client.GetV1Health(context.TODO())
if err != nil || healthCheck == nil || healthCheck.StatusCode != http.StatusOK {
m.logger.Warn().Str("bootstrapNode", bootstrapNode).Msg("Error getting a green health check from bootstrap node")
continue
}

addressBookProvider := rpcABP.NewRPCAddrBookProvider(
rpcABP.WithP2PConfig(
m.GetBus().GetRuntimeMgr().GetConfig().P2P,
),
rpcABP.WithCustomRPCUrl(bootstrapNode),
)

currentHeightProvider := rpcCHP.NewRPCCurrentHeightProvider(rpcCHP.WithCustomRPCUrl(bootstrapNode))

addrBook, err = addressBookProvider.GetStakedAddrBookAtHeight(currentHeightProvider.CurrentHeight())
if err != nil {
m.logger.Warn().Err(err).Str("endpoint", bootstrapNode).Msg("Error getting address book from bootstrap node")
continue
}
}

if len(addrBook) == 0 {
return fmt.Errorf("bootstrap failed")
}

for _, peer := range addrBook {
m.logger.Debug().Str("address", peer.Address.String()).Msg("Adding peer to addrBook")
if err := m.network.AddPeerToAddrBook(peer); err != nil {
return err
}
}
return nil
}

func isValidHostnamePort(str string) bool {
pattern := regexp.MustCompile(`^(https?)://([a-zA-Z0-9.-]+):(\d{1,5})$`)
matches := pattern.FindStringSubmatch(str)
if len(matches) != 4 {
return false
}
protocol := matches[1]
if protocol != "http" && protocol != "https" {
return false
}
port, err := strconv.Atoi(matches[3])
if err != nil || port < 0 || port > 65535 {
return false
}
return true
}
22 changes: 22 additions & 0 deletions p2p/event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/pokt-network/pocket/shared/codec"
coreTypes "github.com/pokt-network/pocket/shared/core/types"
"github.com/pokt-network/pocket/shared/messaging"
"google.golang.org/protobuf/types/known/anypb"
)
Expand Down Expand Up @@ -40,6 +41,27 @@ func (m *p2pModule) HandleEvent(event *anypb.Any) error {
}
}

case messaging.StateMachineTransitionEventType:
stateMachineTransitionEvent, ok := evt.(*messaging.StateMachineTransitionEvent)
if !ok {
return fmt.Errorf("failed to cast event to StateMachineTransitionEvent")
}

if stateMachineTransitionEvent.NewState == string(coreTypes.StateMachineState_P2P_Bootstrapping) {
addrBook := m.network.GetAddrBook()
if len(addrBook) == 0 {
m.logger.Warn().Msg("No peers in addrbook, bootstrapping")

if err := m.bootstrap(); err != nil {
return err
}
}
m.logger.Info().Bool("TODO", true).Msg("Advertise self to network")
if err := m.GetBus().GetStateMachineModule().SendEvent(coreTypes.StateMachineEvent_P2P_IsBootstrapped); err != nil {
return err
}
}

default:
return fmt.Errorf("unknown event type: %s", event.MessageName())
}
Expand Down
6 changes: 6 additions & 0 deletions p2p/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type p2pModule struct {

addrBookProvider providers.AddrBookProvider
currentHeightProvider providers.CurrentHeightProvider

bootstrapNodes []string
}

func Create(bus modules.Bus, options ...modules.ModuleOption) (modules.Module, error) {
Expand All @@ -55,6 +57,10 @@ func (*p2pModule) Create(bus modules.Bus, options ...modules.ModuleOption) (modu
cfg := runtimeMgr.GetConfig()
p2pCfg := cfg.P2P

if err := m.configureBootstrapNodes(); err != nil {
return nil, err
}

privateKey, err := cryptoPocket.NewPrivateKey(p2pCfg.PrivateKey)
if err != nil {
return nil, err
Expand Down
129 changes: 129 additions & 0 deletions p2p/module_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package p2p

import (
"strings"
"testing"

"github.com/golang/mock/gomock"
"github.com/pokt-network/pocket/runtime/configs"
"github.com/pokt-network/pocket/runtime/defaults"
cryptoPocket "github.com/pokt-network/pocket/shared/crypto"
"github.com/pokt-network/pocket/shared/modules"
mockModules "github.com/pokt-network/pocket/shared/modules/mocks"
"github.com/stretchr/testify/require"
)

func Test_Create_configureBootstrapNodes(t *testing.T) {
defaultBootstrapNodes := strings.Split(defaults.DefaultP2PBootstrapNodesCsv, ",")
key := cryptoPocket.GetPrivKeySeed(1)

type args struct {
initialBootstrapNodesCsv string
}
tests := []struct {
name string
args args
wantBootstrapNodes []string
wantErr bool
}{
{
name: "unset boostrap nodes should yield no error and return DefaultP2PBootstrapNodes",
args: args{},
wantErr: false,
wantBootstrapNodes: defaultBootstrapNodes,
},
{
name: "empty string boostrap nodes should yield no error and return DefaultP2PBootstrapNodes",
args: args{
initialBootstrapNodesCsv: "",
},
wantErr: false,
wantBootstrapNodes: defaultBootstrapNodes,
},
{
name: "untrimmed empty string boostrap nodes should yield no error and return DefaultP2PBootstrapNodes",
args: args{
initialBootstrapNodesCsv: " ",
},
wantErr: false,
wantBootstrapNodes: defaultBootstrapNodes,
},
{
name: "untrimmed string boostrap nodes should yield no error and return the trimmed urls",
args: args{
initialBootstrapNodesCsv: " http://somenode:50832 , http://someothernode:50832 ",
},
wantErr: false,
wantBootstrapNodes: []string{"http://somenode:50832", "http://someothernode:50832"},
},
{
name: "custom bootstrap nodes should yield no error and return the custom bootstrap node",
args: args{
initialBootstrapNodesCsv: "http://somenode:50832,http://someothernode:50832",
},
wantBootstrapNodes: []string{"http://somenode:50832", "http://someothernode:50832"},
wantErr: false,
},
{
name: "malformed bootstrap nodes string should yield an error and return nil",
args: args{
initialBootstrapNodesCsv: "\n\n",
},
wantBootstrapNodes: []string(nil),
wantErr: true,
},
{
name: "port number too high yields an error and empty list of bootstrap nodes",
args: args{
initialBootstrapNodesCsv: "http://somenode:99999",
},
wantBootstrapNodes: []string(nil),
wantErr: true,
},
{
name: "negative port number yields an error and empty list of bootstrap nodes",
args: args{
initialBootstrapNodesCsv: "udp://somenode:-8080",
},
wantBootstrapNodes: []string(nil),
wantErr: true,
},
{
name: "wrong protocol yields an error and empty list of bootstrap nodes",
args: args{
initialBootstrapNodesCsv: "udp://somenode:58884",
},
wantBootstrapNodes: []string(nil),
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
mockRuntimeMgr := mockModules.NewMockRuntimeMgr(ctrl)
mockBus := createMockBus(t, mockRuntimeMgr)
mockConsensusModule := mockModules.NewMockConsensusModule(ctrl)
mockBus.EXPECT().GetConsensusModule().Return(mockConsensusModule).AnyTimes()
mockRuntimeMgr.EXPECT().GetConfig().Return(&configs.Config{
PrivateKey: key.String(),
P2P: &configs.P2PConfig{
BootstrapNodesCsv: tt.args.initialBootstrapNodesCsv,
PrivateKey: key.String(),
},
}).AnyTimes()
mockBus.EXPECT().GetRuntimeMgr().Return(mockRuntimeMgr).AnyTimes()

var p2pMod modules.Module
p2pMod, err := Create(mockBus)
if (err != nil) != tt.wantErr {
t.Errorf("p2pModule.Create() error = %v, wantErr %v", err, tt.wantErr)
}

if !tt.wantErr {
actualBootstrapNodes := p2pMod.(*p2pModule).bootstrapNodes
require.EqualValues(t, tt.wantBootstrapNodes, actualBootstrapNodes)
}
})
}
}
2 changes: 1 addition & 1 deletion p2p/providers/current_height_provider/rpc/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (rchp *rpcCurrentHeightProvider) CurrentHeight() uint64 {

func NewRPCCurrentHeightProvider(options ...modules.ModuleOption) *rpcCurrentHeightProvider {
rchp := &rpcCurrentHeightProvider{
rpcUrl: fmt.Sprintf("http://%s:%s", rpcHost, defaults.DefaultRPCPort),
rpcUrl: fmt.Sprintf("http://%s:%s", rpcHost, defaults.DefaultRPCPort), // TODO: Make port configurable
}

for _, o := range options {
Expand Down
10 changes: 1 addition & 9 deletions p2p/utils_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package p2p

import (
"crypto/ed25519"
"encoding/binary"
"fmt"
"log"
"sort"
Expand Down Expand Up @@ -50,13 +48,7 @@ func generateKeys(_ *testing.T, numValidators int) []cryptoPocket.PrivateKey {

for i := range keys {
seedInt := genesisConfigSeedStart + i
seed := make([]byte, ed25519.PrivateKeySize)
binary.LittleEndian.PutUint32(seed, uint32(seedInt))
pk, err := cryptoPocket.NewPrivateKeyFromSeed(seed)
if err != nil {
panic(err)
}
keys[i] = pk
keys[i] = cryptoPocket.GetPrivKeySeed(seedInt)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i].Address().String() < keys[j].Address().String()
Expand Down
1 change: 1 addition & 0 deletions runtime/configs/proto/p2p_config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ message P2PConfig {
conn.ConnectionType connection_type = 4;
uint64 max_mempool_count = 5; // this is used to limit the number of nonces that can be stored in the mempool, after which a FIFO mechanism is used to remove the oldest nonces and make space for the new ones
bool is_client_only = 6;
string bootstrap_nodes_csv = 7; // string in the format "http://somenode:50832,http://someothernode:50832". Refer to `p2p/module_test.go` for additional details.
}
8 changes: 8 additions & 0 deletions runtime/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ var (
DefaultP2PUseRainTree = true
DefaultP2PConnectionType = types.ConnectionType_TCPConnection
DefaultP2PMaxMempoolCount = uint64(1e5)
// DefaultP2PBootstrapNodesCsv is a list of nodes to bootstrap the network with. By convention, for now, the first validator will provide bootstrapping facilities.
//
// In LocalNet, the developer will have only one of the two stack online, therefore this is also a poor's man way to simulate the scenario in which a boostrap node is offline.
DefaultP2PBootstrapNodesCsv = fmt.Sprintf("%s,%s",
fmt.Sprintf("http://%s:%s", Validator1EndpointDockerCompose, DefaultRPCPort),
fmt.Sprintf("http://%s:%s", Validator1EndpointK8S, DefaultRPCPort),
)

// telemetry
DefaultTelemetryEnabled = true
DefaultTelemetryAddress = "0.0.0.0:9000"
Expand Down
Loading

0 comments on commit b3ed290

Please sign in to comment.