Skip to content

Commit

Permalink
feat: seed-based automatic peering
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Apr 9, 2024
1 parent 51c9228 commit 430702d
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 3 deletions.
34 changes: 33 additions & 1 deletion keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
crand "crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io"

libp2p "github.com/libp2p/go-libp2p/core/crypto"
peer "github.com/libp2p/go-libp2p/core/peer"
"github.com/mr-tron/base58"
"golang.org/x/crypto/hkdf"
)
Expand All @@ -24,7 +26,7 @@ func newSeed() (string, error) {
return base58.Encode(bs), nil
}

// derive derives libp2p keys from a b58-encoded seed.
// deriveKey derives libp2p keys from a b58-encoded seed.
func deriveKey(b58secret string, info []byte) (libp2p.PrivKey, error) {
secret, err := base58.Decode(b58secret)
if err != nil {
Expand All @@ -43,3 +45,33 @@ func deriveKey(b58secret string, info []byte) (libp2p.PrivKey, error) {
key := ed25519.NewKeyFromSeed(keySeed)
return libp2p.UnmarshalEd25519PrivateKey(key)
}

// derivePeerIDs derives the peer IDs of all the peers with the same seed up to
// maxIndex. Our peer ID (with index 'ourIndex') is not generated.
func derivePeerIDs(seed string, ourIndex int, maxIndex int) ([]peer.ID, error) {
peerIDs := []peer.ID{}

Check warning on line 52 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L51-L52

Added lines #L51 - L52 were not covered by tests

for i := 0; i <= maxIndex; i++ {
if i == ourIndex {
continue

Check warning on line 56 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L54-L56

Added lines #L54 - L56 were not covered by tests
}

peerPriv, err := deriveKey(seed, deriveInfo(i))
if err != nil {
return nil, err

Check warning on line 61 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L59-L61

Added lines #L59 - L61 were not covered by tests
}

pid, err := peer.IDFromPrivateKey(peerPriv)
if err != nil {
return nil, err

Check warning on line 66 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L64-L66

Added lines #L64 - L66 were not covered by tests
}

peerIDs = append(peerIDs, pid)

Check warning on line 69 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L69

Added line #L69 was not covered by tests
}

return peerIDs, nil

Check warning on line 72 in keys.go

View check run for this annotation

Codecov / codecov/patch

keys.go#L72

Added line #L72 was not covered by tests
}

func deriveInfo(index int) []byte {
return []byte(fmt.Sprintf("rainbow-%d", index))
}
45 changes: 44 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ Generate an identity seed and launch a gateway:
EnvVars: []string{"RAINBOW_SEED_INDEX"},
Usage: "Index to derivate the peerID (needs --seed)",
},
&cli.BoolFlag{
Name: "seed-peering",
Value: false,
EnvVars: []string{"RAINBOW_SEED_PEERING"},
Usage: "Automatic peering with peers with the same seed (requires --seed and --seed-index). Automatically enables --dht-routing and --dht-shared-host",
},
&cli.UintFlag{
Name: "seed-peering-max-index",
Value: 100,
EnvVars: []string{"RAINBOW_SEED_PEERING_MAX_INDEX"},
Usage: "Largest index to derive automatic peering peer IDs for",
},

Check warning on line 103 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L92-L103

Added lines #L92 - L103 were not covered by tests
&cli.StringSliceFlag{
Name: "gateway-domains",
Value: cli.NewStringSlice(),
Expand Down Expand Up @@ -281,7 +293,7 @@ share the same seed as long as the indexes are different.
index := cctx.Int("seed-index")
if len(seed) > 0 && index >= 0 {
fmt.Println("Deriving identity from seed")
priv, err = deriveKey(seed, []byte(fmt.Sprintf("rainbow-%d", index)))
priv, err = deriveKey(seed, deriveInfo(index))

Check warning on line 296 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L296

Added line #L296 was not covered by tests
} else {
fmt.Println("Setting identity from libp2p.key")
keyFile := filepath.Join(secretsDir, "libp2p.key")
Expand All @@ -300,6 +312,37 @@ share the same seed as long as the indexes are different.
peeringAddrs = append(peeringAddrs, *ai)
}

if cctx.Bool("seed-peering") {
if !cctx.IsSet("seed") || !cctx.IsSet("seed-index") {
return errors.New("--seed and --seed-index must be explicitly defined when --seed-peering is enabled")

Check warning on line 317 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L315-L317

Added lines #L315 - L317 were not covered by tests
}

if cctx.String("dht-routing") == "off" {
return errors.New("--dht-routing=off is incompatible with --seeds-peering: DHT needs to run in order to be able to find peers")

Check warning on line 321 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L320-L321

Added lines #L320 - L321 were not covered by tests
}

// If seeds-peering is enabled, automatically use the same libp2p host
// for the DHT. This allows the peer information (id and addresses) to
// be discoverable via the DHT without having to create a new DHT client
// instance just for this purpose.
err = cctx.Set("dht-shared-host", "true")
if err != nil {
return err

Check warning on line 330 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L328-L330

Added lines #L328 - L330 were not covered by tests
}

maxIndex := cctx.Uint("seed-peering-max-index")
peeringIDs, err := derivePeerIDs(seed, index, int(maxIndex))
if err != nil {
return err

Check warning on line 336 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L333-L336

Added lines #L333 - L336 were not covered by tests
}

for _, pid := range peeringIDs {

Check warning on line 339 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L339

Added line #L339 was not covered by tests
// The peering module will automatically perform lookups to find the
// addresses of the given peers.
peeringAddrs = append(peeringAddrs, peer.AddrInfo{ID: pid})

Check warning on line 342 in main.go

View check run for this annotation

Codecov / codecov/patch

main.go#L342

Added line #L342 was not covered by tests
}
}

cfg := Config{
DataDir: ddir,
BlockstoreType: cctx.String("blockstore"),
Expand Down
11 changes: 10 additions & 1 deletion setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/routing"
"github.com/libp2p/go-libp2p/p2p/net/connmgr"
"github.com/libp2p/go-libp2p/p2p/protocol/identify"
"github.com/multiformats/go-multiaddr"
"go.opencensus.io/stats/view"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
Expand All @@ -68,6 +69,11 @@ const (
DHTOff DHTRouting = "off"
)

func init() {
// Lets us discover our own public address with a single observation
identify.ActivationThresh = 1
}

type Node struct {
vs routing.ValueStore
host host.Host
Expand All @@ -80,6 +86,7 @@ type Node struct {
resolver resolver.Resolver

ns namesys.NameSystem
ps *peering.PeeringService

bwc *metrics.BandwidthCounter

Expand Down Expand Up @@ -309,8 +316,9 @@ func Setup(ctx context.Context, cfg Config, key crypto.PrivKey, dnsCache *cached
return nil, err
}

var ps *peering.PeeringService
if len(cfg.Peering) > 0 {
ps := peering.NewPeeringService(h)
ps = peering.NewPeeringService(h)
if err := ps.Start(); err != nil {
return nil, err
}
Expand Down Expand Up @@ -395,6 +403,7 @@ func Setup(ctx context.Context, cfg Config, key crypto.PrivKey, dnsCache *cached
bsClient: bswap,
ns: ns,
vs: router,
ps: ps,
bsrv: bsrv,
resolver: r,
bwc: bwc,
Expand Down
78 changes: 78 additions & 0 deletions setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"context"
"testing"
"time"

"github.com/libp2p/go-libp2p/core/crypto"
peer "github.com/libp2p/go-libp2p/core/peer"
"github.com/stretchr/testify/require"
)

func testSeedPeering(t *testing.T, n int) ([]crypto.PrivKey, []peer.ID, []*Node) {
cdns := newCachedDNS(dnsCacheRefreshInterval)
defer cdns.Close()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

seed, err := newSeed()
require.NoError(t, err)

sks := make([]crypto.PrivKey, n)
pids := make([]peer.ID, n)

for i := 0; i < n; i++ {
sks[i], err = deriveKey(seed, deriveInfo(i))
require.NoError(t, err)

pids[i], err = peer.IDFromPrivateKey(sks[i])
require.NoError(t, err)
}

nodes := make([]*Node, n)

for i := 0; i < n; i++ {
cfg := Config{
DataDir: t.TempDir(),
BlockstoreType: "flatfs",
DHTRouting: DHTStandard,
DHTSharedHost: true,
}

// Add all remaining peers to the peering config.
for j, pid := range pids {
if j == i {
continue
}
cfg.Peering = append(cfg.Peering, peer.AddrInfo{ID: pid})
}

nodes[i], err = Setup(ctx, cfg, sks[i], cdns)
require.NoError(t, err)
}

require.Eventually(t, func() bool {
for _, node := range nodes {
peering := node.ps.ListPeers()
if len(peering) != n-1 {
return false
}

for _, peer := range peering {
peerInfo := node.host.Peerstore().PeerInfo(peer.ID)
if len(peerInfo.Addrs) == 0 {
return false
}
}
}

return true
}, time.Minute, time.Second)
return sks, pids, nodes
}

func TestSeedPeering(t *testing.T) {
testSeedPeering(t, 3)
}

0 comments on commit 430702d

Please sign in to comment.