Skip to content

Commit 80b46ce

Browse files
feat(tm2): implement address book (gnolang#5861)
Closes: gnolang#5859 Discovered peers are now persisted to a local address book file and reloaded on node restart, so the node can reconnect to previously discovered peers without re-running the full discovery process. --------- Co-authored-by: Thomas <thomas.bruyelle@tendermint.com>
1 parent d2869dc commit 80b46ce

7 files changed

Lines changed: 850 additions & 9 deletions

File tree

tm2/pkg/bft/node/node.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log/slog"
1010
"net"
1111
"net/http"
12+
"path/filepath"
1213
"strings"
1314
"sync"
1415
"time"
@@ -42,6 +43,7 @@ import (
4243
dbm "github.com/gnolang/gno/tm2/pkg/db"
4344
"github.com/gnolang/gno/tm2/pkg/errors"
4445
"github.com/gnolang/gno/tm2/pkg/events"
46+
osm "github.com/gnolang/gno/tm2/pkg/os"
4547
"github.com/gnolang/gno/tm2/pkg/p2p"
4648
"github.com/gnolang/gno/tm2/pkg/service"
4749
verset "github.com/gnolang/gno/tm2/pkg/versionset"
@@ -509,7 +511,24 @@ func NewNode(config *cfg.Config,
509511
var discoveryReactor *discovery.Reactor
510512

511513
if config.P2P.PeerExchange {
512-
discoveryReactor = discovery.NewReactor()
514+
// Set up the persistent peer store so discovered peers survive restarts.
515+
// The store file lives in the config directory alongside the node key.
516+
addrBookPath := config.P2P.AddrBookFile()
517+
518+
if err := osm.EnsureDir(filepath.Dir(addrBookPath), cfg.DefaultDirPerm); err != nil {
519+
return nil, fmt.Errorf("unable to create address book directory, %w", err)
520+
}
521+
522+
discoveryStore, err := discovery.NewStore(
523+
addrBookPath,
524+
*nodeInfo.NetAddress,
525+
discovery.WithLogger(logger.With("module", discoveryModuleName)),
526+
)
527+
if err != nil {
528+
return nil, fmt.Errorf("unable to initialize peer store, %w", err)
529+
}
530+
531+
discoveryReactor = discovery.NewReactor(discoveryStore)
513532

514533
discoveryReactor.SetLogger(logger.With("module", discoveryModuleName))
515534

tm2/pkg/p2p/config/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"errors"
5+
"path/filepath"
56
"time"
67
)
78

@@ -12,6 +13,9 @@ var (
1213
ErrInvalidReceiveRate = errors.New("invalid packet receive rate")
1314
)
1415

16+
// defaultAddrBookPath is the default relative path for the persisted peer address book
17+
var defaultAddrBookPath = "config/addrbook.json"
18+
1519
// P2PConfig defines the configuration options for the Tendermint peer-to-peer networking layer
1620
type P2PConfig struct {
1721
RootDir string `json:"rpc" toml:"home"`
@@ -51,6 +55,10 @@ type P2PConfig struct {
5155

5256
// Comma separated list of peer IDs to keep private (will not be gossiped to other peers)
5357
PrivatePeerIDs string `json:"private_peer_ids" toml:"private_peer_ids" comment:"Comma separated list of peer IDs to keep private (will not be gossiped to other peers)"`
58+
59+
// Path to the address book file used to persist discovered peers across restarts.
60+
// When empty, a default path relative to the root directory is used.
61+
AddrBook string `json:"addr_book_file" toml:"addr_book_file" comment:"Path to the address book file used to persist discovered peers across restarts"`
5462
}
5563

5664
// DefaultP2PConfig returns a default configuration for the peer-to-peer layer
@@ -65,6 +73,7 @@ func DefaultP2PConfig() *P2PConfig {
6573
SendRate: 5120000, // 5 mB/s
6674
RecvRate: 5120000, // 5 mB/s
6775
PeerExchange: true,
76+
AddrBook: defaultAddrBookPath,
6877
}
6978
}
7079

@@ -89,3 +98,19 @@ func (cfg *P2PConfig) ValidateBasic() error {
8998

9099
return nil
91100
}
101+
102+
// AddrBookFile returns the absolute path to the address book file.
103+
// When AddrBook is a relative path, it is resolved against RootDir.
104+
// When AddrBook is empty, the default path is used.
105+
func (cfg *P2PConfig) AddrBookFile() string {
106+
path := cfg.AddrBook
107+
if path == "" {
108+
path = defaultAddrBookPath
109+
}
110+
111+
if filepath.IsAbs(path) {
112+
return path
113+
}
114+
115+
return filepath.Join(cfg.RootDir, path)
116+
}

tm2/pkg/p2p/config/config_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"path/filepath"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -57,3 +58,46 @@ func TestP2PConfig_ValidateBasic(t *testing.T) {
5758
assert.NoError(t, cfg.ValidateBasic())
5859
})
5960
}
61+
62+
func TestP2PConfig_AddrBookFile(t *testing.T) {
63+
t.Parallel()
64+
65+
t.Run("default relative path", func(t *testing.T) {
66+
t.Parallel()
67+
68+
cfg := DefaultP2PConfig()
69+
cfg.RootDir = "/root"
70+
71+
assert.Equal(t, filepath.Join("/root", defaultAddrBookPath), cfg.AddrBookFile())
72+
})
73+
74+
t.Run("empty uses default", func(t *testing.T) {
75+
t.Parallel()
76+
77+
cfg := DefaultP2PConfig()
78+
cfg.RootDir = "/root"
79+
cfg.AddrBook = ""
80+
81+
assert.Equal(t, filepath.Join("/root", defaultAddrBookPath), cfg.AddrBookFile())
82+
})
83+
84+
t.Run("absolute path preserved", func(t *testing.T) {
85+
t.Parallel()
86+
87+
cfg := DefaultP2PConfig()
88+
cfg.RootDir = "/root"
89+
cfg.AddrBook = "/custom/addrbook.json"
90+
91+
assert.Equal(t, "/custom/addrbook.json", cfg.AddrBookFile())
92+
})
93+
94+
t.Run("custom relative path", func(t *testing.T) {
95+
t.Parallel()
96+
97+
cfg := DefaultP2PConfig()
98+
cfg.RootDir = "/root"
99+
cfg.AddrBook = "peers/book.json"
100+
101+
assert.Equal(t, filepath.Join("/root", "peers/book.json"), cfg.AddrBookFile())
102+
})
103+
}

tm2/pkg/p2p/discovery/discovery.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,21 @@ type Reactor struct {
4848
cancelFn context.CancelFunc
4949

5050
discoveryInterval time.Duration
51+
52+
// store persists discovered peers to disk.
53+
store *Store
5154
}
5255

53-
// NewReactor creates a new peer discovery reactor
54-
func NewReactor(opts ...Option) *Reactor {
56+
// NewReactor creates a new peer discovery reactor.
57+
// The store is used to persist discovered peers across restarts.
58+
func NewReactor(store *Store, opts ...Option) *Reactor {
5559
ctx, cancelFn := context.WithCancel(context.Background())
5660

5761
r := &Reactor{
5862
ctx: ctx,
5963
cancelFn: cancelFn,
6064
discoveryInterval: discoveryInterval,
65+
store: store,
6166
}
6267

6368
r.BaseReactor = *p2p.NewBaseReactor("Reactor", r)
@@ -72,6 +77,11 @@ func NewReactor(opts ...Option) *Reactor {
7277

7378
// OnStart runs the peer discovery protocol
7479
func (r *Reactor) OnStart() error {
80+
// Dial peers loaded from the persistent store.
81+
// This allows the node to reconnect to previously discovered peers
82+
// without going through the full discovery process again.
83+
r.dialPersistedPeers()
84+
7585
go func() {
7686
ticker := time.NewTicker(r.discoveryInterval)
7787
defer ticker.Stop()
@@ -102,16 +112,39 @@ func (r *Reactor) OnStart() error {
102112

103113
// Request peers, async
104114
go r.requestPeers(peers[randomPeer.Int64()])
115+
116+
// Persist any newly discovered peers to disk.
117+
// Save is a no-op when the store hasn't changed.
118+
if err := r.store.Save(); err != nil {
119+
r.Logger.Error("unable to save peer store", "err", err)
120+
}
105121
}
106122
}
107123
}()
108124

109125
return nil
110126
}
111127

128+
// dialPersistedPeers loads peers from the store and queues them for dialing
129+
func (r *Reactor) dialPersistedPeers() {
130+
peers := r.store.GetPeers()
131+
if len(peers) == 0 {
132+
return
133+
}
134+
135+
r.Logger.Info("dialing persisted peers", "count", len(peers))
136+
137+
r.Switch.DialPeers(peers...)
138+
}
139+
112140
// OnStop stops the peer discovery protocol
113141
func (r *Reactor) OnStop() {
114142
r.cancelFn()
143+
144+
// Flush the peer store to disk so discovered peers survive a restart
145+
if err := r.store.Flush(); err != nil {
146+
r.Logger.Error("unable to save peer store", "err", err)
147+
}
115148
}
116149

117150
// requestPeers requests the peer set from the given peer
@@ -171,6 +204,9 @@ func (r *Reactor) Receive(chID byte, peer p2p.PeerConn, msgBytes []byte) {
171204
r.Logger.Warn("unable to handle discovery request", "err", err)
172205
}
173206
case *Response:
207+
// Persist the discovered peers so they survive a restart
208+
r.store.AddPeers(msg.Peers...)
209+
174210
// Make the peers available for dialing on the switch
175211
r.Switch.DialPeers(msg.Peers...)
176212
default:

0 commit comments

Comments
 (0)