cmd/bootnode, eth, p2p, p2p/discover: clean up the seeder and mesh into eth.

This commit is contained in:
Péter Szilágyi 2015-04-24 11:19:33 +03:00
parent 971702e7a1
commit 6def110c37
9 changed files with 168 additions and 144 deletions

@ -71,7 +71,7 @@ func main() {
} }
} }
if _, err := discover.ListenUDP(nodeKey, *listenAddr, natm, ""); err != nil { if _, err := discover.ListenUDP(nodeKey, *listenAddr, natm, nil); err != nil {
log.Fatal(err) log.Fatal(err)
} }
select {} select {}

@ -125,6 +125,8 @@ type Ethereum struct {
blockDb common.Database // Block chain database blockDb common.Database // Block chain database
stateDb common.Database // State changes database stateDb common.Database // State changes database
extraDb common.Database // Extra database (txs, etc) extraDb common.Database // Extra database (txs, etc)
seedDb *discover.Cache // Peer database seeding the bootstrap
// Closed when databases are flushed and closed // Closed when databases are flushed and closed
databasesClosed chan bool databasesClosed chan bool
@ -179,7 +181,10 @@ func New(config *Config) (*Ethereum, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
seedDbPath := path.Join(config.DataDir, "seeds") seedDb, err := discover.NewPersistentCache(path.Join(config.DataDir, "seeds"))
if err != nil {
return nil, err
}
// Perform database sanity checks // Perform database sanity checks
d, _ := blockDb.Get([]byte("ProtocolVersion")) d, _ := blockDb.Get([]byte("ProtocolVersion"))
@ -207,6 +212,7 @@ func New(config *Config) (*Ethereum, error) {
blockDb: blockDb, blockDb: blockDb,
stateDb: stateDb, stateDb: stateDb,
extraDb: extraDb, extraDb: extraDb,
seedDb: seedDb,
eventMux: &event.TypeMux{}, eventMux: &event.TypeMux{},
accountManager: config.AccountManager, accountManager: config.AccountManager,
DataDir: config.DataDir, DataDir: config.DataDir,
@ -244,7 +250,7 @@ func New(config *Config) (*Ethereum, error) {
NAT: config.NAT, NAT: config.NAT,
NoDial: !config.Dial, NoDial: !config.Dial,
BootstrapNodes: config.parseBootNodes(), BootstrapNodes: config.parseBootNodes(),
SeedCache: seedDbPath, SeedCache: seedDb,
} }
if len(config.Port) > 0 { if len(config.Port) > 0 {
eth.net.ListenAddr = ":" + config.Port eth.net.ListenAddr = ":" + config.Port
@ -423,6 +429,7 @@ done:
} }
} }
s.seedDb.Close()
s.blockDb.Close() s.blockDb.Close()
s.stateDb.Close() s.stateDb.Close()
s.extraDb.Close() s.extraDb.Close()

134
p2p/discover/cache.go Normal file

@ -0,0 +1,134 @@
// Contains the discovery cache, storing previously seen nodes to act as seed
// servers during bootstrapping the network.
package discover
import (
"bytes"
"encoding/binary"
"net"
"os"
"github.com/ethereum/go-ethereum/rlp"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
// Cache stores all nodes we know about.
type Cache struct {
db *leveldb.DB
}
// Cache version to allow dumping old data if it changes.
var cacheVersionKey = []byte("pv")
// NewMemoryCache creates a new in-memory peer cache without a persistent backend.
func NewMemoryCache() (*Cache, error) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
return nil, err
}
return &Cache{db: db}, nil
}
// NewPersistentCache creates/opens a leveldb backed persistent peer cache, also
// flushing its contents in case of a version mismatch.
func NewPersistentCache(path string) (*Cache, error) {
// Try to open the cache, recovering any corruption
db, err := leveldb.OpenFile(path, nil)
if _, iscorrupted := err.(leveldb.ErrCorrupted); iscorrupted {
db, err = leveldb.RecoverFile(path, nil)
}
if err != nil {
return nil, err
}
// The nodes contained in the cache correspond to a certain protocol version.
// Flush all nodes if the version doesn't match.
currentVer := make([]byte, binary.MaxVarintLen64)
currentVer = currentVer[:binary.PutVarint(currentVer, Version)]
blob, err := db.Get(cacheVersionKey, nil)
switch err {
case leveldb.ErrNotFound:
// Version not found (i.e. empty cache), insert it
err = db.Put(cacheVersionKey, currentVer, nil)
case nil:
// Version present, flush if different
if !bytes.Equal(blob, currentVer) {
db.Close()
if err = os.RemoveAll(path); err != nil {
return nil, err
}
return NewPersistentCache(path)
}
}
// Clean up in case of an error
if err != nil {
db.Close()
return nil, err
}
return &Cache{db: db}, nil
}
// get retrieves a node with a given id from the seed da
func (c *Cache) get(id NodeID) *Node {
blob, err := c.db.Get(id[:], nil)
if err != nil {
return nil
}
node := new(Node)
if err := rlp.DecodeBytes(blob, node); err != nil {
return nil
}
return node
}
// list retrieves a batch of nodes from the database.
func (c *Cache) list(n int) []*Node {
it := c.db.NewIterator(nil, nil)
defer it.Release()
nodes := make([]*Node, 0, n)
for i := 0; i < n && it.Next(); i++ {
var id NodeID
copy(id[:], it.Key())
if node := c.get(id); node != nil {
nodes = append(nodes, node)
}
}
return nodes
}
// update inserts - potentially overwriting - a node in the seed database.
func (c *Cache) update(node *Node) error {
blob, err := rlp.EncodeToBytes(node)
if err != nil {
return err
}
return c.db.Put(node.ID[:], blob, nil)
}
// add inserts a new node into the seed database.
func (c *Cache) add(id NodeID, addr *net.UDPAddr, tcpPort uint16) *Node {
node := &Node{
ID: id,
IP: addr.IP,
DiscPort: addr.Port,
TCPPort: int(tcpPort),
}
c.update(node)
return node
}
// delete removes a node from the database.
func (c *Cache) delete(id NodeID) error {
return c.db.Delete(id[:], nil)
}
// Close flushes and closes the database files.
func (c *Cache) Close() {
c.db.Close()
}

@ -1,10 +1,8 @@
package discover package discover
import ( import (
"bytes"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@ -13,16 +11,12 @@ import (
"math/rand" "math/rand"
"net" "net"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/ethereum/go-ethereum/crypto/secp256k1"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
) )
const nodeIDBits = 512 const nodeIDBits = 512
@ -310,111 +304,3 @@ func randomID(a NodeID, n int) (b NodeID) {
} }
return b return b
} }
// nodeDB stores all nodes we know about.
type nodeDB struct {
ldb *leveldb.DB
}
var dbVersionKey = []byte("pv")
// Opens the backing LevelDB. If path is "", we use an in-memory database.
func newNodeDB(path string, version int64) (db *nodeDB, err error) {
db = new(nodeDB)
opts := new(opt.Options)
if path == "" {
db.ldb, err = leveldb.Open(storage.NewMemStorage(), opts)
} else {
db.ldb, err = openNodeDB(path, opts, version)
}
return db, err
}
// openNodeDB opens a persistent seed cache, flushing old versions.
func openNodeDB(path string, opts *opt.Options, version int64) (*leveldb.DB, error) {
ldb, err := leveldb.OpenFile(path, opts)
if _, iscorrupted := err.(leveldb.ErrCorrupted); iscorrupted {
ldb, err = leveldb.RecoverFile(path, opts)
}
if err != nil {
return nil, err
}
// The nodes contained in the database correspond to a certain
// protocol version. Flush all nodes if the DB version doesn't match.
// There is no need to do this for memory databases because they
// won't ever be used with a different protocol version.
shouldVal := make([]byte, binary.MaxVarintLen64)
shouldVal = shouldVal[:binary.PutVarint(shouldVal, version)]
val, err := ldb.Get(dbVersionKey, nil)
if err == leveldb.ErrNotFound {
err = ldb.Put(dbVersionKey, shouldVal, nil)
} else if err == nil && !bytes.Equal(val, shouldVal) {
// Delete and start over.
ldb.Close()
if err = os.RemoveAll(path); err != nil {
return nil, err
}
return openNodeDB(path, opts, version)
}
if err != nil {
ldb.Close()
ldb = nil
}
return ldb, err
}
// get retrieves a node with a given id from the seed da
func (db *nodeDB) get(id NodeID) *Node {
v, err := db.ldb.Get(id[:], nil)
if err != nil {
return nil
}
n := new(Node)
if err := rlp.DecodeBytes(v, n); err != nil {
return nil
}
return n
}
// list retrieves a batch of nodes from the database.
func (db *nodeDB) list(n int) []*Node {
it := db.ldb.NewIterator(nil, nil)
defer it.Release()
nodes := make([]*Node, 0, n)
for i := 0; i < n && it.Next(); i++ {
var id NodeID
copy(id[:], it.Key())
if node := db.get(id); node != nil {
nodes = append(nodes, node)
}
}
return nodes
}
// update inserts - potentially overwriting - a node in the seed database.
func (db *nodeDB) update(n *Node) error {
v, err := rlp.EncodeToBytes(n)
if err != nil {
return err
}
return db.ldb.Put(n.ID[:], v, nil)
}
// add inserts a new node into the seed database.
func (db *nodeDB) add(id NodeID, addr *net.UDPAddr, tcpPort uint16) *Node {
n := &Node{ID: id, IP: addr.IP, DiscPort: addr.Port, TCPPort: int(tcpPort)}
db.update(n)
return n
}
// delete removes a node from the database.
func (db *nodeDB) delete(id NodeID) error {
return db.ldb.Delete(id[:], nil)
}
// close flushes and closes the database files.
func (db *nodeDB) close() {
db.ldb.Close()
}

@ -27,6 +27,7 @@ type Table struct {
mutex sync.Mutex // protects buckets, their content, and nursery mutex sync.Mutex // protects buckets, their content, and nursery
buckets [nBuckets]*bucket // index of known nodes by distance buckets [nBuckets]*bucket // index of known nodes by distance
nursery []*Node // bootstrap nodes nursery []*Node // bootstrap nodes
cache *Cache // cache of known nodes
bondmu sync.Mutex bondmu sync.Mutex
bonding map[NodeID]*bondproc bonding map[NodeID]*bondproc
@ -34,7 +35,6 @@ type Table struct {
net transport net transport
self *Node // metadata of the local node self *Node // metadata of the local node
db *nodeDB
} }
type bondproc struct { type bondproc struct {
@ -61,17 +61,15 @@ type bucket struct {
entries []*Node entries []*Node
} }
func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, seedCache string) *Table { func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, seeder *Cache) *Table {
// Load the bootstrap seed cache (use in memory db upon failure) // If no seed cache was given, use an in-memory one
db, err := newNodeDB(seedCache, Version) if seeder == nil {
if err != nil { seeder, _ = NewMemoryCache()
glog.V(logger.Warn).Infoln("Failed to open bootstrap seed cache:", err)
db, _ = newNodeDB("", Version)
} }
// Create the bootstrap table // Create the bootstrap table
tab := &Table{ tab := &Table{
net: t, net: t,
db: db, cache: seeder,
self: newNode(ourID, ourAddr), self: newNode(ourID, ourAddr),
bonding: make(map[NodeID]*bondproc), bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs), bondslots: make(chan struct{}, maxBondingPingPongs),
@ -93,7 +91,6 @@ func (tab *Table) Self() *Node {
// Close terminates the network listener and flushes the seed cache. // Close terminates the network listener and flushes the seed cache.
func (tab *Table) Close() { func (tab *Table) Close() {
tab.net.close() tab.net.close()
tab.db.close()
} }
// Bootstrap sets the bootstrap nodes. These nodes are used to connect // Bootstrap sets the bootstrap nodes. These nodes are used to connect
@ -178,10 +175,10 @@ func (tab *Table) refresh() {
result := tab.Lookup(randomID(tab.self.ID, ld)) result := tab.Lookup(randomID(tab.self.ID, ld))
if len(result) == 0 { if len(result) == 0 {
// Pick a batch of previously know seeds to lookup with and discard them (will come back if they are still live) // Pick a batch of previously know seeds to lookup with and discard them (will come back if they are still live)
seeds := tab.db.list(10) seeds := tab.cache.list(10)
for _, seed := range seeds { for _, seed := range seeds {
glog.V(logger.Debug).Infoln("Seeding network with:", seed) glog.V(logger.Debug).Infoln("Seeding network with:", seed)
tab.db.delete(seed.ID) tab.cache.delete(seed.ID)
} }
// Bootstrap the table with a self lookup // Bootstrap the table with a self lookup
all := tab.bondall(append(tab.nursery, seeds...)) all := tab.bondall(append(tab.nursery, seeds...))
@ -252,7 +249,7 @@ func (tab *Table) bondall(nodes []*Node) (result []*Node) {
// of the process can be skipped. // of the process can be skipped.
func (tab *Table) bond(pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) (*Node, error) { func (tab *Table) bond(pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) (*Node, error) {
var n *Node var n *Node
if n = tab.db.get(id); n == nil { if n = tab.cache.get(id); n == nil {
tab.bondmu.Lock() tab.bondmu.Lock()
w := tab.bonding[id] w := tab.bonding[id]
if w != nil { if w != nil {
@ -297,7 +294,7 @@ func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAdd
// waitping will simply time out. // waitping will simply time out.
tab.net.waitping(id) tab.net.waitping(id)
} }
w.n = tab.db.add(id, addr, tcpPort) w.n = tab.cache.add(id, addr, tcpPort)
close(w.done) close(w.done)
} }

@ -15,7 +15,7 @@ import (
func TestTable_pingReplace(t *testing.T) { func TestTable_pingReplace(t *testing.T) {
doit := func(newNodeIsResponding, lastInBucketIsResponding bool) { doit := func(newNodeIsResponding, lastInBucketIsResponding bool) {
transport := newPingRecorder() transport := newPingRecorder()
tab := newTable(transport, NodeID{}, &net.UDPAddr{}, "") tab := newTable(transport, NodeID{}, &net.UDPAddr{}, nil)
last := fillBucket(tab, 200) last := fillBucket(tab, 200)
pingSender := randomID(tab.self.ID, 200) pingSender := randomID(tab.self.ID, 200)
@ -145,7 +145,7 @@ func TestTable_closest(t *testing.T) {
test := func(test *closeTest) bool { test := func(test *closeTest) bool {
// for any node table, Target and N // for any node table, Target and N
tab := newTable(nil, test.Self, &net.UDPAddr{}, "") tab := newTable(nil, test.Self, &net.UDPAddr{}, nil)
tab.add(test.All) tab.add(test.All)
// check that doClosest(Target, N) returns nodes // check that doClosest(Target, N) returns nodes
@ -217,7 +217,7 @@ func TestTable_Lookup(t *testing.T) {
self := gen(NodeID{}, quickrand).(NodeID) self := gen(NodeID{}, quickrand).(NodeID)
target := randomID(self, 200) target := randomID(self, 200)
transport := findnodeOracle{t, target} transport := findnodeOracle{t, target}
tab := newTable(transport, self, &net.UDPAddr{}, "") tab := newTable(transport, self, &net.UDPAddr{}, nil)
// lookup on empty table returns no nodes // lookup on empty table returns no nodes
if results := tab.Lookup(target); len(results) > 0 { if results := tab.Lookup(target); len(results) > 0 {

@ -144,7 +144,7 @@ type reply struct {
} }
// ListenUDP returns a new table that listens for UDP packets on laddr. // ListenUDP returns a new table that listens for UDP packets on laddr.
func ListenUDP(priv *ecdsa.PrivateKey, laddr string, natm nat.Interface, seedCache string) (*Table, error) { func ListenUDP(priv *ecdsa.PrivateKey, laddr string, natm nat.Interface, seeder *Cache) (*Table, error) {
addr, err := net.ResolveUDPAddr("udp", laddr) addr, err := net.ResolveUDPAddr("udp", laddr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -153,12 +153,12 @@ func ListenUDP(priv *ecdsa.PrivateKey, laddr string, natm nat.Interface, seedCac
if err != nil { if err != nil {
return nil, err return nil, err
} }
tab, _ := newUDP(priv, conn, natm, seedCache) tab, _ := newUDP(priv, conn, natm, seeder)
glog.V(logger.Info).Infoln("Listening,", tab.self) glog.V(logger.Info).Infoln("Listening,", tab.self)
return tab, nil return tab, nil
} }
func newUDP(priv *ecdsa.PrivateKey, c conn, natm nat.Interface, seedCache string) (*Table, *udp) { func newUDP(priv *ecdsa.PrivateKey, c conn, natm nat.Interface, seeder *Cache) (*Table, *udp) {
udp := &udp{ udp := &udp{
conn: c, conn: c,
priv: priv, priv: priv,
@ -176,7 +176,7 @@ func newUDP(priv *ecdsa.PrivateKey, c conn, natm nat.Interface, seedCache string
realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
} }
} }
udp.Table = newTable(udp, PubkeyID(&priv.PublicKey), realaddr, seedCache) udp.Table = newTable(udp, PubkeyID(&priv.PublicKey), realaddr, seeder)
go udp.loop() go udp.loop()
go udp.readLoop() go udp.readLoop()
return udp.Table, udp return udp.Table, udp
@ -449,7 +449,7 @@ func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte
if expired(req.Expiration) { if expired(req.Expiration) {
return errExpired return errExpired
} }
if t.db.get(fromID) == nil { if t.cache.get(fromID) == nil {
// No bond exists, we don't process the packet. This prevents // No bond exists, we don't process the packet. This prevents
// an attack vector where the discovery protocol could be used // an attack vector where the discovery protocol could be used
// to amplify traffic in a DDOS attack. A malicious actor // to amplify traffic in a DDOS attack. A malicious actor

@ -41,7 +41,7 @@ func newUDPTest(t *testing.T) *udpTest {
remotekey: newkey(), remotekey: newkey(),
remoteaddr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 30303}, remoteaddr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 30303},
} }
test.table, test.udp = newUDP(test.localkey, test.pipe, nil, "") test.table, test.udp = newUDP(test.localkey, test.pipe, nil, nil)
return test return test
} }
@ -157,7 +157,7 @@ func TestUDP_findnode(t *testing.T) {
// ensure there's a bond with the test node, // ensure there's a bond with the test node,
// findnode won't be accepted otherwise. // findnode won't be accepted otherwise.
test.table.db.add(PubkeyID(&test.remotekey.PublicKey), test.remoteaddr, 99) test.table.cache.add(PubkeyID(&test.remotekey.PublicKey), test.remoteaddr, 99)
// check that closest neighbors are returned. // check that closest neighbors are returned.
test.packetIn(nil, findnodePacket, &findnode{Target: testTarget, Expiration: futureExp}) test.packetIn(nil, findnodePacket, &findnode{Target: testTarget, Expiration: futureExp})

@ -59,9 +59,9 @@ type Server struct {
// with the rest of the network. // with the rest of the network.
BootstrapNodes []*discover.Node BootstrapNodes []*discover.Node
// SeedCache is the path to the database containing the previously seen live // SeedCache is the database containing the previously seen live nodes in
// nodes in the network to use as potential bootstrap seeds. // the network to use as potential bootstrap seeds.
SeedCache string SeedCache *discover.Cache
// Protocols should contain the protocols supported // Protocols should contain the protocols supported
// by the server. Matching protocols are launched for // by the server. Matching protocols are launched for