diff --git a/cmd/bootnode/main.go b/cmd/bootnode/main.go index eef90f270244..8f6449b1ca48 100644 --- a/cmd/bootnode/main.go +++ b/cmd/bootnode/main.go @@ -29,6 +29,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/p2p/discv5" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/nat" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" ) @@ -37,7 +38,7 @@ func main() { var ( listenAddr = flag.String("addr", ":30301", "listen address") genKey = flag.String("genkey", "", "generate a node key") - writeAddr = flag.Bool("writeaddress", false, "write out the node's pubkey hash and quit") + writeAddr = flag.Bool("writeaddress", false, "write out the node's public key and quit") nodeKeyFile = flag.String("nodekey", "", "private key filename") nodeKeyHex = flag.String("nodekeyhex", "", "private key as hex (for testing)") natdesc = flag.String("nat", "none", "port mapping mechanism (any|none|upnp|pmp|extip:)") @@ -85,7 +86,7 @@ func main() { } if *writeAddr { - fmt.Printf("%v\n", discover.PubkeyID(&nodeKey.PublicKey)) + fmt.Printf("%x\n", crypto.FromECDSAPub(&nodeKey.PublicKey)[1:]) os.Exit(0) } @@ -111,26 +112,38 @@ func main() { if !realaddr.IP.IsLoopback() { go nat.Map(natm, nil, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") } - // TODO: react to external IP changes over time. if ext, err := natm.ExternalIP(); err == nil { realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} } } + printNotice(&nodeKey.PublicKey, *realaddr) + if *runv5 { - if _, err := discv5.ListenUDP(nodeKey, conn, realaddr, "", restrictList); err != nil { + if _, err := discv5.ListenUDP(nodeKey, conn, "", restrictList); err != nil { utils.Fatalf("%v", err) } } else { + db, _ := enode.OpenDB("") + ln := enode.NewLocalNode(db, nodeKey) cfg := discover.Config{ - PrivateKey: nodeKey, - AnnounceAddr: realaddr, - NetRestrict: restrictList, + PrivateKey: nodeKey, + NetRestrict: restrictList, } - if _, err := discover.ListenUDP(conn, cfg); err != nil { + if _, err := discover.ListenUDP(conn, ln, cfg); err != nil { utils.Fatalf("%v", err) } } select {} } + +func printNotice(nodeKey *ecdsa.PublicKey, addr net.UDPAddr) { + if addr.IP.IsUnspecified() { + addr.IP = net.IP{127, 0, 0, 1} + } + n := enode.NewV4(nodeKey, addr.IP, 0, addr.Port) + fmt.Println(n.String()) + fmt.Println("Note: you're using cmd/bootnode, a developer tool.") + fmt.Println("We recommend using a regular node as bootstrap node for production deployments.") +} diff --git a/cmd/p2psim/main.go b/cmd/p2psim/main.go index 3b0faa8ccd64..b864b6ee7f00 100644 --- a/cmd/p2psim/main.go +++ b/cmd/p2psim/main.go @@ -47,7 +47,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/internal/flags" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" "github.com/XinFinOrg/XDPoSChain/rpc" @@ -300,7 +300,7 @@ func createNode(ctx *cli.Context) error { if err != nil { return err } - config.ID = discover.PubkeyID(&privKey.PublicKey) + config.ID = enode.PubkeyToIDV4(&privKey.PublicKey) config.PrivateKey = privKey } if services := ctx.String("services"); services != "" { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9c80b527a218..9183b60bc4b9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -61,8 +61,8 @@ import ( "github.com/XinFinOrg/XDPoSChain/miner" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/p2p/discv5" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/nat" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" "github.com/XinFinOrg/XDPoSChain/params" @@ -938,19 +938,20 @@ func setAllowlistAndDenylistForPeers(ctx *cli.Context, cfg *p2p.Config) { // setup allowlist for peers if ctx.IsSet(PeersAllowlistFlag.Name) { urls := SplitAndTrim(ctx.String(PeersAllowlistFlag.Name)) - cfg.AllowPeers = make(map[discover.NodeID]struct{}, len(urls)) + cfg.AllowPeers = make(map[enode.ID]struct{}, len(urls)) for _, url := range urls { if url != "" { - node1, err1 := discover.HexID(url) + var node1 enode.ID + err1 := node1.UnmarshalText([]byte(url)) if err1 == nil { cfg.AllowPeers[node1] = struct{}{} log.Info("Add peer to allowlist", "id", node1) continue } - node2, err2 := discover.ParseNode(url) + node2, err2 := enode.ParseV4(url) if err2 == nil { - cfg.AllowPeers[node2.ID] = struct{}{} - log.Info("Add peer to allowlist", "enode", url, "id", node2.ID) + cfg.AllowPeers[node2.ID()] = struct{}{} + log.Info("Add peer to allowlist", "enode", url, "id", node2.ID()) continue } log.Crit("Invalid peer id for allowlist", "url", url, "err1", err1, "err2", err2) @@ -961,19 +962,20 @@ func setAllowlistAndDenylistForPeers(ctx *cli.Context, cfg *p2p.Config) { // setup denylist for peers if ctx.IsSet(PeersDenylistFlag.Name) { urls := SplitAndTrim(ctx.String(PeersDenylistFlag.Name)) - cfg.DenyPeers = make(map[discover.NodeID]struct{}, len(urls)) + cfg.DenyPeers = make(map[enode.ID]struct{}, len(urls)) for _, url := range urls { if url != "" { - node1, err1 := discover.HexID(url) + var node1 enode.ID + err1 := node1.UnmarshalText([]byte(url)) if err1 == nil { cfg.DenyPeers[node1] = struct{}{} log.Info("Add peer to denylist", "id", node1) continue } - node2, err2 := discover.ParseNode(url) + node2, err2 := enode.ParseV4(url) if err2 == nil { - cfg.DenyPeers[node2.ID] = struct{}{} - log.Info("Add peer to denylist", "enode", url, "id", node2.ID) + cfg.DenyPeers[node2.ID()] = struct{}{} + log.Info("Add peer to denylist", "enode", url, "id", node2.ID()) continue } log.Crit("Invalid peer id for denylist", "url", url, "err1", err1, "err2", err2) @@ -988,10 +990,10 @@ func removeDenylistedPeers(cfg *p2p.Config) { return } - filteredNodes := make([]*discover.Node, 0, len(cfg.BootstrapNodes)) + filteredNodes := make([]*enode.Node, 0, len(cfg.BootstrapNodes)) for _, node := range cfg.BootstrapNodes { - if _, ok := cfg.DenyPeers[node.ID]; ok { - log.Info("Remove denylisted peer", "enode", node, "id", node.ID) + if _, ok := cfg.DenyPeers[node.ID()]; ok { + log.Info("Remove denylisted peer", "enode", node, "id", node.ID()) continue } filteredNodes = append(filteredNodes, node) @@ -1027,11 +1029,11 @@ func setBootstrapNodes(ctx *cli.Context, cfg *p2p.Config) { cfg.BootstrapNodes = mustParseBootnodes(urls) } -func mustParseBootnodes(urls []string) []*discover.Node { - nodes := make([]*discover.Node, 0, len(urls)) +func mustParseBootnodes(urls []string) []*enode.Node { + nodes := make([]*enode.Node, 0, len(urls)) for _, url := range urls { if url != "" { - node, err := discover.ParseNode(url) + node, err := enode.ParseV4(url) if err != nil { log.Crit("Bootstrap URL invalid", "enode", url, "err", err) return nil diff --git a/eth/handler.go b/eth/handler.go index bd8e2c2bc375..95a72e37193e 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -39,7 +39,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/rlp" ) @@ -184,7 +184,7 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne NodeInfo: func() interface{} { return manager.NodeInfo() }, - PeerInfo: func(id discover.NodeID) interface{} { + PeerInfo: func(id enode.ID) interface{} { if p := manager.peers.Peer(fmt.Sprintf("%x", id[:8])); p != nil { return p.Info() } @@ -364,41 +364,38 @@ func (pm *ProtocolManager) handle(p *peer) error { rw.Init(p.version) } // Register the peer locally - err := pm.peers.Register(p) - if err != nil && err != p2p.ErrAddPairPeer { + if err := pm.peers.Register(p); err != nil { p.Log().Error("Ethereum peer registration failed", "err", err) return err } defer pm.removePeer(p.id) - if err != p2p.ErrAddPairPeer { - // Register the peer in the downloader. If the downloader considers it banned, we disconnect - if err := pm.downloader.RegisterPeer(p.id, p.version, p); err != nil { + // Register the peer in the downloader. If the downloader considers it banned, we disconnect + if err := pm.downloader.RegisterPeer(p.id, p.version, p); err != nil { + return err + } + p.Log().Info("Register peer", "nodeid", p.ID().String(), "version", p.version, "addr", p.RemoteAddr()) + // Propagate existing transactions. new transactions appearing + // after this will be sent via broadcasts. + pm.syncTransactions(p) + + // If we're DAO hard-fork aware, validate any remote peer with regard to the hard-fork + if daoBlock := pm.chainconfig.DAOForkBlock; daoBlock != nil { + // Request the peer's DAO fork header for extra-data validation + if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil { return err } - p.Log().Info("Register peer", "nodeid", p.ID().String(), "version", p.version, "addr", p.RemoteAddr()) - // Propagate existing transactions. new transactions appearing - // after this will be sent via broadcasts. - pm.syncTransactions(p) - - // If we're DAO hard-fork aware, validate any remote peer with regard to the hard-fork - if daoBlock := pm.chainconfig.DAOForkBlock; daoBlock != nil { - // Request the peer's DAO fork header for extra-data validation - if err := p.RequestHeadersByNumber(daoBlock.Uint64(), 1, 0, false); err != nil { - return err + // Start a timer to disconnect if the peer doesn't reply in time + p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() { + p.Log().Debug("Timed out DAO fork-check, dropping") + pm.removePeer(p.id) + }) + // Make sure it's cleaned up if the peer dies off + defer func() { + if p.forkDrop != nil { + p.forkDrop.Stop() + p.forkDrop = nil } - // Start a timer to disconnect if the peer doesn't reply in time - p.forkDrop = time.AfterFunc(daoChallengeTimeout, func() { - p.Log().Debug("Timed out DAO fork-check, dropping") - pm.removePeer(p.id) - }) - // Make sure it's cleaned up if the peer dies off - defer func() { - if p.forkDrop != nil { - p.forkDrop.Stop() - p.forkDrop = nil - } - }() - } + }() } // main loop. handle incoming messages. for { diff --git a/eth/helper_test.go b/eth/helper_test.go index 2106afda1f1e..c586cd9a5828 100644 --- a/eth/helper_test.go +++ b/eth/helper_test.go @@ -40,7 +40,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/ethdb" "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/params" "github.com/holiman/uint256" ) @@ -206,7 +206,7 @@ func newTestPeer(name string, version int, pm *ProtocolManager, shake bool) (*te app, net := p2p.MsgPipe() // Generate a random id and create the peer - var id discover.NodeID + var id enode.ID rand.Read(id[:]) peer := pm.newPeer(version, p2p.NewPeer(id, name, nil), net) diff --git a/eth/metrics.go b/eth/metrics.go index 59d9258a0625..6569ea3a795e 100644 --- a/eth/metrics.go +++ b/eth/metrics.go @@ -111,7 +111,33 @@ func (rw *meteredMsgReadWriter) ReadMsg() (p2p.Msg, error) { } func (rw *meteredMsgReadWriter) WriteMsg(msg p2p.Msg) error { - // Account for the data traffic + rw.meterOut(msg) + // Send the packet to the p2p layer + return rw.MsgReadWriter.WriteMsg(msg) +} + +// Compile-time check that meteredMsgReadWriter forwards the priority lane. +// Without this, a future refactor could silently drop the wrapper back to +// MsgReadWriter and downgrade BFT consensus messages to the normal lane. +var _ p2p.PriorityMsgWriter = (*meteredMsgReadWriter)(nil) + +// WriteMsgPriority implements p2p.PriorityMsgWriter so that consensus messages +// retain their priority routing when the metrics wrapper is enabled. The +// outbound counters are updated identically to WriteMsg before the message is +// forwarded to the underlying writer's priority lane (falling back to WriteMsg +// if the underlying writer does not support priorities). +func (rw *meteredMsgReadWriter) WriteMsgPriority(msg p2p.Msg, high bool) error { + rw.meterOut(msg) + if pw, ok := rw.MsgReadWriter.(p2p.PriorityMsgWriter); ok { + return pw.WriteMsgPriority(msg, high) + } + return rw.MsgReadWriter.WriteMsg(msg) +} + +// meterOut updates the outbound packet/traffic meters for msg. It is shared +// between WriteMsg and WriteMsgPriority so both paths produce identical +// metrics. +func (rw *meteredMsgReadWriter) meterOut(msg p2p.Msg) { packets, traffic := miscOutPacketsMeter, miscOutTrafficMeter switch { case msg.Code == BlockHeadersMsg: @@ -133,7 +159,4 @@ func (rw *meteredMsgReadWriter) WriteMsg(msg p2p.Msg) error { } packets.Mark(1) traffic.Mark(int64(msg.Size)) - - // Send the packet to the p2p layer - return rw.MsgReadWriter.WriteMsg(msg) } diff --git a/eth/metrics_test.go b/eth/metrics_test.go new file mode 100644 index 000000000000..392ba7bdc3e4 --- /dev/null +++ b/eth/metrics_test.go @@ -0,0 +1,100 @@ +// Copyright 2025 The XDPoSChain Authors +// This file is part of the XDPoSChain library. +// +// The XDPoSChain library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The XDPoSChain library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the XDPoSChain library. If not, see . + +package eth + +import ( + "io" + "testing" + + "github.com/XinFinOrg/XDPoSChain/p2p" +) + +// priorityRecordingRW records whether the priority lane was used for the +// last WriteMsg* call on it. +type priorityRecordingRW struct { + last p2p.Msg + high bool + priority bool // true if WriteMsgPriority was called +} + +func (rw *priorityRecordingRW) ReadMsg() (p2p.Msg, error) { return p2p.Msg{}, io.EOF } + +func (rw *priorityRecordingRW) WriteMsg(msg p2p.Msg) error { + rw.last = msg + rw.priority = false + return nil +} + +func (rw *priorityRecordingRW) WriteMsgPriority(msg p2p.Msg, high bool) error { + rw.last = msg + rw.high = high + rw.priority = true + return nil +} + +// TestMeteredMsgReadWriterForwardsPriority verifies that meteredMsgReadWriter +// implements p2p.PriorityMsgWriter and forwards priority writes to the +// underlying writer's high-priority lane. Without this, BFT consensus +// messages (VoteMsg/TimeoutMsg/SyncInfoMsg) would silently fall back to the +// normal lane on nodes that have metrics enabled. +func TestMeteredMsgReadWriterForwardsPriority(t *testing.T) { + inner := &priorityRecordingRW{} + mrw := &meteredMsgReadWriter{MsgReadWriter: inner} + mrw.Init(eth63) + + if _, ok := p2p.MsgReadWriter(mrw).(p2p.PriorityMsgWriter); !ok { + t.Fatal("meteredMsgReadWriter does not implement PriorityMsgWriter") + } + + if err := p2p.SendPriority(mrw, VoteMsg, []uint{}); err != nil { + t.Fatalf("SendPriority: %v", err) + } + if !inner.priority { + t.Fatal("priority lane was not used on inner writer") + } + if !inner.high { + t.Fatal("high flag was not propagated to inner writer") + } + if inner.last.Code != VoteMsg { + t.Fatalf("code: got %d, want %d", inner.last.Code, VoteMsg) + } +} + +// nonPriorityRW only implements p2p.MsgReadWriter and is used to verify the +// fallback path in meteredMsgReadWriter.WriteMsgPriority. +type nonPriorityRW struct { + last p2p.Msg +} + +func (rw *nonPriorityRW) ReadMsg() (p2p.Msg, error) { return p2p.Msg{}, io.EOF } +func (rw *nonPriorityRW) WriteMsg(msg p2p.Msg) error { + rw.last = msg + return nil +} + +func TestMeteredMsgReadWriterPriorityFallback(t *testing.T) { + inner := &nonPriorityRW{} + mrw := &meteredMsgReadWriter{MsgReadWriter: inner} + mrw.Init(eth63) + + if err := mrw.WriteMsgPriority(p2p.Msg{Code: VoteMsg}, true); err != nil { + t.Fatalf("WriteMsgPriority: %v", err) + } + if inner.last.Code != VoteMsg { + t.Fatalf("code: got %d, want %d", inner.last.Code, VoteMsg) + } +} diff --git a/eth/peer.go b/eth/peer.go index b864f8cec244..45c75e867155 100644 --- a/eth/peer.go +++ b/eth/peer.go @@ -59,8 +59,7 @@ type peer struct { id string *p2p.Peer - rw p2p.MsgReadWriter - pairRw p2p.MsgReadWriter + rw p2p.MsgReadWriter version int // Protocol version negotiated forkDrop *time.Timer // Timed connection dropper if forks aren't validated in time @@ -262,59 +261,35 @@ func (p *peer) SendNewBlock(block *types.Block, td *big.Int) error { } p.knownBlocks.Add(block.Hash()) - if p.pairRw != nil { - return p2p.Send(p.pairRw, NewBlockMsg, []interface{}{block, td}) - } else { - return p2p.Send(p.rw, NewBlockMsg, []interface{}{block, td}) - } + return p2p.Send(p.rw, NewBlockMsg, []interface{}{block, td}) } // SendBlockHeaders sends a batch of block headers to the remote peer. func (p *peer) SendBlockHeaders(headers []*types.Header) error { - if p.pairRw != nil { - return p2p.Send(p.pairRw, BlockHeadersMsg, headers) - } else { - return p2p.Send(p.rw, BlockHeadersMsg, headers) - } + return p2p.Send(p.rw, BlockHeadersMsg, headers) } // SendBlockBodies sends a batch of block contents to the remote peer. func (p *peer) SendBlockBodies(bodies []*blockBody) error { - if p.pairRw != nil { - return p2p.Send(p.pairRw, BlockBodiesMsg, blockBodiesData(bodies)) - } else { - return p2p.Send(p.rw, BlockBodiesMsg, blockBodiesData(bodies)) - } + return p2p.Send(p.rw, BlockBodiesMsg, blockBodiesData(bodies)) } // SendBlockBodiesRLP sends a batch of block contents to the remote peer from // an already RLP encoded format. func (p *peer) SendBlockBodiesRLP(bodies []rlp.RawValue) error { - if p.pairRw != nil { - return p2p.Send(p.pairRw, BlockBodiesMsg, bodies) - } else { - return p2p.Send(p.rw, BlockBodiesMsg, bodies) - } + return p2p.Send(p.rw, BlockBodiesMsg, bodies) } // SendNodeDataRLP sends a batch of arbitrary internal data, corresponding to the // hashes requested. func (p *peer) SendNodeData(data [][]byte) error { - if p.pairRw != nil { - return p2p.Send(p.pairRw, NodeDataMsg, data) - } else { - return p2p.Send(p.rw, NodeDataMsg, data) - } + return p2p.Send(p.rw, NodeDataMsg, data) } // SendReceiptsRLP sends a batch of transaction receipts, corresponding to the // ones requested from an already RLP encoded format. func (p *peer) SendReceiptsRLP(receipts []rlp.RawValue) error { - if p.pairRw != nil { - return p2p.Send(p.pairRw, ReceiptsMsg, receipts) - } else { - return p2p.Send(p.rw, ReceiptsMsg, receipts) - } + return p2p.Send(p.rw, ReceiptsMsg, receipts) } func (p *peer) SendVote(vote *types.Vote) error { @@ -323,11 +298,7 @@ func (p *peer) SendVote(vote *types.Vote) error { } p.knownVote.Add(vote.Hash()) - if p.pairRw != nil { - return p2p.Send(p.pairRw, VoteMsg, vote) - } else { - return p2p.Send(p.rw, VoteMsg, vote) - } + return p2p.SendPriority(p.rw, VoteMsg, vote) } /* @@ -341,11 +312,7 @@ func (p *peer) SendTimeout(timeout *types.Timeout) error { } p.knownTimeout.Add(timeout.Hash()) - if p.pairRw != nil { - return p2p.Send(p.pairRw, TimeoutMsg, timeout) - } else { - return p2p.Send(p.rw, TimeoutMsg, timeout) - } + return p2p.SendPriority(p.rw, TimeoutMsg, timeout) } /* @@ -359,11 +326,7 @@ func (p *peer) SendSyncInfo(syncInfo *types.SyncInfo) error { } p.knownSyncInfo.Add(syncInfo.Hash()) - if p.pairRw != nil { - return p2p.Send(p.pairRw, SyncInfoMsg, syncInfo) - } else { - return p2p.Send(p.rw, SyncInfoMsg, syncInfo) - } + return p2p.SendPriority(p.rw, SyncInfoMsg, syncInfo) } /* @@ -376,65 +339,41 @@ func (p *peer) AsyncSendSyncInfo() { // single header. It is used solely by the fetcher. func (p *peer) RequestOneHeader(hash common.Hash) error { p.Log().Debug("Fetching single header", "hash", hash) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: hash}, Amount: uint64(1), Skip: uint64(0), Reverse: false}) - } else { - return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: hash}, Amount: uint64(1), Skip: uint64(0), Reverse: false}) - } + return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: hash}, Amount: uint64(1), Skip: uint64(0), Reverse: false}) } // RequestHeadersByHash fetches a batch of blocks' headers corresponding to the // specified header query, based on the hash of an origin block. func (p *peer) RequestHeadersByHash(origin common.Hash, amount int, skip int, reverse bool) error { p.Log().Debug("Fetching batch of headers", "count", amount, "fromhash", origin, "skip", skip, "reverse", reverse) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) - } else { - return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) - } + return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Hash: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) } // RequestHeadersByNumber fetches a batch of blocks' headers corresponding to the // specified header query, based on the number of an origin block. func (p *peer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool) error { p.Log().Debug("Fetching batch of headers", "count", amount, "fromnum", origin, "skip", skip, "reverse", reverse) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Number: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) - } else { - return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Number: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) - } + return p2p.Send(p.rw, GetBlockHeadersMsg, &getBlockHeadersData{Origin: hashOrNumber{Number: origin}, Amount: uint64(amount), Skip: uint64(skip), Reverse: reverse}) } // RequestBodies fetches a batch of blocks' bodies corresponding to the hashes // specified. func (p *peer) RequestBodies(hashes []common.Hash) error { p.Log().Debug("Fetching batch of block bodies", "count", len(hashes)) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetBlockBodiesMsg, hashes) - } else { - return p2p.Send(p.rw, GetBlockBodiesMsg, hashes) - } + return p2p.Send(p.rw, GetBlockBodiesMsg, hashes) } // RequestNodeData fetches a batch of arbitrary data from a node's known state // data, corresponding to the specified hashes. func (p *peer) RequestNodeData(hashes []common.Hash) error { p.Log().Debug("Fetching batch of state data", "count", len(hashes)) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetNodeDataMsg, hashes) - } else { - return p2p.Send(p.rw, GetNodeDataMsg, hashes) - } + return p2p.Send(p.rw, GetNodeDataMsg, hashes) } // RequestReceipts fetches a batch of transaction receipts from a remote node. func (p *peer) RequestReceipts(hashes []common.Hash) error { p.Log().Debug("Fetching batch of receipts", "count", len(hashes)) - if p.pairRw != nil { - return p2p.Send(p.pairRw, GetReceiptsMsg, hashes) - } else { - return p2p.Send(p.rw, GetReceiptsMsg, hashes) - } + return p2p.Send(p.rw, GetReceiptsMsg, hashes) } // Handshake executes the eth protocol handshake, negotiating version number, @@ -530,14 +469,8 @@ func (ps *peerSet) Register(p *peer) error { if ps.closed { return errClosed } - if existPeer, ok := ps.peers[p.id]; ok { - if existPeer.pairRw != nil { - return errAlreadyRegistered - } - existPeer.SetPairPeer(p.Peer) - existPeer.pairRw = p.rw - p.SetPairPeer(existPeer.Peer) - return p2p.ErrAddPairPeer + if _, ok := ps.peers[p.id]; ok { + return errAlreadyRegistered } ps.peers[p.id] = p return nil diff --git a/eth/sync.go b/eth/sync.go index 627434933afb..f299d815a35d 100644 --- a/eth/sync.go +++ b/eth/sync.go @@ -26,7 +26,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/eth/downloader" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" ) const ( @@ -69,7 +69,7 @@ func (pm *ProtocolManager) syncTransactions(p *peer) { // the transactions in small packs to one peer at a time. func (pm *ProtocolManager) txsyncLoop() { var ( - pending = make(map[discover.NodeID]*txsync) + pending = make(map[enode.ID]*txsync) sending = false // whether a send is active pack = new(txsync) // the pack that is being sent done = make(chan error, 1) // result of the send diff --git a/eth/sync_test.go b/eth/sync_test.go index 3880e1857237..8c25a98190b4 100644 --- a/eth/sync_test.go +++ b/eth/sync_test.go @@ -23,7 +23,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/eth/downloader" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" ) // Tests that fast sync gets disabled as soon as a real block is successfully @@ -42,8 +42,8 @@ func TestFastSyncDisabling(t *testing.T) { // Sync up the two peers io1, io2 := p2p.MsgPipe() - go pmFull.handle(pmFull.newPeer(63, p2p.NewPeer(discover.NodeID{}, "empty", nil), io2)) - go pmEmpty.handle(pmEmpty.newPeer(63, p2p.NewPeer(discover.NodeID{}, "full", nil), io1)) + go pmFull.handle(pmFull.newPeer(63, p2p.NewPeer(enode.ID{}, "empty", nil), io2)) + go pmEmpty.handle(pmEmpty.newPeer(63, p2p.NewPeer(enode.ID{}, "full", nil), io1)) time.Sleep(250 * time.Millisecond) pmEmpty.synchronise(pmEmpty.peers.BestPeer()) diff --git a/node/api.go b/node/api.go index 2d8f205b4449..8b2b8f08179f 100644 --- a/node/api.go +++ b/node/api.go @@ -26,7 +26,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/internal/debug" "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/rpc" ) @@ -61,19 +61,19 @@ func (api *adminAPI) AddPeer(url string) (bool, error) { return false, ErrNodeStopped } // Try to add the url as a static peer and return - node, err := discover.ParseNode(url) + node, err := enode.ParseV4(url) if err != nil { return false, fmt.Errorf("invalid enode: %v", err) } // only accept the node which is in peer allowlist if the list is not empty if len(server.AllowPeers) > 0 { - if _, ok := server.AllowPeers[node.ID]; !ok { - return false, fmt.Errorf("peer is not in allowlist: %v, ID: %s", url, node.ID) + if _, ok := server.AllowPeers[node.ID()]; !ok { + return false, fmt.Errorf("peer is not in allowlist: %v, ID: %s", url, node.ID()) } } // reject the node which is in peer blacklist - if _, ok := server.DenyPeers[node.ID]; ok { - return false, fmt.Errorf("peer is in blacklist: %v, ID: %s", url, node.ID) + if _, ok := server.DenyPeers[node.ID()]; ok { + return false, fmt.Errorf("peer is in blacklist: %v, ID: %s", url, node.ID()) } server.AddPeer(node) return true, nil @@ -87,7 +87,7 @@ func (api *adminAPI) RemovePeer(url string) (bool, error) { return false, ErrNodeStopped } // Try to remove the url as a static peer and return - node, err := discover.ParseNode(url) + node, err := enode.ParseV4(url) if err != nil { return false, fmt.Errorf("invalid enode: %v", err) } @@ -102,19 +102,19 @@ func (api *adminAPI) AddTrustedPeer(url string) (bool, error) { if server == nil { return false, ErrNodeStopped } - node, err := discover.ParseNode(url) + node, err := enode.ParseV4(url) if err != nil { return false, fmt.Errorf("invalid enode: %v", err) } // only accept the node which is in peer allowlist if the list is not empty if len(server.AllowPeers) > 0 { - if _, ok := server.AllowPeers[node.ID]; !ok { - return false, fmt.Errorf("trusted peer is not in allowlist: %v, ID: %s", url, node.ID) + if _, ok := server.AllowPeers[node.ID()]; !ok { + return false, fmt.Errorf("trusted peer is not in allowlist: %v, ID: %s", url, node.ID()) } } // reject the node which is in peer blacklist - if _, ok := server.DenyPeers[node.ID]; ok { - return false, fmt.Errorf("trusted peer is in blacklist: %v, ID: %s", url, node.ID) + if _, ok := server.DenyPeers[node.ID()]; ok { + return false, fmt.Errorf("trusted peer is in blacklist: %v, ID: %s", url, node.ID()) } server.AddTrustedPeer(node) return true, nil @@ -128,7 +128,7 @@ func (api *adminAPI) RemoveTrustedPeer(url string) (bool, error) { if server == nil { return false, ErrNodeStopped } - node, err := discover.ParseNode(url) + node, err := enode.ParseV4(url) if err != nil { return false, fmt.Errorf("invalid enode: %v", err) } diff --git a/p2p/dial.go b/p2p/dial.go index 62320af5e241..70262714c2cb 100644 --- a/p2p/dial.go +++ b/p2p/dial.go @@ -18,14 +18,13 @@ package p2p import ( "container/heap" - "crypto/rand" "errors" "fmt" "net" "time" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" ) @@ -50,7 +49,7 @@ const ( // NodeDialer is used to connect to nodes in the network, typically by using // an underlying net.Dialer but also using net.Pipe in tests type NodeDialer interface { - Dial(*discover.Node) (net.Conn, error) + Dial(*enode.Node) (net.Conn, error) } // TCPDialer implements the NodeDialer interface by using a net.Dialer to @@ -60,8 +59,8 @@ type TCPDialer struct { } // Dial creates a TCP connection to the node -func (t TCPDialer) Dial(dest *discover.Node) (net.Conn, error) { - addr := &net.TCPAddr{IP: dest.IP, Port: int(dest.TCP)} +func (t TCPDialer) Dial(dest *enode.Node) (net.Conn, error) { + addr := &net.TCPAddr{IP: dest.IP(), Port: dest.TCP()} return t.Dialer.Dial("tcp", addr.String()) } @@ -72,24 +71,24 @@ type dialstate struct { maxDynDials int ntab discoverTable netrestrict *netutil.Netlist + self enode.ID lookupRunning bool - dialing map[discover.NodeID]connFlag - lookupBuf []*discover.Node // current discovery lookup results - randomNodes []*discover.Node // filled from Table - static map[discover.NodeID]*dialTask + dialing map[enode.ID]connFlag + lookupBuf []*enode.Node // current discovery lookup results + randomNodes []*enode.Node // filled from Table + static map[enode.ID]*dialTask hist *dialHistory - start time.Time // time when the dialer was first used - bootnodes []*discover.Node // default dials when there are no peers + start time.Time // time when the dialer was first used + bootnodes []*enode.Node // default dials when there are no peers } type discoverTable interface { - Self() *discover.Node Close() - Resolve(target discover.NodeID) *discover.Node - Lookup(target discover.NodeID) []*discover.Node - ReadRandomNodes([]*discover.Node) int + Resolve(*enode.Node) *enode.Node + LookupRandom() []*enode.Node + ReadRandomNodes([]*enode.Node) int } // the dial history remembers recent dials. @@ -97,7 +96,7 @@ type dialHistory []pastDial // pastDial is an entry in the dial history. type pastDial struct { - id discover.NodeID + id enode.ID exp time.Time } @@ -109,7 +108,7 @@ type task interface { // fields cannot be accessed while the task is running. type dialTask struct { flags connFlag - dest *discover.Node + dest *enode.Node lastResolved time.Time resolveDelay time.Duration } @@ -118,7 +117,7 @@ type dialTask struct { // Only one discoverTask is active at any time. // discoverTask.Do performs a random lookup. type discoverTask struct { - results []*discover.Node + results []*enode.Node } // A waitExpireTask is generated if there are no other tasks @@ -127,15 +126,16 @@ type waitExpireTask struct { time.Duration } -func newDialState(static []*discover.Node, bootnodes []*discover.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate { +func newDialState(self enode.ID, static []*enode.Node, bootnodes []*enode.Node, ntab discoverTable, maxdyn int, netrestrict *netutil.Netlist) *dialstate { s := &dialstate{ maxDynDials: maxdyn, ntab: ntab, + self: self, netrestrict: netrestrict, - static: make(map[discover.NodeID]*dialTask), - dialing: make(map[discover.NodeID]connFlag), - bootnodes: make([]*discover.Node, len(bootnodes)), - randomNodes: make([]*discover.Node, maxdyn/2), + static: make(map[enode.ID]*dialTask), + dialing: make(map[enode.ID]connFlag), + bootnodes: make([]*enode.Node, len(bootnodes)), + randomNodes: make([]*enode.Node, maxdyn/2), hist: new(dialHistory), } copy(s.bootnodes, bootnodes) @@ -145,32 +145,32 @@ func newDialState(static []*discover.Node, bootnodes []*discover.Node, ntab disc return s } -func (s *dialstate) addStatic(n *discover.Node) { - // This overwites the task instead of updating an existing +func (s *dialstate) addStatic(n *enode.Node) { + // This overwrites the task instead of updating an existing // entry, giving users the opportunity to force a resolve operation. - s.static[n.ID] = &dialTask{flags: staticDialedConn, dest: n} + s.static[n.ID()] = &dialTask{flags: staticDialedConn, dest: n} } -func (s *dialstate) removeStatic(n *discover.Node) { +func (s *dialstate) removeStatic(n *enode.Node) { // This removes a task so future attempts to connect will not be made. - delete(s.static, n.ID) + delete(s.static, n.ID()) // This removes a previous dial timestamp so that application // can force a server to reconnect with chosen peer immediately. - s.hist.remove(n.ID) + s.hist.remove(n.ID()) } -func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now time.Time) []task { +func (s *dialstate) newTasks(nRunning int, peers map[enode.ID]*Peer, now time.Time) []task { if s.start.IsZero() { s.start = now } var newtasks []task - addDial := func(flag connFlag, n *discover.Node) bool { + addDial := func(flag connFlag, n *enode.Node) bool { if err := s.checkDial(n, peers); err != nil { - log.Trace("Skipping dial candidate", "id", n.ID, "addr", &net.TCPAddr{IP: n.IP, Port: int(n.TCP)}, "err", err) + log.Trace("Skipping dial candidate", "id", n.ID(), "addr", &net.TCPAddr{IP: n.IP(), Port: n.TCP()}, "err", err) return false } - s.dialing[n.ID] = flag + s.dialing[n.ID()] = flag newtasks = append(newtasks, &dialTask{flags: flag, dest: n}) return true } @@ -196,8 +196,8 @@ func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now err := s.checkDial(t.dest, peers) switch err { case errNotAllowlisted, errSelf: - log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP, Port: int(t.dest.TCP)}, "err", err) - delete(s.static, t.dest.ID) + log.Warn("Removing static dial candidate", "id", t.dest.ID(), "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}, "err", err) + delete(s.static, t.dest.ID()) case nil: s.dialing[id] = t.flags newtasks = append(newtasks, t) @@ -260,21 +260,18 @@ var ( errNotAllowlisted = errors.New("not contained in netrestrict allowlist") ) -func (s *dialstate) checkDial(n *discover.Node, peers map[discover.NodeID]*Peer) error { - _, dialing := s.dialing[n.ID] +func (s *dialstate) checkDial(n *enode.Node, peers map[enode.ID]*Peer) error { + _, dialing := s.dialing[n.ID()] switch { case dialing: return errAlreadyDialing - case peers[n.ID] != nil: - existPeer := peers[n.ID] - if existPeer.PairPeer() != nil { - return errAlreadyConnected - } - case s.ntab != nil && n.ID == s.ntab.Self().ID: + case peers[n.ID()] != nil: + return errAlreadyConnected + case n.ID() == s.self: return errSelf - case s.netrestrict != nil && !s.netrestrict.Contains(n.IP): + case s.netrestrict != nil && !s.netrestrict.Contains(n.IP()): return errNotAllowlisted - case s.hist.contains(n.ID): + case s.hist.contains(n.ID()): return errRecentlyDialed } return nil @@ -283,8 +280,8 @@ func (s *dialstate) checkDial(n *discover.Node, peers map[discover.NodeID]*Peer) func (s *dialstate) taskDone(t task, now time.Time) { switch t := t.(type) { case *dialTask: - s.hist.add(t.dest.ID, now.Add(dialHistoryExpiration)) - delete(s.dialing, t.dest.ID) + s.hist.add(t.dest.ID(), now.Add(dialHistoryExpiration)) + delete(s.dialing, t.dest.ID()) case *discoverTask: s.lookupRunning = false s.lookupBuf = append(s.lookupBuf, t.results...) @@ -303,26 +300,10 @@ func (t *dialTask) Do(srv *Server) { // Try resolving the ID of static nodes if dialing failed. if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 { if t.resolve(srv) { - err = t.dial(srv, t.dest) + t.dial(srv, t.dest) } } } - if err == nil { - err = t.dial(srv, t.dest) - if err != nil { - // Try resolving the ID of static nodes if dialing failed. - if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 { - if t.resolve(srv) { - err = t.dial(srv, t.dest) - } - } - } - if err == nil { - log.Trace("Dial pair connection success", "task", t.dest) - } else { - log.Trace("Dial pair connection error", "task", t.dest, "err", err) - } - } } // resolve attempts to find the current endpoint for the destination @@ -342,20 +323,20 @@ func (t *dialTask) resolve(srv *Server) bool { if time.Since(t.lastResolved) < t.resolveDelay { return false } - resolved := srv.ntab.Resolve(t.dest.ID) + resolved := srv.ntab.Resolve(t.dest) t.lastResolved = time.Now() if resolved == nil { t.resolveDelay *= 2 if t.resolveDelay > maxResolveDelay { t.resolveDelay = maxResolveDelay } - log.Debug("Resolving node failed", "id", t.dest.ID, "newdelay", t.resolveDelay) + log.Debug("Resolving node failed", "id", t.dest.ID(), "newdelay", t.resolveDelay) return false } // The node was found. t.resolveDelay = initialResolveDelay t.dest = resolved - log.Debug("Resolved node", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP, Port: int(t.dest.TCP)}) + log.Debug("Resolved node", "id", t.dest.ID(), "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}) return true } @@ -364,18 +345,20 @@ type dialError struct { } // dial performs the actual connection attempt. -func (t *dialTask) dial(srv *Server, dest *discover.Node) error { +func (t *dialTask) dial(srv *Server, dest *enode.Node) error { dialMeter.Mark(1) fd, err := srv.Dialer.Dial(dest) if err != nil { dialConnectionError.Mark(1) return &dialError{err} } - return srv.SetupConn(newMeteredConn(fd), t.flags, dest) + mfd := newMeteredConn(fd) + return srv.SetupConn(mfd, t.flags, dest) } func (t *dialTask) String() string { - return fmt.Sprintf("%v %x %v:%d", t.flags, t.dest.ID[:8], t.dest.IP, t.dest.TCP) + id := t.dest.ID() + return fmt.Sprintf("%v %x %v:%d", t.flags, id[:8], t.dest.IP(), t.dest.TCP()) } func (t *discoverTask) Do(srv *Server) { @@ -387,9 +370,7 @@ func (t *discoverTask) Do(srv *Server) { time.Sleep(next.Sub(now)) } srv.lastLookup = time.Now() - var target discover.NodeID - rand.Read(target[:]) - t.results = srv.ntab.Lookup(target) + t.results = srv.ntab.LookupRandom() } func (t *discoverTask) String() string { @@ -411,10 +392,10 @@ func (t waitExpireTask) String() string { func (h dialHistory) min() pastDial { return h[0] } -func (h *dialHistory) add(id discover.NodeID, exp time.Time) { +func (h *dialHistory) add(id enode.ID, exp time.Time) { heap.Push(h, pastDial{id, exp}) } -func (h *dialHistory) remove(id discover.NodeID) bool { +func (h *dialHistory) remove(id enode.ID) bool { for i, v := range *h { if v.id == id { heap.Remove(h, i) @@ -423,7 +404,7 @@ func (h *dialHistory) remove(id discover.NodeID) bool { } return false } -func (h dialHistory) contains(id discover.NodeID) bool { +func (h dialHistory) contains(id enode.ID) bool { for _, v := range h { if v.id == id { return true diff --git a/p2p/dial_test.go b/p2p/dial_test.go index 0e9928782654..f2d8010f30fd 100644 --- a/p2p/dial_test.go +++ b/p2p/dial_test.go @@ -23,7 +23,8 @@ import ( "testing" "time" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" "github.com/davecgh/go-spew/spew" ) @@ -48,10 +49,10 @@ func runDialTest(t *testing.T, test dialtest) { vtime time.Time running int ) - pm := func(ps []*Peer) map[discover.NodeID]*Peer { - m := make(map[discover.NodeID]*Peer) + pm := func(ps []*Peer) map[enode.ID]*Peer { + m := make(map[enode.ID]*Peer) for _, p := range ps { - m[p.rw.id] = p + m[p.ID()] = p } return m } @@ -69,6 +70,7 @@ func runDialTest(t *testing.T, test dialtest) { t.Errorf("round %d: new tasks mismatch:\ngot %v\nwant %v\nstate: %v\nrunning: %v\n", i, spew.Sdump(new), spew.Sdump(round.new), spew.Sdump(test.init), spew.Sdump(running)) } + t.Log("tasks:", spew.Sdump(new)) // Time advances by 16 seconds on every round. vtime = vtime.Add(16 * time.Second) @@ -76,79 +78,79 @@ func runDialTest(t *testing.T, test dialtest) { } } -type fakeTable []*discover.Node +type fakeTable []*enode.Node -func (t fakeTable) Self() *discover.Node { return new(discover.Node) } -func (t fakeTable) Close() {} -func (t fakeTable) Lookup(discover.NodeID) []*discover.Node { return nil } -func (t fakeTable) Resolve(discover.NodeID) *discover.Node { return nil } -func (t fakeTable) ReadRandomNodes(buf []*discover.Node) int { return copy(buf, t) } +func (t fakeTable) Self() *enode.Node { return new(enode.Node) } +func (t fakeTable) Close() {} +func (t fakeTable) LookupRandom() []*enode.Node { return nil } +func (t fakeTable) Resolve(*enode.Node) *enode.Node { return nil } +func (t fakeTable) ReadRandomNodes(buf []*enode.Node) int { return copy(buf, t) } // This test checks that dynamic dials are launched from discovery results. func TestDialStateDynDial(t *testing.T) { runDialTest(t, dialtest{ - init: newDialState(nil, nil, fakeTable{}, 5, nil), + init: newDialState(enode.ID{}, nil, nil, fakeTable{}, 5, nil), rounds: []round{ // A discovery query is launched. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, new: []task{&discoverTask{}}, }, // Dynamic dials are launched when it completes. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, done: []task{ - &discoverTask{results: []*discover.Node{ - {ID: uintID(2)}, // this one is already connected and not dialed. - {ID: uintID(3)}, - {ID: uintID(4)}, - {ID: uintID(5)}, - {ID: uintID(6)}, // these are not tried because max dyn dials is 5 - {ID: uintID(7)}, // ... + &discoverTask{results: []*enode.Node{ + newNode(uintID(2), nil), // this one is already connected and not dialed. + newNode(uintID(3), nil), + newNode(uintID(4), nil), + newNode(uintID(5), nil), + newNode(uintID(6), nil), // these are not tried because max dyn dials is 5 + newNode(uintID(7), nil), // ... }}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, }, // Some of the dials complete but no new ones are launched yet because // the sum of active dial count and dynamic peer count is == maxDynDials. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(4), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, }, }, // No new dial tasks are launched in the this round because // maxDynDials has been reached. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(4), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(5), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, new: []task{ &waitExpireTask{Duration: 14 * time.Second}, @@ -158,29 +160,31 @@ func TestDialStateDynDial(t *testing.T) { // results from last discovery lookup are reused. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(3)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(4), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(5), nil)}}, + }, + new: []task{ + &dialTask{flags: dynDialedConn, dest: newNode(uintID(6), nil)}, }, - new: []task{}, }, // More peers (3,4) drop off and dial for ID 6 completes. // The last query result from the discovery lookup is reused // and a new one is spawned because more candidates are needed. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(5), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(6)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(6), nil)}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(7)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(7), nil)}, + &discoverTask{}, }, }, // Peer 7 is connected, but there still aren't enough dynamic peers @@ -188,29 +192,29 @@ func TestDialStateDynDial(t *testing.T) { // no new is started. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(7)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(5), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(7), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(7)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(7), nil)}, }, }, // Finish the running node discovery with an empty set. A new lookup // should be immediately requested. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(0)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(5)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(7)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(0), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(5), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(7), nil)}}, }, done: []task{ &discoverTask{}, }, new: []task{ - &waitExpireTask{Duration: 14 * time.Second}, + &discoverTask{}, }, }, }, @@ -219,34 +223,34 @@ func TestDialStateDynDial(t *testing.T) { // Tests that bootnodes are dialed if no peers are connectd, but not otherwise. func TestDialStateDynDialBootnode(t *testing.T) { - bootnodes := []*discover.Node{ - {ID: uintID(1)}, - {ID: uintID(2)}, - {ID: uintID(3)}, + bootnodes := []*enode.Node{ + newNode(uintID(1), nil), + newNode(uintID(2), nil), + newNode(uintID(3), nil), } table := fakeTable{ - {ID: uintID(4)}, - {ID: uintID(5)}, - {ID: uintID(6)}, - {ID: uintID(7)}, - {ID: uintID(8)}, + newNode(uintID(4), nil), + newNode(uintID(5), nil), + newNode(uintID(6), nil), + newNode(uintID(7), nil), + newNode(uintID(8), nil), } runDialTest(t, dialtest{ - init: newDialState(nil, bootnodes, table, 5, nil), + init: newDialState(enode.ID{}, nil, bootnodes, table, 5, nil), rounds: []round{ // 2 dynamic dials attempted, bootnodes pending fallback interval { new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, &discoverTask{}, }, }, // No dials succeed, bootnodes still pending fallback interval { done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, }, // No dials succeed, bootnodes still pending fallback interval @@ -254,54 +258,51 @@ func TestDialStateDynDialBootnode(t *testing.T) { // No dials succeed, 2 dynamic dials attempted and 1 bootnode too as fallback interval was reached { new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, }, // No dials succeed, 2nd bootnode is attempted { done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(2), nil)}, }, }, // No dials succeed, 3rd bootnode is attempted { done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(2), nil)}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, }, }, // No dials succeed, 1st bootnode is attempted again, expired random nodes retried { done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, }, // Random dial succeeds, no more bootnodes are attempted { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(4)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(4), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, - }, - new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, }, }, }, @@ -312,83 +313,79 @@ func TestDialStateDynDialFromTable(t *testing.T) { // This table always returns the same random nodes // in the order given below. table := fakeTable{ - {ID: uintID(1)}, - {ID: uintID(2)}, - {ID: uintID(3)}, - {ID: uintID(4)}, - {ID: uintID(5)}, - {ID: uintID(6)}, - {ID: uintID(7)}, - {ID: uintID(8)}, + newNode(uintID(1), nil), + newNode(uintID(2), nil), + newNode(uintID(3), nil), + newNode(uintID(4), nil), + newNode(uintID(5), nil), + newNode(uintID(6), nil), + newNode(uintID(7), nil), + newNode(uintID(8), nil), } runDialTest(t, dialtest{ - init: newDialState(nil, nil, table, 10, nil), + init: newDialState(enode.ID{}, nil, nil, table, 10, nil), rounds: []round{ // 5 out of 8 of the nodes returned by ReadRandomNodes are dialed. { new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(2), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, &discoverTask{}, }, }, // Dialing nodes 1,2 succeeds. Dials from the lookup are launched. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &discoverTask{results: []*discover.Node{ - {ID: uintID(10)}, - {ID: uintID(11)}, - {ID: uintID(12)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(2), nil)}, + &discoverTask{results: []*enode.Node{ + newNode(uintID(10), nil), + newNode(uintID(11), nil), + newNode(uintID(12), nil), }}, }, new: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(10)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(11)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(12)}}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(10), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(11), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(12), nil)}, + &discoverTask{}, }, }, // Dialing nodes 3,4,5 fails. The dials from the lookup succeed. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(10), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(11), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(12), nil)}}, }, done: []task{ - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(3)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(5)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(10)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(11)}}, - &dialTask{flags: dynDialedConn, dest: &discover.Node{ID: uintID(12)}}, - }, - new: []task{ - &discoverTask{}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(3), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(5), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(10), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(11), nil)}, + &dialTask{flags: dynDialedConn, dest: newNode(uintID(12), nil)}, }, }, // Waiting for expiry. No waitExpireTask is launched because the // discovery query is still running. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(10), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(11), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(12), nil)}}, }, }, // Nodes 3,4 are not tried again because only the first two @@ -396,36 +393,44 @@ func TestDialStateDynDialFromTable(t *testing.T) { // already connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(10)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(11)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(12)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(10), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(11), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(12), nil)}}, }, }, }, }) } +func newNode(id enode.ID, ip net.IP) *enode.Node { + var r enr.Record + if ip != nil { + r.Set(enr.IP(ip)) + } + return enode.SignNull(&r, id) +} + // This test checks that candidates that do not match the netrestrict list are not dialed. func TestDialStateNetRestrict(t *testing.T) { // This table always returns the same random nodes // in the order given below. table := fakeTable{ - {ID: uintID(1), IP: net.ParseIP("127.0.0.1")}, - {ID: uintID(2), IP: net.ParseIP("127.0.0.2")}, - {ID: uintID(3), IP: net.ParseIP("127.0.0.3")}, - {ID: uintID(4), IP: net.ParseIP("127.0.0.4")}, - {ID: uintID(5), IP: net.ParseIP("127.0.2.5")}, - {ID: uintID(6), IP: net.ParseIP("127.0.2.6")}, - {ID: uintID(7), IP: net.ParseIP("127.0.2.7")}, - {ID: uintID(8), IP: net.ParseIP("127.0.2.8")}, + newNode(uintID(1), net.ParseIP("127.0.0.1")), + newNode(uintID(2), net.ParseIP("127.0.0.2")), + newNode(uintID(3), net.ParseIP("127.0.0.3")), + newNode(uintID(4), net.ParseIP("127.0.0.4")), + newNode(uintID(5), net.ParseIP("127.0.2.5")), + newNode(uintID(6), net.ParseIP("127.0.2.6")), + newNode(uintID(7), net.ParseIP("127.0.2.7")), + newNode(uintID(8), net.ParseIP("127.0.2.8")), } restrict := new(netutil.Netlist) restrict.Add("127.0.2.0/24") runDialTest(t, dialtest{ - init: newDialState(nil, nil, table, 10, restrict), + init: newDialState(enode.ID{}, nil, nil, table, 10, restrict), rounds: []round{ { new: []task{ @@ -437,87 +442,83 @@ func TestDialStateNetRestrict(t *testing.T) { }) } -// This test checks that static dials are launched. func TestDialStateStaticDial(t *testing.T) { - wantStatic := []*discover.Node{ - {ID: uintID(1)}, - {ID: uintID(2)}, - {ID: uintID(3)}, - {ID: uintID(4)}, - {ID: uintID(5)}, + wantStatic := []*enode.Node{ + newNode(uintID(1), nil), + newNode(uintID(2), nil), + newNode(uintID(3), nil), + newNode(uintID(4), nil), + newNode(uintID(5), nil), } runDialTest(t, dialtest{ - init: newDialState(wantStatic, nil, fakeTable{}, 0, nil), + init: newDialState(enode.ID{}, wantStatic, nil, fakeTable{}, 0, nil), rounds: []round{ // Static dials are launched for the nodes that // aren't yet connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(3), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(5), nil)}, }, }, // No new tasks are launched in this round because all static // nodes are either connected or still being dialed. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - }, - new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(3), nil)}}, }, done: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(3), nil)}, }, }, // No new dial tasks are launched because all static // nodes are now connected. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(4)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(4), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(5), nil)}}, }, done: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(4), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(5), nil)}, }, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(4)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(5)}}, + &waitExpireTask{Duration: 14 * time.Second}, }, }, // Wait a round for dial history to expire, no new tasks should spawn. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(4)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(4), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(5), nil)}}, }, }, // If a static node is dropped, it should be immediately redialed, // irrespective whether it was originally static or dynamic. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(3)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(5)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(3), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(5), nil)}}, + }, + new: []task{ + &dialTask{flags: staticDialedConn, dest: newNode(uintID(2), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(4), nil)}, }, - new: []task{}, }, }, }) @@ -525,9 +526,9 @@ func TestDialStateStaticDial(t *testing.T) { // This test checks that static peers will be redialed immediately if they were re-added to a static list. func TestDialStaticAfterReset(t *testing.T) { - wantStatic := []*discover.Node{ - {ID: uintID(1)}, - {ID: uintID(2)}, + wantStatic := []*enode.Node{ + newNode(uintID(1), nil), + newNode(uintID(2), nil), } rounds := []round{ @@ -535,104 +536,100 @@ func TestDialStaticAfterReset(t *testing.T) { { peers: nil, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(2), nil)}, }, }, // No new dial tasks, all peers are connected. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(1)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(2)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(2), nil)}}, }, done: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(2), nil)}, }, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &waitExpireTask{Duration: 30 * time.Second}, }, }, } dTest := dialtest{ - init: newDialState(wantStatic, nil, fakeTable{}, 0, nil), + init: newDialState(enode.ID{}, wantStatic, nil, fakeTable{}, 0, nil), rounds: rounds, } runDialTest(t, dTest) for _, n := range wantStatic { dTest.init.removeStatic(n) dTest.init.addStatic(n) - delete(dTest.init.dialing, n.ID) } - // without removing peers they will be considered recently dialed runDialTest(t, dTest) } // This test checks that past dials are not retried for some time. func TestDialStateCache(t *testing.T) { - wantStatic := []*discover.Node{ - {ID: uintID(1)}, - {ID: uintID(2)}, - {ID: uintID(3)}, + wantStatic := []*enode.Node{ + newNode(uintID(1), nil), + newNode(uintID(2), nil), + newNode(uintID(3), nil), } runDialTest(t, dialtest{ - init: newDialState(wantStatic, nil, fakeTable{}, 0, nil), + init: newDialState(enode.ID{}, wantStatic, nil, fakeTable{}, 0, nil), rounds: []round{ // Static dials are launched for the nodes that // aren't yet connected. { peers: nil, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(2), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(3), nil)}, }, }, // No new tasks are launched in this round because all static // nodes are either connected or still being dialed. { peers: []*Peer{ - {rw: &conn{flags: staticDialedConn, id: uintID(1)}}, - {rw: &conn{flags: staticDialedConn, id: uintID(2)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: staticDialedConn, node: newNode(uintID(2), nil)}}, }, done: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, - }, - new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(1)}}, - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(2)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(1), nil)}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(2), nil)}, }, }, // A salvage task is launched to wait for node 3's history // entry to expire. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, done: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(3), nil)}, + }, + new: []task{ + &waitExpireTask{Duration: 14 * time.Second}, }, }, // Still waiting for node 3's entry to expire in the cache. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, }, // The cache entry for node 3 has expired and is retried. { peers: []*Peer{ - {rw: &conn{flags: dynDialedConn, id: uintID(1)}}, - {rw: &conn{flags: dynDialedConn, id: uintID(2)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(1), nil)}}, + {rw: &conn{flags: dynDialedConn, node: newNode(uintID(2), nil)}}, }, new: []task{ - &dialTask{flags: staticDialedConn, dest: &discover.Node{ID: uintID(3)}}, + &dialTask{flags: staticDialedConn, dest: newNode(uintID(3), nil)}, }, }, }, @@ -640,12 +637,12 @@ func TestDialStateCache(t *testing.T) { } func TestDialResolve(t *testing.T) { - resolved := discover.NewNode(uintID(1), net.IP{127, 0, 55, 234}, 3333, 4444) + resolved := newNode(uintID(1), net.IP{127, 0, 55, 234}) table := &resolveMock{answer: resolved} - state := newDialState(nil, nil, table, 0, nil) + state := newDialState(enode.ID{}, nil, nil, table, 0, nil) // Check that the task is generated with an incomplete ID. - dest := discover.NewNode(uintID(1), nil, 0, 0) + dest := newNode(uintID(1), nil) state.addStatic(dest) tasks := state.newTasks(0, nil, time.Time{}) if !reflect.DeepEqual(tasks, []task{&dialTask{flags: staticDialedConn, dest: dest}}) { @@ -656,7 +653,7 @@ func TestDialResolve(t *testing.T) { config := Config{Dialer: TCPDialer{&net.Dialer{Deadline: time.Now().Add(-5 * time.Minute)}}} srv := &Server{ntab: table, Config: config} tasks[0].Do(srv) - if !reflect.DeepEqual(table.resolveCalls, []discover.NodeID{dest.ID}) { + if !reflect.DeepEqual(table.resolveCalls, []*enode.Node{dest}) { t.Fatalf("wrong resolve calls, got %v", table.resolveCalls) } @@ -684,25 +681,72 @@ next: return true } -func uintID(i uint32) discover.NodeID { - var id discover.NodeID +func uintID(i uint32) enode.ID { + var id enode.ID binary.BigEndian.PutUint32(id[:], i) return id } // implements discoverTable for TestDialResolve type resolveMock struct { - resolveCalls []discover.NodeID - answer *discover.Node + resolveCalls []*enode.Node + answer *enode.Node } -func (t *resolveMock) Resolve(id discover.NodeID) *discover.Node { - t.resolveCalls = append(t.resolveCalls, id) +func (t *resolveMock) Resolve(n *enode.Node) *enode.Node { + t.resolveCalls = append(t.resolveCalls, n) return t.answer } -func (t *resolveMock) Self() *discover.Node { return new(discover.Node) } -func (t *resolveMock) Close() {} -func (t *resolveMock) Bootstrap([]*discover.Node) {} -func (t *resolveMock) Lookup(discover.NodeID) []*discover.Node { return nil } -func (t *resolveMock) ReadRandomNodes(buf []*discover.Node) int { return 0 } +func (t *resolveMock) Self() *enode.Node { return new(enode.Node) } +func (t *resolveMock) Close() {} +func (t *resolveMock) LookupRandom() []*enode.Node { return nil } +func (t *resolveMock) ReadRandomNodes(buf []*enode.Node) int { return 0 } + +// countingDialer counts the number of times Dial is invoked. It returns one +// end of a net.Pipe so that the (test) transport can be set up on top of it +// without performing any real I/O. +type countingDialer struct { + count int +} + +func (d *countingDialer) Dial(*enode.Node) (net.Conn, error) { + d.count++ + c, _ := net.Pipe() + return c, nil +} + +// TestDialTaskDoSingleDial verifies that a successful dial does not trigger a +// second "pair" dial against the same node. The legacy XDPoSChain dialer used +// to open two TCP connections per peer; the dual-connection behaviour has been +// removed and the upstream single-connection semantics restored. +func TestDialTaskDoSingleDial(t *testing.T) { + remkey := newkey() + srv := startTestServer(t, &remkey.PublicKey, func(p *Peer) {}) + defer srv.Stop() + + dialer := &countingDialer{} + srv.Dialer = dialer + + task := &dialTask{ + flags: dynDialedConn, + dest: enode.NewV4(&remkey.PublicKey, net.IP{127, 0, 0, 1}, 30303, 30303), + } + task.Do(srv) + + if dialer.count != 1 { + t.Fatalf("Dialer.Dial called %d times, want 1", dialer.count) + } +} + +// This test checks that a second connection to an already connected peer is rejected. +func TestDialStateCheckDialRejectsAlreadyConnectedPeer(t *testing.T) { + id := uintID(1) + state := newDialState(enode.ID{}, nil, nil, fakeTable{}, 10, nil) + existing := &Peer{rw: &conn{node: newNode(id, nil)}} + + err := state.checkDial(newNode(id, nil), map[enode.ID]*Peer{id: existing}) + if err != errAlreadyConnected { + t.Fatalf("expected %v, got %v", errAlreadyConnected, err) + } +} diff --git a/p2p/discover/database.go b/p2p/discover/database.go deleted file mode 100644 index 1f5d80f6445e..000000000000 --- a/p2p/discover/database.go +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Contains the node database, storing previously seen nodes and any collected -// metadata about them for QoS purposes. - -package discover - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "os" - "sync" - "time" - - "github.com/XinFinOrg/XDPoSChain/crypto" - "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/rlp" - "github.com/syndtr/goleveldb/leveldb" - "github.com/syndtr/goleveldb/leveldb/errors" - "github.com/syndtr/goleveldb/leveldb/iterator" - "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/syndtr/goleveldb/leveldb/storage" - "github.com/syndtr/goleveldb/leveldb/util" -) - -var ( - nodeDBNilNodeID = NodeID{} // Special node ID to use as a nil element. - nodeDBNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped. - nodeDBCleanupCycle = time.Hour // Time period for running the expiration task. -) - -// nodeDB stores all nodes we know about. -type nodeDB struct { - lvl *leveldb.DB // Interface to the database itself - self NodeID // Own node id to prevent adding it into the database - runner sync.Once // Ensures we can start at most one expirer - quit chan struct{} // Channel to signal the expiring thread to stop -} - -// Schema layout for the node database -var ( - nodeDBVersionKey = []byte("version") // Version of the database to flush if changes - nodeDBItemPrefix = []byte("n:") // Identifier to prefix node entries with - - nodeDBDiscoverRoot = ":discover" - nodeDBDiscoverPing = nodeDBDiscoverRoot + ":lastping" - nodeDBDiscoverPong = nodeDBDiscoverRoot + ":lastpong" - nodeDBDiscoverFindFails = nodeDBDiscoverRoot + ":findfail" -) - -// newNodeDB creates a new node database for storing and retrieving infos about -// known peers in the network. If no path is given, an in-memory, temporary -// database is constructed. -func newNodeDB(path string, version int, self NodeID) (*nodeDB, error) { - if path == "" { - return newMemoryNodeDB(self) - } - return newPersistentNodeDB(path, version, self) -} - -// newMemoryNodeDB creates a new in-memory node database without a persistent -// backend. -func newMemoryNodeDB(self NodeID) (*nodeDB, error) { - db, err := leveldb.Open(storage.NewMemStorage(), nil) - if err != nil { - return nil, err - } - return &nodeDB{ - lvl: db, - self: self, - quit: make(chan struct{}), - }, nil -} - -// newPersistentNodeDB creates/opens a leveldb backed persistent node database, -// also flushing its contents in case of a version mismatch. -func newPersistentNodeDB(path string, version int, self NodeID) (*nodeDB, error) { - opts := &opt.Options{OpenFilesCacheCapacity: 5} - db, err := leveldb.OpenFile(path, opts) - if _, iscorrupted := err.(*errors.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, int64(version))] - - blob, err := db.Get(nodeDBVersionKey, nil) - switch err { - case leveldb.ErrNotFound: - // Version not found (i.e. empty cache), insert it - if err := db.Put(nodeDBVersionKey, currentVer, nil); err != nil { - db.Close() - return nil, err - } - - 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 newPersistentNodeDB(path, version, self) - } - } - return &nodeDB{ - lvl: db, - self: self, - quit: make(chan struct{}), - }, nil -} - -// makeKey generates the leveldb key-blob from a node id and its particular -// field of interest. -func makeKey(id NodeID, field string) []byte { - if bytes.Equal(id[:], nodeDBNilNodeID[:]) { - return []byte(field) - } - return append(nodeDBItemPrefix, append(id[:], field...)...) -} - -// splitKey tries to split a database key into a node id and a field part. -func splitKey(key []byte) (id NodeID, field string) { - // If the key is not of a node, return it plainly - if !bytes.HasPrefix(key, nodeDBItemPrefix) { - return NodeID{}, string(key) - } - // Otherwise split the id and field - item := key[len(nodeDBItemPrefix):] - copy(id[:], item[:len(id)]) - field = string(item[len(id):]) - - return id, field -} - -// fetchInt64 retrieves an integer instance associated with a particular -// database key. -func (db *nodeDB) fetchInt64(key []byte) int64 { - blob, err := db.lvl.Get(key, nil) - if err != nil { - return 0 - } - val, read := binary.Varint(blob) - if read <= 0 { - return 0 - } - return val -} - -// storeInt64 update a specific database entry to the current time instance as a -// unix timestamp. -func (db *nodeDB) storeInt64(key []byte, n int64) error { - blob := make([]byte, binary.MaxVarintLen64) - blob = blob[:binary.PutVarint(blob, n)] - - return db.lvl.Put(key, blob, nil) -} - -// node retrieves a node with a given id from the database. -func (db *nodeDB) node(id NodeID) *Node { - blob, err := db.lvl.Get(makeKey(id, nodeDBDiscoverRoot), nil) - if err != nil { - return nil - } - node := new(Node) - if err := rlp.DecodeBytes(blob, node); err != nil { - log.Error("Failed to decode node RLP", "err", err) - return nil - } - node.sha = crypto.Keccak256Hash(node.ID[:]) - return node -} - -// updateNode inserts - potentially overwriting - a node into the peer database. -func (db *nodeDB) updateNode(node *Node) error { - blob, err := rlp.EncodeToBytes(node) - if err != nil { - return err - } - return db.lvl.Put(makeKey(node.ID, nodeDBDiscoverRoot), blob, nil) -} - -// deleteNode deletes all information/keys associated with a node. -func (db *nodeDB) deleteNode(id NodeID) error { - deleter := db.lvl.NewIterator(util.BytesPrefix(makeKey(id, "")), nil) - for deleter.Next() { - if err := db.lvl.Delete(deleter.Key(), nil); err != nil { - return err - } - } - return nil -} - -// ensureExpirer is a small helper method ensuring that the data expiration -// mechanism is running. If the expiration goroutine is already running, this -// method simply returns. -// -// The goal is to start the data evacuation only after the network successfully -// bootstrapped itself (to prevent dumping potentially useful seed nodes). Since -// it would require significant overhead to exactly trace the first successful -// convergence, it's simpler to "ensure" the correct state when an appropriate -// condition occurs (i.e. a successful bonding), and discard further events. -func (db *nodeDB) ensureExpirer() { - db.runner.Do(func() { go db.expirer() }) -} - -// expirer should be started in a go routine, and is responsible for looping ad -// infinitum and dropping stale data from the database. -func (db *nodeDB) expirer() { - tick := time.NewTicker(nodeDBCleanupCycle) - defer tick.Stop() - for { - select { - case <-tick.C: - if err := db.expireNodes(); err != nil { - log.Error("Failed to expire nodedb items", "err", err) - } - case <-db.quit: - return - } - } -} - -// expireNodes iterates over the database and deletes all nodes that have not -// been seen (i.e. received a pong from) for some allotted time. -func (db *nodeDB) expireNodes() error { - threshold := time.Now().Add(-nodeDBNodeExpiration) - - // Find discovered nodes that are older than the allowance - it := db.lvl.NewIterator(nil, nil) - defer it.Release() - - for it.Next() { - // Skip the item if not a discovery node - id, field := splitKey(it.Key()) - if field != nodeDBDiscoverRoot { - continue - } - // Skip the node if not expired yet (and not self) - if !bytes.Equal(id[:], db.self[:]) { - if seen := db.bondTime(id); seen.After(threshold) { - continue - } - } - // Otherwise delete all associated information - db.deleteNode(id) - } - return nil -} - -// lastPing retrieves the time of the last ping packet send to a remote node, -// requesting binding. -func (db *nodeDB) lastPing(id NodeID) time.Time { - return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPing)), 0) -} - -// updateLastPing updates the last time we tried contacting a remote node. -func (db *nodeDB) updateLastPing(id NodeID, instance time.Time) error { - return db.storeInt64(makeKey(id, nodeDBDiscoverPing), instance.Unix()) -} - -// bondTime retrieves the time of the last successful pong from remote node. -func (db *nodeDB) bondTime(id NodeID) time.Time { - return time.Unix(db.fetchInt64(makeKey(id, nodeDBDiscoverPong)), 0) -} - -// hasBond reports whether the given node is considered bonded. -func (db *nodeDB) hasBond(id NodeID) bool { - return time.Since(db.bondTime(id)) < nodeDBNodeExpiration -} - -// updateBondTime updates the last pong time of a node. -func (db *nodeDB) updateBondTime(id NodeID, instance time.Time) error { - return db.storeInt64(makeKey(id, nodeDBDiscoverPong), instance.Unix()) -} - -// findFails retrieves the number of findnode failures since bonding. -func (db *nodeDB) findFails(id NodeID) int { - return int(db.fetchInt64(makeKey(id, nodeDBDiscoverFindFails))) -} - -// updateFindFails updates the number of findnode failures since bonding. -func (db *nodeDB) updateFindFails(id NodeID, fails int) error { - return db.storeInt64(makeKey(id, nodeDBDiscoverFindFails), int64(fails)) -} - -// querySeeds retrieves random nodes to be used as potential seed nodes -// for bootstrapping. -func (db *nodeDB) querySeeds(n int, maxAge time.Duration) []*Node { - var ( - now = time.Now() - nodes = make([]*Node, 0, n) - it = db.lvl.NewIterator(nil, nil) - id NodeID - ) - defer it.Release() - -seek: - for seeks := 0; len(nodes) < n && seeks < n*5; seeks++ { - // Seek to a random entry. The first byte is incremented by a - // random amount each time in order to increase the likelihood - // of hitting all existing nodes in very small databases. - ctr := id[0] - rand.Read(id[:]) - id[0] = ctr + id[0]%16 - it.Seek(makeKey(id, nodeDBDiscoverRoot)) - - n := nextNode(it) - if n == nil { - id[0] = 0 - continue seek // iterator exhausted - } - if n.ID == db.self { - continue seek - } - if now.Sub(db.bondTime(n.ID)) > maxAge { - continue seek - } - for i := range nodes { - if nodes[i].ID == n.ID { - continue seek // duplicate - } - } - nodes = append(nodes, n) - } - return nodes -} - -// reads the next node record from the iterator, skipping over other -// database entries. -func nextNode(it iterator.Iterator) *Node { - for end := false; !end; end = !it.Next() { - id, field := splitKey(it.Key()) - if field != nodeDBDiscoverRoot { - continue - } - var n Node - if err := rlp.DecodeBytes(it.Value(), &n); err != nil { - log.Warn("Failed to decode node RLP", "id", id, "err", err) - continue - } - return &n - } - return nil -} - -// close flushes and closes the database files. -func (db *nodeDB) close() { - close(db.quit) - db.lvl.Close() -} diff --git a/p2p/discover/database_test.go b/p2p/discover/database_test.go deleted file mode 100644 index f1ea25139c8f..000000000000 --- a/p2p/discover/database_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package discover - -import ( - "bytes" - "net" - "path/filepath" - "reflect" - "testing" - "time" -) - -var nodeDBKeyTests = []struct { - id NodeID - field string - key []byte -}{ - { - id: NodeID{}, - field: "version", - key: []byte{0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e}, // field - }, - { - id: MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - field: ":discover", - key: []byte{0x6e, 0x3a, // prefix - 0x1d, 0xd9, 0xd6, 0x5c, 0x45, 0x52, 0xb5, 0xeb, // node id - 0x43, 0xd5, 0xad, 0x55, 0xa2, 0xee, 0x3f, 0x56, // - 0xc6, 0xcb, 0xc1, 0xc6, 0x4a, 0x5c, 0x8d, 0x65, // - 0x9f, 0x51, 0xfc, 0xd5, 0x1b, 0xac, 0xe2, 0x43, // - 0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // - 0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, // - 0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, // - 0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, // - 0x3a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, // field - }, - }, -} - -func TestNodeDBKeys(t *testing.T) { - for i, tt := range nodeDBKeyTests { - if key := makeKey(tt.id, tt.field); !bytes.Equal(key, tt.key) { - t.Errorf("make test %d: key mismatch: have %#x, want %#x", i, key, tt.key) - } - id, field := splitKey(tt.key) - if !bytes.Equal(id[:], tt.id[:]) { - t.Errorf("split test %d: id mismatch: have %#x, want %#x", i, id, tt.id) - } - if field != tt.field { - t.Errorf("split test %d: field mismatch: have %#x, want %#x", i, field, tt.field) - } - } -} - -var nodeDBInt64Tests = []struct { - key []byte - value int64 -}{ - {key: []byte{0x01}, value: 1}, - {key: []byte{0x02}, value: 2}, - {key: []byte{0x03}, value: 3}, -} - -func TestNodeDBInt64(t *testing.T) { - db, _ := newNodeDB("", Version, NodeID{}) - defer db.close() - - tests := nodeDBInt64Tests - for i := 0; i < len(tests); i++ { - // Insert the next value - if err := db.storeInt64(tests[i].key, tests[i].value); err != nil { - t.Errorf("test %d: failed to store value: %v", i, err) - } - // Check all existing and non existing values - for j := 0; j < len(tests); j++ { - num := db.fetchInt64(tests[j].key) - switch { - case j <= i && num != tests[j].value: - t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, tests[j].value) - case j > i && num != 0: - t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, 0) - } - } - } -} - -func TestNodeDBFetchStore(t *testing.T) { - node := NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{192, 168, 0, 1}, - 30303, - 30303, - ) - inst := time.Now() - num := 314 - - db, _ := newNodeDB("", Version, NodeID{}) - defer db.close() - - // Check fetch/store operations on a node ping object - if stored := db.lastPing(node.ID); stored.Unix() != 0 { - t.Errorf("ping: non-existing object: %v", stored) - } - if err := db.updateLastPing(node.ID, inst); err != nil { - t.Errorf("ping: failed to update: %v", err) - } - if stored := db.lastPing(node.ID); stored.Unix() != inst.Unix() { - t.Errorf("ping: value mismatch: have %v, want %v", stored, inst) - } - // Check fetch/store operations on a node pong object - if stored := db.bondTime(node.ID); stored.Unix() != 0 { - t.Errorf("pong: non-existing object: %v", stored) - } - if err := db.updateBondTime(node.ID, inst); err != nil { - t.Errorf("pong: failed to update: %v", err) - } - if stored := db.bondTime(node.ID); stored.Unix() != inst.Unix() { - t.Errorf("pong: value mismatch: have %v, want %v", stored, inst) - } - // Check fetch/store operations on a node findnode-failure object - if stored := db.findFails(node.ID); stored != 0 { - t.Errorf("find-node fails: non-existing object: %v", stored) - } - if err := db.updateFindFails(node.ID, num); err != nil { - t.Errorf("find-node fails: failed to update: %v", err) - } - if stored := db.findFails(node.ID); stored != num { - t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num) - } - // Check fetch/store operations on an actual node object - if stored := db.node(node.ID); stored != nil { - t.Errorf("node: non-existing object: %v", stored) - } - if err := db.updateNode(node); err != nil { - t.Errorf("node: failed to update: %v", err) - } - if stored := db.node(node.ID); stored == nil { - t.Errorf("node: not found") - } else if !reflect.DeepEqual(stored, node) { - t.Errorf("node: data mismatch: have %v, want %v", stored, node) - } -} - -var nodeDBSeedQueryNodes = []struct { - node *Node - pong time.Time -}{ - // This one should not be in the result set because its last - // pong time is too far in the past. - { - node: NewNode( - MustHexID("0x84d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 3}, - 30303, - 30303, - ), - pong: time.Now().Add(-3 * time.Hour), - }, - // This one shouldn't be in in the result set because its - // nodeID is the local node's ID. - { - node: NewNode( - MustHexID("0x57d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 3}, - 30303, - 30303, - ), - pong: time.Now().Add(-4 * time.Second), - }, - - // These should be in the result set. - { - node: NewNode( - MustHexID("0x22d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 1}, - 30303, - 30303, - ), - pong: time.Now().Add(-2 * time.Second), - }, - { - node: NewNode( - MustHexID("0x44d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 2}, - 30303, - 30303, - ), - pong: time.Now().Add(-3 * time.Second), - }, - { - node: NewNode( - MustHexID("0xe2d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 3}, - 30303, - 30303, - ), - pong: time.Now().Add(-1 * time.Second), - }, -} - -func TestNodeDBSeedQuery(t *testing.T) { - db, _ := newNodeDB("", Version, nodeDBSeedQueryNodes[1].node.ID) - defer db.close() - - // Insert a batch of nodes for querying - for i, seed := range nodeDBSeedQueryNodes { - if err := db.updateNode(seed.node); err != nil { - t.Fatalf("node %d: failed to insert: %v", i, err) - } - if err := db.updateBondTime(seed.node.ID, seed.pong); err != nil { - t.Fatalf("node %d: failed to insert bondTime: %v", i, err) - } - } - - // Retrieve the entire batch and check for duplicates - seeds := db.querySeeds(len(nodeDBSeedQueryNodes)*2, time.Hour) - have := make(map[NodeID]struct{}) - for _, seed := range seeds { - have[seed.ID] = struct{}{} - } - want := make(map[NodeID]struct{}) - for _, seed := range nodeDBSeedQueryNodes[2:] { - want[seed.node.ID] = struct{}{} - } - if len(seeds) != len(want) { - t.Errorf("seed count mismatch: have %v, want %v", len(seeds), len(want)) - } - for id := range have { - if _, ok := want[id]; !ok { - t.Errorf("extra seed: %v", id) - } - } - for id := range want { - if _, ok := have[id]; !ok { - t.Errorf("missing seed: %v", id) - } - } -} - -func TestNodeDBPersistency(t *testing.T) { - root := t.TempDir() - - var ( - testKey = []byte("somekey") - testInt = int64(314) - ) - - // Create a persistent database and store some values - db, err := newNodeDB(filepath.Join(root, "database"), Version, NodeID{}) - if err != nil { - t.Fatalf("failed to create persistent database: %v", err) - } - if err := db.storeInt64(testKey, testInt); err != nil { - t.Fatalf("failed to store value: %v.", err) - } - db.close() - - // Reopen the database and check the value - db, err = newNodeDB(filepath.Join(root, "database"), Version, NodeID{}) - if err != nil { - t.Fatalf("failed to open persistent database: %v", err) - } - if val := db.fetchInt64(testKey); val != testInt { - t.Fatalf("value mismatch: have %v, want %v", val, testInt) - } - db.close() - - // Change the database version and check flush - db, err = newNodeDB(filepath.Join(root, "database"), Version+1, NodeID{}) - if err != nil { - t.Fatalf("failed to open persistent database: %v", err) - } - if val := db.fetchInt64(testKey); val != 0 { - t.Fatalf("value mismatch: have %v, want %v", val, 0) - } - db.close() -} - -var nodeDBExpirationNodes = []struct { - node *Node - pong time.Time - exp bool -}{ - { - node: NewNode( - MustHexID("0x01d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 1}, - 30303, - 30303, - ), - pong: time.Now().Add(-nodeDBNodeExpiration + time.Minute), - exp: false, - }, { - node: NewNode( - MustHexID("0x02d9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{127, 0, 0, 2}, - 30303, - 30303, - ), - pong: time.Now().Add(-nodeDBNodeExpiration - time.Minute), - exp: true, - }, -} - -func TestNodeDBExpiration(t *testing.T) { - db, _ := newNodeDB("", Version, NodeID{}) - defer db.close() - - // Add all the test nodes and set their last pong time - for i, seed := range nodeDBExpirationNodes { - if err := db.updateNode(seed.node); err != nil { - t.Fatalf("node %d: failed to insert: %v", i, err) - } - if err := db.updateBondTime(seed.node.ID, seed.pong); err != nil { - t.Fatalf("node %d: failed to update bondTime: %v", i, err) - } - } - // Expire some of them, and check the rest - if err := db.expireNodes(); err != nil { - t.Fatalf("failed to expire nodes: %v", err) - } - for i, seed := range nodeDBExpirationNodes { - node := db.node(seed.node.ID) - if (node == nil && !seed.exp) || (node != nil && seed.exp) { - t.Errorf("node %d: expiration mismatch: have %v, want %v", i, node, seed.exp) - } - } -} - -func TestNodeDBSelfExpiration(t *testing.T) { - // Find a node in the tests that shouldn't expire, and assign it as self - var self NodeID - for _, node := range nodeDBExpirationNodes { - if !node.exp { - self = node.node.ID - break - } - } - db, _ := newNodeDB("", Version, self) - defer db.close() - - // Add all the test nodes and set their last pong time - for i, seed := range nodeDBExpirationNodes { - if err := db.updateNode(seed.node); err != nil { - t.Fatalf("node %d: failed to insert: %v", i, err) - } - if err := db.updateBondTime(seed.node.ID, seed.pong); err != nil { - t.Fatalf("node %d: failed to update bondTime: %v", i, err) - } - } - // Expire the nodes and make sure self has been evacuated too - if err := db.expireNodes(); err != nil { - t.Fatalf("failed to expire nodes: %v", err) - } - node := db.node(self) - if node != nil { - t.Errorf("self not evacuated") - } -} diff --git a/p2p/discover/node.go b/p2p/discover/node.go index eb959998dc22..b4efef565dd4 100644 --- a/p2p/discover/node.go +++ b/p2p/discover/node.go @@ -18,414 +18,88 @@ package discover import ( "crypto/ecdsa" - "crypto/elliptic" - "encoding/hex" "errors" - "fmt" "math/big" - "math/rand" "net" - "net/url" - "regexp" - "strconv" - "strings" "time" - "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/common/math" "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/crypto/secp256k1" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" ) -const NodeIDBits = 512 - -// Node represents a host on the network. +// node represents a host on the network. // The fields of Node may not be modified. -type Node struct { - IP net.IP // len 4 for IPv4 or 16 for IPv6 - UDP, TCP uint16 // port numbers - ID NodeID // the node's public key - - // This is a cached copy of sha3(ID) which is used for node - // distance calculations. This is part of Node in order to make it - // possible to write tests that need a node at a certain distance. - // In those tests, the content of sha will not actually correspond - // with ID. - sha common.Hash - - // Time when the node was added to the table. - addedAt time.Time -} - -// NewNode creates a new node. It is mostly meant to be used for -// testing purposes. -func NewNode(id NodeID, ip net.IP, udpPort, tcpPort uint16) *Node { - if ipv4 := ip.To4(); ipv4 != nil { - ip = ipv4 - } - return &Node{ - IP: ip, - UDP: udpPort, - TCP: tcpPort, - ID: id, - sha: crypto.Keccak256Hash(id[:]), - } -} - -func (n *Node) addr() *net.UDPAddr { - return &net.UDPAddr{IP: n.IP, Port: int(n.UDP)} -} - -// Incomplete returns true for nodes with no IP address. -func (n *Node) Incomplete() bool { - return n.IP == nil -} - -// checks whether n is a valid complete node. -func (n *Node) validateComplete() error { - if n.Incomplete() { - return errors.New("incomplete node") - } - if n.UDP == 0 { - return errors.New("missing UDP port") - } - if n.TCP == 0 { - return errors.New("missing TCP port") - } - if n.IP.IsMulticast() || n.IP.IsUnspecified() { - return errors.New("invalid IP (multicast/unspecified)") - } - _, err := n.ID.Pubkey() // validate the key (on curve, etc.) - return err -} - -// The string representation of a Node is a URL. -// Please see ParseNode for a description of the format. -func (n *Node) String() string { - u := url.URL{Scheme: "enode"} - if n.Incomplete() { - u.Host = fmt.Sprintf("%x", n.ID[:]) - } else { - addr := net.TCPAddr{IP: n.IP, Port: int(n.TCP)} - u.User = url.User(fmt.Sprintf("%x", n.ID[:])) - u.Host = addr.String() - if n.UDP != n.TCP { - u.RawQuery = "discport=" + strconv.Itoa(int(n.UDP)) - } - } - return u.String() -} - -var incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$") - -// ParseNode parses a node designator. -// -// There are two basic forms of node designators -// - incomplete nodes, which only have the public key (node ID) -// - complete nodes, which contain the public key and IP/Port information -// -// For incomplete nodes, the designator must look like one of these -// -// enode:// -// -// -// For complete nodes, the node ID is encoded in the username portion -// of the URL, separated from the host by an @ sign. The hostname can -// only be given as an IP address, DNS domain names are not allowed. -// The port in the host name section is the TCP listening port. If the -// TCP and UDP (discovery) ports differ, the UDP port is specified as -// query parameter "discport". -// -// In the following example, the node URL describes -// a node with IP address 10.3.58.6, TCP listening port 30303 -// and UDP discovery port 30301. -// -// enode://@10.3.58.6:30303?discport=30301 -func ParseNode(rawurl string) (*Node, error) { - if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil { - id, err := HexID(m[1]) - if err != nil { - return nil, fmt.Errorf("invalid node ID (%v)", err) - } - return NewNode(id, nil, 0, 0), nil - } - return parseComplete(rawurl) +type node struct { + enode.Node + addedAt time.Time // time when the node was added to the table + livenessChecks uint // how often liveness was checked } -func parseComplete(rawurl string) (*Node, error) { - var ( - id NodeID - ip net.IP - tcpPort, udpPort uint64 - ) - u, err := url.Parse(rawurl) - if err != nil { - return nil, err - } - if u.Scheme != "enode" { - return nil, errors.New("invalid URL scheme, want \"enode\"") - } - // Parse the Node ID from the user portion. - if u.User == nil { - return nil, errors.New("does not contain node ID") - } - if id, err = HexID(u.User.String()); err != nil { - return nil, fmt.Errorf("invalid node ID (%v)", err) - } - // Parse the IP address. - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - return nil, fmt.Errorf("invalid host: %v", err) - } - if ip = net.ParseIP(host); ip == nil { - return nil, errors.New("invalid IP address") - } - // Ensure the IP is 4 bytes long for IPv4 addresses. - if ipv4 := ip.To4(); ipv4 != nil { - ip = ipv4 - } - // Parse the port numbers. - if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil { - return nil, errors.New("invalid port") - } - udpPort = tcpPort - qv := u.Query() - if qv.Get("discport") != "" { - udpPort, err = strconv.ParseUint(qv.Get("discport"), 10, 16) - if err != nil { - return nil, errors.New("invalid discport in query") - } - } - return NewNode(id, ip, uint16(udpPort), uint16(tcpPort)), nil -} - -// MustParseNode parses a node URL. It panics if the URL is not valid. -func MustParseNode(rawurl string) *Node { - n, err := ParseNode(rawurl) - if err != nil { - panic("invalid node URL: " + err.Error()) - } - return n -} +type encPubkey [64]byte -// MarshalText implements encoding.TextMarshaler. -func (n *Node) MarshalText() ([]byte, error) { - return []byte(n.String()), nil +func encodePubkey(key *ecdsa.PublicKey) encPubkey { + var e encPubkey + math.ReadBits(key.X, e[:len(e)/2]) + math.ReadBits(key.Y, e[len(e)/2:]) + return e } -// UnmarshalText implements encoding.TextUnmarshaler. -func (n *Node) UnmarshalText(text []byte) error { - dec, err := ParseNode(string(text)) - if err == nil { - *n = *dec - } - return err -} - -// NodeID is a unique identifier for each node. -// The node identifier is a marshaled elliptic curve public key. -type NodeID [NodeIDBits / 8]byte - -// Bytes returns a byte slice representation of the NodeID -func (n NodeID) Bytes() []byte { - return n[:] -} - -// NodeID prints as a long hexadecimal number. -func (n NodeID) String() string { - return fmt.Sprintf("%x", n[:]) -} - -// The Go syntax representation of a NodeID is a call to HexID. -func (n NodeID) GoString() string { - return fmt.Sprintf("discover.HexID(\"%x\")", n[:]) -} - -// TerminalString returns a shortened hex string for terminal logging. -func (n NodeID) TerminalString() string { - return hex.EncodeToString(n[:8]) -} - -// MarshalText implements the encoding.TextMarshaler interface. -func (n NodeID) MarshalText() ([]byte, error) { - return []byte(hex.EncodeToString(n[:])), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -func (n *NodeID) UnmarshalText(text []byte) error { - id, err := HexID(string(text)) - if err != nil { - return err - } - *n = id - return nil -} - -// BytesID converts a byte slice to a NodeID -func BytesID(b []byte) (NodeID, error) { - var id NodeID - if len(b) != len(id) { - return id, fmt.Errorf("wrong length, want %d bytes", len(id)) - } - copy(id[:], b) - return id, nil -} - -// MustBytesID converts a byte slice to a NodeID. -// It panics if the byte slice is not a valid NodeID. -func MustBytesID(b []byte) NodeID { - id, err := BytesID(b) - if err != nil { - panic(err) +func decodePubkey(e encPubkey) (*ecdsa.PublicKey, error) { + p := &ecdsa.PublicKey{Curve: crypto.S256(), X: new(big.Int), Y: new(big.Int)} + half := len(e) / 2 + p.X.SetBytes(e[:half]) + p.Y.SetBytes(e[half:]) + if !p.Curve.IsOnCurve(p.X, p.Y) { + return nil, errors.New("invalid secp256k1 curve point") } - return id + return p, nil } -// HexID converts a hex string to a NodeID. -// The string may be prefixed with 0x. -func HexID(in string) (NodeID, error) { - var id NodeID - b, err := hex.DecodeString(strings.TrimPrefix(in, "0x")) - if err != nil { - return id, err - } else if len(b) != len(id) { - return id, fmt.Errorf("wrong length, want %d hex chars", len(id)*2) - } - copy(id[:], b) - return id, nil +func (e encPubkey) id() enode.ID { + return enode.ID(crypto.Keccak256Hash(e[:])) } -// MustHexID converts a hex string to a NodeID. -// It panics if the string is not a valid NodeID. -func MustHexID(in string) NodeID { - id, err := HexID(in) +// recoverNodeKey computes the public key used to sign the +// given hash from the signature. +func recoverNodeKey(hash, sig []byte) (key encPubkey, err error) { + pubkey, err := secp256k1.RecoverPubkey(hash, sig) if err != nil { - panic(err) + return key, err } - return id + copy(key[:], pubkey[1:]) + return key, nil } -// PubkeyID returns a marshaled representation of the given public key. -func PubkeyID(pub *ecdsa.PublicKey) NodeID { - var id NodeID - pbytes := elliptic.Marshal(pub.Curve, pub.X, pub.Y) - if len(pbytes)-1 != len(id) { - panic(fmt.Errorf("need %d bit pubkey, got %d bits", (len(id)+1)*8, len(pbytes))) - } - copy(id[:], pbytes[1:]) - return id +func wrapNode(n *enode.Node) *node { + return &node{Node: *n} } -// Pubkey returns the public key represented by the node ID. -// It returns an error if the ID is not a point on the curve. -func (n NodeID) Pubkey() (*ecdsa.PublicKey, error) { - p := &ecdsa.PublicKey{Curve: crypto.S256(), X: new(big.Int), Y: new(big.Int)} - half := len(n) / 2 - p.X.SetBytes(n[:half]) - p.Y.SetBytes(n[half:]) - if !p.Curve.IsOnCurve(p.X, p.Y) { - return nil, errors.New("invalid secp256k1 curve point") +func wrapNodes(ns []*enode.Node) []*node { + result := make([]*node, len(ns)) + for i, n := range ns { + result[i] = wrapNode(n) } - return p, nil + return result } -// recoverNodeID computes the public key used to sign the -// given hash from the signature. -func recoverNodeID(hash, sig []byte) (id NodeID, err error) { - pubkey, err := crypto.Ecrecover(hash, sig) - if err != nil { - return id, err - } - if len(pubkey)-1 != len(id) { - return id, fmt.Errorf("recovered pubkey has %d bits, want %d bits", len(pubkey)*8, (len(id)+1)*8) - } - for i := range id { - id[i] = pubkey[i+1] - } - return id, nil +func unwrapNode(n *node) *enode.Node { + return &n.Node } -// distcmp compares the distances a->target and b->target. -// Returns -1 if a is closer to target, 1 if b is closer to target -// and 0 if they are equal. -func distcmp(target, a, b common.Hash) int { - for i := range target { - da := a[i] ^ target[i] - db := b[i] ^ target[i] - if da > db { - return 1 - } else if da < db { - return -1 - } +func unwrapNodes(ns []*node) []*enode.Node { + result := make([]*enode.Node, len(ns)) + for i, n := range ns { + result[i] = unwrapNode(n) } - return 0 + return result } -// table of leading zero counts for bytes [0..255] -var lzcount = [256]int{ - 8, 7, 6, 6, 5, 5, 5, 5, - 4, 4, 4, 4, 4, 4, 4, 4, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, +func (n *node) addr() *net.UDPAddr { + return &net.UDPAddr{IP: n.IP(), Port: n.UDP()} } -// logdist returns the logarithmic distance between a and b, log2(a ^ b). -func logdist(a, b common.Hash) int { - lz := 0 - for i := range a { - x := a[i] ^ b[i] - if x == 0 { - lz += 8 - } else { - lz += lzcount[x] - break - } - } - return len(a)*8 - lz -} - -// hashAtDistance returns a random hash such that logdist(a, b) == n -func hashAtDistance(a common.Hash, n int) (b common.Hash) { - if n == 0 { - return a - } - // flip bit at position n, fill the rest with random bits - b = a - pos := len(a) - n/8 - 1 - bit := byte(0x01) << (byte(n%8) - 1) - if bit == 0 { - pos++ - bit = 0x80 - } - b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits - for i := pos + 1; i < len(a); i++ { - b[i] = byte(rand.Intn(255)) - } - return b +func (n *node) String() string { + return n.Node.String() } diff --git a/p2p/discover/node_test.go b/p2p/discover/node_test.go deleted file mode 100644 index 846fca179ed0..000000000000 --- a/p2p/discover/node_test.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package discover - -import ( - "bytes" - "fmt" - "math/big" - "math/rand" - "net" - "reflect" - "strings" - "testing" - "testing/quick" - "time" - - "github.com/XinFinOrg/XDPoSChain/common" - "github.com/XinFinOrg/XDPoSChain/crypto" -) - -func ExampleNewNode() { - id := MustHexID("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439") - - // Complete nodes contain UDP and TCP endpoints: - n1 := NewNode(id, net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), 52150, 30303) - fmt.Println("n1:", n1) - fmt.Println("n1.Incomplete() ->", n1.Incomplete()) - - // An incomplete node can be created by passing zero values - // for all parameters except id. - n2 := NewNode(id, nil, 0, 0) - fmt.Println("n2:", n2) - fmt.Println("n2.Incomplete() ->", n2.Incomplete()) - - // Output: - // n1: enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:30303?discport=52150 - // n1.Incomplete() -> false - // n2: enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439 - // n2.Incomplete() -> true -} - -var parseNodeTests = []struct { - rawurl string - wantError string - wantResult *Node -}{ - { - rawurl: "http://foobar", - wantError: `invalid URL scheme, want "enode"`, - }, - { - rawurl: "enode://01010101@123.124.125.126:3", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, - }, - // Complete nodes with IP address. - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", - wantError: `invalid IP address`, - }, - //{ - // rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", - // wantError: `parse enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo: invalid port ":foo" after host`, - //}, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", - wantError: `invalid discport in query`, - }, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{0x7f, 0x0, 0x0, 0x1}, - 52150, - 52150, - ), - }, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.ParseIP("::"), - 52150, - 52150, - ), - }, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), - 52150, - 52150, - ), - }, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - net.IP{0x7f, 0x0, 0x0, 0x1}, - 22334, - 52150, - ), - }, - // Incomplete nodes with no address. - { - rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - nil, 0, 0, - ), - }, - { - rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", - wantResult: NewNode( - MustHexID("0x1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), - nil, 0, 0, - ), - }, - // Invalid URLs - { - rawurl: "01010101", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, - }, - { - rawurl: "enode://01010101", - wantError: `invalid node ID (wrong length, want 128 hex chars)`, - }, - { - // This test checks that errors from url.Parse are handled. - rawurl: "://foo", - wantError: `parse "://foo": missing protocol scheme`, - }, -} - -func TestParseNode(t *testing.T) { - for _, test := range parseNodeTests { - n, err := ParseNode(test.rawurl) - if test.wantError != "" { - if err == nil { - t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, test.wantError) - continue - } else if err.Error() != test.wantError { - t.Errorf("test %q:\n got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError) - continue - } - } else { - if err != nil { - t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err) - continue - } - if !reflect.DeepEqual(n, test.wantResult) { - t.Errorf("test %q:\n result mismatch:\ngot: %#v, want: %#v", test.rawurl, n, test.wantResult) - } - } - } -} - -func TestNodeString(t *testing.T) { - for i, test := range parseNodeTests { - if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") { - str := test.wantResult.String() - if str != test.rawurl { - t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) - } - } - } -} - -func TestHexID(t *testing.T) { - ref := NodeID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} - id1 := MustHexID("0x000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - id2 := MustHexID("000000000000000000000000000000000000000000000000000000000000000000000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") - - if id1 != ref { - t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) - } - if id2 != ref { - t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) - } -} - -func TestNodeID_textEncoding(t *testing.T) { - ref := NodeID{ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, - 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, - 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, - 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, - 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x50, - 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x60, - 0x61, 0x62, 0x63, 0x64, - } - hex := "01020304050607080910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364" - - text, err := ref.MarshalText() - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(text, []byte(hex)) { - t.Fatalf("text encoding did not match\nexpected: %s\ngot: %s", hex, text) - } - - id := new(NodeID) - if err := id.UnmarshalText(text); err != nil { - t.Fatal(err) - } - if *id != ref { - t.Fatalf("text decoding did not match\nexpected: %s\ngot: %s", ref, id) - } -} - -func TestNodeID_recover(t *testing.T) { - prv := newkey() - hash := make([]byte, 32) - sig, err := crypto.Sign(hash, prv) - if err != nil { - t.Fatalf("signing error: %v", err) - } - - pub := PubkeyID(&prv.PublicKey) - recpub, err := recoverNodeID(hash, sig) - if err != nil { - t.Fatalf("recovery error: %v", err) - } - if pub != recpub { - t.Errorf("recovered wrong pubkey:\ngot: %v\nwant: %v", recpub, pub) - } - - ecdsa, err := pub.Pubkey() - if err != nil { - t.Errorf("Pubkey error: %v", err) - } - if !reflect.DeepEqual(ecdsa, &prv.PublicKey) { - t.Errorf("Pubkey mismatch:\n got: %#v\n want: %#v", ecdsa, &prv.PublicKey) - } -} - -func TestNodeID_pubkeyBad(t *testing.T) { - ecdsa, err := NodeID{}.Pubkey() - if err == nil { - t.Error("expected error for zero ID") - } - if ecdsa != nil { - t.Error("expected nil result") - } -} - -func TestNodeID_distcmp(t *testing.T) { - distcmpBig := func(target, a, b common.Hash) int { - tbig := new(big.Int).SetBytes(target[:]) - abig := new(big.Int).SetBytes(a[:]) - bbig := new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) - } - if err := quick.CheckEqual(distcmp, distcmpBig, quickcfg()); err != nil { - t.Error(err) - } -} - -// the random tests is likely to miss the case where they're equal. -func TestNodeID_distcmpEqual(t *testing.T) { - base := common.Hash{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - x := common.Hash{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} - if distcmp(base, x, x) != 0 { - t.Errorf("distcmp(base, x, x) != 0") - } -} - -func TestNodeID_logdist(t *testing.T) { - logdistBig := func(a, b common.Hash) int { - abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) - return new(big.Int).Xor(abig, bbig).BitLen() - } - if err := quick.CheckEqual(logdist, logdistBig, quickcfg()); err != nil { - t.Error(err) - } -} - -// the random tests is likely to miss the case where they're equal. -func TestNodeID_logdistEqual(t *testing.T) { - x := common.Hash{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - if logdist(x, x) != 0 { - t.Errorf("logdist(x, x) != 0") - } -} - -func TestNodeID_hashAtDistance(t *testing.T) { - // we don't use quick.Check here because its output isn't - // very helpful when the test fails. - cfg := quickcfg() - for i := 0; i < cfg.MaxCount; i++ { - a := gen(common.Hash{}, cfg.Rand).(common.Hash) - dist := cfg.Rand.Intn(len(common.Hash{}) * 8) - result := hashAtDistance(a, dist) - actualdist := logdist(result, a) - - if dist != actualdist { - t.Log("a: ", a) - t.Log("result:", result) - t.Fatalf("#%d: distance of result is %d, want %d", i, actualdist, dist) - } - } -} - -func quickcfg() *quick.Config { - return &quick.Config{ - MaxCount: 5000, - Rand: rand.New(rand.NewSource(time.Now().Unix())), - } -} - -// TODO: The Generate method can be dropped when we require Go >= 1.5 -// because testing/quick learned to generate arrays in 1.5. - -func (NodeID) Generate(rand *rand.Rand, size int) reflect.Value { - var id NodeID - m := rand.Intn(len(id)) - for i := len(id) - 1; i > m; i-- { - id[i] = byte(rand.Uint32()) - } - return reflect.ValueOf(id) -} diff --git a/p2p/discover/table.go b/p2p/discover/table.go index 7aafbb24b587..a6f805a553be 100644 --- a/p2p/discover/table.go +++ b/p2p/discover/table.go @@ -23,10 +23,9 @@ package discover import ( - "context" + "crypto/ecdsa" crand "crypto/rand" "encoding/binary" - "errors" "fmt" mrand "math/rand" "net" @@ -37,14 +36,14 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/metrics" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" ) const ( - alpha = 3 // Kademlia concurrency factor - bucketSize = 200 // Kademlia bucket size - maxReplacements = 10 // Size of per-bucket replacement list + alpha = 3 // Kademlia concurrency factor + bucketSize = 16 // Kademlia bucket size + maxReplacements = 10 // Size of per-bucket replacement list // We keep buckets for the upper 1/15 of distances because // it's very unlikely we'll ever encounter a node that's closer. @@ -56,83 +55,56 @@ const ( bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24 tableIPLimit, tableSubnet = 10, 24 - maxBondingPingPongs = 16 // Limit on the number of concurrent ping/pong interactions - maxFindnodeFailures = 5 // Nodes exceeding this limit are dropped - - refreshInterval = 30 * time.Minute - revalidateInterval = 10 * time.Second - copyNodesInterval = 30 * time.Second - seedMinTableTime = 5 * time.Minute - seedCount = 30 - seedMaxAge = 5 * 24 * time.Hour + maxFindnodeFailures = 5 // Nodes exceeding this limit are dropped + refreshInterval = 30 * time.Minute + revalidateInterval = 10 * time.Second + copyNodesInterval = 30 * time.Second + seedMinTableTime = 5 * time.Minute + seedCount = 30 + seedMaxAge = 5 * 24 * time.Hour ) type Table struct { mutex sync.Mutex // protects buckets, bucket content, nursery, rand buckets [nBuckets]*bucket // index of known nodes by distance - nursery []*Node // bootstrap nodes + nursery []*node // bootstrap nodes rand *mrand.Rand // source of randomness, periodically reseeded ips netutil.DistinctNetSet - db *nodeDB // database of known nodes - log log.Logger - - // loop channels + db *enode.DB // database of known nodes + net transport refreshReq chan chan struct{} initDone chan struct{} - closeReq chan struct{} - closed chan struct{} - - bondmu sync.Mutex - bonding map[NodeID]*bondproc - bondslots chan struct{} // limits total number of active bonding processes - nodeAddedHook func(*bucket, *Node) - nodeRemovedHook func(*bucket, *Node) + closeOnce sync.Once + closeReq chan struct{} + closed chan struct{} - net transport - self *Node // metadata of the local node -} - -type bondproc struct { - err error - n *Node - done chan struct{} + nodeAddedHook func(*node) // for testing } // transport is implemented by the UDP transport. // it is an interface so we can test without opening lots of UDP // sockets and without generating a private key. type transport interface { - ping(NodeID, *net.UDPAddr) error - waitping(NodeID) error - findnode(toid NodeID, addr *net.UDPAddr, target NodeID) ([]*Node, error) + self() *enode.Node + ping(enode.ID, *net.UDPAddr) error + findnode(toid enode.ID, addr *net.UDPAddr, target encPubkey) ([]*node, error) close() } // bucket contains nodes, ordered by their last activity. the entry // that was most recently active is the first element in entries. type bucket struct { - entries []*Node // live entries, sorted by time of last contact - replacements []*Node // recently seen nodes to be used if revalidation fails + entries []*node // live entries, sorted by time of last contact + replacements []*node // recently seen nodes to be used if revalidation fails ips netutil.DistinctNetSet - index int } -func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string, bootnodes []*Node) (*Table, error) { - // If no node database was given, use an in-memory one - db, err := newNodeDB(nodeDBPath, Version, ourID) - if err != nil { - return nil, err - } - +func newTable(t transport, db *enode.DB, bootnodes []*enode.Node) (*Table, error) { tab := &Table{ net: t, db: db, - log: log.Root(), - self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)), - bonding: make(map[NodeID]*bondproc), - bondslots: make(chan struct{}, maxBondingPingPongs), refreshReq: make(chan chan struct{}), initDone: make(chan struct{}), closeReq: make(chan struct{}), @@ -143,39 +115,20 @@ func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string if err := tab.setFallbackNodes(bootnodes); err != nil { return nil, err } - for i := 0; i < cap(tab.bondslots); i++ { - tab.bondslots <- struct{}{} - } for i := range tab.buckets { tab.buckets[i] = &bucket{ - index: i, - ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit}, + ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit}, } } tab.seedRand() - tab.loadSeedNodes(false) - // Start the background expiration goroutine after loading seeds so that the search for - // seed nodes also considers older nodes that would otherwise be removed by the - // expiration. - tab.db.ensureExpirer() + tab.loadSeedNodes() + go tab.loop() return tab, nil } -func newMeteredTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string, bootnodes []*Node) (*Table, error) { - tab, err := newTable(t, ourID, ourAddr, nodeDBPath, bootnodes) - if err != nil { - return nil, err - } - if metrics.Enabled() { - tab.nodeAddedHook = func(b *bucket, n *Node) { - bucketsCounter[b.index].Inc(1) - } - tab.nodeRemovedHook = func(b *bucket, n *Node) { - bucketsCounter[b.index].Dec(1) - } - } - return tab, nil +func (tab *Table) self() *enode.Node { + return tab.net.self() } func (tab *Table) seedRand() { @@ -187,16 +140,9 @@ func (tab *Table) seedRand() { tab.mutex.Unlock() } -// Self returns the local node. -// The returned node should not be modified by the caller. -func (tab *Table) Self() *Node { - return tab.self -} - -// ReadRandomNodes fills the given slice with random nodes from the -// table. It will not write the same node more than once. The nodes in -// the slice are copies and can be modified by the caller. -func (tab *Table) ReadRandomNodes(buf []*Node) (n int) { +// ReadRandomNodes fills the given slice with random nodes from the table. The results +// are guaranteed to be unique for a single invocation, no node will appear twice. +func (tab *Table) ReadRandomNodes(buf []*enode.Node) (n int) { if !tab.isInitDone() { return 0 } @@ -204,10 +150,10 @@ func (tab *Table) ReadRandomNodes(buf []*Node) (n int) { defer tab.mutex.Unlock() // Find all non-empty buckets and get a fresh slice of their entries. - var buckets [][]*Node - for _, b := range tab.buckets { + var buckets [][]*node + for _, b := range &tab.buckets { if len(b.entries) > 0 { - buckets = append(buckets, b.entries[:]) + buckets = append(buckets, b.entries) } } if len(buckets) == 0 { @@ -222,7 +168,7 @@ func (tab *Table) ReadRandomNodes(buf []*Node) (n int) { var i, j int for ; i < len(buf); i, j = i+1, (j+1)%len(buckets) { b := buckets[j] - buf[i] = &(*b[0]) + buf[i] = unwrapNode(b[0]) buckets[j] = b[1:] if len(b) == 1 { buckets = append(buckets[:j], buckets[j+1:]...) @@ -236,31 +182,26 @@ func (tab *Table) ReadRandomNodes(buf []*Node) (n int) { // Close terminates the network listener and flushes the node database. func (tab *Table) Close() { - select { - case <-tab.closed: - // already closed. - case tab.closeReq <- struct{}{}: - <-tab.closed // wait for refreshLoop to end. - } + tab.closeOnce.Do(func() { + if tab.net != nil { + tab.net.close() + } + // Wait for loop to end. + close(tab.closeReq) + <-tab.closed + }) } // setFallbackNodes sets the initial points of contact. These nodes // are used to connect to the network if the table is empty and there // are no known nodes in the database. -func (tab *Table) setFallbackNodes(nodes []*Node) error { +func (tab *Table) setFallbackNodes(nodes []*enode.Node) error { for _, n := range nodes { - if err := n.validateComplete(); err != nil { - return fmt.Errorf("bad bootstrap/fallback node %q (%v)", n, err) + if err := n.ValidateComplete(); err != nil { + return fmt.Errorf("bad bootstrap node %q: %v", n, err) } } - tab.nursery = make([]*Node, 0, len(nodes)) - for _, n := range nodes { - cpy := *n - // Recompute cpy.sha because the node might not have been - // created by NewNode or ParseNode. - cpy.sha = crypto.Keccak256Hash(n.ID[:]) - tab.nursery = append(tab.nursery, &cpy) - } + tab.nursery = wrapNodes(nodes) return nil } @@ -276,47 +217,48 @@ func (tab *Table) isInitDone() bool { // Resolve searches for a specific node with the given ID. // It returns nil if the node could not be found. -func (tab *Table) Resolve(targetID NodeID) *Node { +func (tab *Table) Resolve(n *enode.Node) *enode.Node { // If the node is present in the local table, no // network interaction is required. - hash := crypto.Keccak256Hash(targetID[:]) + hash := n.ID() tab.mutex.Lock() cl := tab.closest(hash, 1) tab.mutex.Unlock() - if len(cl.entries) > 0 && cl.entries[0].ID == targetID { - return cl.entries[0] + if len(cl.entries) > 0 && cl.entries[0].ID() == hash { + return unwrapNode(cl.entries[0]) } // Otherwise, do a network lookup. - result := tab.Lookup(targetID) + result := tab.lookup(encodePubkey(n.Pubkey()), true) for _, n := range result { - if n.ID == targetID { - return n + if n.ID() == hash { + return unwrapNode(n) } } return nil } -// Lookup performs a network search for nodes close -// to the given target. It approaches the target by querying -// nodes that are closer to it on each iteration. -// The given target does not need to be an actual node -// identifier. -func (tab *Table) Lookup(targetID NodeID) []*Node { - return tab.lookup(targetID, true) +// LookupRandom finds random nodes in the network. +func (tab *Table) LookupRandom() []*enode.Node { + var target encPubkey + crand.Read(target[:]) + return unwrapNodes(tab.lookup(target, true)) } -func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node { +// lookup performs a network search for nodes close to the given target. It approaches the +// target by querying nodes that are closer to it on each iteration. The given target does +// not need to be an actual node identifier. +func (tab *Table) lookup(targetKey encPubkey, refreshIfEmpty bool) []*node { var ( - target = crypto.Keccak256Hash(targetID[:]) - asked = make(map[NodeID]bool) - seen = make(map[NodeID]bool) - reply = make(chan []*Node, alpha) + target = enode.ID(crypto.Keccak256Hash(targetKey[:])) + asked = make(map[enode.ID]bool) + seen = make(map[enode.ID]bool) + reply = make(chan []*node, alpha) pendingQueries = 0 result *nodesByDistance ) // don't query further if we hit ourself. // unlikely to happen often in practice. - asked[tab.self.ID] = true + asked[tab.self().ID()] = true for { tab.mutex.Lock() @@ -338,48 +280,64 @@ func (tab *Table) lookup(targetID NodeID, refreshIfEmpty bool) []*Node { // ask the alpha closest nodes that we haven't asked yet for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ { n := result.entries[i] - if !asked[n.ID] { - asked[n.ID] = true + if !asked[n.ID()] { + asked[n.ID()] = true pendingQueries++ - go func() { - // Find potential neighbors to bond with - r, err := tab.net.findnode(n.ID, n.addr(), targetID) - if err != nil { - // Bump the failure counter to detect and evacuate non-bonded entries - fails := tab.db.findFails(n.ID) + 1 - tab.db.updateFindFails(n.ID, fails) - tab.log.Trace("Bumping findnode failure counter", "id", n.ID, "failcount", fails) - - if fails >= maxFindnodeFailures { - tab.log.Trace("Too many findnode failures, dropping", "id", n.ID, "failcount", fails) - tab.delete(n) - } - } - reply <- tab.bondall(r) - }() + go tab.findnode(n, targetKey, reply) } } if pendingQueries == 0 { // we have asked all closest nodes, stop the search break } - // wait for the next reply - for _, n := range <-reply { - if n != nil && !seen[n.ID] { - seen[n.ID] = true - result.push(n, bucketSize) + select { + case nodes := <-reply: + for _, n := range nodes { + if n != nil && !seen[n.ID()] { + seen[n.ID()] = true + result.push(n, bucketSize) + } } + case <-tab.closeReq: + return nil // shutdown, no need to continue. } pendingQueries-- } return result.entries } +func (tab *Table) findnode(n *node, targetKey encPubkey, reply chan<- []*node) { + fails := tab.db.FindFails(n.ID(), n.IP()) + r, err := tab.net.findnode(n.ID(), n.addr(), targetKey) + if err == errClosed { + // Avoid recording failures on shutdown. + reply <- nil + return + } else if err != nil || len(r) == 0 { + fails++ + tab.db.UpdateFindFails(n.ID(), n.IP(), fails) + log.Trace("Findnode failed", "id", n.ID(), "failcount", fails, "err", err) + if fails >= maxFindnodeFailures { + log.Trace("Too many findnode failures, dropping", "id", n.ID(), "failcount", fails) + tab.delete(n) + } + } else if fails > 0 { + tab.db.UpdateFindFails(n.ID(), n.IP(), fails-1) + } + + // Grab as many nodes as possible. Some of them might not be alive anymore, but we'll + // just remove those again during revalidation. + for _, n := range r { + tab.add(n) + } + reply <- r +} + func (tab *Table) refresh() <-chan struct{} { done := make(chan struct{}) select { case tab.refreshReq <- done: - case <-tab.closed: + case <-tab.closeReq: close(done) } return done @@ -391,8 +349,8 @@ func (tab *Table) loop() { revalidate = time.NewTimer(tab.nextRevalidateTime()) refresh = time.NewTicker(refreshInterval) copyNodes = time.NewTicker(copyNodesInterval) - revalidateDone = make(chan struct{}) refreshDone = make(chan struct{}) // where doRefresh reports completion + revalidateDone chan struct{} // where doRevalidate reports completion waiting = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs ) defer refresh.Stop() @@ -423,26 +381,27 @@ loop: } waiting, refreshDone = nil, nil case <-revalidate.C: + revalidateDone = make(chan struct{}) go tab.doRevalidate(revalidateDone) case <-revalidateDone: revalidate.Reset(tab.nextRevalidateTime()) + revalidateDone = nil case <-copyNodes.C: - go tab.copyBondedNodes() + go tab.copyLiveNodes() case <-tab.closeReq: break loop } } - if tab.net != nil { - tab.net.close() - } if refreshDone != nil { <-refreshDone } for _, ch := range waiting { close(ch) } - tab.db.close() + if revalidateDone != nil { + <-revalidateDone + } close(tab.closed) } @@ -455,10 +414,14 @@ func (tab *Table) doRefresh(done chan struct{}) { // Load nodes from the database and insert // them. This should yield a few previously seen nodes that are // (hopefully) still alive. - tab.loadSeedNodes(true) + tab.loadSeedNodes() // Run self lookup to discover new neighbor nodes. - tab.lookup(tab.self.ID, false) + // We can only do this if we have a secp256k1 identity. + var key ecdsa.PublicKey + if err := tab.self().Load((*enode.Secp256k1)(&key)); err == nil { + tab.lookup(encodePubkey(&key), false) + } // The Kademlia paper specifies that the bucket refresh should // perform a lookup in the least recently used bucket. We cannot @@ -467,23 +430,20 @@ func (tab *Table) doRefresh(done chan struct{}) { // sha3 preimage that falls into a chosen bucket. // We perform a few lookups with a random target instead. for i := 0; i < 3; i++ { - var target NodeID + var target encPubkey crand.Read(target[:]) tab.lookup(target, false) } } -func (tab *Table) loadSeedNodes(bond bool) { - seeds := tab.db.querySeeds(seedCount, seedMaxAge) +func (tab *Table) loadSeedNodes() { + seeds := wrapNodes(tab.db.QuerySeeds(seedCount, seedMaxAge)) seeds = append(seeds, tab.nursery...) - if bond { - seeds = tab.bondall(seeds) - } for i := range seeds { seed := seeds[i] - if tab.log.Enabled(context.Background(), log.LevelTrace) { - age := time.Since(tab.db.bondTime(seed.ID)) - tab.log.Debug("Found seed node in database", "id", seed.ID, "addr", seed.addr(), "age", age) + if log.Enabled(log.LevelTrace) { + age := time.Since(tab.db.LastPongReceived(seed.ID(), seed.IP())) + log.Trace("Found seed node in database", "id", seed.ID(), "addr", seed.addr(), "age", age) } tab.add(seed) } @@ -501,28 +461,29 @@ func (tab *Table) doRevalidate(done chan<- struct{}) { } // Ping the selected node and wait for a pong. - err := tab.ping(last.ID, last.addr()) + err := tab.net.ping(last.ID(), last.addr()) tab.mutex.Lock() defer tab.mutex.Unlock() b := tab.buckets[bi] if err == nil { // The node responded, move it to the front. - tab.log.Debug("Revalidated node", "b", bi, "id", last.ID) + last.livenessChecks++ + log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks) b.bump(last) return } // No reply received, pick a replacement or delete the node if there aren't // any replacements. if r := tab.replace(b, last); r != nil { - tab.log.Debug("Replaced dead node", "b", bi, "id", last.ID, "ip", last.IP, "r", r.ID, "rip", r.IP) + log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP()) } else { - tab.log.Debug("Removed dead node", "b", bi, "id", last.ID, "ip", last.IP) + log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks) } } // nodeToRevalidate returns the last node in a random, non-empty bucket. -func (tab *Table) nodeToRevalidate() (n *Node, bi int) { +func (tab *Table) nodeToRevalidate() (n *node, bi int) { tab.mutex.Lock() defer tab.mutex.Unlock() @@ -543,17 +504,17 @@ func (tab *Table) nextRevalidateTime() time.Duration { return time.Duration(tab.rand.Int63n(int64(revalidateInterval))) } -// copyBondedNodes adds nodes from the table to the database if they have been in the table +// copyLiveNodes adds nodes from the table to the database if they have been in the table // longer then minTableTime. -func (tab *Table) copyBondedNodes() { +func (tab *Table) copyLiveNodes() { tab.mutex.Lock() defer tab.mutex.Unlock() now := time.Now() - for _, b := range tab.buckets { + for _, b := range &tab.buckets { for _, n := range b.entries { - if now.Sub(n.addedAt) >= seedMinTableTime { - tab.db.updateNode(n) + if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime { + tab.db.UpdateNode(unwrapNode(n)) } } } @@ -561,190 +522,75 @@ func (tab *Table) copyBondedNodes() { // closest returns the n nodes in the table that are closest to the // given id. The caller must hold tab.mutex. -func (tab *Table) closest(target common.Hash, nresults int) *nodesByDistance { +func (tab *Table) closest(target enode.ID, nresults int) *nodesByDistance { // This is a very wasteful way to find the closest nodes but // obviously correct. I believe that tree-based buckets would make // this easier to implement efficiently. close := &nodesByDistance{target: target} - for _, b := range tab.buckets { + for _, b := range &tab.buckets { for _, n := range b.entries { - close.push(n, nresults) + if n.livenessChecks > 0 { + close.push(n, nresults) + } } } return close } func (tab *Table) len() (n int) { - for _, b := range tab.buckets { + for _, b := range &tab.buckets { n += len(b.entries) } return n } -// bondall bonds with all given nodes concurrently and returns -// those nodes for which bonding has probably succeeded. -func (tab *Table) bondall(nodes []*Node) (result []*Node) { - rc := make(chan *Node, len(nodes)) - for i := range nodes { - go func(n *Node) { - nn, _ := tab.bond(false, n.ID, n.addr(), n.TCP) - rc <- nn - }(nodes[i]) - } - for range nodes { - if n := <-rc; n != nil { - result = append(result, n) - } - } - return result -} - -// bond ensures the local node has a bond with the given remote node. -// It also attempts to insert the node into the table if bonding succeeds. -// The caller must not hold tab.mutex. -// -// A bond is must be established before sending findnode requests. -// Both sides must have completed a ping/pong exchange for a bond to -// exist. The total number of active bonding processes is limited in -// order to restrain network use. -// -// bond is meant to operate idempotently in that bonding with a remote -// node which still remembers a previously established bond will work. -// The remote node will simply not send a ping back, causing waitping -// to time out. -// -// If pinged is true, the remote node has just pinged us and one half -// of the process can be skipped. -func (tab *Table) bond(pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) (*Node, error) { - if id == tab.self.ID { - return nil, errors.New("is self") - } - if pinged && !tab.isInitDone() { - return nil, errors.New("still initializing") - } - // Start bonding if we haven't seen this node for a while or if it failed findnode too often. - node, fails := tab.db.node(id), tab.db.findFails(id) - age := time.Since(tab.db.bondTime(id)) - var result error - if fails > 0 || age > nodeDBNodeExpiration { - tab.log.Trace("Starting bonding ping/pong", "id", id, "known", node != nil, "failcount", fails, "age", age) - - tab.bondmu.Lock() - w := tab.bonding[id] - if w != nil { - // Wait for an existing bonding process to complete. - tab.bondmu.Unlock() - <-w.done - } else { - // Register a new bonding process. - w = &bondproc{done: make(chan struct{})} - tab.bonding[id] = w - tab.bondmu.Unlock() - // Do the ping/pong. The result goes into w. - tab.pingpong(w, pinged, id, addr, tcpPort) - // Unregister the process after it's done. - tab.bondmu.Lock() - delete(tab.bonding, id) - tab.bondmu.Unlock() - } - // Retrieve the bonding results - result = w.err - if result == nil { - node = w.n - } - } - // Add the node to the table even if the bonding ping/pong - // fails. It will be relaced quickly if it continues to be - // unresponsive. - if node != nil { - tab.add(node) - tab.db.updateFindFails(id, 0) - } - return node, result -} - -func (tab *Table) pingpong(w *bondproc, pinged bool, id NodeID, addr *net.UDPAddr, tcpPort uint16) { - // Request a bonding slot to limit network usage - <-tab.bondslots - defer func() { tab.bondslots <- struct{}{} }() - - // Ping the remote side and wait for a pong. - if w.err = tab.ping(id, addr); w.err != nil { - close(w.done) - return - } - if !pinged { - // Give the remote node a chance to ping us before we start - // sending findnode requests. If they still remember us, - // waitping will simply time out. - tab.net.waitping(id) - } - // Bonding succeeded, update the node database. - w.n = NewNode(id, addr.IP, uint16(addr.Port), tcpPort) - close(w.done) -} - -// ping a remote endpoint and wait for a reply, also updating the node -// database accordingly. -func (tab *Table) ping(id NodeID, addr *net.UDPAddr) error { - tab.db.updateLastPing(id, time.Now()) - if err := tab.net.ping(id, addr); err != nil { - return err - } - tab.db.updateBondTime(id, time.Now()) - return nil -} - // bucket returns the bucket for the given node ID hash. -func (tab *Table) bucket(sha common.Hash) *bucket { - d := logdist(tab.self.sha, sha) +func (tab *Table) bucket(id enode.ID) *bucket { + d := enode.LogDist(tab.self().ID(), id) if d <= bucketMinDistance { return tab.buckets[0] } return tab.buckets[d-bucketMinDistance-1] } -// add attempts to add the given node its corresponding bucket. If the -// bucket has space available, adding the node succeeds immediately. -// Otherwise, the node is added if the least recently active node in -// the bucket does not respond to a ping packet. +// add attempts to add the given node to its corresponding bucket. If the bucket has space +// available, adding the node succeeds immediately. Otherwise, the node is added if the +// least recently active node in the bucket does not respond to a ping packet. // // The caller must not hold tab.mutex. -func (tab *Table) add(new *Node) { +func (tab *Table) add(n *node) { + if n.ID() == tab.self().ID() { + return + } + tab.mutex.Lock() defer tab.mutex.Unlock() - - b := tab.bucket(new.sha) - if !tab.bumpOrAdd(b, new) { + b := tab.bucket(n.ID()) + if !tab.bumpOrAdd(b, n) { // Node is not in table. Add it to the replacement list. - tab.addReplacement(b, new) + tab.addReplacement(b, n) } } -// stuff adds nodes the table to the end of their corresponding bucket -// if the bucket is not full. The caller must not hold tab.mutex. -func (tab *Table) stuff(nodes []*Node) { - tab.mutex.Lock() - defer tab.mutex.Unlock() - - for _, n := range nodes { - if n.ID == tab.self.ID { - continue // don't add self - } - b := tab.bucket(n.sha) - if len(b.entries) < bucketSize { - tab.bumpOrAdd(b, n) - } +// addThroughPing adds the given node to the table. Compared to plain +// 'add' there is an additional safety measure: if the table is still +// initializing the node is not added. This prevents an attack where the +// table could be filled by just sending ping repeatedly. +// +// The caller must not hold tab.mutex. +func (tab *Table) addThroughPing(n *node) { + if !tab.isInitDone() { + return } + tab.add(n) } -// delete removes an entry from the node table (used to evacuate -// failed/non-bonded discovery peers). -func (tab *Table) delete(node *Node) { +// delete removes an entry from the node table. It is used to evacuate dead nodes. +func (tab *Table) delete(node *node) { tab.mutex.Lock() defer tab.mutex.Unlock() - tab.deleteInBucket(tab.bucket(node.sha), node) + tab.deleteInBucket(tab.bucket(node.ID()), node) } func (tab *Table) addIP(b *bucket, ip net.IP) bool { @@ -752,11 +598,11 @@ func (tab *Table) addIP(b *bucket, ip net.IP) bool { return true } if !tab.ips.Add(ip) { - tab.log.Debug("IP exceeds table limit", "ip", ip) + log.Debug("IP exceeds table limit", "ip", ip) return false } if !b.ips.Add(ip) { - tab.log.Debug("IP exceeds bucket limit", "ip", ip) + log.Debug("IP exceeds bucket limit", "ip", ip) tab.ips.Remove(ip) return false } @@ -771,27 +617,27 @@ func (tab *Table) removeIP(b *bucket, ip net.IP) { b.ips.Remove(ip) } -func (tab *Table) addReplacement(b *bucket, n *Node) { +func (tab *Table) addReplacement(b *bucket, n *node) { for _, e := range b.replacements { - if e.ID == n.ID { + if e.ID() == n.ID() { return // already in list } } - if !tab.addIP(b, n.IP) { + if !tab.addIP(b, n.IP()) { return } - var removed *Node + var removed *node b.replacements, removed = pushNode(b.replacements, n, maxReplacements) if removed != nil { - tab.removeIP(b, removed.IP) + tab.removeIP(b, removed.IP()) } } // replace removes n from the replacement list and replaces 'last' with it if it is the // last entry in the bucket. If 'last' isn't the last entry, it has either been replaced // with someone else or became active. -func (tab *Table) replace(b *bucket, last *Node) *Node { - if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID != last.ID { +func (tab *Table) replace(b *bucket, last *node) *node { + if len(b.entries) == 0 || b.entries[len(b.entries)-1].ID() != last.ID() { // Entry has moved, don't replace it. return nil } @@ -803,15 +649,15 @@ func (tab *Table) replace(b *bucket, last *Node) *Node { r := b.replacements[tab.rand.Intn(len(b.replacements))] b.replacements = deleteNode(b.replacements, r) b.entries[len(b.entries)-1] = r - tab.removeIP(b, last.IP) + tab.removeIP(b, last.IP()) return r } // bump moves the given node to the front of the bucket entry list // if it is contained in that list. -func (b *bucket) bump(n *Node) bool { +func (b *bucket) bump(n *node) bool { for i := range b.entries { - if b.entries[i].ID == n.ID { + if b.entries[i].ID() == n.ID() { // move it to the front copy(b.entries[1:], b.entries[:i]) b.entries[0] = n @@ -823,46 +669,29 @@ func (b *bucket) bump(n *Node) bool { // bumpOrAdd moves n to the front of the bucket entry list or adds it if the list isn't // full. The return value is true if n is in the bucket. -func (tab *Table) bumpOrAdd(b *bucket, n *Node) bool { +func (tab *Table) bumpOrAdd(b *bucket, n *node) bool { if b.bump(n) { return true } - if len(b.entries) >= bucketSize || !tab.addIP(b, n.IP) { + if len(b.entries) >= bucketSize || !tab.addIP(b, n.IP()) { return false } b.entries, _ = pushNode(b.entries, n, bucketSize) b.replacements = deleteNode(b.replacements, n) n.addedAt = time.Now() if tab.nodeAddedHook != nil { - tab.nodeAddedHook(b, n) + tab.nodeAddedHook(n) } return true } -func (tab *Table) deleteInBucket(b *bucket, n *Node) { - // Check if the node is actually in the bucket so the removed hook - // isn't called multiple times for the same node. - if !contains(b.entries, n.ID) { - return - } +func (tab *Table) deleteInBucket(b *bucket, n *node) { b.entries = deleteNode(b.entries, n) - tab.removeIP(b, n.IP) - if tab.nodeRemovedHook != nil { - tab.nodeRemovedHook(b, n) - } -} - -func contains(ns []*Node, id NodeID) bool { - for _, n := range ns { - if n.ID == id { - return true - } - } - return false + tab.removeIP(b, n.IP()) } // pushNode adds n to the front of list, keeping at most max items. -func pushNode(list []*Node, n *Node, max int) ([]*Node, *Node) { +func pushNode(list []*node, n *node, max int) ([]*node, *node) { if len(list) < max { list = append(list, nil) } @@ -873,9 +702,9 @@ func pushNode(list []*Node, n *Node, max int) ([]*Node, *Node) { } // deleteNode removes n from list. -func deleteNode(list []*Node, n *Node) []*Node { +func deleteNode(list []*node, n *node) []*node { for i := range list { - if list[i].ID == n.ID { + if list[i].ID() == n.ID() { return append(list[:i], list[i+1:]...) } } @@ -885,14 +714,14 @@ func deleteNode(list []*Node, n *Node) []*Node { // nodesByDistance is a list of nodes, ordered by // distance to target. type nodesByDistance struct { - entries []*Node - target common.Hash + entries []*node + target enode.ID } // push adds the given node to the list, keeping the total size below maxElems. -func (h *nodesByDistance) push(n *Node, maxElems int) { +func (h *nodesByDistance) push(n *node, maxElems int) { ix := sort.Search(len(h.entries), func(i int) bool { - return distcmp(h.target, h.entries[i].sha, n.sha) > 0 + return enode.DistCmp(h.target, h.entries[i].ID(), n.ID()) > 0 }) if len(h.entries) < maxElems { h.entries = append(h.entries, n) diff --git a/p2p/discover/table_test.go b/p2p/discover/table_test.go index 1902ef27236a..9e6af6fd3ec3 100644 --- a/p2p/discover/table_test.go +++ b/p2p/discover/table_test.go @@ -20,16 +20,15 @@ import ( "crypto/ecdsa" "fmt" "math/rand" - "sync" - "net" "reflect" "testing" "testing/quick" "time" - "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" ) func TestTable_pingReplace(t *testing.T) { @@ -49,30 +48,27 @@ func TestTable_pingReplace(t *testing.T) { func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding bool) { transport := newPingRecorder() - tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "", nil) + tab, db := newTestTable(transport) + defer db.Close() defer tab.Close() - // Wait for init so bond is accepted. <-tab.initDone - // fill up the sender's bucket. - pingSender := NewNode(MustHexID("a502af0f59b2aab7746995408c79e9ca312d2793cc997e44fc55eda62f0150bbb8c59a6f9269ba3a081518b62699ee807c7c19c20125ddfccca872608af9e370"), net.IP{}, 99, 99) + // Fill up the sender's bucket. + pingKey, _ := crypto.HexToECDSA("45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8") + pingSender := wrapNode(enode.NewV4(&pingKey.PublicKey, net.IP{}, 99, 99)) last := fillBucket(tab, pingSender) - // this call to bond should replace the last node - // in its bucket if the node is not responding. - transport.dead[last.ID] = !lastInBucketIsResponding - transport.dead[pingSender.ID] = !newNodeIsResponding - tab.bond(true, pingSender.ID, &net.UDPAddr{}, 0) + // Add the sender as if it just pinged us. Revalidate should replace the last node in + // its bucket if it is unresponsive. Revalidate again to ensure that + transport.dead[last.ID()] = !lastInBucketIsResponding + transport.dead[pingSender.ID()] = !newNodeIsResponding + tab.add(pingSender) + tab.doRevalidate(make(chan struct{}, 1)) tab.doRevalidate(make(chan struct{}, 1)) - // first ping goes to sender (bonding pingback) - if !transport.pinged[pingSender.ID] { - t.Error("table did not ping back sender") - } - if !transport.pinged[last.ID] { - // second ping goes to oldest node in bucket - // to see whether it is still alive. + if !transport.pinged[last.ID()] { + // Oldest node in bucket is pinged to see whether it is still alive. t.Error("table did not ping last node in bucket") } @@ -82,14 +78,14 @@ func testPingReplace(t *testing.T, newNodeIsResponding, lastInBucketIsResponding if !lastInBucketIsResponding && !newNodeIsResponding { wantSize-- } - if l := len(tab.bucket(pingSender.sha).entries); l != wantSize { + if l := len(tab.bucket(pingSender.ID()).entries); l != wantSize { t.Errorf("wrong bucket size after bond: got %d, want %d", l, wantSize) } - if found := contains(tab.bucket(pingSender.sha).entries, last.ID); found != lastInBucketIsResponding { + if found := contains(tab.bucket(pingSender.ID()).entries, last.ID()); found != lastInBucketIsResponding { t.Errorf("last entry found: %t, want: %t", found, lastInBucketIsResponding) } wantNewEntry := newNodeIsResponding && !lastInBucketIsResponding - if found := contains(tab.bucket(pingSender.sha).entries, pingSender.ID); found != wantNewEntry { + if found := contains(tab.bucket(pingSender.ID()).entries, pingSender.ID()); found != wantNewEntry { t.Errorf("new entry found: %t, want: %t", found, wantNewEntry) } } @@ -102,9 +98,9 @@ func TestBucket_bumpNoDuplicates(t *testing.T) { Values: func(args []reflect.Value, rand *rand.Rand) { // generate a random list of nodes. this will be the content of the bucket. n := rand.Intn(bucketSize-1) + 1 - nodes := make([]*Node, n) + nodes := make([]*node, n) for i := range nodes { - nodes[i] = nodeAtDistance(common.Hash{}, 200) + nodes[i] = nodeAtDistance(enode.ID{}, 200, intIP(200)) } args[0] = reflect.ValueOf(nodes) // generate random bump positions. @@ -116,8 +112,8 @@ func TestBucket_bumpNoDuplicates(t *testing.T) { }, } - prop := func(nodes []*Node, bumps []int) (ok bool) { - b := &bucket{entries: make([]*Node, len(nodes))} + prop := func(nodes []*node, bumps []int) (ok bool) { + b := &bucket{entries: make([]*node, len(nodes))} copy(b.entries, nodes) for i, pos := range bumps { b.bump(b.entries[pos]) @@ -139,12 +135,12 @@ func TestBucket_bumpNoDuplicates(t *testing.T) { // This checks that the table-wide IP limit is applied correctly. func TestTable_IPLimit(t *testing.T) { transport := newPingRecorder() - tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "", nil) + tab, db := newTestTable(transport) + defer db.Close() defer tab.Close() for i := 0; i < tableIPLimit+1; i++ { - n := nodeAtDistance(tab.self.sha, i) - n.IP = net.IP{172, 0, 1, byte(i)} + n := nodeAtDistance(tab.self().ID(), i, net.IP{172, 0, 1, byte(i)}) tab.add(n) } if tab.len() > tableIPLimit { @@ -152,16 +148,16 @@ func TestTable_IPLimit(t *testing.T) { } } -// This checks that the table-wide IP limit is applied correctly. +// This checks that the per-bucket IP limit is applied correctly. func TestTable_BucketIPLimit(t *testing.T) { transport := newPingRecorder() - tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "", nil) + tab, db := newTestTable(transport) + defer db.Close() defer tab.Close() d := 3 for i := 0; i < bucketIPLimit+1; i++ { - n := nodeAtDistance(tab.self.sha, d) - n.IP = net.IP{172, 0, 1, byte(i)} + n := nodeAtDistance(tab.self().ID(), d, net.IP{172, 0, 1, byte(i)}) tab.add(n) } if tab.len() > bucketIPLimit { @@ -169,70 +165,18 @@ func TestTable_BucketIPLimit(t *testing.T) { } } -// fillBucket inserts nodes into the given bucket until -// it is full. The node's IDs dont correspond to their -// hashes. -func fillBucket(tab *Table, n *Node) (last *Node) { - ld := logdist(tab.self.sha, n.sha) - b := tab.bucket(n.sha) - for len(b.entries) < bucketSize { - b.entries = append(b.entries, nodeAtDistance(tab.self.sha, ld)) - } - return b.entries[bucketSize-1] -} - -// nodeAtDistance creates a node for which logdist(base, n.sha) == ld. -// The node's ID does not correspond to n.sha. -func nodeAtDistance(base common.Hash, ld int) (n *Node) { - n = new(Node) - n.sha = hashAtDistance(base, ld) - n.IP = net.IP{byte(ld), 0, 2, byte(ld)} - copy(n.ID[:], n.sha[:]) // ensure the node still has a unique ID - return n -} - -type pingRecorder struct { - mu sync.Mutex - dead, pinged map[NodeID]bool -} - -func newPingRecorder() *pingRecorder { - return &pingRecorder{ - dead: make(map[NodeID]bool), - pinged: make(map[NodeID]bool), - } -} - -func (t *pingRecorder) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node, error) { - return nil, nil -} -func (t *pingRecorder) close() {} -func (t *pingRecorder) waitping(from NodeID) error { - return nil // remote always pings -} -func (t *pingRecorder) ping(toid NodeID, toaddr *net.UDPAddr) error { - t.mu.Lock() - defer t.mu.Unlock() - - t.pinged[toid] = true - if t.dead[toid] { - return errTimeout - } else { - return nil - } -} - func TestTable_closest(t *testing.T) { t.Parallel() test := func(test *closeTest) bool { // for any node table, Target and N transport := newPingRecorder() - tab, _ := newTable(transport, test.Self, &net.UDPAddr{}, "", nil) + tab, db := newTestTable(transport) + defer db.Close() defer tab.Close() - tab.stuff(test.All) + fillTable(tab, test.All) - // check that doClosest(Target, N) returns nodes + // check that closest(Target, N) returns nodes result := tab.closest(test.Target, test.N).entries if hasDuplicates(result) { t.Errorf("result contains duplicates") @@ -258,15 +202,15 @@ func TestTable_closest(t *testing.T) { // check that the result nodes have minimum distance to target. for _, b := range tab.buckets { for _, n := range b.entries { - if contains(result, n.ID) { + if contains(result, n.ID()) { continue // don't run the check below for nodes in result } - farthestResult := result[len(result)-1].sha - if distcmp(test.Target, n.sha, farthestResult) < 0 { + farthestResult := result[len(result)-1].ID() + if enode.DistCmp(test.Target, n.ID(), farthestResult) < 0 { t.Errorf("table contains node that is closer to target but it's not in result") t.Logf(" Target: %v", test.Target) t.Logf(" Farthest Result: %v", farthestResult) - t.Logf(" ID: %v", n.ID) + t.Logf(" ID: %v", n.ID()) return false } } @@ -283,25 +227,26 @@ func TestTable_ReadRandomNodesGetAll(t *testing.T) { MaxCount: 200, Rand: rand.New(rand.NewSource(time.Now().Unix())), Values: func(args []reflect.Value, rand *rand.Rand) { - args[0] = reflect.ValueOf(make([]*Node, rand.Intn(1000))) + args[0] = reflect.ValueOf(make([]*enode.Node, rand.Intn(1000))) }, } - test := func(buf []*Node) bool { + test := func(buf []*enode.Node) bool { transport := newPingRecorder() - tab, _ := newTable(transport, NodeID{}, &net.UDPAddr{}, "", nil) + tab, db := newTestTable(transport) + defer db.Close() defer tab.Close() <-tab.initDone for i := 0; i < len(buf); i++ { ld := cfg.Rand.Intn(len(tab.buckets)) - tab.stuff([]*Node{nodeAtDistance(tab.self.sha, ld)}) + fillTable(tab, []*node{nodeAtDistance(tab.self().ID(), ld, intIP(ld))}) } gotN := tab.ReadRandomNodes(buf) if gotN != tab.len() { t.Errorf("wrong number of nodes, got %d, want %d", gotN, tab.len()) return false } - if hasDuplicates(buf[:gotN]) { + if hasDuplicates(wrapNodes(buf[:gotN])) { t.Errorf("result contains duplicates") return false } @@ -313,287 +258,297 @@ func TestTable_ReadRandomNodesGetAll(t *testing.T) { } type closeTest struct { - Self NodeID - Target common.Hash - All []*Node + Self enode.ID + Target enode.ID + All []*node N int } func (*closeTest) Generate(rand *rand.Rand, size int) reflect.Value { t := &closeTest{ - Self: gen(NodeID{}, rand).(NodeID), - Target: gen(common.Hash{}, rand).(common.Hash), + Self: gen(enode.ID{}, rand).(enode.ID), + Target: gen(enode.ID{}, rand).(enode.ID), N: rand.Intn(bucketSize), } - for _, id := range gen([]NodeID{}, rand).([]NodeID) { - t.All = append(t.All, &Node{ID: id}) + for _, id := range gen([]enode.ID{}, rand).([]enode.ID) { + r := new(enr.Record) + r.Set(enr.IP(genIP(rand))) + n := wrapNode(enode.SignNull(r, id)) + n.livenessChecks = 1 + t.All = append(t.All, n) } return reflect.ValueOf(t) } -//func TestTable_Lookup(t *testing.T) { -// bucketSizeTest := 16 -// self := nodeAtDistance(common.Hash{}, 0) -// tab, _ := newTable(lookupTestnet, self.ID, &net.UDPAddr{}, "", nil) -// defer tab.Close() -// -// // lookup on empty table returns no nodes -// if results := tab.Lookup(lookupTestnet.target); len(results) > 0 { -// t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results) -// } -// // seed table with initial node (otherwise lookup will terminate immediately) -// seed := NewNode(lookupTestnet.dists[256][0], net.IP{}, 256, 0) -// tab.stuff([]*Node{seed}) -// -// results := tab.Lookup(lookupTestnet.target) -// t.Logf("results:") -// for _, e := range results { -// t.Logf(" ld=%d, %x", logdist(lookupTestnet.targetSha, e.sha), e.sha[:]) -// } -// if len(results) != bucketSizeTest { -// t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSizeTest) -// } -// if hasDuplicates(results) { -// t.Errorf("result set contains duplicate entries") -// } -// if !sortedByDistanceTo(lookupTestnet.targetSha, results) { -// t.Errorf("result set not sorted by distance to target") -// } -// // TODO: check result nodes are actually closest -//} +func TestTable_Lookup(t *testing.T) { + tab, db := newTestTable(lookupTestnet) + defer db.Close() + defer tab.Close() + + // lookup on empty table returns no nodes + if results := tab.lookup(lookupTestnet.target, false); len(results) > 0 { + t.Fatalf("lookup on empty table returned %d results: %#v", len(results), results) + } + // seed table with initial node (otherwise lookup will terminate immediately) + seedKey, _ := decodePubkey(lookupTestnet.dists[256][0]) + seed := wrapNode(enode.NewV4(seedKey, net.IP{127, 0, 0, 1}, 0, 256)) + seed.livenessChecks = 1 + fillTable(tab, []*node{seed}) + + results := tab.lookup(lookupTestnet.target, true) + t.Logf("results:") + for _, e := range results { + t.Logf(" ld=%d, %x", enode.LogDist(lookupTestnet.targetSha, e.ID()), e.ID().Bytes()) + } + if len(results) != bucketSize { + t.Errorf("wrong number of results: got %d, want %d", len(results), bucketSize) + } + if hasDuplicates(results) { + t.Errorf("result set contains duplicate entries") + } + if !sortedByDistanceTo(lookupTestnet.targetSha, results) { + t.Errorf("result set not sorted by distance to target") + } + // TODO: check result nodes are actually closest +} // This is the test network for the Lookup test. // The nodes were obtained by running testnet.mine with a random NodeID as target. var lookupTestnet = &preminedTestnet{ - target: MustHexID("166aea4f556532c6d34e8b740e5d314af7e9ac0ca79833bd751d6b665f12dfd38ec563c363b32f02aef4a80b44fd3def94612d497b99cb5f17fd24de454927ec"), - targetSha: common.Hash{0x5c, 0x94, 0x4e, 0xe5, 0x1c, 0x5a, 0xe9, 0xf7, 0x2a, 0x95, 0xec, 0xcb, 0x8a, 0xed, 0x3, 0x74, 0xee, 0xcb, 0x51, 0x19, 0xd7, 0x20, 0xcb, 0xea, 0x68, 0x13, 0xe8, 0xe0, 0xd6, 0xad, 0x92, 0x61}, - dists: [257][]NodeID{ + target: hexEncPubkey("166aea4f556532c6d34e8b740e5d314af7e9ac0ca79833bd751d6b665f12dfd38ec563c363b32f02aef4a80b44fd3def94612d497b99cb5f17fd24de454927ec"), + targetSha: enode.HexID("5c944ee51c5ae9f72a95eccb8aed0374eecb5119d720cbea6813e8e0d6ad9261"), + dists: [257][]encPubkey{ 240: { - MustHexID("2001ad5e3e80c71b952161bc0186731cf5ffe942d24a79230a0555802296238e57ea7a32f5b6f18564eadc1c65389448481f8c9338df0a3dbd18f708cbc2cbcb"), - MustHexID("6ba3f4f57d084b6bf94cc4555b8c657e4a8ac7b7baf23c6874efc21dd1e4f56b7eb2721e07f5242d2f1d8381fc8cae535e860197c69236798ba1ad231b105794"), + hexEncPubkey("2001ad5e3e80c71b952161bc0186731cf5ffe942d24a79230a0555802296238e57ea7a32f5b6f18564eadc1c65389448481f8c9338df0a3dbd18f708cbc2cbcb"), + hexEncPubkey("6ba3f4f57d084b6bf94cc4555b8c657e4a8ac7b7baf23c6874efc21dd1e4f56b7eb2721e07f5242d2f1d8381fc8cae535e860197c69236798ba1ad231b105794"), }, 244: { - MustHexID("696ba1f0a9d55c59246f776600542a9e6432490f0cd78f8bb55a196918df2081a9b521c3c3ba48e465a75c10768807717f8f689b0b4adce00e1c75737552a178"), + hexEncPubkey("696ba1f0a9d55c59246f776600542a9e6432490f0cd78f8bb55a196918df2081a9b521c3c3ba48e465a75c10768807717f8f689b0b4adce00e1c75737552a178"), }, 246: { - MustHexID("d6d32178bdc38416f46ffb8b3ec9e4cb2cfff8d04dd7e4311a70e403cb62b10be1b447311b60b4f9ee221a8131fc2cbd45b96dd80deba68a949d467241facfa8"), - MustHexID("3ea3d04a43a3dfb5ac11cffc2319248cf41b6279659393c2f55b8a0a5fc9d12581a9d97ef5d8ff9b5abf3321a290e8f63a4f785f450dc8a672aba3ba2ff4fdab"), - MustHexID("2fc897f05ae585553e5c014effd3078f84f37f9333afacffb109f00ca8e7a3373de810a3946be971cbccdfd40249f9fe7f322118ea459ac71acca85a1ef8b7f4"), + hexEncPubkey("d6d32178bdc38416f46ffb8b3ec9e4cb2cfff8d04dd7e4311a70e403cb62b10be1b447311b60b4f9ee221a8131fc2cbd45b96dd80deba68a949d467241facfa8"), + hexEncPubkey("3ea3d04a43a3dfb5ac11cffc2319248cf41b6279659393c2f55b8a0a5fc9d12581a9d97ef5d8ff9b5abf3321a290e8f63a4f785f450dc8a672aba3ba2ff4fdab"), + hexEncPubkey("2fc897f05ae585553e5c014effd3078f84f37f9333afacffb109f00ca8e7a3373de810a3946be971cbccdfd40249f9fe7f322118ea459ac71acca85a1ef8b7f4"), }, 247: { - MustHexID("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"), - MustHexID("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"), - MustHexID("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"), - MustHexID("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"), - MustHexID("8b58c6073dd98bbad4e310b97186c8f822d3a5c7d57af40e2136e88e315afd115edb27d2d0685a908cfe5aa49d0debdda6e6e63972691d6bd8c5af2d771dd2a9"), - MustHexID("2cbb718b7dc682da19652e7d9eb4fefaf7b7147d82c1c2b6805edf77b85e29fde9f6da195741467ff2638dc62c8d3e014ea5686693c15ed0080b6de90354c137"), - MustHexID("e84027696d3f12f2de30a9311afea8fbd313c2360daff52bb5fc8c7094d5295758bec3134e4eef24e4cdf377b40da344993284628a7a346eba94f74160998feb"), - MustHexID("f1357a4f04f9d33753a57c0b65ba20a5d8777abbffd04e906014491c9103fb08590e45548d37aa4bd70965e2e81ddba94f31860348df01469eec8c1829200a68"), - MustHexID("4ab0a75941b12892369b4490a1928c8ca52a9ad6d3dffbd1d8c0b907bc200fe74c022d011ec39b64808a39c0ca41f1d3254386c3e7733e7044c44259486461b6"), - MustHexID("d45150a72dc74388773e68e03133a3b5f51447fe91837d566706b3c035ee4b56f160c878c6273394daee7f56cc398985269052f22f75a8057df2fe6172765354"), + hexEncPubkey("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"), + hexEncPubkey("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"), + hexEncPubkey("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"), + hexEncPubkey("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"), + hexEncPubkey("8b58c6073dd98bbad4e310b97186c8f822d3a5c7d57af40e2136e88e315afd115edb27d2d0685a908cfe5aa49d0debdda6e6e63972691d6bd8c5af2d771dd2a9"), + hexEncPubkey("2cbb718b7dc682da19652e7d9eb4fefaf7b7147d82c1c2b6805edf77b85e29fde9f6da195741467ff2638dc62c8d3e014ea5686693c15ed0080b6de90354c137"), + hexEncPubkey("e84027696d3f12f2de30a9311afea8fbd313c2360daff52bb5fc8c7094d5295758bec3134e4eef24e4cdf377b40da344993284628a7a346eba94f74160998feb"), + hexEncPubkey("f1357a4f04f9d33753a57c0b65ba20a5d8777abbffd04e906014491c9103fb08590e45548d37aa4bd70965e2e81ddba94f31860348df01469eec8c1829200a68"), + hexEncPubkey("4ab0a75941b12892369b4490a1928c8ca52a9ad6d3dffbd1d8c0b907bc200fe74c022d011ec39b64808a39c0ca41f1d3254386c3e7733e7044c44259486461b6"), + hexEncPubkey("d45150a72dc74388773e68e03133a3b5f51447fe91837d566706b3c035ee4b56f160c878c6273394daee7f56cc398985269052f22f75a8057df2fe6172765354"), }, 248: { - MustHexID("6aadfce366a189bab08ac84721567483202c86590642ea6d6a14f37ca78d82bdb6509eb7b8b2f6f63c78ae3ae1d8837c89509e41497d719b23ad53dd81574afa"), - MustHexID("a605ecfd6069a4cf4cf7f5840e5bc0ce10d23a3ac59e2aaa70c6afd5637359d2519b4524f56fc2ca180cdbebe54262f720ccaae8c1b28fd553c485675831624d"), - MustHexID("29701451cb9448ca33fc33680b44b840d815be90146eb521641efbffed0859c154e8892d3906eae9934bfacee72cd1d2fa9dd050fd18888eea49da155ab0efd2"), - MustHexID("3ed426322dee7572b08592e1e079f8b6c6b30e10e6243edd144a6a48fdbdb83df73a6e41b1143722cb82604f2203a32758610b5d9544f44a1a7921ba001528c1"), - MustHexID("b2e2a2b7fdd363572a3256e75435fab1da3b16f7891a8bd2015f30995dae665d7eabfd194d87d99d5df628b4bbc7b04e5b492c596422dd8272746c7a1b0b8e4f"), - MustHexID("0c69c9756162c593e85615b814ce57a2a8ca2df6c690b9c4e4602731b61e1531a3bbe3f7114271554427ffabea80ad8f36fa95a49fa77b675ae182c6ccac1728"), - MustHexID("8d28be21d5a97b0876442fa4f5e5387f5bf3faad0b6f13b8607b64d6e448c0991ca28dd7fe2f64eb8eadd7150bff5d5666aa6ed868b84c71311f4ba9a38569dd"), - MustHexID("2c677e1c64b9c9df6359348a7f5f33dc79e22f0177042486d125f8b6ca7f0dc756b1f672aceee5f1746bcff80aaf6f92a8dc0c9fbeb259b3fa0da060de5ab7e8"), - MustHexID("3994880f94a8678f0cd247a43f474a8af375d2a072128da1ad6cae84a244105ff85e94fc7d8496f639468de7ee998908a91c7e33ef7585fff92e984b210941a1"), - MustHexID("b45a9153c08d002a48090d15d61a7c7dad8c2af85d4ff5bd36ce23a9a11e0709bf8d56614c7b193bc028c16cbf7f20dfbcc751328b64a924995d47b41e452422"), - MustHexID("057ab3a9e53c7a84b0f3fc586117a525cdd18e313f52a67bf31798d48078e325abe5cfee3f6c2533230cb37d0549289d692a29dd400e899b8552d4b928f6f907"), - MustHexID("0ddf663d308791eb92e6bd88a2f8cb45e4f4f35bb16708a0e6ff7f1362aa6a73fedd0a1b1557fb3365e38e1b79d6918e2fae2788728b70c9ab6b51a3b94a4338"), - MustHexID("f637e07ff50cc1e3731735841c4798411059f2023abcf3885674f3e8032531b0edca50fd715df6feb489b6177c345374d64f4b07d257a7745de393a107b013a5"), - MustHexID("e24ec7c6eec094f63c7b3239f56d311ec5a3e45bc4e622a1095a65b95eea6fe13e29f3b6b7a2cbfe40906e3989f17ac834c3102dd0cadaaa26e16ee06d782b72"), - MustHexID("b76ea1a6fd6506ef6e3506a4f1f60ed6287fff8114af6141b2ff13e61242331b54082b023cfea5b3083354a4fb3f9eb8be01fb4a518f579e731a5d0707291a6b"), - MustHexID("9b53a37950ca8890ee349b325032d7b672cab7eced178d3060137b24ef6b92a43977922d5bdfb4a3409a2d80128e02f795f9dae6d7d99973ad0e23a2afb8442f"), + hexEncPubkey("6aadfce366a189bab08ac84721567483202c86590642ea6d6a14f37ca78d82bdb6509eb7b8b2f6f63c78ae3ae1d8837c89509e41497d719b23ad53dd81574afa"), + hexEncPubkey("a605ecfd6069a4cf4cf7f5840e5bc0ce10d23a3ac59e2aaa70c6afd5637359d2519b4524f56fc2ca180cdbebe54262f720ccaae8c1b28fd553c485675831624d"), + hexEncPubkey("29701451cb9448ca33fc33680b44b840d815be90146eb521641efbffed0859c154e8892d3906eae9934bfacee72cd1d2fa9dd050fd18888eea49da155ab0efd2"), + hexEncPubkey("3ed426322dee7572b08592e1e079f8b6c6b30e10e6243edd144a6a48fdbdb83df73a6e41b1143722cb82604f2203a32758610b5d9544f44a1a7921ba001528c1"), + hexEncPubkey("b2e2a2b7fdd363572a3256e75435fab1da3b16f7891a8bd2015f30995dae665d7eabfd194d87d99d5df628b4bbc7b04e5b492c596422dd8272746c7a1b0b8e4f"), + hexEncPubkey("0c69c9756162c593e85615b814ce57a2a8ca2df6c690b9c4e4602731b61e1531a3bbe3f7114271554427ffabea80ad8f36fa95a49fa77b675ae182c6ccac1728"), + hexEncPubkey("8d28be21d5a97b0876442fa4f5e5387f5bf3faad0b6f13b8607b64d6e448c0991ca28dd7fe2f64eb8eadd7150bff5d5666aa6ed868b84c71311f4ba9a38569dd"), + hexEncPubkey("2c677e1c64b9c9df6359348a7f5f33dc79e22f0177042486d125f8b6ca7f0dc756b1f672aceee5f1746bcff80aaf6f92a8dc0c9fbeb259b3fa0da060de5ab7e8"), + hexEncPubkey("3994880f94a8678f0cd247a43f474a8af375d2a072128da1ad6cae84a244105ff85e94fc7d8496f639468de7ee998908a91c7e33ef7585fff92e984b210941a1"), + hexEncPubkey("b45a9153c08d002a48090d15d61a7c7dad8c2af85d4ff5bd36ce23a9a11e0709bf8d56614c7b193bc028c16cbf7f20dfbcc751328b64a924995d47b41e452422"), + hexEncPubkey("057ab3a9e53c7a84b0f3fc586117a525cdd18e313f52a67bf31798d48078e325abe5cfee3f6c2533230cb37d0549289d692a29dd400e899b8552d4b928f6f907"), + hexEncPubkey("0ddf663d308791eb92e6bd88a2f8cb45e4f4f35bb16708a0e6ff7f1362aa6a73fedd0a1b1557fb3365e38e1b79d6918e2fae2788728b70c9ab6b51a3b94a4338"), + hexEncPubkey("f637e07ff50cc1e3731735841c4798411059f2023abcf3885674f3e8032531b0edca50fd715df6feb489b6177c345374d64f4b07d257a7745de393a107b013a5"), + hexEncPubkey("e24ec7c6eec094f63c7b3239f56d311ec5a3e45bc4e622a1095a65b95eea6fe13e29f3b6b7a2cbfe40906e3989f17ac834c3102dd0cadaaa26e16ee06d782b72"), + hexEncPubkey("b76ea1a6fd6506ef6e3506a4f1f60ed6287fff8114af6141b2ff13e61242331b54082b023cfea5b3083354a4fb3f9eb8be01fb4a518f579e731a5d0707291a6b"), + hexEncPubkey("9b53a37950ca8890ee349b325032d7b672cab7eced178d3060137b24ef6b92a43977922d5bdfb4a3409a2d80128e02f795f9dae6d7d99973ad0e23a2afb8442f"), }, 249: { - MustHexID("675ae65567c3c72c50c73bc0fd4f61f202ea5f93346ca57b551de3411ccc614fad61cb9035493af47615311b9d44ee7a161972ee4d77c28fe1ec029d01434e6a"), - MustHexID("8eb81408389da88536ae5800392b16ef5109d7ea132c18e9a82928047ecdb502693f6e4a4cdd18b54296caf561db937185731456c456c98bfe7de0baf0eaa495"), - MustHexID("2adba8b1612a541771cb93a726a38a4b88e97b18eced2593eb7daf82f05a5321ca94a72cc780c306ff21e551a932fc2c6d791e4681907b5ceab7f084c3fa2944"), - MustHexID("b1b4bfbda514d9b8f35b1c28961da5d5216fe50548f4066f69af3b7666a3b2e06eac646735e963e5c8f8138a2fb95af15b13b23ff00c6986eccc0efaa8ee6fb4"), - MustHexID("d2139281b289ad0e4d7b4243c4364f5c51aac8b60f4806135de06b12b5b369c9e43a6eb494eab860d115c15c6fbb8c5a1b0e382972e0e460af395b8385363de7"), - MustHexID("4a693df4b8fc5bdc7cec342c3ed2e228d7c5b4ab7321ddaa6cccbeb45b05a9f1d95766b4002e6d4791c2deacb8a667aadea6a700da28a3eea810a30395701bbc"), - MustHexID("ab41611195ec3c62bb8cd762ee19fb182d194fd141f4a66780efbef4b07ce916246c022b841237a3a6b512a93431157edd221e854ed2a259b72e9c5351f44d0c"), - MustHexID("68e8e26099030d10c3c703ae7045c0a48061fb88058d853b3e67880014c449d4311014da99d617d3150a20f1a3da5e34bf0f14f1c51fe4dd9d58afd222823176"), - MustHexID("3fbcacf546fb129cd70fc48de3b593ba99d3c473798bc309292aca280320e0eacc04442c914cad5c4cf6950345ba79b0d51302df88285d4e83ee3fe41339eee7"), - MustHexID("1d4a623659f7c8f80b6c3939596afdf42e78f892f682c768ad36eb7bfba402dbf97aea3a268f3badd8fe7636be216edf3d67ee1e08789ebbc7be625056bd7109"), - MustHexID("a283c474ab09da02bbc96b16317241d0627646fcc427d1fe790b76a7bf1989ced90f92101a973047ae9940c92720dffbac8eff21df8cae468a50f72f9e159417"), - MustHexID("dbf7e5ad7f87c3dfecae65d87c3039e14ed0bdc56caf00ce81931073e2e16719d746295512ff7937a15c3b03603e7c41a4f9df94fcd37bb200dd8f332767e9cb"), - MustHexID("caaa070a26692f64fc77f30d7b5ae980d419b4393a0f442b1c821ef58c0862898b0d22f74a4f8c5d83069493e3ec0b92f17dc1fe6e4cd437c1ec25039e7ce839"), - MustHexID("874cc8d1213beb65c4e0e1de38ef5d8165235893ac74ab5ea937c885eaab25c8d79dad0456e9fd3e9450626cac7e107b004478fb59842f067857f39a47cee695"), - MustHexID("d94193f236105010972f5df1b7818b55846592a0445b9cdc4eaed811b8c4c0f7c27dc8cc9837a4774656d6b34682d6d329d42b6ebb55da1d475c2474dc3dfdf4"), - MustHexID("edd9af6aded4094e9785637c28fccbd3980cbe28e2eb9a411048a23c2ace4bd6b0b7088a7817997b49a3dd05fc6929ca6c7abbb69438dbdabe65e971d2a794b2"), + hexEncPubkey("675ae65567c3c72c50c73bc0fd4f61f202ea5f93346ca57b551de3411ccc614fad61cb9035493af47615311b9d44ee7a161972ee4d77c28fe1ec029d01434e6a"), + hexEncPubkey("8eb81408389da88536ae5800392b16ef5109d7ea132c18e9a82928047ecdb502693f6e4a4cdd18b54296caf561db937185731456c456c98bfe7de0baf0eaa495"), + hexEncPubkey("2adba8b1612a541771cb93a726a38a4b88e97b18eced2593eb7daf82f05a5321ca94a72cc780c306ff21e551a932fc2c6d791e4681907b5ceab7f084c3fa2944"), + hexEncPubkey("b1b4bfbda514d9b8f35b1c28961da5d5216fe50548f4066f69af3b7666a3b2e06eac646735e963e5c8f8138a2fb95af15b13b23ff00c6986eccc0efaa8ee6fb4"), + hexEncPubkey("d2139281b289ad0e4d7b4243c4364f5c51aac8b60f4806135de06b12b5b369c9e43a6eb494eab860d115c15c6fbb8c5a1b0e382972e0e460af395b8385363de7"), + hexEncPubkey("4a693df4b8fc5bdc7cec342c3ed2e228d7c5b4ab7321ddaa6cccbeb45b05a9f1d95766b4002e6d4791c2deacb8a667aadea6a700da28a3eea810a30395701bbc"), + hexEncPubkey("ab41611195ec3c62bb8cd762ee19fb182d194fd141f4a66780efbef4b07ce916246c022b841237a3a6b512a93431157edd221e854ed2a259b72e9c5351f44d0c"), + hexEncPubkey("68e8e26099030d10c3c703ae7045c0a48061fb88058d853b3e67880014c449d4311014da99d617d3150a20f1a3da5e34bf0f14f1c51fe4dd9d58afd222823176"), + hexEncPubkey("3fbcacf546fb129cd70fc48de3b593ba99d3c473798bc309292aca280320e0eacc04442c914cad5c4cf6950345ba79b0d51302df88285d4e83ee3fe41339eee7"), + hexEncPubkey("1d4a623659f7c8f80b6c3939596afdf42e78f892f682c768ad36eb7bfba402dbf97aea3a268f3badd8fe7636be216edf3d67ee1e08789ebbc7be625056bd7109"), + hexEncPubkey("a283c474ab09da02bbc96b16317241d0627646fcc427d1fe790b76a7bf1989ced90f92101a973047ae9940c92720dffbac8eff21df8cae468a50f72f9e159417"), + hexEncPubkey("dbf7e5ad7f87c3dfecae65d87c3039e14ed0bdc56caf00ce81931073e2e16719d746295512ff7937a15c3b03603e7c41a4f9df94fcd37bb200dd8f332767e9cb"), + hexEncPubkey("caaa070a26692f64fc77f30d7b5ae980d419b4393a0f442b1c821ef58c0862898b0d22f74a4f8c5d83069493e3ec0b92f17dc1fe6e4cd437c1ec25039e7ce839"), + hexEncPubkey("874cc8d1213beb65c4e0e1de38ef5d8165235893ac74ab5ea937c885eaab25c8d79dad0456e9fd3e9450626cac7e107b004478fb59842f067857f39a47cee695"), + hexEncPubkey("d94193f236105010972f5df1b7818b55846592a0445b9cdc4eaed811b8c4c0f7c27dc8cc9837a4774656d6b34682d6d329d42b6ebb55da1d475c2474dc3dfdf4"), + hexEncPubkey("edd9af6aded4094e9785637c28fccbd3980cbe28e2eb9a411048a23c2ace4bd6b0b7088a7817997b49a3dd05fc6929ca6c7abbb69438dbdabe65e971d2a794b2"), }, 250: { - MustHexID("53a5bd1215d4ab709ae8fdc2ced50bba320bced78bd9c5dc92947fb402250c914891786db0978c898c058493f86fc68b1c5de8a5cb36336150ac7a88655b6c39"), - MustHexID("b7f79e3ab59f79262623c9ccefc8f01d682323aee56ffbe295437487e9d5acaf556a9c92e1f1c6a9601f2b9eb6b027ae1aeaebac71d61b9b78e88676efd3e1a3"), - MustHexID("d374bf7e8d7ffff69cc00bebff38ef5bc1dcb0a8d51c1a3d70e61ac6b2e2d6617109254b0ac224354dfbf79009fe4239e09020c483cc60c071e00b9238684f30"), - MustHexID("1e1eac1c9add703eb252eb991594f8f5a173255d526a855fab24ae57dc277e055bc3c7a7ae0b45d437c4f47a72d97eb7b126f2ba344ba6c0e14b2c6f27d4b1e6"), - MustHexID("ae28953f63d4bc4e706712a59319c111f5ff8f312584f65d7436b4cd3d14b217b958f8486bad666b4481fe879019fb1f767cf15b3e3e2711efc33b56d460448a"), - MustHexID("934bb1edf9c7a318b82306aca67feb3d6b434421fa275d694f0b4927afd8b1d3935b727fd4ff6e3d012e0c82f1824385174e8c6450ade59c2a43281a4b3446b6"), - MustHexID("9eef3f28f70ce19637519a0916555bf76d26de31312ac656cf9d3e379899ea44e4dd7ffcce923b4f3563f8a00489a34bd6936db0cbb4c959d32c49f017e07d05"), - MustHexID("82200872e8f871c48f1fad13daec6478298099b591bb3dbc4ef6890aa28ebee5860d07d70be62f4c0af85085a90ae8179ee8f937cf37915c67ea73e704b03ee7"), - MustHexID("6c75a5834a08476b7fc37ff3dc2011dc3ea3b36524bad7a6d319b18878fad813c0ba76d1f4555cacd3890c865438c21f0e0aed1f80e0a157e642124c69f43a11"), - MustHexID("995b873742206cb02b736e73a88580c2aacb0bd4a3c97a647b647bcab3f5e03c0e0736520a8b3600da09edf4248991fb01091ec7ff3ec7cdc8a1beae011e7aae"), - MustHexID("c773a056594b5cdef2e850d30891ff0e927c3b1b9c35cd8e8d53a1017001e237468e1ece3ae33d612ca3e6abb0a9169aa352e9dcda358e5af2ad982b577447db"), - MustHexID("2b46a5f6923f475c6be99ec6d134437a6d11f6bb4b4ac6bcd94572fa1092639d1c08aeefcb51f0912f0a060f71d4f38ee4da70ecc16010b05dd4a674aab14c3a"), - MustHexID("af6ab501366debbaa0d22e20e9688f32ef6b3b644440580fd78de4fe0e99e2a16eb5636bbae0d1c259df8ddda77b35b9a35cbc36137473e9c68fbc9d203ba842"), - MustHexID("c9f6f2dd1a941926f03f770695bda289859e85fabaf94baaae20b93e5015dc014ba41150176a36a1884adb52f405194693e63b0c464a6891cc9cc1c80d450326"), - MustHexID("5b116f0751526868a909b61a30b0c5282c37df6925cc03ddea556ef0d0602a9595fd6c14d371f8ed7d45d89918a032dcd22be4342a8793d88fdbeb3ca3d75bd7"), - MustHexID("50f3222fb6b82481c7c813b2172e1daea43e2710a443b9c2a57a12bd160dd37e20f87aa968c82ad639af6972185609d47036c0d93b4b7269b74ebd7073221c10"), + hexEncPubkey("53a5bd1215d4ab709ae8fdc2ced50bba320bced78bd9c5dc92947fb402250c914891786db0978c898c058493f86fc68b1c5de8a5cb36336150ac7a88655b6c39"), + hexEncPubkey("b7f79e3ab59f79262623c9ccefc8f01d682323aee56ffbe295437487e9d5acaf556a9c92e1f1c6a9601f2b9eb6b027ae1aeaebac71d61b9b78e88676efd3e1a3"), + hexEncPubkey("d374bf7e8d7ffff69cc00bebff38ef5bc1dcb0a8d51c1a3d70e61ac6b2e2d6617109254b0ac224354dfbf79009fe4239e09020c483cc60c071e00b9238684f30"), + hexEncPubkey("1e1eac1c9add703eb252eb991594f8f5a173255d526a855fab24ae57dc277e055bc3c7a7ae0b45d437c4f47a72d97eb7b126f2ba344ba6c0e14b2c6f27d4b1e6"), + hexEncPubkey("ae28953f63d4bc4e706712a59319c111f5ff8f312584f65d7436b4cd3d14b217b958f8486bad666b4481fe879019fb1f767cf15b3e3e2711efc33b56d460448a"), + hexEncPubkey("934bb1edf9c7a318b82306aca67feb3d6b434421fa275d694f0b4927afd8b1d3935b727fd4ff6e3d012e0c82f1824385174e8c6450ade59c2a43281a4b3446b6"), + hexEncPubkey("9eef3f28f70ce19637519a0916555bf76d26de31312ac656cf9d3e379899ea44e4dd7ffcce923b4f3563f8a00489a34bd6936db0cbb4c959d32c49f017e07d05"), + hexEncPubkey("82200872e8f871c48f1fad13daec6478298099b591bb3dbc4ef6890aa28ebee5860d07d70be62f4c0af85085a90ae8179ee8f937cf37915c67ea73e704b03ee7"), + hexEncPubkey("6c75a5834a08476b7fc37ff3dc2011dc3ea3b36524bad7a6d319b18878fad813c0ba76d1f4555cacd3890c865438c21f0e0aed1f80e0a157e642124c69f43a11"), + hexEncPubkey("995b873742206cb02b736e73a88580c2aacb0bd4a3c97a647b647bcab3f5e03c0e0736520a8b3600da09edf4248991fb01091ec7ff3ec7cdc8a1beae011e7aae"), + hexEncPubkey("c773a056594b5cdef2e850d30891ff0e927c3b1b9c35cd8e8d53a1017001e237468e1ece3ae33d612ca3e6abb0a9169aa352e9dcda358e5af2ad982b577447db"), + hexEncPubkey("2b46a5f6923f475c6be99ec6d134437a6d11f6bb4b4ac6bcd94572fa1092639d1c08aeefcb51f0912f0a060f71d4f38ee4da70ecc16010b05dd4a674aab14c3a"), + hexEncPubkey("af6ab501366debbaa0d22e20e9688f32ef6b3b644440580fd78de4fe0e99e2a16eb5636bbae0d1c259df8ddda77b35b9a35cbc36137473e9c68fbc9d203ba842"), + hexEncPubkey("c9f6f2dd1a941926f03f770695bda289859e85fabaf94baaae20b93e5015dc014ba41150176a36a1884adb52f405194693e63b0c464a6891cc9cc1c80d450326"), + hexEncPubkey("5b116f0751526868a909b61a30b0c5282c37df6925cc03ddea556ef0d0602a9595fd6c14d371f8ed7d45d89918a032dcd22be4342a8793d88fdbeb3ca3d75bd7"), + hexEncPubkey("50f3222fb6b82481c7c813b2172e1daea43e2710a443b9c2a57a12bd160dd37e20f87aa968c82ad639af6972185609d47036c0d93b4b7269b74ebd7073221c10"), }, 251: { - MustHexID("9b8f702a62d1bee67bedfeb102eca7f37fa1713e310f0d6651cc0c33ea7c5477575289ccd463e5a2574a00a676a1fdce05658ba447bb9d2827f0ba47b947e894"), - MustHexID("b97532eb83054ed054b4abdf413bb30c00e4205545c93521554dbe77faa3cfaa5bd31ef466a107b0b34a71ec97214c0c83919720142cddac93aa7a3e928d4708"), - MustHexID("2f7a5e952bfb67f2f90b8441b5fadc9ee13b1dcde3afeeb3dd64bf937f86663cc5c55d1fa83952b5422763c7df1b7f2794b751c6be316ebc0beb4942e65ab8c1"), - MustHexID("42c7483781727051a0b3660f14faf39e0d33de5e643702ae933837d036508ab856ce7eec8ec89c4929a4901256e5233a3d847d5d4893f91bcf21835a9a880fee"), - MustHexID("873bae27bf1dc854408fba94046a53ab0c965cebe1e4e12290806fc62b88deb1f4a47f9e18f78fc0e7913a0c6e42ac4d0fc3a20cea6bc65f0c8a0ca90b67521e"), - MustHexID("a7e3a370bbd761d413f8d209e85886f68bf73d5c3089b2dc6fa42aab1ecb5162635497eed95dee2417f3c9c74a3e76319625c48ead2e963c7de877cd4551f347"), - MustHexID("528597534776a40df2addaaea15b6ff832ce36b9748a265768368f657e76d58569d9f30dbb91e91cf0ae7efe8f402f17aa0ae15f5c55051ba03ba830287f4c42"), - MustHexID("461d8bd4f13c3c09031fdb84f104ed737a52f630261463ce0bdb5704259bab4b737dda688285b8444dbecaecad7f50f835190b38684ced5e90c54219e5adf1bc"), - MustHexID("6ec50c0be3fd232737090fc0111caaf0bb6b18f72be453428087a11a97fd6b52db0344acbf789a689bd4f5f50f79017ea784f8fd6fe723ad6ae675b9e3b13e21"), - MustHexID("12fc5e2f77a83fdcc727b79d8ae7fe6a516881138d3011847ee136b400fed7cfba1f53fd7a9730253c7aa4f39abeacd04f138417ba7fcb0f36cccc3514e0dab6"), - MustHexID("4fdbe75914ccd0bce02101606a1ccf3657ec963e3b3c20239d5fec87673fe446d649b4f15f1fe1a40e6cfbd446dda2d31d40bb602b1093b8fcd5f139ba0eb46a"), - MustHexID("3753668a0f6281e425ea69b52cb2d17ab97afbe6eb84cf5d25425bc5e53009388857640668fadd7c110721e6047c9697803bd8a6487b43bb343bfa32ebf24039"), - MustHexID("2e81b16346637dec4410fd88e527346145b9c0a849dbf2628049ac7dae016c8f4305649d5659ec77f1e8a0fac0db457b6080547226f06283598e3740ad94849a"), - MustHexID("802c3cc27f91c89213223d758f8d2ecd41135b357b6d698f24d811cdf113033a81c38e0bdff574a5c005b00a8c193dc2531f8c1fa05fa60acf0ab6f2858af09f"), - MustHexID("fcc9a2e1ac3667026ff16192876d1813bb75abdbf39b929a92863012fe8b1d890badea7a0de36274d5c1eb1e8f975785532c50d80fd44b1a4b692f437303393f"), - MustHexID("6d8b3efb461151dd4f6de809b62726f5b89e9b38e9ba1391967f61cde844f7528fecf821b74049207cee5a527096b31f3ad623928cd3ce51d926fa345a6b2951"), + hexEncPubkey("9b8f702a62d1bee67bedfeb102eca7f37fa1713e310f0d6651cc0c33ea7c5477575289ccd463e5a2574a00a676a1fdce05658ba447bb9d2827f0ba47b947e894"), + hexEncPubkey("b97532eb83054ed054b4abdf413bb30c00e4205545c93521554dbe77faa3cfaa5bd31ef466a107b0b34a71ec97214c0c83919720142cddac93aa7a3e928d4708"), + hexEncPubkey("2f7a5e952bfb67f2f90b8441b5fadc9ee13b1dcde3afeeb3dd64bf937f86663cc5c55d1fa83952b5422763c7df1b7f2794b751c6be316ebc0beb4942e65ab8c1"), + hexEncPubkey("42c7483781727051a0b3660f14faf39e0d33de5e643702ae933837d036508ab856ce7eec8ec89c4929a4901256e5233a3d847d5d4893f91bcf21835a9a880fee"), + hexEncPubkey("873bae27bf1dc854408fba94046a53ab0c965cebe1e4e12290806fc62b88deb1f4a47f9e18f78fc0e7913a0c6e42ac4d0fc3a20cea6bc65f0c8a0ca90b67521e"), + hexEncPubkey("a7e3a370bbd761d413f8d209e85886f68bf73d5c3089b2dc6fa42aab1ecb5162635497eed95dee2417f3c9c74a3e76319625c48ead2e963c7de877cd4551f347"), + hexEncPubkey("528597534776a40df2addaaea15b6ff832ce36b9748a265768368f657e76d58569d9f30dbb91e91cf0ae7efe8f402f17aa0ae15f5c55051ba03ba830287f4c42"), + hexEncPubkey("461d8bd4f13c3c09031fdb84f104ed737a52f630261463ce0bdb5704259bab4b737dda688285b8444dbecaecad7f50f835190b38684ced5e90c54219e5adf1bc"), + hexEncPubkey("6ec50c0be3fd232737090fc0111caaf0bb6b18f72be453428087a11a97fd6b52db0344acbf789a689bd4f5f50f79017ea784f8fd6fe723ad6ae675b9e3b13e21"), + hexEncPubkey("12fc5e2f77a83fdcc727b79d8ae7fe6a516881138d3011847ee136b400fed7cfba1f53fd7a9730253c7aa4f39abeacd04f138417ba7fcb0f36cccc3514e0dab6"), + hexEncPubkey("4fdbe75914ccd0bce02101606a1ccf3657ec963e3b3c20239d5fec87673fe446d649b4f15f1fe1a40e6cfbd446dda2d31d40bb602b1093b8fcd5f139ba0eb46a"), + hexEncPubkey("3753668a0f6281e425ea69b52cb2d17ab97afbe6eb84cf5d25425bc5e53009388857640668fadd7c110721e6047c9697803bd8a6487b43bb343bfa32ebf24039"), + hexEncPubkey("2e81b16346637dec4410fd88e527346145b9c0a849dbf2628049ac7dae016c8f4305649d5659ec77f1e8a0fac0db457b6080547226f06283598e3740ad94849a"), + hexEncPubkey("802c3cc27f91c89213223d758f8d2ecd41135b357b6d698f24d811cdf113033a81c38e0bdff574a5c005b00a8c193dc2531f8c1fa05fa60acf0ab6f2858af09f"), + hexEncPubkey("fcc9a2e1ac3667026ff16192876d1813bb75abdbf39b929a92863012fe8b1d890badea7a0de36274d5c1eb1e8f975785532c50d80fd44b1a4b692f437303393f"), + hexEncPubkey("6d8b3efb461151dd4f6de809b62726f5b89e9b38e9ba1391967f61cde844f7528fecf821b74049207cee5a527096b31f3ad623928cd3ce51d926fa345a6b2951"), }, 252: { - MustHexID("f1ae93157cc48c2075dd5868fbf523e79e06caf4b8198f352f6e526680b78ff4227263de92612f7d63472bd09367bb92a636fff16fe46ccf41614f7a72495c2a"), - MustHexID("587f482d111b239c27c0cb89b51dd5d574db8efd8de14a2e6a1400c54d4567e77c65f89c1da52841212080b91604104768350276b6682f2f961cdaf4039581c7"), - MustHexID("e3f88274d35cefdaabdf205afe0e80e936cc982b8e3e47a84ce664c413b29016a4fb4f3a3ebae0a2f79671f8323661ed462bf4390af94c424dc8ace0c301b90f"), - MustHexID("0ddc736077da9a12ba410dc5ea63cbcbe7659dd08596485b2bff3435221f82c10d263efd9af938e128464be64a178b7cd22e19f400d5802f4c9df54bf89f2619"), - MustHexID("784aa34d833c6ce63fcc1279630113c3272e82c4ae8c126c5a52a88ac461b6baeed4244e607b05dc14e5b2f41c70a273c3804dea237f14f7a1e546f6d1309d14"), - MustHexID("f253a2c354ee0e27cfcae786d726753d4ad24be6516b279a936195a487de4a59dbc296accf20463749ff55293263ed8c1b6365eecb248d44e75e9741c0d18205"), - MustHexID("a1910b80357b3ad9b4593e0628922939614dc9056a5fbf477279c8b2c1d0b4b31d89a0c09d0d41f795271d14d3360ef08a3f821e65e7e1f56c07a36afe49c7c5"), - MustHexID("f1168552c2efe541160f0909b0b4a9d6aeedcf595cdf0e9b165c97e3e197471a1ee6320e93389edfba28af6eaf10de98597ad56e7ab1b504ed762451996c3b98"), - MustHexID("b0c8e5d2c8634a7930e1a6fd082e448c6cf9d2d8b7293558b59238815a4df926c286bf297d2049f14e8296a6eb3256af614ec1812c4f2bbe807673b58bf14c8c"), - MustHexID("0fb346076396a38badc342df3679b55bd7f40a609ab103411fe45082c01f12ea016729e95914b2b5540e987ff5c9b133e85862648e7f36abdfd23100d248d234"), - MustHexID("f736e0cc83417feaa280d9483f5d4d72d1b036cd0c6d9cbdeb8ac35ceb2604780de46dddaa32a378474e1d5ccdf79b373331c30c7911ade2ae32f98832e5de1f"), - MustHexID("8b02991457602f42b38b342d3f2259ae4100c354b3843885f7e4e07bd644f64dab94bb7f38a3915f8b7f11d8e3f81c28e07a0078cf79d7397e38a7b7e0c857e2"), - MustHexID("9221d9f04a8a184993d12baa91116692bb685f887671302999d69300ad103eb2d2c75a09d8979404c6dd28f12362f58a1a43619c493d9108fd47588a23ce5824"), - MustHexID("652797801744dada833fff207d67484742eea6835d695925f3e618d71b68ec3c65bdd85b4302b2cdcb835ad3f94fd00d8da07e570b41bc0d2bcf69a8de1b3284"), - MustHexID("d84f06fe64debc4cd0625e36d19b99014b6218375262cc2209202bdbafd7dffcc4e34ce6398e182e02fd8faeed622c3e175545864902dfd3d1ac57647cddf4c6"), - MustHexID("d0ed87b294f38f1d741eb601020eeec30ac16331d05880fe27868f1e454446de367d7457b41c79e202eaf9525b029e4f1d7e17d85a55f83a557c005c68d7328a"), + hexEncPubkey("f1ae93157cc48c2075dd5868fbf523e79e06caf4b8198f352f6e526680b78ff4227263de92612f7d63472bd09367bb92a636fff16fe46ccf41614f7a72495c2a"), + hexEncPubkey("587f482d111b239c27c0cb89b51dd5d574db8efd8de14a2e6a1400c54d4567e77c65f89c1da52841212080b91604104768350276b6682f2f961cdaf4039581c7"), + hexEncPubkey("e3f88274d35cefdaabdf205afe0e80e936cc982b8e3e47a84ce664c413b29016a4fb4f3a3ebae0a2f79671f8323661ed462bf4390af94c424dc8ace0c301b90f"), + hexEncPubkey("0ddc736077da9a12ba410dc5ea63cbcbe7659dd08596485b2bff3435221f82c10d263efd9af938e128464be64a178b7cd22e19f400d5802f4c9df54bf89f2619"), + hexEncPubkey("784aa34d833c6ce63fcc1279630113c3272e82c4ae8c126c5a52a88ac461b6baeed4244e607b05dc14e5b2f41c70a273c3804dea237f14f7a1e546f6d1309d14"), + hexEncPubkey("f253a2c354ee0e27cfcae786d726753d4ad24be6516b279a936195a487de4a59dbc296accf20463749ff55293263ed8c1b6365eecb248d44e75e9741c0d18205"), + hexEncPubkey("a1910b80357b3ad9b4593e0628922939614dc9056a5fbf477279c8b2c1d0b4b31d89a0c09d0d41f795271d14d3360ef08a3f821e65e7e1f56c07a36afe49c7c5"), + hexEncPubkey("f1168552c2efe541160f0909b0b4a9d6aeedcf595cdf0e9b165c97e3e197471a1ee6320e93389edfba28af6eaf10de98597ad56e7ab1b504ed762451996c3b98"), + hexEncPubkey("b0c8e5d2c8634a7930e1a6fd082e448c6cf9d2d8b7293558b59238815a4df926c286bf297d2049f14e8296a6eb3256af614ec1812c4f2bbe807673b58bf14c8c"), + hexEncPubkey("0fb346076396a38badc342df3679b55bd7f40a609ab103411fe45082c01f12ea016729e95914b2b5540e987ff5c9b133e85862648e7f36abdfd23100d248d234"), + hexEncPubkey("f736e0cc83417feaa280d9483f5d4d72d1b036cd0c6d9cbdeb8ac35ceb2604780de46dddaa32a378474e1d5ccdf79b373331c30c7911ade2ae32f98832e5de1f"), + hexEncPubkey("8b02991457602f42b38b342d3f2259ae4100c354b3843885f7e4e07bd644f64dab94bb7f38a3915f8b7f11d8e3f81c28e07a0078cf79d7397e38a7b7e0c857e2"), + hexEncPubkey("9221d9f04a8a184993d12baa91116692bb685f887671302999d69300ad103eb2d2c75a09d8979404c6dd28f12362f58a1a43619c493d9108fd47588a23ce5824"), + hexEncPubkey("652797801744dada833fff207d67484742eea6835d695925f3e618d71b68ec3c65bdd85b4302b2cdcb835ad3f94fd00d8da07e570b41bc0d2bcf69a8de1b3284"), + hexEncPubkey("d84f06fe64debc4cd0625e36d19b99014b6218375262cc2209202bdbafd7dffcc4e34ce6398e182e02fd8faeed622c3e175545864902dfd3d1ac57647cddf4c6"), + hexEncPubkey("d0ed87b294f38f1d741eb601020eeec30ac16331d05880fe27868f1e454446de367d7457b41c79e202eaf9525b029e4f1d7e17d85a55f83a557c005c68d7328a"), }, 253: { - MustHexID("ad4485e386e3cc7c7310366a7c38fb810b8896c0d52e55944bfd320ca294e7912d6c53c0a0cf85e7ce226e92491d60430e86f8f15cda0161ed71893fb4a9e3a1"), - MustHexID("36d0e7e5b7734f98c6183eeeb8ac5130a85e910a925311a19c4941b1290f945d4fc3996b12ef4966960b6fa0fb29b1604f83a0f81bd5fd6398d2e1a22e46af0c"), - MustHexID("7d307d8acb4a561afa23bdf0bd945d35c90245e26345ec3a1f9f7df354222a7cdcb81339c9ed6744526c27a1a0c8d10857e98df942fa433602facac71ac68a31"), - MustHexID("d97bf55f88c83fae36232661af115d66ca600fc4bd6d1fb35ff9bb4dad674c02cf8c8d05f317525b5522250db58bb1ecafb7157392bf5aa61b178c61f098d995"), - MustHexID("7045d678f1f9eb7a4613764d17bd5698796494d0bf977b16f2dbc272b8a0f7858a60805c022fc3d1fe4f31c37e63cdaca0416c0d053ef48a815f8b19121605e0"), - MustHexID("14e1f21418d445748de2a95cd9a8c3b15b506f86a0acabd8af44bb968ce39885b19c8822af61b3dd58a34d1f265baec30e3ae56149dc7d2aa4a538f7319f69c8"), - MustHexID("b9453d78281b66a4eac95a1546017111eaaa5f92a65d0de10b1122940e92b319728a24edf4dec6acc412321b1c95266d39c7b3a5d265c629c3e49a65fb022c09"), - MustHexID("e8a49248419e3824a00d86af422f22f7366e2d4922b304b7169937616a01d9d6fa5abf5cc01061a352dc866f48e1fa2240dbb453d872b1d7be62bdfc1d5e248c"), - MustHexID("bebcff24b52362f30e0589ee573ce2d86f073d58d18e6852a592fa86ceb1a6c9b96d7fb9ec7ed1ed98a51b6743039e780279f6bb49d0a04327ac7a182d9a56f6"), - MustHexID("d0835e5a4291db249b8d2fca9f503049988180c7d247bedaa2cf3a1bad0a76709360a85d4f9a1423b2cbc82bb4d94b47c0cde20afc430224834c49fe312a9ae3"), - MustHexID("6b087fe2a2da5e4f0b0f4777598a4a7fb66bf77dbd5bfc44e8a7eaa432ab585a6e226891f56a7d4f5ed11a7c57b90f1661bba1059590ca4267a35801c2802913"), - MustHexID("d901e5bde52d1a0f4ddf010a686a53974cdae4ebe5c6551b3c37d6b6d635d38d5b0e5f80bc0186a2c7809dbf3a42870dd09643e68d32db896c6da8ba734579e7"), - MustHexID("96419fb80efae4b674402bb969ebaab86c1274f29a83a311e24516d36cdf148fe21754d46c97688cdd7468f24c08b13e4727c29263393638a3b37b99ff60ebca"), - MustHexID("7b9c1889ae916a5d5abcdfb0aaedcc9c6f9eb1c1a4f68d0c2d034fe79ac610ce917c3abc670744150fa891bfcd8ab14fed6983fca964de920aa393fa7b326748"), - MustHexID("7a369b2b8962cc4c65900be046482fbf7c14f98a135bbbae25152c82ad168fb2097b3d1429197cf46d3ce9fdeb64808f908a489cc6019725db040060fdfe5405"), - MustHexID("47bcae48288da5ecc7f5058dfa07cf14d89d06d6e449cb946e237aa6652ea050d9f5a24a65efdc0013ccf232bf88670979eddef249b054f63f38da9d7796dbd8"), + hexEncPubkey("ad4485e386e3cc7c7310366a7c38fb810b8896c0d52e55944bfd320ca294e7912d6c53c0a0cf85e7ce226e92491d60430e86f8f15cda0161ed71893fb4a9e3a1"), + hexEncPubkey("36d0e7e5b7734f98c6183eeeb8ac5130a85e910a925311a19c4941b1290f945d4fc3996b12ef4966960b6fa0fb29b1604f83a0f81bd5fd6398d2e1a22e46af0c"), + hexEncPubkey("7d307d8acb4a561afa23bdf0bd945d35c90245e26345ec3a1f9f7df354222a7cdcb81339c9ed6744526c27a1a0c8d10857e98df942fa433602facac71ac68a31"), + hexEncPubkey("d97bf55f88c83fae36232661af115d66ca600fc4bd6d1fb35ff9bb4dad674c02cf8c8d05f317525b5522250db58bb1ecafb7157392bf5aa61b178c61f098d995"), + hexEncPubkey("7045d678f1f9eb7a4613764d17bd5698796494d0bf977b16f2dbc272b8a0f7858a60805c022fc3d1fe4f31c37e63cdaca0416c0d053ef48a815f8b19121605e0"), + hexEncPubkey("14e1f21418d445748de2a95cd9a8c3b15b506f86a0acabd8af44bb968ce39885b19c8822af61b3dd58a34d1f265baec30e3ae56149dc7d2aa4a538f7319f69c8"), + hexEncPubkey("b9453d78281b66a4eac95a1546017111eaaa5f92a65d0de10b1122940e92b319728a24edf4dec6acc412321b1c95266d39c7b3a5d265c629c3e49a65fb022c09"), + hexEncPubkey("e8a49248419e3824a00d86af422f22f7366e2d4922b304b7169937616a01d9d6fa5abf5cc01061a352dc866f48e1fa2240dbb453d872b1d7be62bdfc1d5e248c"), + hexEncPubkey("bebcff24b52362f30e0589ee573ce2d86f073d58d18e6852a592fa86ceb1a6c9b96d7fb9ec7ed1ed98a51b6743039e780279f6bb49d0a04327ac7a182d9a56f6"), + hexEncPubkey("d0835e5a4291db249b8d2fca9f503049988180c7d247bedaa2cf3a1bad0a76709360a85d4f9a1423b2cbc82bb4d94b47c0cde20afc430224834c49fe312a9ae3"), + hexEncPubkey("6b087fe2a2da5e4f0b0f4777598a4a7fb66bf77dbd5bfc44e8a7eaa432ab585a6e226891f56a7d4f5ed11a7c57b90f1661bba1059590ca4267a35801c2802913"), + hexEncPubkey("d901e5bde52d1a0f4ddf010a686a53974cdae4ebe5c6551b3c37d6b6d635d38d5b0e5f80bc0186a2c7809dbf3a42870dd09643e68d32db896c6da8ba734579e7"), + hexEncPubkey("96419fb80efae4b674402bb969ebaab86c1274f29a83a311e24516d36cdf148fe21754d46c97688cdd7468f24c08b13e4727c29263393638a3b37b99ff60ebca"), + hexEncPubkey("7b9c1889ae916a5d5abcdfb0aaedcc9c6f9eb1c1a4f68d0c2d034fe79ac610ce917c3abc670744150fa891bfcd8ab14fed6983fca964de920aa393fa7b326748"), + hexEncPubkey("7a369b2b8962cc4c65900be046482fbf7c14f98a135bbbae25152c82ad168fb2097b3d1429197cf46d3ce9fdeb64808f908a489cc6019725db040060fdfe5405"), + hexEncPubkey("47bcae48288da5ecc7f5058dfa07cf14d89d06d6e449cb946e237aa6652ea050d9f5a24a65efdc0013ccf232bf88670979eddef249b054f63f38da9d7796dbd8"), }, 254: { - MustHexID("099739d7abc8abd38ecc7a816c521a1168a4dbd359fa7212a5123ab583ffa1cf485a5fed219575d6475dbcdd541638b2d3631a6c7fce7474e7fe3cba1d4d5853"), - MustHexID("c2b01603b088a7182d0cf7ef29fb2b04c70acb320fccf78526bf9472e10c74ee70b3fcfa6f4b11d167bd7d3bc4d936b660f2c9bff934793d97cb21750e7c3d31"), - MustHexID("20e4d8f45f2f863e94b45548c1ef22a11f7d36f263e4f8623761e05a64c4572379b000a52211751e2561b0f14f4fc92dd4130410c8ccc71eb4f0e95a700d4ca9"), - MustHexID("27f4a16cc085e72d86e25c98bd2eca173eaaee7565c78ec5a52e9e12b2211f35de81b5b45e9195de2ebfe29106742c59112b951a04eb7ae48822911fc1f9389e"), - MustHexID("55db5ee7d98e7f0b1c3b9d5be6f2bc619a1b86c3cdd513160ad4dcf267037a5fffad527ac15d50aeb32c59c13d1d4c1e567ebbf4de0d25236130c8361f9aac63"), - MustHexID("883df308b0130fc928a8559fe50667a0fff80493bc09685d18213b2db241a3ad11310ed86b0ef662b3ce21fc3d9aa7f3fc24b8d9afe17c7407e9afd3345ae548"), - MustHexID("c7af968cc9bc8200c3ee1a387405f7563be1dce6710a3439f42ea40657d0eae9d2b3c16c42d779605351fcdece4da637b9804e60ca08cfb89aec32c197beffa6"), - MustHexID("3e66f2b788e3ff1d04106b80597915cd7afa06c405a7ae026556b6e583dca8e05cfbab5039bb9a1b5d06083ffe8de5780b1775550e7218f5e98624bf7af9a0a8"), - MustHexID("4fc7f53764de3337fdaec0a711d35d3a923e72fa65025444d12230b3552ed43d9b2d1ad08ccb11f2d50c58809e6dd74dde910e195294fca3b47ae5a3967cc479"), - MustHexID("bafdfdcf6ccaa989436752fa97c77477b6baa7deb374b16c095492c529eb133e8e2f99e1977012b64767b9d34b2cf6d2048ed489bd822b5139b523f6a423167b"), - MustHexID("7f5d78008a4312fe059104ce80202c82b8915c2eb4411c6b812b16f7642e57c00f2c9425121f5cbac4257fe0b3e81ef5dea97ea2dbaa98f6a8b6fd4d1e5980bb"), - MustHexID("598c37fe78f922751a052f463aeb0cb0bc7f52b7c2a4cf2da72ec0931c7c32175d4165d0f8998f7320e87324ac3311c03f9382a5385c55f0407b7a66b2acd864"), - MustHexID("f758c4136e1c148777a7f3275a76e2db0b2b04066fd738554ec398c1c6cc9fb47e14a3b4c87bd47deaeab3ffd2110514c3855685a374794daff87b605b27ee2e"), - MustHexID("0307bb9e4fd865a49dcf1fe4333d1b944547db650ab580af0b33e53c4fef6c789531110fac801bbcbce21fc4d6f61b6d5b24abdf5b22e3030646d579f6dca9c2"), - MustHexID("82504b6eb49bb2c0f91a7006ce9cefdbaf6df38706198502c2e06601091fc9dc91e4f15db3410d45c6af355bc270b0f268d3dff560f956985c7332d4b10bd1ed"), - MustHexID("b39b5b677b45944ceebe76e76d1f051de2f2a0ec7b0d650da52135743e66a9a5dba45f638258f9a7545d9a790c7fe6d3fdf82c25425c7887323e45d27d06c057"), + hexEncPubkey("099739d7abc8abd38ecc7a816c521a1168a4dbd359fa7212a5123ab583ffa1cf485a5fed219575d6475dbcdd541638b2d3631a6c7fce7474e7fe3cba1d4d5853"), + hexEncPubkey("c2b01603b088a7182d0cf7ef29fb2b04c70acb320fccf78526bf9472e10c74ee70b3fcfa6f4b11d167bd7d3bc4d936b660f2c9bff934793d97cb21750e7c3d31"), + hexEncPubkey("20e4d8f45f2f863e94b45548c1ef22a11f7d36f263e4f8623761e05a64c4572379b000a52211751e2561b0f14f4fc92dd4130410c8ccc71eb4f0e95a700d4ca9"), + hexEncPubkey("27f4a16cc085e72d86e25c98bd2eca173eaaee7565c78ec5a52e9e12b2211f35de81b5b45e9195de2ebfe29106742c59112b951a04eb7ae48822911fc1f9389e"), + hexEncPubkey("55db5ee7d98e7f0b1c3b9d5be6f2bc619a1b86c3cdd513160ad4dcf267037a5fffad527ac15d50aeb32c59c13d1d4c1e567ebbf4de0d25236130c8361f9aac63"), + hexEncPubkey("883df308b0130fc928a8559fe50667a0fff80493bc09685d18213b2db241a3ad11310ed86b0ef662b3ce21fc3d9aa7f3fc24b8d9afe17c7407e9afd3345ae548"), + hexEncPubkey("c7af968cc9bc8200c3ee1a387405f7563be1dce6710a3439f42ea40657d0eae9d2b3c16c42d779605351fcdece4da637b9804e60ca08cfb89aec32c197beffa6"), + hexEncPubkey("3e66f2b788e3ff1d04106b80597915cd7afa06c405a7ae026556b6e583dca8e05cfbab5039bb9a1b5d06083ffe8de5780b1775550e7218f5e98624bf7af9a0a8"), + hexEncPubkey("4fc7f53764de3337fdaec0a711d35d3a923e72fa65025444d12230b3552ed43d9b2d1ad08ccb11f2d50c58809e6dd74dde910e195294fca3b47ae5a3967cc479"), + hexEncPubkey("bafdfdcf6ccaa989436752fa97c77477b6baa7deb374b16c095492c529eb133e8e2f99e1977012b64767b9d34b2cf6d2048ed489bd822b5139b523f6a423167b"), + hexEncPubkey("7f5d78008a4312fe059104ce80202c82b8915c2eb4411c6b812b16f7642e57c00f2c9425121f5cbac4257fe0b3e81ef5dea97ea2dbaa98f6a8b6fd4d1e5980bb"), + hexEncPubkey("598c37fe78f922751a052f463aeb0cb0bc7f52b7c2a4cf2da72ec0931c7c32175d4165d0f8998f7320e87324ac3311c03f9382a5385c55f0407b7a66b2acd864"), + hexEncPubkey("f758c4136e1c148777a7f3275a76e2db0b2b04066fd738554ec398c1c6cc9fb47e14a3b4c87bd47deaeab3ffd2110514c3855685a374794daff87b605b27ee2e"), + hexEncPubkey("0307bb9e4fd865a49dcf1fe4333d1b944547db650ab580af0b33e53c4fef6c789531110fac801bbcbce21fc4d6f61b6d5b24abdf5b22e3030646d579f6dca9c2"), + hexEncPubkey("82504b6eb49bb2c0f91a7006ce9cefdbaf6df38706198502c2e06601091fc9dc91e4f15db3410d45c6af355bc270b0f268d3dff560f956985c7332d4b10bd1ed"), + hexEncPubkey("b39b5b677b45944ceebe76e76d1f051de2f2a0ec7b0d650da52135743e66a9a5dba45f638258f9a7545d9a790c7fe6d3fdf82c25425c7887323e45d27d06c057"), }, 255: { - MustHexID("5c4d58d46e055dd1f093f81ee60a675e1f02f54da6206720adee4dccef9b67a31efc5c2a2949c31a04ee31beadc79aba10da31440a1f9ff2a24093c63c36d784"), - MustHexID("ea72161ffdd4b1e124c7b93b0684805f4c4b58d617ed498b37a145c670dbc2e04976f8785583d9c805ffbf343c31d492d79f841652bbbd01b61ed85640b23495"), - MustHexID("51caa1d93352d47a8e531692a3612adac1e8ac68d0a200d086c1c57ae1e1a91aa285ab242e8c52ef9d7afe374c9485b122ae815f1707b875569d0433c1c3ce85"), - MustHexID("c08397d5751b47bd3da044b908be0fb0e510d3149574dff7aeab33749b023bb171b5769990fe17469dbebc100bc150e798aeda426a2dcc766699a225fddd75c6"), - MustHexID("0222c1c194b749736e593f937fad67ee348ac57287a15c7e42877aa38a9b87732a408bca370f812efd0eedbff13e6d5b854bf3ba1dec431a796ed47f32552b09"), - MustHexID("03d859cd46ef02d9bfad5268461a6955426845eef4126de6be0fa4e8d7e0727ba2385b78f1a883a8239e95ebb814f2af8379632c7d5b100688eebc5841209582"), - MustHexID("64d5004b7e043c39ff0bd10cb20094c287721d5251715884c280a612b494b3e9e1c64ba6f67614994c7d969a0d0c0295d107d53fc225d47c44c4b82852d6f960"), - MustHexID("b0a5eefb2dab6f786670f35bf9641eefe6dd87fd3f1362bcab4aaa792903500ab23d88fae68411372e0813b057535a601d46e454323745a948017f6063a47b1f"), - MustHexID("0cc6df0a3433d448b5684d2a3ffa9d1a825388177a18f44ad0008c7bd7702f1ec0fc38b83506f7de689c3b6ecb552599927e29699eed6bb867ff08f80068b287"), - MustHexID("50772f7b8c03a4e153355fbbf79c8a80cf32af656ff0c7873c99911099d04a0dae0674706c357e0145ad017a0ade65e6052cb1b0d574fcd6f67da3eee0ace66b"), - MustHexID("1ae37829c9ef41f8b508b82259ebac76b1ed900d7a45c08b7970f25d2d48ddd1829e2f11423a18749940b6dab8598c6e416cef0efd47e46e51f29a0bc65b37cd"), - MustHexID("ba973cab31c2af091fc1644a93527d62b2394999e2b6ccbf158dd5ab9796a43d408786f1803ef4e29debfeb62fce2b6caa5ab2b24d1549c822a11c40c2856665"), - MustHexID("bc413ad270dd6ea25bddba78f3298b03b8ba6f8608ac03d06007d4116fa78ef5a0cfe8c80155089382fc7a193243ee5500082660cb5d7793f60f2d7d18650964"), - MustHexID("5a6a9ef07634d9eec3baa87c997b529b92652afa11473dfee41ef7037d5c06e0ddb9fe842364462d79dd31cff8a59a1b8d5bc2b810dea1d4cbbd3beb80ecec83"), - MustHexID("f492c6ee2696d5f682f7f537757e52744c2ae560f1090a07024609e903d334e9e174fc01609c5a229ddbcac36c9d21adaf6457dab38a25bfd44f2f0ee4277998"), - MustHexID("459e4db99298cb0467a90acee6888b08bb857450deac11015cced5104853be5adce5b69c740968bc7f931495d671a70cad9f48546d7cd203357fe9af0e8d2164"), + hexEncPubkey("5c4d58d46e055dd1f093f81ee60a675e1f02f54da6206720adee4dccef9b67a31efc5c2a2949c31a04ee31beadc79aba10da31440a1f9ff2a24093c63c36d784"), + hexEncPubkey("ea72161ffdd4b1e124c7b93b0684805f4c4b58d617ed498b37a145c670dbc2e04976f8785583d9c805ffbf343c31d492d79f841652bbbd01b61ed85640b23495"), + hexEncPubkey("51caa1d93352d47a8e531692a3612adac1e8ac68d0a200d086c1c57ae1e1a91aa285ab242e8c52ef9d7afe374c9485b122ae815f1707b875569d0433c1c3ce85"), + hexEncPubkey("c08397d5751b47bd3da044b908be0fb0e510d3149574dff7aeab33749b023bb171b5769990fe17469dbebc100bc150e798aeda426a2dcc766699a225fddd75c6"), + hexEncPubkey("0222c1c194b749736e593f937fad67ee348ac57287a15c7e42877aa38a9b87732a408bca370f812efd0eedbff13e6d5b854bf3ba1dec431a796ed47f32552b09"), + hexEncPubkey("03d859cd46ef02d9bfad5268461a6955426845eef4126de6be0fa4e8d7e0727ba2385b78f1a883a8239e95ebb814f2af8379632c7d5b100688eebc5841209582"), + hexEncPubkey("64d5004b7e043c39ff0bd10cb20094c287721d5251715884c280a612b494b3e9e1c64ba6f67614994c7d969a0d0c0295d107d53fc225d47c44c4b82852d6f960"), + hexEncPubkey("b0a5eefb2dab6f786670f35bf9641eefe6dd87fd3f1362bcab4aaa792903500ab23d88fae68411372e0813b057535a601d46e454323745a948017f6063a47b1f"), + hexEncPubkey("0cc6df0a3433d448b5684d2a3ffa9d1a825388177a18f44ad0008c7bd7702f1ec0fc38b83506f7de689c3b6ecb552599927e29699eed6bb867ff08f80068b287"), + hexEncPubkey("50772f7b8c03a4e153355fbbf79c8a80cf32af656ff0c7873c99911099d04a0dae0674706c357e0145ad017a0ade65e6052cb1b0d574fcd6f67da3eee0ace66b"), + hexEncPubkey("1ae37829c9ef41f8b508b82259ebac76b1ed900d7a45c08b7970f25d2d48ddd1829e2f11423a18749940b6dab8598c6e416cef0efd47e46e51f29a0bc65b37cd"), + hexEncPubkey("ba973cab31c2af091fc1644a93527d62b2394999e2b6ccbf158dd5ab9796a43d408786f1803ef4e29debfeb62fce2b6caa5ab2b24d1549c822a11c40c2856665"), + hexEncPubkey("bc413ad270dd6ea25bddba78f3298b03b8ba6f8608ac03d06007d4116fa78ef5a0cfe8c80155089382fc7a193243ee5500082660cb5d7793f60f2d7d18650964"), + hexEncPubkey("5a6a9ef07634d9eec3baa87c997b529b92652afa11473dfee41ef7037d5c06e0ddb9fe842364462d79dd31cff8a59a1b8d5bc2b810dea1d4cbbd3beb80ecec83"), + hexEncPubkey("f492c6ee2696d5f682f7f537757e52744c2ae560f1090a07024609e903d334e9e174fc01609c5a229ddbcac36c9d21adaf6457dab38a25bfd44f2f0ee4277998"), + hexEncPubkey("459e4db99298cb0467a90acee6888b08bb857450deac11015cced5104853be5adce5b69c740968bc7f931495d671a70cad9f48546d7cd203357fe9af0e8d2164"), }, 256: { - MustHexID("a8593af8a4aef7b806b5197612017951bac8845a1917ca9a6a15dd6086d608505144990b245785c4cd2d67a295701c7aac2aa18823fb0033987284b019656268"), - MustHexID("d2eebef914928c3aad77fc1b2a495f52d2294acf5edaa7d8a530b540f094b861a68fe8348a46a7c302f08ab609d85912a4968eacfea0740847b29421b4795d9e"), - MustHexID("b14bfcb31495f32b650b63cf7d08492e3e29071fdc73cf2da0da48d4b191a70ba1a65f42ad8c343206101f00f8a48e8db4b08bf3f622c0853e7323b250835b91"), - MustHexID("7feaee0d818c03eb30e4e0bf03ade0f3c21ca38e938a761aa1781cf70bda8cc5cd631a6cc53dd44f1d4a6d3e2dae6513c6c66ee50cb2f0e9ad6f7e319b309fd9"), - MustHexID("4ca3b657b139311db8d583c25dd5963005e46689e1317620496cc64129c7f3e52870820e0ec7941d28809311df6db8a2867bbd4f235b4248af24d7a9c22d1232"), - MustHexID("1181defb1d16851d42dd951d84424d6bd1479137f587fa184d5a8152be6b6b16ed08bcdb2c2ed8539bcde98c80c432875f9f724737c316a2bd385a39d3cab1d8"), - MustHexID("d9dd818769fa0c3ec9f553c759b92476f082817252a04a47dc1777740b1731d280058c66f982812f173a294acf4944a85ba08346e2de153ba3ba41ce8a62cb64"), - MustHexID("bd7c4f8a9e770aa915c771b15e107ca123d838762da0d3ffc53aa6b53e9cd076cffc534ec4d2e4c334c683f1f5ea72e0e123f6c261915ed5b58ac1b59f003d88"), - MustHexID("3dd5739c73649d510456a70e9d6b46a855864a4a3f744e088fd8c8da11b18e4c9b5f2d7da50b1c147b2bae5ca9609ae01f7a3cdea9dce34f80a91d29cd82f918"), - MustHexID("f0d7df1efc439b4bcc0b762118c1cfa99b2a6143a9f4b10e3c9465125f4c9fca4ab88a2504169bbcad65492cf2f50da9dd5d077c39574a944f94d8246529066b"), - MustHexID("dd598b9ba441448e5fb1a6ec6c5f5aa9605bad6e223297c729b1705d11d05f6bfd3d41988b694681ae69bb03b9a08bff4beab5596503d12a39bffb5cd6e94c7c"), - MustHexID("3fce284ac97e567aebae681b15b7a2b6df9d873945536335883e4bbc26460c064370537f323fd1ada828ea43154992d14ac0cec0940a2bd2a3f42ec156d60c83"), - MustHexID("7c8dfa8c1311cb14fb29a8ac11bca23ecc115e56d9fcf7b7ac1db9066aa4eb39f8b1dabf46e192a65be95ebfb4e839b5ab4533fef414921825e996b210dd53bd"), - MustHexID("cafa6934f82120456620573d7f801390ed5e16ed619613a37e409e44ab355ef755e83565a913b48a9466db786f8d4fbd590bfec474c2524d4a2608d4eafd6abd"), - MustHexID("9d16600d0dd310d77045769fed2cb427f32db88cd57d86e49390c2ba8a9698cfa856f775be2013237226e7bf47b248871cf865d23015937d1edeb20db5e3e760"), - MustHexID("17be6b6ba54199b1d80eff866d348ea11d8a4b341d63ad9a6681d3ef8a43853ac564d153eb2a8737f0afc9ab320f6f95c55aa11aaa13bbb1ff422fd16bdf8188"), + hexEncPubkey("a8593af8a4aef7b806b5197612017951bac8845a1917ca9a6a15dd6086d608505144990b245785c4cd2d67a295701c7aac2aa18823fb0033987284b019656268"), + hexEncPubkey("d2eebef914928c3aad77fc1b2a495f52d2294acf5edaa7d8a530b540f094b861a68fe8348a46a7c302f08ab609d85912a4968eacfea0740847b29421b4795d9e"), + hexEncPubkey("b14bfcb31495f32b650b63cf7d08492e3e29071fdc73cf2da0da48d4b191a70ba1a65f42ad8c343206101f00f8a48e8db4b08bf3f622c0853e7323b250835b91"), + hexEncPubkey("7feaee0d818c03eb30e4e0bf03ade0f3c21ca38e938a761aa1781cf70bda8cc5cd631a6cc53dd44f1d4a6d3e2dae6513c6c66ee50cb2f0e9ad6f7e319b309fd9"), + hexEncPubkey("4ca3b657b139311db8d583c25dd5963005e46689e1317620496cc64129c7f3e52870820e0ec7941d28809311df6db8a2867bbd4f235b4248af24d7a9c22d1232"), + hexEncPubkey("1181defb1d16851d42dd951d84424d6bd1479137f587fa184d5a8152be6b6b16ed08bcdb2c2ed8539bcde98c80c432875f9f724737c316a2bd385a39d3cab1d8"), + hexEncPubkey("d9dd818769fa0c3ec9f553c759b92476f082817252a04a47dc1777740b1731d280058c66f982812f173a294acf4944a85ba08346e2de153ba3ba41ce8a62cb64"), + hexEncPubkey("bd7c4f8a9e770aa915c771b15e107ca123d838762da0d3ffc53aa6b53e9cd076cffc534ec4d2e4c334c683f1f5ea72e0e123f6c261915ed5b58ac1b59f003d88"), + hexEncPubkey("3dd5739c73649d510456a70e9d6b46a855864a4a3f744e088fd8c8da11b18e4c9b5f2d7da50b1c147b2bae5ca9609ae01f7a3cdea9dce34f80a91d29cd82f918"), + hexEncPubkey("f0d7df1efc439b4bcc0b762118c1cfa99b2a6143a9f4b10e3c9465125f4c9fca4ab88a2504169bbcad65492cf2f50da9dd5d077c39574a944f94d8246529066b"), + hexEncPubkey("dd598b9ba441448e5fb1a6ec6c5f5aa9605bad6e223297c729b1705d11d05f6bfd3d41988b694681ae69bb03b9a08bff4beab5596503d12a39bffb5cd6e94c7c"), + hexEncPubkey("3fce284ac97e567aebae681b15b7a2b6df9d873945536335883e4bbc26460c064370537f323fd1ada828ea43154992d14ac0cec0940a2bd2a3f42ec156d60c83"), + hexEncPubkey("7c8dfa8c1311cb14fb29a8ac11bca23ecc115e56d9fcf7b7ac1db9066aa4eb39f8b1dabf46e192a65be95ebfb4e839b5ab4533fef414921825e996b210dd53bd"), + hexEncPubkey("cafa6934f82120456620573d7f801390ed5e16ed619613a37e409e44ab355ef755e83565a913b48a9466db786f8d4fbd590bfec474c2524d4a2608d4eafd6abd"), + hexEncPubkey("9d16600d0dd310d77045769fed2cb427f32db88cd57d86e49390c2ba8a9698cfa856f775be2013237226e7bf47b248871cf865d23015937d1edeb20db5e3e760"), + hexEncPubkey("17be6b6ba54199b1d80eff866d348ea11d8a4b341d63ad9a6681d3ef8a43853ac564d153eb2a8737f0afc9ab320f6f95c55aa11aaa13bbb1ff422fd16bdf8188"), }, }, } type preminedTestnet struct { - target NodeID - targetSha common.Hash // sha3(target) - dists [hashBits + 1][]NodeID + target encPubkey + targetSha enode.ID // sha3(target) + dists [hashBits + 1][]encPubkey } -func (tn *preminedTestnet) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node, error) { +func (tn *preminedTestnet) self() *enode.Node { + return nullNode +} + +func (tn *preminedTestnet) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { // current log distance is encoded in port number // fmt.Println("findnode query at dist", toaddr.Port) if toaddr.Port == 0 { panic("query to node at distance 0") } - next := uint16(toaddr.Port) - 1 - var result []*Node - for i, id := range tn.dists[toaddr.Port] { - result = append(result, NewNode(id, net.ParseIP("127.0.0.1"), next, uint16(i))) + next := toaddr.Port - 1 + var result []*node + for i, ekey := range tn.dists[toaddr.Port] { + key, _ := decodePubkey(ekey) + node := wrapNode(enode.NewV4(key, net.ParseIP("127.0.0.1"), i, next)) + result = append(result, node) } return result, nil } -func (*preminedTestnet) close() {} -func (*preminedTestnet) waitping(from NodeID) error { return nil } -func (*preminedTestnet) ping(toid NodeID, toaddr *net.UDPAddr) error { return nil } +func (*preminedTestnet) close() {} +func (*preminedTestnet) waitping(from enode.ID) error { return nil } +func (*preminedTestnet) ping(toid enode.ID, toaddr *net.UDPAddr) error { return nil } // mine generates a testnet struct literal with nodes at // various distances to the given target. -func (tn *preminedTestnet) mine(target NodeID) { +func (tn *preminedTestnet) mine(target encPubkey) { tn.target = target - tn.targetSha = crypto.Keccak256Hash(tn.target[:]) + tn.targetSha = tn.target.id() found := 0 for found < bucketSize*10 { k := newkey() - id := PubkeyID(&k.PublicKey) - sha := crypto.Keccak256Hash(id[:]) - ld := logdist(tn.targetSha, sha) + key := encodePubkey(&k.PublicKey) + ld := enode.LogDist(tn.targetSha, key.id()) if len(tn.dists[ld]) < bucketSize { - tn.dists[ld] = append(tn.dists[ld], id) + tn.dists[ld] = append(tn.dists[ld], key) fmt.Println("found ID with ld", ld) found++ } @@ -601,14 +556,14 @@ func (tn *preminedTestnet) mine(target NodeID) { fmt.Println("&preminedTestnet{") fmt.Printf(" target: %#v,\n", tn.target) fmt.Printf(" targetSha: %#v,\n", tn.targetSha) - fmt.Printf(" dists: [%d][]NodeID{\n", len(tn.dists)) + fmt.Printf(" dists: [%d][]encPubkey{\n", len(tn.dists)) for ld, ns := range tn.dists { if len(ns) == 0 { continue } - fmt.Printf(" %d: []NodeID{\n", ld) + fmt.Printf(" %d: []encPubkey{\n", ld) for _, n := range ns { - fmt.Printf(" MustHexID(\"%x\"),\n", n[:]) + fmt.Printf(" hexEncPubkey(\"%x\"),\n", n[:]) } fmt.Println(" },") } @@ -616,31 +571,6 @@ func (tn *preminedTestnet) mine(target NodeID) { fmt.Println("}") } -func hasDuplicates(slice []*Node) bool { - seen := make(map[NodeID]bool) - for i, e := range slice { - if e == nil { - panic(fmt.Sprintf("nil *Node at %d", i)) - } - if seen[e.ID] { - return true - } - seen[e.ID] = true - } - return false -} - -func sortedByDistanceTo(distbase common.Hash, slice []*Node) bool { - var last common.Hash - for i, e := range slice { - if i > 0 && distcmp(distbase, e.sha, last) < 0 { - return false - } - last = e.sha - } - return true -} - // gen wraps quick.Value so it's easier to use. // it generates a random value of the given value's type. func gen(typ interface{}, rand *rand.Rand) interface{} { @@ -651,6 +581,19 @@ func gen(typ interface{}, rand *rand.Rand) interface{} { return v.Interface() } +func genIP(rand *rand.Rand) net.IP { + ip := make(net.IP, 4) + rand.Read(ip) + return ip +} + +func quickcfg() *quick.Config { + return &quick.Config{ + MaxCount: 5000, + Rand: rand.New(rand.NewSource(time.Now().Unix())), + } +} + func newkey() *ecdsa.PrivateKey { key, err := crypto.GenerateKey() if err != nil { diff --git a/p2p/discover/table_util_test.go b/p2p/discover/table_util_test.go new file mode 100644 index 000000000000..afc435e8c94f --- /dev/null +++ b/p2p/discover/table_util_test.go @@ -0,0 +1,195 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package discover + +import ( + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/rand" + "net" + "sync" + + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" +) + +var nullNode *enode.Node + +func init() { + var r enr.Record + r.Set(enr.IP{0, 0, 0, 0}) + nullNode = enode.SignNull(&r, enode.ID{}) +} + +func newTestTable(t transport) (*Table, *enode.DB) { + db, _ := enode.OpenDB("") + tab, _ := newTable(t, db, nil) + return tab, db +} + +// nodeAtDistance creates a node for which enode.LogDist(base, n.id) == ld. +func nodeAtDistance(base enode.ID, ld int, ip net.IP) *node { + var r enr.Record + r.Set(enr.IP(ip)) + return wrapNode(enode.SignNull(&r, idAtDistance(base, ld))) +} + +// idAtDistance returns a random hash such that enode.LogDist(a, b) == n +func idAtDistance(a enode.ID, n int) (b enode.ID) { + if n == 0 { + return a + } + // flip bit at position n, fill the rest with random bits + b = a + pos := len(a) - n/8 - 1 + bit := byte(0x01) << (byte(n%8) - 1) + if bit == 0 { + pos++ + bit = 0x80 + } + b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits + for i := pos + 1; i < len(a); i++ { + b[i] = byte(rand.Intn(255)) + } + return b +} + +func intIP(i int) net.IP { + return net.IP{byte(i), 0, 2, byte(i)} +} + +// fillBucket inserts nodes into the given bucket until it is full. +func fillBucket(tab *Table, n *node) (last *node) { + ld := enode.LogDist(tab.self().ID(), n.ID()) + b := tab.bucket(n.ID()) + for len(b.entries) < bucketSize { + b.entries = append(b.entries, nodeAtDistance(tab.self().ID(), ld, intIP(ld))) + } + return b.entries[bucketSize-1] +} + +// fillTable adds nodes the table to the end of their corresponding bucket +// if the bucket is not full. The caller must not hold tab.mutex. +func fillTable(tab *Table, nodes []*node) { + tab.mutex.Lock() + defer tab.mutex.Unlock() + + for _, n := range nodes { + if n.ID() == tab.self().ID() { + continue // don't add self + } + b := tab.bucket(n.ID()) + if len(b.entries) < bucketSize { + tab.bumpOrAdd(b, n) + } + } +} + +type pingRecorder struct { + mu sync.Mutex + dead, pinged map[enode.ID]bool + n *enode.Node +} + +func newPingRecorder() *pingRecorder { + var r enr.Record + r.Set(enr.IP{0, 0, 0, 0}) + n := enode.SignNull(&r, enode.ID{}) + + return &pingRecorder{ + dead: make(map[enode.ID]bool), + pinged: make(map[enode.ID]bool), + n: n, + } +} + +func (t *pingRecorder) self() *enode.Node { + return nullNode +} + +func (t *pingRecorder) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { + return nil, nil +} + +func (t *pingRecorder) ping(toid enode.ID, toaddr *net.UDPAddr) error { + t.mu.Lock() + defer t.mu.Unlock() + + t.pinged[toid] = true + if t.dead[toid] { + return errTimeout + } else { + return nil + } +} + +func (t *pingRecorder) close() {} + +func hasDuplicates(slice []*node) bool { + seen := make(map[enode.ID]bool) + for i, e := range slice { + if e == nil { + panic(fmt.Sprintf("nil *Node at %d", i)) + } + if seen[e.ID()] { + return true + } + seen[e.ID()] = true + } + return false +} + +func contains(ns []*node, id enode.ID) bool { + for _, n := range ns { + if n.ID() == id { + return true + } + } + return false +} + +func sortedByDistanceTo(distbase enode.ID, slice []*node) bool { + var last enode.ID + for i, e := range slice { + if i > 0 && enode.DistCmp(distbase, e.ID(), last) < 0 { + return false + } + last = e.ID() + } + return true +} + +func hexEncPubkey(h string) (ret encPubkey) { + b, err := hex.DecodeString(h) + if err != nil { + panic(err) + } + if len(b) != len(ret) { + panic("invalid length") + } + copy(ret[:], b) + return ret +} + +func hexPubkey(h string) *ecdsa.PublicKey { + k, err := decodePubkey(hexEncPubkey(h)) + if err != nil { + panic(err) + } + return k +} diff --git a/p2p/discover/udp.go b/p2p/discover/udp.go index 6e0e8acdf530..bfe317c091cc 100644 --- a/p2p/discover/udp.go +++ b/p2p/discover/udp.go @@ -23,17 +23,16 @@ import ( "errors" "fmt" "net" + "sync" "time" "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/p2p/nat" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" "github.com/XinFinOrg/XDPoSChain/rlp" ) -const Version = 4 - // Errors var ( errPacketTooSmall = errors.New("too small") @@ -48,8 +47,9 @@ var ( // Timeouts const ( - respTimeout = 500 * time.Millisecond - expiration = 20 * time.Second + respTimeout = 500 * time.Millisecond + expiration = 20 * time.Second + bondExpiration = 24 * time.Hour ntpFailureThreshold = 32 // Continuous timeouts after which to check NTP ntpWarningCooldown = 10 * time.Minute // Minimum amount of time to pass before repeating NTP warning @@ -62,12 +62,13 @@ const ( pongPacket findnodePacket neighborsPacket - pingXDC ) // RPC request structures type ( ping struct { + senderKey *ecdsa.PublicKey // filled in by preverify + Version uint From, To rpcEndpoint Expiration uint64 @@ -90,7 +91,7 @@ type ( // findnode is a query for nodes close to the given target. findnode struct { - Target NodeID // doesn't need to be an actual public key + Target encPubkey Expiration uint64 // Ignore additional fields (for forward compatibility). Rest []rlp.RawValue `rlp:"tail"` @@ -108,7 +109,7 @@ type ( IP net.IP // len 4 for IPv4 or 16 for IPv6 UDP uint16 // for discovery protocol TCP uint16 // for RLPx protocol - ID NodeID + ID encPubkey } rpcEndpoint struct { @@ -119,14 +120,16 @@ type ( ) func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint { - ip := addr.IP.To4() - if ip == nil { - ip = addr.IP.To16() + ip := net.IP{} + if ip4 := addr.IP.To4(); ip4 != nil { + ip = ip4 + } else if ip6 := addr.IP.To16(); ip6 != nil { + ip = ip6 } return rpcEndpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort} } -func (t *udp) nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*Node, error) { +func (t *udp) nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*node, error) { if rn.UDP <= 1024 { return nil, errors.New("low port") } @@ -134,19 +137,33 @@ func (t *udp) nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*Node, error) { return nil, err } if t.netrestrict != nil && !t.netrestrict.Contains(rn.IP) { - return nil, errors.New("not contained in netrestrict allowlist") + return nil, errors.New("not contained in netrestrict whitelist") } - n := NewNode(rn.ID, rn.IP, rn.UDP, rn.TCP) - err := n.validateComplete() + key, err := decodePubkey(rn.ID) + if err != nil { + return nil, err + } + n := wrapNode(enode.NewV4(key, rn.IP, int(rn.TCP), int(rn.UDP))) + err = n.ValidateComplete() return n, err } -func nodeToRPC(n *Node) rpcNode { - return rpcNode{ID: n.ID, IP: n.IP, UDP: n.UDP, TCP: n.TCP} +func nodeToRPC(n *node) rpcNode { + var key ecdsa.PublicKey + var ekey encPubkey + if err := n.Load((*enode.Secp256k1)(&key)); err == nil { + ekey = encodePubkey(&key) + } + return rpcNode{ID: ekey, IP: n.IP(), UDP: uint16(n.UDP()), TCP: uint16(n.TCP())} } +// packet is implemented by all protocol messages. type packet interface { - handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error + // preverify checks whether the packet is valid and should be handled at all. + preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error + // handle handles the packet. + handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) + // name returns the name of the packet for logging purposes. name() string } @@ -157,54 +174,58 @@ type conn interface { LocalAddr() net.Addr } -// udp implements the RPC protocol. +// udp implements the discovery v4 UDP wire protocol. type udp struct { conn conn netrestrict *netutil.Netlist priv *ecdsa.PrivateKey - ourEndpoint rpcEndpoint - - addpending chan *pending - gotreply chan reply - - closing chan struct{} - nat nat.Interface - - *Table + localNode *enode.LocalNode + db *enode.DB + tab *Table + wg sync.WaitGroup + + addReplyMatcher chan *replyMatcher + gotreply chan reply + closing chan struct{} } // pending represents a pending reply. // -// some implementations of the protocol wish to send more than one -// reply packet to findnode. in general, any neighbors packet cannot +// Some implementations of the protocol wish to send more than one +// reply packet to findnode. In general, any neighbors packet cannot // be matched up with a specific findnode packet. // -// our implementation handles this by storing a callback function for -// each pending reply. incoming packets from a node are dispatched -// to all the callback functions for that node. -type pending struct { +// Our implementation handles this by storing a callback function for +// each pending reply. Incoming packets from a node are dispatched +// to all callback functions for that node. +type replyMatcher struct { // these fields must match in the reply. - from NodeID + from enode.ID + ip net.IP ptype byte // time when the request must complete deadline time.Time - // callback is called when a matching reply arrives. if it returns - // true, the callback is removed from the pending reply queue. - // if it returns false, the reply is considered incomplete and - // the callback will be invoked again for the next matching reply. - callback func(resp interface{}) (done bool) + // callback is called when a matching reply arrives. If it returns matched == true, the + // reply was acceptable. The second return value indicates whether the callback should + // be removed from the pending reply queue. If it returns false, the reply is considered + // incomplete and the callback will be invoked again for the next matching reply. + callback replyMatchFunc // errc receives nil when the callback indicates completion or an // error if no further reply is received within the timeout. errc chan<- error } +type replyMatchFunc func(interface{}) (matched bool, requestDone bool) + type reply struct { - from NodeID + from enode.ID + ip net.IP ptype byte - data interface{} + data packet + // loop indicates whether there was // a matching request by sending on this channel. matched chan<- bool @@ -222,87 +243,110 @@ type Config struct { PrivateKey *ecdsa.PrivateKey // These settings are optional: - AnnounceAddr *net.UDPAddr // local address announced in the DHT - NodeDBPath string // if set, the node database is stored at this filesystem location - NetRestrict *netutil.Netlist // network allowlist - Bootnodes []*Node // list of bootstrap nodes - Unhandled chan<- ReadPacket // unhandled packets are sent on this channel - - // The options below are useful in very specific cases, like in unit tests. - Log log.Logger // if set, log messages go here + NetRestrict *netutil.Netlist // network whitelist + Bootnodes []*enode.Node // list of bootstrap nodes + Unhandled chan<- ReadPacket // unhandled packets are sent on this channel } // ListenUDP returns a new table that listens for UDP packets on laddr. -func ListenUDP(c conn, cfg Config) (*Table, error) { - tab, _, err := newUDP(c, cfg) +func ListenUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, error) { + tab, _, err := newUDP(c, ln, cfg) if err != nil { return nil, err } - log.Info("UDP listener up", "self", tab.self) return tab, nil } -func newUDP(c conn, cfg Config) (*Table, *udp, error) { +func newUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, *udp, error) { udp := &udp{ - conn: c, - priv: cfg.PrivateKey, - netrestrict: cfg.NetRestrict, - closing: make(chan struct{}), - gotreply: make(chan reply), - addpending: make(chan *pending), - } - realaddr := c.LocalAddr().(*net.UDPAddr) - if cfg.AnnounceAddr != nil { - realaddr = cfg.AnnounceAddr - } - // TODO: separate TCP port - udp.ourEndpoint = makeEndpoint(realaddr, uint16(realaddr.Port)) - tab, err := newMeteredTable(udp, PubkeyID(&cfg.PrivateKey.PublicKey), realaddr, cfg.NodeDBPath, cfg.Bootnodes) + conn: c, + priv: cfg.PrivateKey, + netrestrict: cfg.NetRestrict, + localNode: ln, + db: ln.Database(), + closing: make(chan struct{}), + gotreply: make(chan reply), + addReplyMatcher: make(chan *replyMatcher), + } + tab, err := newTable(udp, ln.Database(), cfg.Bootnodes) if err != nil { return nil, nil, err } - udp.Table = tab + udp.tab = tab + udp.wg.Add(2) go udp.loop() go udp.readLoop(cfg.Unhandled) - return udp.Table, udp, nil + return udp.tab, udp, nil +} + +func (t *udp) self() *enode.Node { + return t.localNode.Node() } func (t *udp) close() { close(t.closing) t.conn.Close() - // TODO: wait for the loops to end. + t.wg.Wait() +} + +func (t *udp) ourEndpoint() rpcEndpoint { + n := t.self() + a := &net.UDPAddr{IP: n.IP(), Port: n.UDP()} + return makeEndpoint(a, uint16(n.TCP())) } // ping sends a ping message to the given node and waits for a reply. -func (t *udp) ping(toid NodeID, toaddr *net.UDPAddr) error { +func (t *udp) ping(toid enode.ID, toaddr *net.UDPAddr) error { + return <-t.sendPing(toid, toaddr, nil) +} + +// sendPing sends a ping message to the given node and invokes the callback +// when the reply arrives. +func (t *udp) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <-chan error { req := &ping{ - Version: Version, - From: t.ourEndpoint, + Version: 4, + From: t.ourEndpoint(), To: makeEndpoint(toaddr, 0), // TODO: maybe use known TCP port from DB Expiration: uint64(time.Now().Add(expiration).Unix()), } - packet, hash, err := encodePacket(t.priv, pingXDC, req) + packet, hash, err := encodePacket(t.priv, pingPacket, req) if err != nil { - return err - } - errc := t.pending(toid, pongPacket, func(p interface{}) bool { - return bytes.Equal(p.(*pong).ReplyTok, hash) + errc := make(chan error, 1) + errc <- err + return errc + } + // Add a matcher for the reply to the pending reply queue. Pongs are matched if they + // reference the ping we're about to send. + errc := t.pending(toid, toaddr.IP, pongPacket, func(p interface{}) (matched bool, requestDone bool) { + matched = bytes.Equal(p.(*pong).ReplyTok, hash) + if matched && callback != nil { + callback() + } + return matched, matched }) - t.write(toaddr, req.name(), packet) - return <-errc -} - -func (t *udp) waitping(from NodeID) error { - return <-t.pending(from, pingXDC, func(interface{}) bool { return true }) + // Send the packet. + t.localNode.UDPContact(toaddr) + t.write(toaddr, toid, req.name(), packet) + return errc } // findnode sends a findnode request to the given node and waits until // the node has sent up to k neighbors. -func (t *udp) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node, error) { - nodes := make([]*Node, 0, bucketSize) +func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) { + // If we haven't seen a ping from the destination node for a while, it won't remember + // our endpoint proof and reject findnode. Solicit a ping first. + if time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration { + t.ping(toid, toaddr) + // Wait for them to ping back and process our pong. + time.Sleep(respTimeout) + } + + // Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is + // active until enough nodes have been received. + nodes := make([]*node, 0, bucketSize) nreceived := 0 - errc := t.pending(toid, neighborsPacket, func(r interface{}) bool { + errc := t.pending(toid, toaddr.IP, neighborsPacket, func(r interface{}) (matched bool, requestDone bool) { reply := r.(*neighbors) for _, rn := range reply.Nodes { nreceived++ @@ -313,23 +357,22 @@ func (t *udp) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node } nodes = append(nodes, n) } - return nreceived >= bucketSize + return true, nreceived >= bucketSize }) - t.send(toaddr, findnodePacket, &findnode{ + t.send(toaddr, toid, findnodePacket, &findnode{ Target: target, Expiration: uint64(time.Now().Add(expiration).Unix()), }) - err := <-errc - return nodes, err + return nodes, <-errc } -// pending adds a reply callback to the pending reply queue. -// see the documentation of type pending for a detailed explanation. -func (t *udp) pending(id NodeID, ptype byte, callback func(interface{}) bool) <-chan error { +// pending adds a reply matcher to the pending reply queue. +// see the documentation of type replyMatcher for a detailed explanation. +func (t *udp) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) <-chan error { ch := make(chan error, 1) - p := &pending{from: id, ptype: ptype, callback: callback, errc: ch} + p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch} select { - case t.addpending <- p: + case t.addReplyMatcher <- p: // loop will handle it case <-t.closing: ch <- errClosed @@ -337,10 +380,12 @@ func (t *udp) pending(id NodeID, ptype byte, callback func(interface{}) bool) <- return ch } -func (t *udp) handleReply(from NodeID, ptype byte, req packet) bool { +// handleReply dispatches a reply packet, invoking reply matchers. It returns +// whether any matcher considered the packet acceptable. +func (t *udp) handleReply(from enode.ID, fromIP net.IP, ptype byte, req packet) bool { matched := make(chan bool, 1) select { - case t.gotreply <- reply{from, ptype, req, matched}: + case t.gotreply <- reply{from, fromIP, ptype, req, matched}: // loop will handle it return <-matched case <-t.closing: @@ -351,11 +396,13 @@ func (t *udp) handleReply(from NodeID, ptype byte, req packet) bool { // loop runs in its own goroutine. it keeps track of // the refresh timer and the pending reply queue. func (t *udp) loop() { + defer t.wg.Done() + var ( plist = list.New() timeout = time.NewTimer(0) - nextTimeout *pending // head of plist when timeout was last reset - contTimeouts = 0 // number of continuous timeouts to do NTP checks + nextTimeout *replyMatcher // head of plist when timeout was last reset + contTimeouts = 0 // number of continuous timeouts to do NTP checks ntpWarnTime = time.Unix(0, 0) ) <-timeout.C // ignore first timeout @@ -368,7 +415,7 @@ func (t *udp) loop() { // Start the timer so it fires when the next pending reply has expired. now := time.Now() for el := plist.Front(); el != nil; el = el.Next() { - nextTimeout = el.Value.(*pending) + nextTimeout = el.Value.(*replyMatcher) if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout { timeout.Reset(dist) return @@ -389,25 +436,23 @@ func (t *udp) loop() { select { case <-t.closing: for el := plist.Front(); el != nil; el = el.Next() { - el.Value.(*pending).errc <- errClosed + el.Value.(*replyMatcher).errc <- errClosed } return - case p := <-t.addpending: + case p := <-t.addReplyMatcher: p.deadline = time.Now().Add(respTimeout) plist.PushBack(p) case r := <-t.gotreply: - var matched bool + var matched bool // whether any replyMatcher considered the reply acceptable. for el := plist.Front(); el != nil; el = el.Next() { - p := el.Value.(*pending) - if p.from == r.from && p.ptype == r.ptype { - matched = true - // Remove the matcher if its callback indicates - // that all replies have been received. This is - // required for packet types that expect multiple - // reply packets. - if p.callback(r.data) { + p := el.Value.(*replyMatcher) + if p.from == r.from && p.ptype == r.ptype && p.ip.Equal(r.ip) { + ok, requestDone := p.callback(r.data) + matched = matched || ok + // Remove the matcher if callback indicates that all replies have been received. + if requestDone { p.errc <- nil plist.Remove(el) } @@ -422,7 +467,7 @@ func (t *udp) loop() { // Notify and remove callbacks whose deadline is in the past. for el := plist.Front(); el != nil; el = el.Next() { - p := el.Value.(*pending) + p := el.Value.(*replyMatcher) if now.After(p.deadline) || now.Equal(p.deadline) { p.errc <- errTimeout plist.Remove(el) @@ -473,17 +518,17 @@ func init() { } } -func (t *udp) send(toaddr *net.UDPAddr, ptype byte, req packet) ([]byte, error) { +func (t *udp) send(toaddr *net.UDPAddr, toid enode.ID, ptype byte, req packet) ([]byte, error) { packet, hash, err := encodePacket(t.priv, ptype, req) if err != nil { return hash, err } - return hash, t.write(toaddr, req.name(), packet) + return hash, t.write(toaddr, toid, req.name(), packet) } -func (t *udp) write(toaddr *net.UDPAddr, what string, packet []byte) error { +func (t *udp) write(toaddr *net.UDPAddr, toid enode.ID, what string, packet []byte) error { _, err := t.conn.WriteToUDP(packet, toaddr) - log.Trace(">> "+what, "addr", toaddr, "err", err) + log.Trace(">> "+what, "id", toid, "addr", toaddr, "err", err) return err } @@ -512,10 +557,11 @@ func encodePacket(priv *ecdsa.PrivateKey, ptype byte, req interface{}) (packet, // readLoop runs in its own goroutine. it handles incoming UDP packets. func (t *udp) readLoop(unhandled chan<- ReadPacket) { - defer t.conn.Close() + defer t.wg.Done() if unhandled != nil { defer close(unhandled) } + // Discovery packets are defined to be no larger than 1280 bytes. // Packets larger than this size will be cut at the end and treated // as invalid because their hash won't match. @@ -541,32 +587,37 @@ func (t *udp) readLoop(unhandled chan<- ReadPacket) { } func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error { - packet, fromID, hash, err := decodePacket(buf) + packet, fromKey, hash, err := decodePacket(buf) if err != nil { log.Debug("Bad discv4 packet", "addr", from, "err", err) return err } - err = packet.handle(t, from, fromID, hash) - log.Trace("<< "+packet.name(), "addr", from, "err", err) + fromID := fromKey.id() + err = packet.preverify(t, from, fromID, fromKey) + log.Trace("<< "+packet.name(), "id", fromID, "addr", from, "err", err) + if err == nil { + packet.handle(t, from, fromID, hash) + } return err } -func decodePacket(buf []byte) (packet, NodeID, []byte, error) { +func decodePacket(buf []byte) (packet, encPubkey, []byte, error) { if len(buf) < headSize+1 { - return nil, NodeID{}, nil, errPacketTooSmall + return nil, encPubkey{}, nil, errPacketTooSmall } hash, sig, sigdata := buf[:macSize], buf[macSize:headSize], buf[headSize:] shouldhash := crypto.Keccak256(buf[macSize:]) if !bytes.Equal(hash, shouldhash) { - return nil, NodeID{}, nil, errBadHash + return nil, encPubkey{}, nil, errBadHash } - fromID, err := recoverNodeID(crypto.Keccak256(buf[headSize:]), sig) + fromKey, err := recoverNodeKey(crypto.Keccak256(buf[headSize:]), sig) if err != nil { - return nil, NodeID{}, hash, err + return nil, fromKey, hash, err } + var req packet switch ptype := sigdata[0]; ptype { - case pingXDC: + case pingPacket: req = new(ping) case pongPacket: req = new(pong) @@ -575,94 +626,126 @@ func decodePacket(buf []byte) (packet, NodeID, []byte, error) { case neighborsPacket: req = new(neighbors) default: - return nil, fromID, hash, fmt.Errorf("unknown type: %d", ptype) + return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype) } s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0) err = s.Decode(req) - return req, fromID, hash, err + return req, fromKey, hash, err } -func (req *ping) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { +// Packet Handlers + +func (req *ping) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - t.send(from, pongPacket, &pong{ + key, err := decodePubkey(fromKey) + if err != nil { + return errors.New("invalid public key") + } + req.senderKey = key + return nil +} + +func (req *ping) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) { + // Reply. + t.send(from, fromID, pongPacket, &pong{ To: makeEndpoint(from, req.From.TCP), ReplyTok: mac, Expiration: uint64(time.Now().Add(expiration).Unix()), }) - if !t.handleReply(fromID, pingXDC, req) { - // Note: we're ignoring the provided IP address right now - go t.bond(true, fromID, from, req.From.TCP) + + // Ping back if our last pong on file is too far in the past. + n := wrapNode(enode.NewV4(req.senderKey, from.IP, int(req.From.TCP), from.Port)) + if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration { + t.sendPing(fromID, from, func() { + t.tab.addThroughPing(n) + }) + } else { + t.tab.addThroughPing(n) } - return nil + + // Update node database and endpoint predictor. + t.db.UpdateLastPingReceived(n.ID(), from.IP, time.Now()) + t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) } -func (req *ping) name() string { return "PING XDC/v4" } +func (req *ping) name() string { return "PING/v4" } -func (req *pong) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { +func (req *pong) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if !t.handleReply(fromID, pongPacket, req) { + if !t.handleReply(fromID, from.IP, pongPacket, req) { return errUnsolicitedReply } return nil } +func (req *pong) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) { + t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)}) + t.db.UpdateLastPongReceived(fromID, from.IP, time.Now()) +} + func (req *pong) name() string { return "PONG/v4" } -func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { +func (req *findnode) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if !t.db.hasBond(fromID) { - // No bond exists, we don't process the packet. This prevents - // an attack vector where the discovery protocol could be used - // to amplify traffic in a DDOS attack. A malicious actor - // would send a findnode request with the IP address and UDP - // port of the target as the source address. The recipient of - // the findnode packet would then send a neighbors packet - // (which is a much bigger packet than findnode) to the victim. + if time.Since(t.db.LastPongReceived(fromID, from.IP)) > bondExpiration { + // No endpoint proof pong exists, we don't process the packet. This prevents an + // attack vector where the discovery protocol could be used to amplify traffic in a + // DDOS attack. A malicious actor would send a findnode request with the IP address + // and UDP port of the target as the source address. The recipient of the findnode + // packet would then send a neighbors packet (which is a much bigger packet than + // findnode) to the victim. return errUnknownNode } - target := crypto.Keccak256Hash(req.Target[:]) - t.mutex.Lock() - closest := t.closest(target, bucketSize).entries - t.mutex.Unlock() - log.Trace("find neighbors ", "from", from, "fromID", fromID, "closest", len(closest)) - p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())} - var sent bool + return nil +} + +func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) { + // Determine closest nodes. + target := enode.ID(crypto.Keccak256Hash(req.Target[:])) + t.tab.mutex.Lock() + closest := t.tab.closest(target, bucketSize).entries + t.tab.mutex.Unlock() + // Send neighbors in chunks with at most maxNeighbors per packet // to stay below the 1280 byte limit. + p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())} + var sent bool for _, n := range closest { - if netutil.CheckRelayIP(from.IP, n.IP) == nil { + if netutil.CheckRelayIP(from.IP, n.IP()) == nil { p.Nodes = append(p.Nodes, nodeToRPC(n)) } if len(p.Nodes) == maxNeighbors { - t.send(from, neighborsPacket, &p) + t.send(from, fromID, neighborsPacket, &p) p.Nodes = p.Nodes[:0] sent = true } } if len(p.Nodes) > 0 || !sent { - t.send(from, neighborsPacket, &p) + t.send(from, fromID, neighborsPacket, &p) } - return nil } func (req *findnode) name() string { return "FINDNODE/v4" } -func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { +func (req *neighbors) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error { if expired(req.Expiration) { return errExpired } - if !t.handleReply(fromID, neighborsPacket, req) { + if !t.handleReply(fromID, from.IP, neighborsPacket, req) { return errUnsolicitedReply } return nil } +func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) { +} + func (req *neighbors) name() string { return "NEIGHBORS/v4" } func expired(ts uint64) bool { diff --git a/p2p/discover/udp_test.go b/p2p/discover/udp_test.go index 7ec99cc422e8..5bfd3a9cf000 100644 --- a/p2p/discover/udp_test.go +++ b/p2p/discover/udp_test.go @@ -19,6 +19,7 @@ package discover import ( "bytes" "crypto/ecdsa" + crand "crypto/rand" "encoding/binary" "encoding/hex" "errors" @@ -35,6 +36,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/rlp" "github.com/davecgh/go-spew/spew" ) @@ -46,7 +48,7 @@ func init() { // shared test variables var ( futureExp = uint64(time.Now().Add(10 * time.Hour).Unix()) - testTarget = NodeID{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1} + testTarget = encPubkey{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1} testRemote = rpcEndpoint{IP: net.ParseIP("1.1.1.1").To4(), UDP: 1, TCP: 2} testLocalAnnounced = rpcEndpoint{IP: net.ParseIP("2.2.2.2").To4(), UDP: 3, TCP: 4} testLocal = rpcEndpoint{IP: net.ParseIP("3.3.3.3").To4(), UDP: 5, TCP: 6} @@ -56,6 +58,7 @@ type udpTest struct { t *testing.T pipe *dgramPipe table *Table + db *enode.DB udp *udp sent [][]byte localkey, remotekey *ecdsa.PrivateKey @@ -70,20 +73,32 @@ func newUDPTest(t *testing.T) *udpTest { remotekey: newkey(), remoteaddr: &net.UDPAddr{IP: net.IP{10, 0, 1, 99}, Port: 30303}, } - test.table, test.udp, _ = newUDP(test.pipe, Config{PrivateKey: test.localkey}) + test.db, _ = enode.OpenDB("") + ln := enode.NewLocalNode(test.db, test.localkey) + test.table, test.udp, _ = newUDP(test.pipe, ln, Config{PrivateKey: test.localkey}) // Wait for initial refresh so the table doesn't send unexpected findnode. <-test.table.initDone return test } +func (test *udpTest) close() { + test.table.Close() + test.db.Close() +} + // handles a packet as if it had been sent to the transport. func (test *udpTest) packetIn(wantError error, ptype byte, data packet) error { - enc, _, err := encodePacket(test.remotekey, ptype, data) + return test.packetInFrom(wantError, test.remotekey, test.remoteaddr, ptype, data) +} + +// handles a packet as if it had been sent to the transport by the key/endpoint. +func (test *udpTest) packetInFrom(wantError error, key *ecdsa.PrivateKey, addr *net.UDPAddr, ptype byte, data packet) error { + enc, _, err := encodePacket(key, ptype, data) if err != nil { return test.errorf("packet (%d) encode error: %v", ptype, err) } test.sent = append(test.sent, enc) - if err = test.udp.handlePacket(test.remoteaddr, enc); err != wantError { + if err = test.udp.handlePacket(addr, enc); err != wantError { return test.errorf("error mismatch: got %q, want %q", err, wantError) } return nil @@ -91,19 +106,19 @@ func (test *udpTest) packetIn(wantError error, ptype byte, data packet) error { // waits for a packet to be sent by the transport. // validate should have type func(*udpTest, X) error, where X is a packet type. -func (test *udpTest) waitPacketOut(validate interface{}) ([]byte, error) { +func (test *udpTest) waitPacketOut(validate interface{}) (*net.UDPAddr, []byte, error) { dgram := test.pipe.waitPacketOut() - p, _, hash, err := decodePacket(dgram) + p, _, hash, err := decodePacket(dgram.data) if err != nil { - return hash, test.errorf("sent packet decode error: %v", err) + return &dgram.to, hash, test.errorf("sent packet decode error: %v", err) } fn := reflect.ValueOf(validate) exptype := fn.Type().In(0) if reflect.TypeOf(p) != exptype { - return hash, test.errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype) + return &dgram.to, hash, test.errorf("sent packet type mismatch, got: %v, want: %v", reflect.TypeOf(p), exptype) } fn.Call([]reflect.Value{reflect.ValueOf(p)}) - return hash, nil + return &dgram.to, hash, nil } func (test *udpTest) errorf(format string, args ...interface{}) error { @@ -122,9 +137,9 @@ func (test *udpTest) errorf(format string, args ...interface{}) error { func TestUDP_packetErrors(t *testing.T) { test := newUDPTest(t) - defer test.table.Close() + defer test.close() - test.packetIn(errExpired, pingXDC, &ping{From: testRemote, To: testLocalAnnounced, Version: Version}) + test.packetIn(errExpired, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4}) test.packetIn(errUnsolicitedReply, pongPacket, &pong{ReplyTok: []byte{}, Expiration: futureExp}) test.packetIn(errUnknownNode, findnodePacket, &findnode{Expiration: futureExp}) test.packetIn(errUnsolicitedReply, neighborsPacket, &neighbors{Expiration: futureExp}) @@ -133,10 +148,10 @@ func TestUDP_packetErrors(t *testing.T) { func TestUDP_pingTimeout(t *testing.T) { t.Parallel() test := newUDPTest(t) - defer test.table.Close() + defer test.close() toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} - toid := NodeID{1, 2, 3, 4} + toid := enode.ID{1, 2, 3, 4} if err := test.udp.ping(toid, toaddr); err != errTimeout { t.Error("expected timeout error, got", err) } @@ -145,8 +160,9 @@ func TestUDP_pingTimeout(t *testing.T) { func TestUDP_responseTimeouts(t *testing.T) { t.Parallel() test := newUDPTest(t) - defer test.table.Close() + defer test.close() + rand.Seed(time.Now().UnixNano()) randomDuration := func(max time.Duration) time.Duration { return time.Duration(rand.Int63n(int64(max))) } @@ -162,20 +178,20 @@ func TestUDP_responseTimeouts(t *testing.T) { // with ptype <= 128 will not get a reply and should time out. // For all other requests, a reply is scheduled to arrive // within the timeout window. - p := &pending{ + p := &replyMatcher{ ptype: byte(rand.Intn(255)), - callback: func(interface{}) bool { return true }, + callback: func(interface{}) (bool, bool) { return true, true }, } binary.BigEndian.PutUint64(p.from[:], uint64(i)) if p.ptype <= 128 { p.errc = timeoutErr - test.udp.addpending <- p + test.udp.addReplyMatcher <- p nTimeouts++ } else { p.errc = nilErr - test.udp.addpending <- p + test.udp.addReplyMatcher <- p time.AfterFunc(randomDuration(60*time.Millisecond), func() { - if !test.udp.handleReply(p.from, p.ptype, nil) { + if !test.udp.handleReply(p.from, p.ip, p.ptype, nil) { t.Logf("not matched: %v", p) } }) @@ -216,11 +232,11 @@ func TestUDP_responseTimeouts(t *testing.T) { func TestUDP_findnodeTimeout(t *testing.T) { t.Parallel() test := newUDPTest(t) - defer test.table.Close() + defer test.close() toaddr := &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 2222} - toid := NodeID{1, 2, 3, 4} - target := NodeID{4, 5, 6, 7} + toid := enode.ID{1, 2, 3, 4} + target := encPubkey{4, 5, 6, 7} result, err := test.udp.findnode(toid, toaddr, target) if err != errTimeout { t.Error("expected timeout error, got", err) @@ -231,52 +247,71 @@ func TestUDP_findnodeTimeout(t *testing.T) { } func TestUDP_findnode(t *testing.T) { - bucketSizeTest := 16 test := newUDPTest(t) - defer test.table.Close() + defer test.close() // put a few nodes into the table. their exact // distribution shouldn't matter much, although we need to // take care not to overflow any bucket. - targetHash := crypto.Keccak256Hash(testTarget[:]) - nodes := &nodesByDistance{target: targetHash} - for i := 0; i < bucketSizeTest; i++ { - nodes.push(nodeAtDistance(test.table.self.sha, i+2), bucketSizeTest) + nodes := &nodesByDistance{target: testTarget.id()} + live := make(map[enode.ID]bool) + numCandidates := 2 * bucketSize + for i := 0; i < numCandidates; i++ { + key := newkey() + ip := net.IP{10, 13, 0, byte(i)} + n := wrapNode(enode.NewV4(&key.PublicKey, ip, 0, 2000)) + // Ensure half of table content isn't verified live yet. + if i > numCandidates/2 { + n.livenessChecks = 1 + live[n.ID()] = true + } + nodes.push(n, numCandidates) } - test.table.stuff(nodes.entries) + fillTable(test.table, nodes.entries) // ensure there's a bond with the test node, // findnode won't be accepted otherwise. - test.table.db.updateBondTime(PubkeyID(&test.remotekey.PublicKey), time.Now()) + remoteID := encodePubkey(&test.remotekey.PublicKey).id() + test.table.db.UpdateLastPongReceived(remoteID, test.remoteaddr.IP, time.Now()) // check that closest neighbors are returned. + expected := test.table.closest(testTarget.id(), bucketSize) test.packetIn(nil, findnodePacket, &findnode{Target: testTarget, Expiration: futureExp}) - expected := test.table.closest(targetHash, bucketSizeTest) - - waitNeighbors := func(want []*Node) { + waitNeighbors := func(want []*node) { test.waitPacketOut(func(p *neighbors) { if len(p.Nodes) != len(want) { - t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSizeTest) + t.Errorf("wrong number of results: got %d, want %d", len(p.Nodes), bucketSize) } - for i := range p.Nodes { - if p.Nodes[i].ID != want[i].ID { - t.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, p.Nodes[i], expected.entries[i]) + for i, n := range p.Nodes { + if n.ID.id() != want[i].ID() { + t.Errorf("result mismatch at %d:\n got: %v\n want: %v", i, n, expected.entries[i]) + } + if !live[n.ID.id()] { + t.Errorf("result includes dead node %v", n.ID.id()) } } }) } - waitNeighbors(expected.entries[:maxNeighbors]) - waitNeighbors(expected.entries[maxNeighbors:]) + // Receive replies. + want := expected.entries + if len(want) > maxNeighbors { + waitNeighbors(want[:maxNeighbors]) + want = want[maxNeighbors:] + } + waitNeighbors(want) } func TestUDP_findnodeMultiReply(t *testing.T) { test := newUDPTest(t) - defer test.table.Close() + defer test.close() + + rid := enode.PubkeyToIDV4(&test.remotekey.PublicKey) + test.table.db.UpdateLastPingReceived(rid, test.remoteaddr.IP, time.Now()) // queue a pending findnode request - resultc, errc := make(chan []*Node), make(chan error) + resultc, errc := make(chan []*node), make(chan error) go func() { - rid := PubkeyID(&test.remotekey.PublicKey) + rid := encodePubkey(&test.remotekey.PublicKey).id() ns, err := test.udp.findnode(rid, test.remoteaddr, testTarget) if err != nil && len(ns) == 0 { errc <- err @@ -294,11 +329,11 @@ func TestUDP_findnodeMultiReply(t *testing.T) { }) // send the reply as two packets. - list := []*Node{ - MustParseNode("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304"), - MustParseNode("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303"), - MustParseNode("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17"), - MustParseNode("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303"), + list := []*node{ + wrapNode(enode.MustParseV4("enode://ba85011c70bcc5c04d8607d3a0ed29aa6179c092cbdda10d5d32684fb33ed01bd94f588ca8f91ac48318087dcb02eaf36773a7a453f0eedd6742af668097b29c@10.0.1.16:30303?discport=30304")), + wrapNode(enode.MustParseV4("enode://81fa361d25f157cd421c60dcc28d8dac5ef6a89476633339c5df30287474520caca09627da18543d9079b5b288698b542d56167aa5c09111e55acdbbdf2ef799@10.0.1.16:30303")), + wrapNode(enode.MustParseV4("enode://9bffefd833d53fac8e652415f4973bee289e8b1a5c6c4cbe70abf817ce8a64cee11b823b66a987f51aaa9fba0d6a91b3e6bf0d5a5d1042de8e9eeea057b217f8@10.0.1.36:30301?discport=17")), + wrapNode(enode.MustParseV4("enode://1b5b4aa662d7cb44a7221bfba67302590b643028197a7d5214790f3bac7aaa4a3241be9e83c09cf1f6c69d007c634faae3dc1b1221793e8446c0b3a09de65960@10.0.1.16:30303")), } rpclist := make([]rpcNode, len(list)) for i := range list { @@ -321,14 +356,43 @@ func TestUDP_findnodeMultiReply(t *testing.T) { } } +func TestUDP_pingMatch(t *testing.T) { + test := newUDPTest(t) + defer test.close() + + randToken := make([]byte, 32) + crand.Read(randToken) + + test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) + test.waitPacketOut(func(*pong) error { return nil }) + test.waitPacketOut(func(*ping) error { return nil }) + test.packetIn(errUnsolicitedReply, pongPacket, &pong{ReplyTok: randToken, To: testLocalAnnounced, Expiration: futureExp}) +} + +func TestUDP_pingMatchIP(t *testing.T) { + test := newUDPTest(t) + defer test.close() + + test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) + test.waitPacketOut(func(*pong) error { return nil }) + + _, hash, _ := test.waitPacketOut(func(*ping) error { return nil }) + wrongAddr := &net.UDPAddr{IP: net.IP{33, 44, 1, 2}, Port: 30000} + test.packetInFrom(errUnsolicitedReply, test.remotekey, wrongAddr, pongPacket, &pong{ + ReplyTok: hash, + To: testLocalAnnounced, + Expiration: futureExp, + }) +} + func TestUDP_successfulPing(t *testing.T) { test := newUDPTest(t) - added := make(chan *Node, 1) - test.table.nodeAddedHook = func(b *bucket, n *Node) { added <- n } - defer test.table.Close() + added := make(chan *node, 1) + test.table.nodeAddedHook = func(n *node) { added <- n } + defer test.close() // The remote side sends a ping packet to initiate the exchange. - go test.packetIn(nil, pingXDC, &ping{From: testRemote, To: testLocalAnnounced, Version: Version, Expiration: futureExp}) + go test.packetIn(nil, pingPacket, &ping{From: testRemote, To: testLocalAnnounced, Version: 4, Expiration: futureExp}) // the ping is replied to. test.waitPacketOut(func(p *pong) { @@ -348,13 +412,14 @@ func TestUDP_successfulPing(t *testing.T) { }) // remote is unknown, the table pings back. - hash, _ := test.waitPacketOut(func(p *ping) error { - if !reflect.DeepEqual(p.From, test.udp.ourEndpoint) { - t.Errorf("got ping.From %v, want %v", p.From, test.udp.ourEndpoint) + _, hash, _ := test.waitPacketOut(func(p *ping) error { + if !reflect.DeepEqual(p.From, test.udp.ourEndpoint()) { + t.Errorf("got ping.From %#v, want %#v", p.From, test.udp.ourEndpoint()) } wantTo := rpcEndpoint{ // The mirrored UDP address is the UDP packet sender. - IP: test.remoteaddr.IP, UDP: uint16(test.remoteaddr.Port), + IP: test.remoteaddr.IP, + UDP: uint16(test.remoteaddr.Port), TCP: 0, } if !reflect.DeepEqual(p.To, wantTo) { @@ -368,18 +433,18 @@ func TestUDP_successfulPing(t *testing.T) { // pong packet. select { case n := <-added: - rid := PubkeyID(&test.remotekey.PublicKey) - if n.ID != rid { - t.Errorf("node has wrong ID: got %v, want %v", n.ID, rid) + rid := encodePubkey(&test.remotekey.PublicKey).id() + if n.ID() != rid { + t.Errorf("node has wrong ID: got %v, want %v", n.ID(), rid) } - if !n.IP.Equal(test.remoteaddr.IP) { - t.Errorf("node has wrong IP: got %v, want: %v", n.IP, test.remoteaddr.IP) + if !n.IP().Equal(test.remoteaddr.IP) { + t.Errorf("node has wrong IP: got %v, want: %v", n.IP(), test.remoteaddr.IP) } - if int(n.UDP) != test.remoteaddr.Port { - t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP, test.remoteaddr.Port) + if int(n.UDP()) != test.remoteaddr.Port { + t.Errorf("node has wrong UDP port: got %v, want: %v", n.UDP(), test.remoteaddr.Port) } - if n.TCP != testRemote.TCP { - t.Errorf("node has wrong TCP port: got %v, want: %v", n.TCP, testRemote.TCP) + if n.TCP() != int(testRemote.TCP) { + t.Errorf("node has wrong TCP port: got %v, want: %v", n.TCP(), testRemote.TCP) } case <-time.After(2 * time.Second): t.Errorf("node was not added within 2 seconds") @@ -391,7 +456,7 @@ var testPackets = []struct { wantPacket interface{} }{ { - input: "95a4d7d1909e6a58f115e9a451d47a8f016776a8874140366e702e33e85c7b4cd58a82ebece6acd0973342b66b9e716fece46b5c67a3560fc8624063dd15a310469de42ca599474b9d8cb6eb8dc41b0d5236539ea7ae10ef3c630cd94faefd800005ea04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a355", + input: "71dbda3a79554728d4f94411e42ee1f8b0d561c10e1e5f5893367948c6a7d70bb87b235fa28a77070271b6c164a2dce8c7e13a5739b53b5e96f2e5acb0e458a02902f5965d55ecbeb2ebb6cabb8b2b232896a36b737666c55265ad0a68412f250001ea04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a355", wantPacket: &ping{ Version: 4, From: rpcEndpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544}, @@ -401,7 +466,7 @@ var testPackets = []struct { }, }, { - input: "57b1c182cc24e21e9297baa70d57a67ade498439123c968ffc048541addf9d463d1d25d10cf473a7f90a3efd6a070818097ebeaef58cd53843cb3af28acaee354272cfe7801b7fa7dbd8aa13309b6059fce877ad376c8dad7524dc34de626bd80105ec04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a3550102", + input: "e9614ccfd9fc3e74360018522d30e1419a143407ffcce748de3e22116b7e8dc92ff74788c0b6663aaa3d67d641936511c8f8d6ad8698b820a7cf9e1be7155e9a241f556658c55428ec0563514365799a4be2be5a685a80971ddcfa80cb422cdd0101ec04cb847f000001820cfa8215a8d790000000000000000000000000000000018208ae820d058443b9a3550102", wantPacket: &ping{ Version: 4, From: rpcEndpoint{net.ParseIP("127.0.0.1").To4(), 3322, 5544}, @@ -411,7 +476,7 @@ var testPackets = []struct { }, }, { - input: "e3e987421accd2c75967d4a7229c436c18760def054738d8d9669697ee4726cdc9949c51df3e90d795d33d3f57d508c4687913338f6eb9caa89873aaae9dd49a5473ade5ea452c4df9d1f842eadf03439dbc373c0de8b20b412b6760d7b479140105f83e82022bd79020010db83c4d001500000000abcdef12820cfa8215a8d79020010db885a308d313198a2e037073488208ae82823a8443b9a355c50102030405", + input: "577be4349c4dd26768081f58de4c6f375a7a22f3f7adda654d1428637412c3d7fe917cadc56d4e5e7ffae1dbe3efffb9849feb71b262de37977e7c7a44e677295680e9e38ab26bee2fcbae207fba3ff3d74069a50b902a82c9903ed37cc993c50001f83e82022bd79020010db83c4d001500000000abcdef12820cfa8215a8d79020010db885a308d313198a2e037073488208ae82823a8443b9a355c5010203040531b9019afde696e582a78fa8d95ea13ce3297d4afb8ba6433e4154caa5ac6431af1b80ba76023fa4090c408f6b4bc3701562c031041d4702971d102c9ab7fa5eed4cd6bab8f7af956f7d565ee1917084a95398b6a21eac920fe3dd1345ec0a7ef39367ee69ddf092cbfe5b93e5e568ebc491983c09c76d922dc3", wantPacket: &ping{ Version: 555, From: rpcEndpoint{net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), 3322, 5544}, @@ -432,7 +497,7 @@ var testPackets = []struct { { input: "c7c44041b9f7c7e41934417ebac9a8e1a4c6298f74553f2fcfdcae6ed6fe53163eb3d2b52e39fe91831b8a927bf4fc222c3902202027e5e9eb812195f95d20061ef5cd31d502e47ecb61183f74a504fe04c51e73df81f25c4d506b26db4517490103f84eb840ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f8443b9a35582999983999999280dc62cc8255c73471e0a61da0c89acdc0e035e260add7fc0c04ad9ebf3919644c91cb247affc82b69bd2ca235c71eab8e49737c937a2c396", wantPacket: &findnode{ - Target: MustHexID("ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f"), + Target: hexEncPubkey("ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f"), Expiration: 1136239445, Rest: []rlp.RawValue{{0x82, 0x99, 0x99}, {0x83, 0x99, 0x99, 0x99}}, }, @@ -442,25 +507,25 @@ var testPackets = []struct { wantPacket: &neighbors{ Nodes: []rpcNode{ { - ID: MustHexID("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"), + ID: hexEncPubkey("3155e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa829115d224c523596b401065a97f74010610fce76382c0bf32"), IP: net.ParseIP("99.33.22.55").To4(), UDP: 4444, TCP: 4445, }, { - ID: MustHexID("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"), + ID: hexEncPubkey("312c55512422cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e829f04c2d314fc2d4e255e0d3bc08792b069db"), IP: net.ParseIP("1.2.3.4").To4(), UDP: 1, TCP: 1, }, { - ID: MustHexID("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"), + ID: hexEncPubkey("38643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aac"), IP: net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), UDP: 3333, TCP: 3333, }, { - ID: MustHexID("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"), + ID: hexEncPubkey("8dcab8618c3253b558d459da53bd8fa68935a719aff8b811197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73"), IP: net.ParseIP("2001:db8:85a3:8d3:1319:8a2e:370:7348"), UDP: 999, TCP: 1000, @@ -474,13 +539,14 @@ var testPackets = []struct { func TestForwardCompatibility(t *testing.T) { testkey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - wantNodeID := PubkeyID(&testkey.PublicKey) + wantNodeKey := encodePubkey(&testkey.PublicKey) + for _, test := range testPackets { input, err := hex.DecodeString(test.input) if err != nil { t.Fatalf("invalid hex: %s", test.input) } - packet, nodeid, _, err := decodePacket(input) + packet, nodekey, _, err := decodePacket(input) if err != nil { t.Errorf("did not accept packet %s\n%v", test.input, err) continue @@ -488,8 +554,8 @@ func TestForwardCompatibility(t *testing.T) { if !reflect.DeepEqual(packet, test.wantPacket) { t.Errorf("got %s\nwant %s", spew.Sdump(packet), spew.Sdump(test.wantPacket)) } - if nodeid != wantNodeID { - t.Errorf("got id %v\nwant id %v", nodeid, wantNodeID) + if nodekey != wantNodeKey { + t.Errorf("got id %v\nwant id %v", nodekey, wantNodeKey) } } } @@ -500,7 +566,12 @@ type dgramPipe struct { cond *sync.Cond closing chan struct{} closed bool - queue [][]byte + queue []dgram +} + +type dgram struct { + to net.UDPAddr + data []byte } func newpipe() *dgramPipe { @@ -521,7 +592,7 @@ func (c *dgramPipe) WriteToUDP(b []byte, to *net.UDPAddr) (n int, err error) { if c.closed { return 0, errors.New("closed") } - c.queue = append(c.queue, msg) + c.queue = append(c.queue, dgram{*to, b}) c.cond.Signal() return len(b), nil } @@ -546,7 +617,7 @@ func (c *dgramPipe) LocalAddr() net.Addr { return &net.UDPAddr{IP: testLocal.IP, Port: int(testLocal.UDP)} } -func (c *dgramPipe) waitPacketOut() []byte { +func (c *dgramPipe) waitPacketOut() dgram { c.mu.Lock() defer c.mu.Unlock() for len(c.queue) == 0 { diff --git a/p2p/discv5/udp.go b/p2p/discv5/udp.go index 2c40431c138d..670e3c0ad3a6 100644 --- a/p2p/discv5/udp.go +++ b/p2p/discv5/udp.go @@ -39,13 +39,13 @@ const Version = 4 var ( errPacketTooSmall = errors.New("too small") errBadPrefix = errors.New("bad prefix") - errTimeout = errors.New("RPC timeout") ) // Timeouts const ( - respTimeout = 500 * time.Millisecond - expiration = 20 * time.Second + respTimeout = 500 * time.Millisecond + expiration = 20 * time.Second + driftThreshold = 10 * time.Second // Allowed clock drift before warning user ) @@ -187,6 +187,10 @@ func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint { return rpcEndpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort} } +func (e1 rpcEndpoint) equal(e2 rpcEndpoint) bool { + return e1.UDP == e2.UDP && e1.TCP == e2.TCP && e1.IP.Equal(e2.IP) +} + func nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*Node, error) { if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil { return nil, err @@ -226,7 +230,8 @@ type udp struct { } // ListenUDP returns a new table that listens for UDP packets on laddr. -func ListenUDP(priv *ecdsa.PrivateKey, conn conn, realaddr *net.UDPAddr, nodeDBPath string, netrestrict *netutil.Netlist) (*Network, error) { +func ListenUDP(priv *ecdsa.PrivateKey, conn conn, nodeDBPath string, netrestrict *netutil.Netlist) (*Network, error) { + realaddr := conn.LocalAddr().(*net.UDPAddr) transport, err := listenUDP(priv, conn, realaddr) if err != nil { return nil, err @@ -269,6 +274,13 @@ func (t *udp) sendPing(remote *Node, toaddr *net.UDPAddr, topics []Topic) (hash return hash } +func (t *udp) sendFindnode(remote *Node, target NodeID) { + t.sendPacket(remote.ID, remote.addr(), byte(findnodePacket), findnode{ + Target: target, + Expiration: uint64(time.Now().Add(expiration).Unix()), + }) +} + func (t *udp) sendNeighbours(remote *Node, results []*Node) { // Send neighbors in chunks with at most maxNeighbors per packet // to stay below the 1280 byte limit. @@ -385,7 +397,6 @@ func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error { pkt := ingressPacket{remoteAddr: from} if err := decodePacket(buf, &pkt); err != nil { log.Debug(fmt.Sprintf("Bad packet from %v: %v", from, err)) - //fmt.Println("bad packet", err) return err } t.net.reqReadPacket(pkt) diff --git a/p2p/enode/idscheme.go b/p2p/enode/idscheme.go new file mode 100644 index 000000000000..04edbed0994f --- /dev/null +++ b/p2p/enode/idscheme.go @@ -0,0 +1,160 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "crypto/ecdsa" + "fmt" + "io" + + "github.com/XinFinOrg/XDPoSChain/common/math" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "github.com/XinFinOrg/XDPoSChain/rlp" + "golang.org/x/crypto/sha3" +) + +// List of known secure identity schemes. +var ValidSchemes = enr.SchemeMap{ + "v4": V4ID{}, +} + +var ValidSchemesForTesting = enr.SchemeMap{ + "v4": V4ID{}, + "null": NullID{}, +} + +// v4ID is the "v4" identity scheme. +type V4ID struct{} + +// SignV4 signs a record using the v4 scheme. +func SignV4(r *enr.Record, privkey *ecdsa.PrivateKey) error { + // Copy r to avoid modifying it if signing fails. + cpy := *r + cpy.Set(enr.ID("v4")) + cpy.Set(Secp256k1(privkey.PublicKey)) + + h := sha3.NewLegacyKeccak256() + rlp.Encode(h, cpy.AppendElements(nil)) + sig, err := crypto.Sign(h.Sum(nil), privkey) + if err != nil { + return err + } + sig = sig[:len(sig)-1] // remove v + if err = cpy.SetSig(V4ID{}, sig); err == nil { + *r = cpy + } + return err +} + +func (V4ID) Verify(r *enr.Record, sig []byte) error { + var entry s256raw + if err := r.Load(&entry); err != nil { + return err + } else if len(entry) != 33 { + return fmt.Errorf("invalid public key") + } + + h := sha3.NewLegacyKeccak256() + rlp.Encode(h, r.AppendElements(nil)) + if !crypto.VerifySignature(entry, h.Sum(nil), sig) { + return enr.ErrInvalidSig + } + return nil +} + +func (V4ID) NodeAddr(r *enr.Record) []byte { + var pubkey Secp256k1 + err := r.Load(&pubkey) + if err != nil { + return nil + } + buf := make([]byte, 64) + math.ReadBits(pubkey.X, buf[:32]) + math.ReadBits(pubkey.Y, buf[32:]) + return crypto.Keccak256(buf) +} + +// Secp256k1 is the "secp256k1" key, which holds a public key. +type Secp256k1 ecdsa.PublicKey + +func (v Secp256k1) ENRKey() string { return "secp256k1" } + +// EncodeRLP implements rlp.Encoder. +func (v Secp256k1) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, crypto.CompressPubkey((*ecdsa.PublicKey)(&v))) +} + +// DecodeRLP implements rlp.Decoder. +func (v *Secp256k1) DecodeRLP(s *rlp.Stream) error { + buf, err := s.Bytes() + if err != nil { + return err + } + pk, err := crypto.DecompressPubkey(buf) + if err != nil { + return err + } + *v = (Secp256k1)(*pk) + return nil +} + +// s256raw is an unparsed secp256k1 public key entry. +type s256raw []byte + +func (s256raw) ENRKey() string { return "secp256k1" } + +// v4CompatID is a weaker and insecure version of the "v4" scheme which only checks for the +// presence of a secp256k1 public key, but doesn't verify the signature. +type v4CompatID struct { + V4ID +} + +func (v4CompatID) Verify(r *enr.Record, sig []byte) error { + var pubkey Secp256k1 + return r.Load(&pubkey) +} + +func signV4Compat(r *enr.Record, pubkey *ecdsa.PublicKey) { + r.Set((*Secp256k1)(pubkey)) + if err := r.SetSig(v4CompatID{}, []byte{}); err != nil { + panic(err) + } +} + +// NullID is the "null" ENR identity scheme. This scheme stores the node +// ID in the record without any signature. +type NullID struct{} + +func (NullID) Verify(r *enr.Record, sig []byte) error { + return nil +} + +func (NullID) NodeAddr(r *enr.Record) []byte { + var id ID + r.Load(enr.WithEntry("nulladdr", &id)) + return id[:] +} + +func SignNull(r *enr.Record, id ID) *Node { + r.Set(enr.ID("null")) + r.Set(enr.WithEntry("nulladdr", id)) + if err := r.SetSig(NullID{}, []byte{}); err != nil { + panic(err) + } + return &Node{r: *r, id: id} +} diff --git a/p2p/enode/idscheme_test.go b/p2p/enode/idscheme_test.go new file mode 100644 index 000000000000..a4d0bb6c25be --- /dev/null +++ b/p2p/enode/idscheme_test.go @@ -0,0 +1,74 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + privkey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + pubkey = &privkey.PublicKey +) + +func TestEmptyNodeID(t *testing.T) { + var r enr.Record + if addr := ValidSchemes.NodeAddr(&r); addr != nil { + t.Errorf("wrong address on empty record: got %v, want %v", addr, nil) + } + + require.NoError(t, SignV4(&r, privkey)) + expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7" + assert.Equal(t, expected, hex.EncodeToString(ValidSchemes.NodeAddr(&r))) +} + +// Checks that failure to sign leaves the record unmodified. +func TestSignError(t *testing.T) { + invalidKey := &ecdsa.PrivateKey{D: new(big.Int), PublicKey: *pubkey} + + var r enr.Record + emptyEnc, _ := rlp.EncodeToBytes(&r) + if err := SignV4(&r, invalidKey); err == nil { + t.Fatal("expected error from SignV4") + } + newEnc, _ := rlp.EncodeToBytes(&r) + if !bytes.Equal(newEnc, emptyEnc) { + t.Fatal("record modified even though signing failed") + } +} + +// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key. +func TestGetSetSecp256k1(t *testing.T) { + var r enr.Record + if err := SignV4(&r, privkey); err != nil { + t.Fatal(err) + } + + var pk Secp256k1 + require.NoError(t, r.Load(&pk)) + assert.EqualValues(t, pubkey, &pk) +} diff --git a/p2p/enode/localnode.go b/p2p/enode/localnode.go new file mode 100644 index 000000000000..70c7a253f2a5 --- /dev/null +++ b/p2p/enode/localnode.go @@ -0,0 +1,246 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "crypto/ecdsa" + "fmt" + "net" + "reflect" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "github.com/XinFinOrg/XDPoSChain/p2p/netutil" +) + +const ( + // IP tracker configuration + iptrackMinStatements = 10 + iptrackWindow = 5 * time.Minute + iptrackContactWindow = 10 * time.Minute +) + +// LocalNode produces the signed node record of a local node, i.e. a node run in the +// current process. Setting ENR entries via the Set method updates the record. A new version +// of the record is signed on demand when the Node method is called. +type LocalNode struct { + cur atomic.Value // holds a non-nil node pointer while the record is up-to-date. + id ID + key *ecdsa.PrivateKey + db *DB + + // everything below is protected by a lock + mu sync.Mutex + seq uint64 + entries map[string]enr.Entry + udpTrack *netutil.IPTracker // predicts external UDP endpoint + staticIP net.IP + fallbackIP net.IP + fallbackUDP int +} + +// NewLocalNode creates a local node. +func NewLocalNode(db *DB, key *ecdsa.PrivateKey) *LocalNode { + ln := &LocalNode{ + id: PubkeyToIDV4(&key.PublicKey), + db: db, + key: key, + udpTrack: netutil.NewIPTracker(iptrackWindow, iptrackContactWindow, iptrackMinStatements), + entries: make(map[string]enr.Entry), + } + ln.seq = db.localSeq(ln.id) + ln.invalidate() + return ln +} + +// Database returns the node database associated with the local node. +func (ln *LocalNode) Database() *DB { + return ln.db +} + +// Node returns the current version of the local node record. +func (ln *LocalNode) Node() *Node { + n := ln.cur.Load().(*Node) + if n != nil { + return n + } + // Record was invalidated, sign a new copy. + ln.mu.Lock() + defer ln.mu.Unlock() + ln.sign() + return ln.cur.Load().(*Node) +} + +// ID returns the local node ID. +func (ln *LocalNode) ID() ID { + return ln.id +} + +// Set puts the given entry into the local record, overwriting +// any existing value. +func (ln *LocalNode) Set(e enr.Entry) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.set(e) +} + +func (ln *LocalNode) set(e enr.Entry) { + val, exists := ln.entries[e.ENRKey()] + if !exists || !reflect.DeepEqual(val, e) { + ln.entries[e.ENRKey()] = e + ln.invalidate() + } +} + +// Delete removes the given entry from the local record. +func (ln *LocalNode) Delete(e enr.Entry) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.delete(e) +} + +func (ln *LocalNode) delete(e enr.Entry) { + _, exists := ln.entries[e.ENRKey()] + if exists { + delete(ln.entries, e.ENRKey()) + ln.invalidate() + } +} + +// SetStaticIP sets the local IP to the given one unconditionally. +// This disables endpoint prediction. +func (ln *LocalNode) SetStaticIP(ip net.IP) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.staticIP = ip + ln.updateEndpoints() +} + +// SetFallbackIP sets the last-resort IP address. This address is used +// if no endpoint prediction can be made and no static IP is set. +func (ln *LocalNode) SetFallbackIP(ip net.IP) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.fallbackIP = ip + ln.updateEndpoints() +} + +// SetFallbackUDP sets the last-resort UDP port. This port is used +// if no endpoint prediction can be made. +func (ln *LocalNode) SetFallbackUDP(port int) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.fallbackUDP = port + ln.updateEndpoints() +} + +// UDPEndpointStatement should be called whenever a statement about the local node's +// UDP endpoint is received. It feeds the local endpoint predictor. +func (ln *LocalNode) UDPEndpointStatement(fromaddr, endpoint *net.UDPAddr) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.udpTrack.AddStatement(fromaddr.String(), endpoint.String()) + ln.updateEndpoints() +} + +// UDPContact should be called whenever the local node has announced itself to another node +// via UDP. It feeds the local endpoint predictor. +func (ln *LocalNode) UDPContact(toaddr *net.UDPAddr) { + ln.mu.Lock() + defer ln.mu.Unlock() + + ln.udpTrack.AddContact(toaddr.String()) + ln.updateEndpoints() +} + +func (ln *LocalNode) updateEndpoints() { + // Determine the endpoints. + newIP := ln.fallbackIP + newUDP := ln.fallbackUDP + if ln.staticIP != nil { + newIP = ln.staticIP + } else if ip, port := predictAddr(ln.udpTrack); ip != nil { + newIP = ip + newUDP = port + } + + // Update the record. + if newIP != nil && !newIP.IsUnspecified() { + ln.set(enr.IP(newIP)) + if newUDP != 0 { + ln.set(enr.UDP(newUDP)) + } else { + ln.delete(enr.UDP(0)) + } + } else { + ln.delete(enr.IP{}) + } +} + +// predictAddr wraps IPTracker.PredictEndpoint, converting from its string-based +// endpoint representation to IP and port types. +func predictAddr(t *netutil.IPTracker) (net.IP, int) { + ep := t.PredictEndpoint() + if ep == "" { + return nil, 0 + } + ipString, portString, _ := net.SplitHostPort(ep) + ip := net.ParseIP(ipString) + port, _ := strconv.Atoi(portString) + return ip, port +} + +func (ln *LocalNode) invalidate() { + ln.cur.Store((*Node)(nil)) +} + +func (ln *LocalNode) sign() { + if n := ln.cur.Load().(*Node); n != nil { + return // no changes + } + + var r enr.Record + for _, e := range ln.entries { + r.Set(e) + } + ln.bumpSeq() + r.SetSeq(ln.seq) + if err := SignV4(&r, ln.key); err != nil { + panic(fmt.Errorf("enode: can't sign record: %v", err)) + } + n, err := New(ValidSchemes, &r) + if err != nil { + panic(fmt.Errorf("enode: can't verify local record: %v", err)) + } + ln.cur.Store(n) + log.Info("New local node record", "seq", ln.seq, "id", n.ID(), "ip", n.IP(), "udp", n.UDP(), "tcp", n.TCP()) +} + +func (ln *LocalNode) bumpSeq() { + ln.seq++ + ln.db.storeLocalSeq(ln.id, ln.seq) +} diff --git a/p2p/enode/localnode_test.go b/p2p/enode/localnode_test.go new file mode 100644 index 000000000000..41bd55a5d3f1 --- /dev/null +++ b/p2p/enode/localnode_test.go @@ -0,0 +1,82 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "testing" + "time" + + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" +) + +func newLocalNodeForTesting() (*LocalNode, *DB) { + db, _ := OpenDB("") + key, _ := crypto.GenerateKey() + return NewLocalNode(db, key), db +} + +func TestLocalNode(t *testing.T) { + ln, db := newLocalNodeForTesting() + defer db.Close() + + if ln.Node().ID() != ln.ID() { + t.Fatal("inconsistent ID") + } + + ln.Set(enr.WithEntry("x", uint(3))) + var x uint + if err := ln.Node().Load(enr.WithEntry("x", &x)); err != nil { + t.Fatal("can't load entry 'x':", err) + } else if x != 3 { + t.Fatal("wrong value for entry 'x':", x) + } +} + +func TestLocalNodeSeqPersist(t *testing.T) { + timestamp := uint64(time.Now().UnixMilli()) + + ln, db := newLocalNodeForTesting() + defer db.Close() + + initialSeq := ln.Node().Seq() + if initialSeq < timestamp { + t.Fatalf("wrong initial seq %d, want at least %d", initialSeq, timestamp) + } + + ln.Set(enr.WithEntry("x", uint(1))) + if s := ln.Node().Seq(); s != initialSeq+1 { + t.Fatalf("wrong seq %d after set, want %d", s, initialSeq+1) + } + + // Create a new instance, it should reload the sequence number. + // The number increases just after that because a new record is + // created without the "x" entry. + ln2 := NewLocalNode(db, ln.key) + if s := ln2.Node().Seq(); s != initialSeq+2 { + t.Fatalf("wrong seq %d on new instance, want %d", s, initialSeq+2) + } + + // Create a new instance with a different node key on the same database. + // This should reset the sequence number. + resetTimestamp := uint64(time.Now().UnixMilli()) + key, _ := crypto.GenerateKey() + ln3 := NewLocalNode(db, key) + if s := ln3.Node().Seq(); s < resetTimestamp { + t.Fatalf("wrong seq %d on instance with changed key, want >= %d", s, resetTimestamp) + } +} diff --git a/p2p/enode/node.go b/p2p/enode/node.go new file mode 100644 index 000000000000..e72ddc0cbe2a --- /dev/null +++ b/p2p/enode/node.go @@ -0,0 +1,255 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "math/bits" + "math/rand" + "net" + "strings" + + "github.com/XinFinOrg/XDPoSChain/p2p/enr" +) + +// Node represents a host on the network. +type Node struct { + r enr.Record + id ID +} + +// New wraps a node record. The record must be valid according to the given +// identity scheme. +func New(validSchemes enr.IdentityScheme, r *enr.Record) (*Node, error) { + if err := r.VerifySignature(validSchemes); err != nil { + return nil, err + } + node := &Node{r: *r} + if n := copy(node.id[:], validSchemes.NodeAddr(&node.r)); n != len(ID{}) { + return nil, fmt.Errorf("invalid node ID length %d, need %d", n, len(ID{})) + } + return node, nil +} + +// ID returns the node identifier. +func (n *Node) ID() ID { + return n.id +} + +// Seq returns the sequence number of the underlying record. +func (n *Node) Seq() uint64 { + return n.r.Seq() +} + +// Incomplete returns true for nodes with no IP address. +func (n *Node) Incomplete() bool { + return n.IP() == nil +} + +// Load retrieves an entry from the underlying record. +func (n *Node) Load(k enr.Entry) error { + return n.r.Load(k) +} + +// IP returns the IP address of the node. +func (n *Node) IP() net.IP { + var ip net.IP + n.Load((*enr.IP)(&ip)) + return ip +} + +// UDP returns the UDP port of the node. +func (n *Node) UDP() int { + var port enr.UDP + n.Load(&port) + return int(port) +} + +// TCP returns the TCP port of the node. +func (n *Node) TCP() int { + var port enr.TCP + n.Load(&port) + return int(port) +} + +// Pubkey returns the secp256k1 public key of the node, if present. +func (n *Node) Pubkey() *ecdsa.PublicKey { + var key ecdsa.PublicKey + if n.Load((*Secp256k1)(&key)) != nil { + return nil + } + return &key +} + +// Record returns the node's record. The return value is a copy and may +// be modified by the caller. +func (n *Node) Record() *enr.Record { + cpy := n.r + return &cpy +} + +// checks whether n is a valid complete node. +func (n *Node) ValidateComplete() error { + if n.Incomplete() { + return errors.New("incomplete node") + } + if n.UDP() == 0 { + return errors.New("missing UDP port") + } + ip := n.IP() + if ip.IsMulticast() || ip.IsUnspecified() { + return errors.New("invalid IP (multicast/unspecified)") + } + // Validate the node key (on curve, etc.). + var key Secp256k1 + return n.Load(&key) +} + +// The string representation of a Node is a URL. +// Please see ParseNode for a description of the format. +func (n *Node) String() string { + return n.v4URL() +} + +// MarshalText implements encoding.TextMarshaler. +func (n *Node) MarshalText() ([]byte, error) { + return []byte(n.v4URL()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (n *Node) UnmarshalText(text []byte) error { + dec, err := ParseV4(string(text)) + if err == nil { + *n = *dec + } + return err +} + +// ID is a unique identifier for each node. +type ID [32]byte + +// Bytes returns a byte slice representation of the ID +func (n ID) Bytes() []byte { + return n[:] +} + +// ID prints as a long hexadecimal number. +func (n ID) String() string { + return fmt.Sprintf("%x", n[:]) +} + +// The Go syntax representation of a ID is a call to HexID. +func (n ID) GoString() string { + return fmt.Sprintf("enode.HexID(\"%x\")", n[:]) +} + +// TerminalString returns a shortened hex string for terminal logging. +func (n ID) TerminalString() string { + return hex.EncodeToString(n[:8]) +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (n ID) MarshalText() ([]byte, error) { + return []byte(hex.EncodeToString(n[:])), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (n *ID) UnmarshalText(text []byte) error { + id, err := parseID(string(text)) + if err != nil { + return err + } + *n = id + return nil +} + +// HexID converts a hex string to an ID. +// The string may be prefixed with 0x. +// It panics if the string is not a valid ID. +func HexID(in string) ID { + id, err := parseID(in) + if err != nil { + panic(err) + } + return id +} + +func parseID(in string) (ID, error) { + var id ID + b, err := hex.DecodeString(strings.TrimPrefix(in, "0x")) + if err != nil { + return id, err + } else if len(b) != len(id) { + return id, fmt.Errorf("wrong length, want %d hex chars", len(id)*2) + } + copy(id[:], b) + return id, nil +} + +// DistCmp compares the distances a->target and b->target. +// Returns -1 if a is closer to target, 1 if b is closer to target +// and 0 if they are equal. +func DistCmp(target, a, b ID) int { + for i := range target { + da := a[i] ^ target[i] + db := b[i] ^ target[i] + if da > db { + return 1 + } else if da < db { + return -1 + } + } + return 0 +} + +// LogDist returns the logarithmic distance between a and b, log2(a ^ b). +func LogDist(a, b ID) int { + lz := 0 + for i := range a { + x := a[i] ^ b[i] + if x == 0 { + lz += 8 + } else { + lz += bits.LeadingZeros8(x) + break + } + } + return len(a)*8 - lz +} + +// RandomID returns a random ID b such that logdist(a, b) == n. +func RandomID(a ID, n int) (b ID) { + if n == 0 { + return a + } + // flip bit at position n, fill the rest with random bits + b = a + pos := len(a) - n/8 - 1 + bit := byte(0x01) << (byte(n%8) - 1) + if bit == 0 { + pos++ + bit = 0x80 + } + b[pos] = a[pos]&^bit | ^a[pos]&bit // TODO: randomize end bits + for i := pos + 1; i < len(a); i++ { + b[i] = byte(rand.Intn(256)) + } + return b +} diff --git a/p2p/enode/node_test.go b/p2p/enode/node_test.go new file mode 100644 index 000000000000..d23bf0500541 --- /dev/null +++ b/p2p/enode/node_test.go @@ -0,0 +1,62 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/stretchr/testify/assert" +) + +var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f") + +// TestPythonInterop checks that we can decode and verify a record produced by the Python +// implementation. +func TestPythonInterop(t *testing.T) { + var r enr.Record + if err := rlp.DecodeBytes(pyRecord, &r); err != nil { + t.Fatalf("can't decode: %v", err) + } + n, err := New(ValidSchemes, &r) + if err != nil { + t.Fatalf("can't verify record: %v", err) + } + + var ( + wantID = HexID("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") + wantSeq = uint64(1) + wantIP = enr.IP{127, 0, 0, 1} + wantUDP = enr.UDP(30303) + ) + if n.Seq() != wantSeq { + t.Errorf("wrong seq: got %d, want %d", n.Seq(), wantSeq) + } + if n.ID() != wantID { + t.Errorf("wrong id: got %x, want %x", n.ID(), wantID) + } + want := map[enr.Entry]interface{}{new(enr.IP): &wantIP, new(enr.UDP): &wantUDP} + for k, v := range want { + desc := fmt.Sprintf("loading key %q", k.ENRKey()) + if assert.NoError(t, n.Load(k), desc) { + assert.Equal(t, k, v, desc) + } + } +} diff --git a/p2p/enode/nodedb.go b/p2p/enode/nodedb.go new file mode 100644 index 000000000000..f440cb3ee927 --- /dev/null +++ b/p2p/enode/nodedb.go @@ -0,0 +1,452 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "net" + "os" + "sync" + "time" + + "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/errors" + "github.com/syndtr/goleveldb/leveldb/iterator" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/storage" + "github.com/syndtr/goleveldb/leveldb/util" +) + +// Keys in the node database. +const ( + dbVersionKey = "version" // Version of the database to flush if changes + dbNodePrefix = "n:" // Identifier to prefix node entries with + dbLocalPrefix = "local:" + dbDiscoverRoot = "v4" + + // These fields are stored per ID and IP, the full key is "n::v4::findfail". + // Use nodeItemKey to create those keys. + dbNodeFindFails = "findfail" + dbNodePing = "lastping" + dbNodePong = "lastpong" + dbNodeSeq = "seq" + + // Local information is keyed by ID only, the full key is "local::seq". + // Use localItemKey to create those keys. + dbLocalSeq = "seq" +) + +const ( + dbNodeExpiration = 24 * time.Hour // Time after which an unseen node should be dropped. + dbCleanupCycle = time.Hour // Time period for running the expiration task. + dbVersion = 8 +) + +var zeroIP = make(net.IP, 16) + +// DB is the node database, storing previously seen nodes and any collected metadata about +// them for QoS purposes. +type DB struct { + lvl *leveldb.DB // Interface to the database itself + runner sync.Once // Ensures we can start at most one expirer + quit chan struct{} // Channel to signal the expiring thread to stop +} + +// OpenDB opens a node database for storing and retrieving infos about known peers in the +// network. If no path is given an in-memory, temporary database is constructed. +func OpenDB(path string) (*DB, error) { + if path == "" { + return newMemoryDB() + } + return newPersistentDB(path) +} + +// newMemoryNodeDB creates a new in-memory node database without a persistent backend. +func newMemoryDB() (*DB, error) { + db, err := leveldb.Open(storage.NewMemStorage(), nil) + if err != nil { + return nil, err + } + return &DB{lvl: db, quit: make(chan struct{})}, nil +} + +// newPersistentNodeDB creates/opens a leveldb backed persistent node database, +// also flushing its contents in case of a version mismatch. +func newPersistentDB(path string) (*DB, error) { + opts := &opt.Options{OpenFilesCacheCapacity: 5} + db, err := leveldb.OpenFile(path, opts) + if _, iscorrupted := err.(*errors.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, int64(dbVersion))] + + blob, err := db.Get([]byte(dbVersionKey), nil) + switch err { + case leveldb.ErrNotFound: + // Version not found (i.e. empty cache), insert it + if err := db.Put([]byte(dbVersionKey), currentVer, nil); err != nil { + db.Close() + return nil, err + } + + 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 newPersistentDB(path) + } + } + return &DB{lvl: db, quit: make(chan struct{})}, nil +} + +// nodeKey returns the database key for a node record. +func nodeKey(id ID) []byte { + key := append([]byte(dbNodePrefix), id[:]...) + key = append(key, ':') + key = append(key, dbDiscoverRoot...) + return key +} + +// splitNodeKey returns the node ID of a key created by nodeKey. +func splitNodeKey(key []byte) (id ID, rest []byte) { + if !bytes.HasPrefix(key, []byte(dbNodePrefix)) { + return ID{}, nil + } + item := key[len(dbNodePrefix):] + copy(id[:], item[:len(id)]) + return id, item[len(id)+1:] +} + +// nodeItemKey returns the database key for a node metadata field. +func nodeItemKey(id ID, ip net.IP, field string) []byte { + ip16 := ip.To16() + if ip16 == nil { + panic(fmt.Errorf("invalid IP (length %d)", len(ip))) + } + return bytes.Join([][]byte{nodeKey(id), ip16, []byte(field)}, []byte{':'}) +} + +// splitNodeItemKey returns the components of a key created by nodeItemKey. +func splitNodeItemKey(key []byte) (id ID, ip net.IP, field string) { + id, key = splitNodeKey(key) + // Skip discover root. + if string(key) == dbDiscoverRoot { + return id, nil, "" + } + key = key[len(dbDiscoverRoot)+1:] + // Split out the IP. + ip = net.IP(key[:16]) + if ip4 := ip.To4(); ip4 != nil { + ip = ip4 + } + key = key[16+1:] + // Field is the remainder of key. + field = string(key) + return id, ip, field +} + +// localItemKey returns the key of a local node item. +func localItemKey(id ID, field string) []byte { + key := append([]byte(dbLocalPrefix), id[:]...) + key = append(key, ':') + key = append(key, field...) + return key +} + +// fetchInt64 retrieves an integer associated with a particular key. +func (db *DB) fetchInt64(key []byte) int64 { + blob, err := db.lvl.Get(key, nil) + if err != nil { + return 0 + } + val, read := binary.Varint(blob) + if read <= 0 { + return 0 + } + return val +} + +// storeInt64 stores an integer in the given key. +func (db *DB) storeInt64(key []byte, n int64) error { + blob := make([]byte, binary.MaxVarintLen64) + blob = blob[:binary.PutVarint(blob, n)] + return db.lvl.Put(key, blob, nil) +} + +// fetchUint64 retrieves an integer associated with a particular key. +func (db *DB) fetchUint64(key []byte) uint64 { + blob, err := db.lvl.Get(key, nil) + if err != nil { + return 0 + } + val, _ := binary.Uvarint(blob) + return val +} + +// storeUint64 stores an integer in the given key. +func (db *DB) storeUint64(key []byte, n uint64) error { + blob := make([]byte, binary.MaxVarintLen64) + blob = blob[:binary.PutUvarint(blob, n)] + return db.lvl.Put(key, blob, nil) +} + +// Node retrieves a node with a given id from the database. +func (db *DB) Node(id ID) *Node { + blob, err := db.lvl.Get(nodeKey(id), nil) + if err != nil { + return nil + } + return mustDecodeNode(id[:], blob) +} + +func mustDecodeNode(id, data []byte) *Node { + node := new(Node) + if err := rlp.DecodeBytes(data, &node.r); err != nil { + panic(fmt.Errorf("p2p/enode: can't decode node %x in DB: %v", id, err)) + } + // Restore node id cache. + copy(node.id[:], id) + return node +} + +// UpdateNode inserts - potentially overwriting - a node into the peer database. +func (db *DB) UpdateNode(node *Node) error { + if node.Seq() < db.NodeSeq(node.ID()) { + return nil + } + blob, err := rlp.EncodeToBytes(&node.r) + if err != nil { + return err + } + if err := db.lvl.Put(nodeKey(node.ID()), blob, nil); err != nil { + return err + } + return db.storeUint64(nodeItemKey(node.ID(), zeroIP, dbNodeSeq), node.Seq()) +} + +// NodeSeq returns the stored record sequence number of the given node. +func (db *DB) NodeSeq(id ID) uint64 { + return db.fetchUint64(nodeItemKey(id, zeroIP, dbNodeSeq)) +} + +// Resolve returns the stored record of the node if it has a larger sequence +// number than n. +func (db *DB) Resolve(n *Node) *Node { + if n.Seq() > db.NodeSeq(n.ID()) { + return n + } + return db.Node(n.ID()) +} + +// DeleteNode deletes all information associated with a node. +func (db *DB) DeleteNode(id ID) { + deleteRange(db.lvl, nodeKey(id)) +} + +func deleteRange(db *leveldb.DB, prefix []byte) { + it := db.NewIterator(util.BytesPrefix(prefix), nil) + defer it.Release() + for it.Next() { + db.Delete(it.Key(), nil) + } +} + +// ensureExpirer is a small helper method ensuring that the data expiration +// mechanism is running. If the expiration goroutine is already running, this +// method simply returns. +// +// The goal is to start the data evacuation only after the network successfully +// bootstrapped itself (to prevent dumping potentially useful seed nodes). Since +// it would require significant overhead to exactly trace the first successful +// convergence, it's simpler to "ensure" the correct state when an appropriate +// condition occurs (i.e. a successful bonding), and discard further events. +func (db *DB) ensureExpirer() { + db.runner.Do(func() { go db.expirer() }) +} + +// expirer should be started in a go routine, and is responsible for looping ad +// infinitum and dropping stale data from the database. +func (db *DB) expirer() { + tick := time.NewTicker(dbCleanupCycle) + defer tick.Stop() + for { + select { + case <-tick.C: + db.expireNodes() + case <-db.quit: + return + } + } +} + +// expireNodes iterates over the database and deletes all nodes that have not +// been seen (i.e. received a pong from) for some time. +func (db *DB) expireNodes() { + it := db.lvl.NewIterator(util.BytesPrefix([]byte(dbNodePrefix)), nil) + defer it.Release() + if !it.Next() { + return + } + + var ( + threshold = time.Now().Add(-dbNodeExpiration).Unix() + youngestPong int64 + atEnd = false + ) + for !atEnd { + id, ip, field := splitNodeItemKey(it.Key()) + if field == dbNodePong { + time, _ := binary.Varint(it.Value()) + if time > youngestPong { + youngestPong = time + } + if time < threshold { + // Last pong from this IP older than threshold, remove fields belonging to it. + deleteRange(db.lvl, nodeItemKey(id, ip, "")) + } + } + atEnd = !it.Next() + nextID, _ := splitNodeKey(it.Key()) + if atEnd || nextID != id { + // We've moved beyond the last entry of the current ID. + // Remove everything if there was no recent enough pong. + if youngestPong > 0 && youngestPong < threshold { + deleteRange(db.lvl, nodeKey(id)) + } + youngestPong = 0 + } + } +} + +// LastPingReceived retrieves the time of the last ping packet received from +// a remote node. +func (db *DB) LastPingReceived(id ID, ip net.IP) time.Time { + return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePing)), 0) +} + +// UpdateLastPingReceived updates the last time we tried contacting a remote node. +func (db *DB) UpdateLastPingReceived(id ID, ip net.IP, instance time.Time) error { + return db.storeInt64(nodeItemKey(id, ip, dbNodePing), instance.Unix()) +} + +// LastPongReceived retrieves the time of the last successful pong from remote node. +func (db *DB) LastPongReceived(id ID, ip net.IP) time.Time { + // Launch expirer + db.ensureExpirer() + return time.Unix(db.fetchInt64(nodeItemKey(id, ip, dbNodePong)), 0) +} + +// UpdateLastPongReceived updates the last pong time of a node. +func (db *DB) UpdateLastPongReceived(id ID, ip net.IP, instance time.Time) error { + return db.storeInt64(nodeItemKey(id, ip, dbNodePong), instance.Unix()) +} + +// FindFails retrieves the number of findnode failures since bonding. +func (db *DB) FindFails(id ID, ip net.IP) int { + return int(db.fetchInt64(nodeItemKey(id, ip, dbNodeFindFails))) +} + +// UpdateFindFails updates the number of findnode failures since bonding. +func (db *DB) UpdateFindFails(id ID, ip net.IP, fails int) error { + return db.storeInt64(nodeItemKey(id, ip, dbNodeFindFails), int64(fails)) +} + +// localSeq retrieves the local record sequence counter, defaulting to the current +// timestamp if no previous exists. This ensures that wiping all data associated +// with a node (apart from its key) will not generate already used sequence nums. +func (db *DB) localSeq(id ID) uint64 { + if seq := db.fetchUint64(localItemKey(id, dbLocalSeq)); seq > 0 { + return seq + } + return uint64(time.Now().UnixMilli()) +} + +// storeLocalSeq stores the local record sequence counter. +func (db *DB) storeLocalSeq(id ID, n uint64) { + db.storeUint64(localItemKey(id, dbLocalSeq), n) +} + +// QuerySeeds retrieves random nodes to be used as potential seed nodes +// for bootstrapping. +func (db *DB) QuerySeeds(n int, maxAge time.Duration) []*Node { + var ( + now = time.Now() + nodes = make([]*Node, 0, n) + it = db.lvl.NewIterator(nil, nil) + id ID + ) + defer it.Release() + +seek: + for seeks := 0; len(nodes) < n && seeks < n*5; seeks++ { + // Seek to a random entry. The first byte is incremented by a + // random amount each time in order to increase the likelihood + // of hitting all existing nodes in very small databases. + ctr := id[0] + rand.Read(id[:]) + id[0] = ctr + id[0]%16 + it.Seek(nodeKey(id)) + + n := nextNode(it) + if n == nil { + id[0] = 0 + continue seek // iterator exhausted + } + if now.Sub(db.LastPongReceived(n.ID(), n.IP())) > maxAge { + continue seek + } + for i := range nodes { + if nodes[i].ID() == n.ID() { + continue seek // duplicate + } + } + nodes = append(nodes, n) + } + return nodes +} + +// reads the next node record from the iterator, skipping over other +// database entries. +func nextNode(it iterator.Iterator) *Node { + for end := false; !end; end = !it.Next() { + id, rest := splitNodeKey(it.Key()) + if string(rest) != dbDiscoverRoot { + continue + } + return mustDecodeNode(id[:], it.Value()) + } + return nil +} + +// close flushes and closes the database files. +func (db *DB) Close() { + close(db.quit) + db.lvl.Close() +} diff --git a/p2p/enode/nodedb_test.go b/p2p/enode/nodedb_test.go new file mode 100644 index 000000000000..341b61a289f7 --- /dev/null +++ b/p2p/enode/nodedb_test.go @@ -0,0 +1,464 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "bytes" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "reflect" + "testing" + "time" +) + +var keytestID = HexID("51232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439") + +func TestDBNodeKey(t *testing.T) { + enc := nodeKey(keytestID) + want := []byte{ + 'n', ':', + 0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id + 0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, // + 0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, // + 0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, // + ':', 'v', '4', + } + if !bytes.Equal(enc, want) { + t.Errorf("wrong encoded key:\ngot %q\nwant %q", enc, want) + } + id, _ := splitNodeKey(enc) + if id != keytestID { + t.Errorf("wrong ID from splitNodeKey") + } +} + +func TestDBNodeItemKey(t *testing.T) { + wantIP := net.IP{127, 0, 0, 3} + wantField := "foobar" + enc := nodeItemKey(keytestID, wantIP, wantField) + want := []byte{ + 'n', ':', + 0x51, 0x23, 0x2b, 0x8d, 0x78, 0x21, 0x61, 0x7d, // node id + 0x2b, 0x29, 0xb5, 0x4b, 0x81, 0xcd, 0xef, 0xb9, // + 0xb3, 0xe9, 0xc3, 0x7d, 0x7f, 0xd5, 0xf6, 0x32, // + 0x70, 0xbc, 0xc9, 0xe1, 0xa6, 0xf6, 0xa4, 0x39, // + ':', 'v', '4', ':', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // IP + 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x03, // + ':', 'f', 'o', 'o', 'b', 'a', 'r', + } + if !bytes.Equal(enc, want) { + t.Errorf("wrong encoded key:\ngot %q\nwant %q", enc, want) + } + id, ip, field := splitNodeItemKey(enc) + if id != keytestID { + t.Errorf("splitNodeItemKey returned wrong ID: %v", id) + } + if !bytes.Equal(ip, wantIP) { + t.Errorf("splitNodeItemKey returned wrong IP: %v", ip) + } + if field != wantField { + t.Errorf("splitNodeItemKey returned wrong field: %q", field) + } +} + +var nodeDBInt64Tests = []struct { + key []byte + value int64 +}{ + {key: []byte{0x01}, value: 1}, + {key: []byte{0x02}, value: 2}, + {key: []byte{0x03}, value: 3}, +} + +func TestDBInt64(t *testing.T) { + db, _ := OpenDB("") + defer db.Close() + + tests := nodeDBInt64Tests + for i := 0; i < len(tests); i++ { + // Insert the next value + if err := db.storeInt64(tests[i].key, tests[i].value); err != nil { + t.Errorf("test %d: failed to store value: %v", i, err) + } + // Check all existing and non existing values + for j := 0; j < len(tests); j++ { + num := db.fetchInt64(tests[j].key) + switch { + case j <= i && num != tests[j].value: + t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, tests[j].value) + case j > i && num != 0: + t.Errorf("test %d, item %d: value mismatch: have %v, want %v", i, j, num, 0) + } + } + } +} + +func TestDBFetchStore(t *testing.T) { + node := NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.IP{192, 168, 0, 1}, + 30303, + 30303, + ) + inst := time.Now() + num := 314 + + db, _ := OpenDB("") + defer db.Close() + + // Check fetch/store operations on a node ping object + if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != 0 { + t.Errorf("ping: non-existing object: %v", stored) + } + if err := db.UpdateLastPingReceived(node.ID(), node.IP(), inst); err != nil { + t.Errorf("ping: failed to update: %v", err) + } + if stored := db.LastPingReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() { + t.Errorf("ping: value mismatch: have %v, want %v", stored, inst) + } + // Check fetch/store operations on a node pong object + if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != 0 { + t.Errorf("pong: non-existing object: %v", stored) + } + if err := db.UpdateLastPongReceived(node.ID(), node.IP(), inst); err != nil { + t.Errorf("pong: failed to update: %v", err) + } + if stored := db.LastPongReceived(node.ID(), node.IP()); stored.Unix() != inst.Unix() { + t.Errorf("pong: value mismatch: have %v, want %v", stored, inst) + } + // Check fetch/store operations on a node findnode-failure object + if stored := db.FindFails(node.ID(), node.IP()); stored != 0 { + t.Errorf("find-node fails: non-existing object: %v", stored) + } + if err := db.UpdateFindFails(node.ID(), node.IP(), num); err != nil { + t.Errorf("find-node fails: failed to update: %v", err) + } + if stored := db.FindFails(node.ID(), node.IP()); stored != num { + t.Errorf("find-node fails: value mismatch: have %v, want %v", stored, num) + } + // Check fetch/store operations on an actual node object + if stored := db.Node(node.ID()); stored != nil { + t.Errorf("node: non-existing object: %v", stored) + } + if err := db.UpdateNode(node); err != nil { + t.Errorf("node: failed to update: %v", err) + } + if stored := db.Node(node.ID()); stored == nil { + t.Errorf("node: not found") + } else if !reflect.DeepEqual(stored, node) { + t.Errorf("node: data mismatch: have %v, want %v", stored, node) + } +} + +var nodeDBSeedQueryNodes = []struct { + node *Node + pong time.Time +}{ + // This one should not be in the result set because its last + // pong time is too far in the past. + { + node: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + pong: time.Now().Add(-3 * time.Hour), + }, + // This one shouldn't be in the result set because its + // nodeID is the local node's ID. + { + node: NewV4( + hexPubkey("ff93ff820abacd4351b0f14e47b324bc82ff014c226f3f66a53535734a3c150e7e38ca03ef0964ba55acddc768f5e99cd59dea95ddd4defbab1339c92fa319b2"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + pong: time.Now().Add(-4 * time.Second), + }, + + // These should be in the result set. + { + node: NewV4( + hexPubkey("c2b5eb3f5dde05f815b63777809ee3e7e0cbb20035a6b00ce327191e6eaa8f26a8d461c9112b7ab94698e7361fa19fd647e603e73239002946d76085b6f928d6"), + net.IP{127, 0, 0, 1}, + 30303, + 30303, + ), + pong: time.Now().Add(-2 * time.Second), + }, + { + node: NewV4( + hexPubkey("6ca1d400c8ddf8acc94bcb0dd254911ad71a57bed5e0ae5aa205beed59b28c2339908e97990c493499613cff8ecf6c3dc7112a8ead220cdcd00d8847ca3db755"), + net.IP{127, 0, 0, 2}, + 30303, + 30303, + ), + pong: time.Now().Add(-3 * time.Second), + }, + { + node: NewV4( + hexPubkey("234dc63fe4d131212b38236c4c3411288d7bec61cbf7b120ff12c43dc60c96182882f4291d209db66f8a38e986c9c010ff59231a67f9515c7d1668b86b221a47"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + pong: time.Now().Add(-1 * time.Second), + }, + { + node: NewV4( + hexPubkey("c013a50b4d1ebce5c377d8af8cb7114fd933ffc9627f96ad56d90fef5b7253ec736fd07ef9a81dc2955a997e54b7bf50afd0aa9f110595e2bec5bb7ce1657004"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + pong: time.Now().Add(-2 * time.Second), + }, + { + node: NewV4( + hexPubkey("f141087e3e08af1aeec261ff75f48b5b1637f594ea9ad670e50051646b0416daa3b134c28788cbe98af26992a47652889cd8577ccc108ac02c6a664db2dc1283"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + pong: time.Now().Add(-2 * time.Second), + }, +} + +func TestDBSeedQuery(t *testing.T) { + // Querying seeds uses seeks an might not find all nodes + // every time when the database is small. Run the test multiple + // times to avoid flakes. + const attempts = 15 + var err error + for i := 0; i < attempts; i++ { + if err = testSeedQuery(); err == nil { + return + } + } + if err != nil { + t.Errorf("no successful run in %d attempts: %v", attempts, err) + } +} + +func testSeedQuery() error { + db, _ := OpenDB("") + defer db.Close() + + // Insert a batch of nodes for querying + for i, seed := range nodeDBSeedQueryNodes { + if err := db.UpdateNode(seed.node); err != nil { + return fmt.Errorf("node %d: failed to insert: %v", i, err) + } + if err := db.UpdateLastPongReceived(seed.node.ID(), seed.node.IP(), seed.pong); err != nil { + return fmt.Errorf("node %d: failed to insert bondTime: %v", i, err) + } + } + + // Retrieve the entire batch and check for duplicates + seeds := db.QuerySeeds(len(nodeDBSeedQueryNodes)*2, time.Hour) + have := make(map[ID]struct{}) + for _, seed := range seeds { + have[seed.ID()] = struct{}{} + } + want := make(map[ID]struct{}) + for _, seed := range nodeDBSeedQueryNodes[1:] { + want[seed.node.ID()] = struct{}{} + } + if len(seeds) != len(want) { + return fmt.Errorf("seed count mismatch: have %v, want %v", len(seeds), len(want)) + } + for id := range have { + if _, ok := want[id]; !ok { + return fmt.Errorf("extra seed: %v", id) + } + } + for id := range want { + if _, ok := have[id]; !ok { + return fmt.Errorf("missing seed: %v", id) + } + } + return nil +} + +func TestDBPersistency(t *testing.T) { + root, err := ioutil.TempDir("", "nodedb-") + if err != nil { + t.Fatalf("failed to create temporary data folder: %v", err) + } + defer os.RemoveAll(root) + + var ( + testKey = []byte("somekey") + testInt = int64(314) + ) + + // Create a persistent database and store some values + db, err := OpenDB(filepath.Join(root, "database")) + if err != nil { + t.Fatalf("failed to create persistent database: %v", err) + } + if err := db.storeInt64(testKey, testInt); err != nil { + t.Fatalf("failed to store value: %v.", err) + } + db.Close() + + // Reopen the database and check the value + db, err = OpenDB(filepath.Join(root, "database")) + if err != nil { + t.Fatalf("failed to open persistent database: %v", err) + } + if val := db.fetchInt64(testKey); val != testInt { + t.Fatalf("value mismatch: have %v, want %v", val, testInt) + } + db.Close() +} + +var nodeDBExpirationNodes = []struct { + node *Node + pong time.Time + storeNode bool + exp bool +}{ + // Node has new enough pong time and isn't expired: + { + node: NewV4( + hexPubkey("8d110e2ed4b446d9b5fb50f117e5f37fb7597af455e1dab0e6f045a6eeaa786a6781141659020d38bdc5e698ed3d4d2bafa8b5061810dfa63e8ac038db2e9b67"), + net.IP{127, 0, 0, 1}, + 30303, + 30303, + ), + storeNode: true, + pong: time.Now().Add(-dbNodeExpiration + time.Minute), + exp: false, + }, + // Node with pong time before expiration is removed: + { + node: NewV4( + hexPubkey("913a205579c32425b220dfba999d215066e5bdbf900226b11da1907eae5e93eb40616d47412cf819664e9eacbdfcca6b0c6e07e09847a38472d4be46ab0c3672"), + net.IP{127, 0, 0, 2}, + 30303, + 30303, + ), + storeNode: true, + pong: time.Now().Add(-dbNodeExpiration - time.Minute), + exp: true, + }, + // Just pong time, no node stored: + { + node: NewV4( + hexPubkey("b56670e0b6bad2c5dab9f9fe6f061a16cf78d68b6ae2cfda3144262d08d97ce5f46fd8799b6d1f709b1abe718f2863e224488bd7518e5e3b43809ac9bd1138ca"), + net.IP{127, 0, 0, 3}, + 30303, + 30303, + ), + storeNode: false, + pong: time.Now().Add(-dbNodeExpiration - time.Minute), + exp: true, + }, + // Node with multiple pong times, all older than expiration. + { + node: NewV4( + hexPubkey("29f619cebfd32c9eab34aec797ed5e3fe15b9b45be95b4df3f5fe6a9ae892f433eb08d7698b2ef3621568b0fb70d57b515ab30d4e72583b798298e0f0a66b9d1"), + net.IP{127, 0, 0, 4}, + 30303, + 30303, + ), + storeNode: true, + pong: time.Now().Add(-dbNodeExpiration - time.Minute), + exp: true, + }, + { + node: NewV4( + hexPubkey("29f619cebfd32c9eab34aec797ed5e3fe15b9b45be95b4df3f5fe6a9ae892f433eb08d7698b2ef3621568b0fb70d57b515ab30d4e72583b798298e0f0a66b9d1"), + net.IP{127, 0, 0, 5}, + 30303, + 30303, + ), + storeNode: false, + pong: time.Now().Add(-dbNodeExpiration - 2*time.Minute), + exp: true, + }, + // Node with multiple pong times, one newer, one older than expiration. + { + node: NewV4( + hexPubkey("3b73a9e5f4af6c4701c57c73cc8cfa0f4802840b24c11eba92aac3aef65644a3728b4b2aec8199f6d72bd66be2c65861c773129039bd47daa091ca90a6d4c857"), + net.IP{127, 0, 0, 6}, + 30303, + 30303, + ), + storeNode: true, + pong: time.Now().Add(-dbNodeExpiration + time.Minute), + exp: false, + }, + { + node: NewV4( + hexPubkey("3b73a9e5f4af6c4701c57c73cc8cfa0f4802840b24c11eba92aac3aef65644a3728b4b2aec8199f6d72bd66be2c65861c773129039bd47daa091ca90a6d4c857"), + net.IP{127, 0, 0, 7}, + 30303, + 30303, + ), + storeNode: false, + pong: time.Now().Add(-dbNodeExpiration - time.Minute), + exp: true, + }, +} + +func TestDBExpiration(t *testing.T) { + db, _ := OpenDB("") + defer db.Close() + + // Add all the test nodes and set their last pong time. + for i, seed := range nodeDBExpirationNodes { + if seed.storeNode { + if err := db.UpdateNode(seed.node); err != nil { + t.Fatalf("node %d: failed to insert: %v", i, err) + } + } + if err := db.UpdateLastPongReceived(seed.node.ID(), seed.node.IP(), seed.pong); err != nil { + t.Fatalf("node %d: failed to update bondTime: %v", i, err) + } + } + + db.expireNodes() + + // Check that expired entries have been removed. + unixZeroTime := time.Unix(0, 0) + for i, seed := range nodeDBExpirationNodes { + node := db.Node(seed.node.ID()) + pong := db.LastPongReceived(seed.node.ID(), seed.node.IP()) + if seed.exp { + if seed.storeNode && node != nil { + t.Errorf("node %d (%s) shouldn't be present after expiration", i, seed.node.ID().TerminalString()) + } + if !pong.Equal(unixZeroTime) { + t.Errorf("pong time %d (%s %v) shouldn't be present after expiration", i, seed.node.ID().TerminalString(), seed.node.IP()) + } + } else { + if seed.storeNode && node == nil { + t.Errorf("node %d (%s) should be present after expiration", i, seed.node.ID().TerminalString()) + } + if !pong.Equal(seed.pong.Truncate(1 * time.Second)) { + t.Errorf("pong time %d (%s) should be %v after expiration, but is %v", i, seed.node.ID().TerminalString(), seed.pong, pong) + } + } + } +} diff --git a/p2p/enode/urlv4.go b/p2p/enode/urlv4.go new file mode 100644 index 000000000000..bb1299694c28 --- /dev/null +++ b/p2p/enode/urlv4.go @@ -0,0 +1,206 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "net" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/XinFinOrg/XDPoSChain/common/math" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" +) + +var incompleteNodeURL = regexp.MustCompile("(?i)^(?:enode://)?([0-9a-f]+)$") + +// MustParseV4 parses a node URL. It panics if the URL is not valid. +func MustParseV4(rawurl string) *Node { + n, err := ParseV4(rawurl) + if err != nil { + panic("invalid node URL: " + err.Error()) + } + return n +} + +// ParseV4 parses a node URL. +// +// There are two basic forms of node URLs: +// +// - incomplete nodes, which only have the public key (node ID) +// - complete nodes, which contain the public key and IP/Port information +// +// For incomplete nodes, the designator must look like one of these +// +// enode:// +// +// +// For complete nodes, the node ID is encoded in the username portion +// of the URL, separated from the host by an @ sign. The hostname can +// only be given as an IP address, DNS domain names are not allowed. +// The port in the host name section is the TCP listening port. If the +// TCP and UDP (discovery) ports differ, the UDP port is specified as +// query parameter "discport". +// +// In the following example, the node URL describes +// a node with IP address 10.3.58.6, TCP listening port 30303 +// and UDP discovery port 30301. +// +// enode://@10.3.58.6:30303?discport=30301 +func ParseV4(rawurl string) (*Node, error) { + if m := incompleteNodeURL.FindStringSubmatch(rawurl); m != nil { + id, err := parsePubkey(m[1]) + if err != nil { + return nil, fmt.Errorf("invalid node ID (%v)", err) + } + return NewV4(id, nil, 0, 0), nil + } + return parseComplete(rawurl) +} + +// NewV4 creates a node from discovery v4 node information. The record +// contained in the node has a zero-length signature. +func NewV4(pubkey *ecdsa.PublicKey, ip net.IP, tcp, udp int) *Node { + var r enr.Record + if ip != nil { + r.Set(enr.IP(ip)) + } + if udp != 0 { + r.Set(enr.UDP(udp)) + } + if tcp != 0 { + r.Set(enr.TCP(tcp)) + } + signV4Compat(&r, pubkey) + n, err := New(v4CompatID{}, &r) + if err != nil { + panic(err) + } + return n +} + +func parseComplete(rawurl string) (*Node, error) { + var ( + id *ecdsa.PublicKey + ip net.IP + tcpPort, udpPort uint64 + ) + u, err := url.Parse(rawurl) + if err != nil { + if isInvalidPortError(err) { + return nil, errors.New("invalid port") + } + return nil, err + } + if u.Scheme != "enode" { + return nil, errors.New("invalid URL scheme, want \"enode\"") + } + // Parse the Node ID from the user portion. + if u.User == nil { + return nil, errors.New("does not contain node ID") + } + if id, err = parsePubkey(u.User.String()); err != nil { + return nil, fmt.Errorf("invalid node ID (%v)", err) + } + // Parse the IP address. + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + return nil, fmt.Errorf("invalid host: %v", err) + } + if ip = net.ParseIP(host); ip == nil { + return nil, errors.New("invalid IP address") + } + // Ensure the IP is 4 bytes long for IPv4 addresses. + if ipv4 := ip.To4(); ipv4 != nil { + ip = ipv4 + } + // Parse the port numbers. + if tcpPort, err = strconv.ParseUint(port, 10, 16); err != nil { + return nil, errors.New("invalid port") + } + udpPort = tcpPort + qv := u.Query() + if qv.Get("discport") != "" { + udpPort, err = strconv.ParseUint(qv.Get("discport"), 10, 16) + if err != nil { + return nil, errors.New("invalid discport in query") + } + } + return NewV4(id, ip, int(tcpPort), int(udpPort)), nil +} + +func isInvalidPortError(err error) bool { + var urlErr *url.Error + if errors.As(err, &urlErr) { + err = urlErr.Err + } + return strings.Contains(err.Error(), "invalid port") +} + +// parsePubkey parses a hex-encoded secp256k1 public key. +func parsePubkey(in string) (*ecdsa.PublicKey, error) { + b, err := hex.DecodeString(in) + if err != nil { + return nil, err + } else if len(b) != 64 { + return nil, fmt.Errorf("wrong length, want %d hex chars", 128) + } + b = append([]byte{0x4}, b...) + return crypto.UnmarshalPubkey(b) +} + +func (n *Node) v4URL() string { + var ( + scheme enr.ID + nodeid string + key ecdsa.PublicKey + ) + n.Load(&scheme) + n.Load((*Secp256k1)(&key)) + switch { + case scheme == "v4" || key != ecdsa.PublicKey{}: + nodeid = fmt.Sprintf("%x", crypto.FromECDSAPub(&key)[1:]) + default: + nodeid = fmt.Sprintf("%s.%x", scheme, n.id[:]) + } + u := url.URL{Scheme: "enode"} + if n.Incomplete() { + u.Host = nodeid + } else { + addr := net.TCPAddr{IP: n.IP(), Port: n.TCP()} + u.User = url.User(nodeid) + u.Host = addr.String() + if n.UDP() != n.TCP() { + u.RawQuery = "discport=" + strconv.Itoa(n.UDP()) + } + } + return u.String() +} + +// PubkeyToIDV4 derives the v4 node address from the given public key. +func PubkeyToIDV4(key *ecdsa.PublicKey) ID { + e := make([]byte, 64) + math.ReadBits(key.X, e[:len(e)/2]) + math.ReadBits(key.Y, e[len(e)/2:]) + return ID(crypto.Keccak256Hash(e)) +} diff --git a/p2p/enode/urlv4_test.go b/p2p/enode/urlv4_test.go new file mode 100644 index 000000000000..104416b97766 --- /dev/null +++ b/p2p/enode/urlv4_test.go @@ -0,0 +1,243 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package enode + +import ( + "bytes" + "crypto/ecdsa" + "math/big" + "net" + "reflect" + "strings" + "testing" + "testing/quick" +) + +var parseNodeTests = []struct { + rawurl string + wantError string + wantResult *Node +}{ + { + rawurl: "http://foobar", + wantError: `invalid URL scheme, want "enode"`, + }, + { + rawurl: "enode://01010101@123.124.125.126:3", + wantError: `invalid node ID (wrong length, want 128 hex chars)`, + }, + // Complete nodes with IP address. + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@hostname:3", + wantError: `invalid IP address`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:foo", + wantError: `invalid port`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:3?discport=foo", + wantError: `invalid discport in query`, + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.IP{0x7f, 0x0, 0x0, 0x1}, + 52150, + 52150, + ), + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[::]:52150", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.ParseIP("::"), + 52150, + 52150, + ), + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.ParseIP("2001:db8:3c4d:15::abcd:ef12"), + 52150, + 52150, + ), + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@127.0.0.1:52150?discport=22334", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + net.IP{0x7f, 0x0, 0x0, 0x1}, + 52150, + 22334, + ), + }, + // Incomplete nodes with no address. + { + rawurl: "1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + nil, 0, 0, + ), + }, + { + rawurl: "enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439", + wantResult: NewV4( + hexPubkey("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439"), + nil, 0, 0, + ), + }, + // Invalid URLs + { + rawurl: "01010101", + wantError: `invalid node ID (wrong length, want 128 hex chars)`, + }, + { + rawurl: "enode://01010101", + wantError: `invalid node ID (wrong length, want 128 hex chars)`, + }, + { + // This test checks that errors from url.Parse are handled. + rawurl: "://foo", + wantError: `parse "://foo": missing protocol scheme`, + }, +} + +func hexPubkey(h string) *ecdsa.PublicKey { + k, err := parsePubkey(h) + if err != nil { + panic(err) + } + return k +} + +func TestParseNode(t *testing.T) { + for _, test := range parseNodeTests { + n, err := ParseV4(test.rawurl) + if test.wantError != "" { + if err == nil { + t.Errorf("test %q:\n got nil error, expected %#q", test.rawurl, test.wantError) + continue + } else if err.Error() != test.wantError { + t.Errorf("test %q:\n got error %#q, expected %#q", test.rawurl, err.Error(), test.wantError) + continue + } + } else { + if err != nil { + t.Errorf("test %q:\n unexpected error: %v", test.rawurl, err) + continue + } + if !reflect.DeepEqual(n, test.wantResult) { + t.Errorf("test %q:\n result mismatch:\ngot: %#v\nwant: %#v", test.rawurl, n, test.wantResult) + } + } + } +} + +func TestNodeString(t *testing.T) { + for i, test := range parseNodeTests { + if test.wantError == "" && strings.HasPrefix(test.rawurl, "enode://") { + str := test.wantResult.String() + if str != test.rawurl { + t.Errorf("test %d: Node.String() mismatch:\ngot: %s\nwant: %s", i, str, test.rawurl) + } + } + } +} + +func TestHexID(t *testing.T) { + ref := ID{0, 0, 0, 0, 0, 0, 0, 128, 106, 217, 182, 31, 165, 174, 1, 67, 7, 235, 220, 150, 66, 83, 173, 205, 159, 44, 10, 57, 42, 161, 26, 188} + id1 := HexID("0x00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + id2 := HexID("00000000000000806ad9b61fa5ae014307ebdc964253adcd9f2c0a392aa11abc") + + if id1 != ref { + t.Errorf("wrong id1\ngot %v\nwant %v", id1[:], ref[:]) + } + if id2 != ref { + t.Errorf("wrong id2\ngot %v\nwant %v", id2[:], ref[:]) + } +} + +func TestID_textEncoding(t *testing.T) { + ref := ID{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, + 0x31, 0x32, + } + hex := "0102030405060708091011121314151617181920212223242526272829303132" + + text, err := ref.MarshalText() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(text, []byte(hex)) { + t.Fatalf("text encoding did not match\nexpected: %s\ngot: %s", hex, text) + } + + id := new(ID) + if err := id.UnmarshalText(text); err != nil { + t.Fatal(err) + } + if *id != ref { + t.Fatalf("text decoding did not match\nexpected: %s\ngot: %s", ref, id) + } +} + +func TestNodeID_distcmp(t *testing.T) { + distcmpBig := func(target, a, b ID) int { + tbig := new(big.Int).SetBytes(target[:]) + abig := new(big.Int).SetBytes(a[:]) + bbig := new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(tbig, abig).Cmp(new(big.Int).Xor(tbig, bbig)) + } + if err := quick.CheckEqual(DistCmp, distcmpBig, nil); err != nil { + t.Error(err) + } +} + +// The random tests is likely to miss the case where a and b are equal, +// this test checks it explicitly. +func TestNodeID_distcmpEqual(t *testing.T) { + base := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + x := ID{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + if DistCmp(base, x, x) != 0 { + t.Errorf("DistCmp(base, x, x) != 0") + } +} + +func TestNodeID_logdist(t *testing.T) { + logdistBig := func(a, b ID) int { + abig, bbig := new(big.Int).SetBytes(a[:]), new(big.Int).SetBytes(b[:]) + return new(big.Int).Xor(abig, bbig).BitLen() + } + if err := quick.CheckEqual(LogDist, logdistBig, nil); err != nil { + t.Error(err) + } +} + +// The random tests is likely to miss the case where a and b are equal, +// this test checks it explicitly. +func TestNodeID_logdistEqual(t *testing.T) { + x := ID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + if LogDist(x, x) != 0 { + t.Errorf("LogDist(x, x) != 0") + } +} diff --git a/p2p/enr/enr.go b/p2p/enr/enr.go index 84151e1789f9..4493a5a16f0e 100644 --- a/p2p/enr/enr.go +++ b/p2p/enr/enr.go @@ -15,14 +15,20 @@ // along with the go-ethereum library. If not, see . // Package enr implements Ethereum Node Records as defined in EIP-778. A node record holds -// arbitrary information about a node on the peer-to-peer network. -// -// Records contain named keys. To store and retrieve key/values in a record, use the Entry +// arbitrary information about a node on the peer-to-peer network. Node information is +// stored in key/value pairs. To store and retrieve key/values in a record, use the Entry // interface. // -// Records must be signed before transmitting them to another node. Decoding a record verifies -// its signature. When creating a record, set the entries you want, then call Sign to add the -// signature. Modifying a record invalidates the signature. +// # Signature Handling +// +// Records must be signed before transmitting them to another node. +// +// Decoding a record doesn't check its signature. Code working with records from an +// untrusted source must always verify two things: that the record uses an identity scheme +// deemed secure, and that the signature is valid according to the declared scheme. +// +// When creating a record, set the entries you want and use a signing function provided by +// the identity scheme to add the signature. Modifying a record invalidates the signature. // // Package enr supports the "secp256k1-keccak" identity scheme. package enr @@ -40,8 +46,7 @@ import ( const SizeLimit = 300 // maximum encoded size of a node record in bytes var ( - errNoID = errors.New("unknown or unspecified identity scheme") - errInvalidSig = errors.New("invalid signature") + ErrInvalidSig = errors.New("invalid signature on node record") errNotSorted = errors.New("record key/value pairs are not sorted by key") errDuplicateKey = errors.New("record contains duplicate key") errIncompletePair = errors.New("record contains incomplete k/v pair") @@ -50,6 +55,32 @@ var ( errNotFound = errors.New("no such key in record") ) +// An IdentityScheme is capable of verifying record signatures and +// deriving node addresses. +type IdentityScheme interface { + Verify(r *Record, sig []byte) error + NodeAddr(r *Record) []byte +} + +// SchemeMap is a registry of named identity schemes. +type SchemeMap map[string]IdentityScheme + +func (m SchemeMap) Verify(r *Record, sig []byte) error { + s := m[r.IdentityScheme()] + if s == nil { + return ErrInvalidSig + } + return s.Verify(r, sig) +} + +func (m SchemeMap) NodeAddr(r *Record) []byte { + s := m[r.IdentityScheme()] + if s == nil { + return nil + } + return s.NodeAddr(r) +} + // Record represents a node record. The zero value is an empty record. type Record struct { seq uint64 // sequence number @@ -64,11 +95,6 @@ type pair struct { v rlp.RawValue } -// Signed reports whether the record has a valid signature. -func (r *Record) Signed() bool { - return r.signature != nil -} - // Seq returns the sequence number. func (r *Record) Seq() uint64 { return r.seq @@ -130,7 +156,7 @@ func (r *Record) Set(e Entry) { } func (r *Record) invalidate() { - if r.signature == nil { + if r.signature != nil { r.seq++ } r.signature = nil @@ -140,7 +166,7 @@ func (r *Record) invalidate() { // EncodeRLP implements rlp.Encoder. Encoding fails if // the record is unsigned. func (r Record) EncodeRLP(w io.Writer) error { - if !r.Signed() { + if r.signature == nil { return errEncodeUnsigned } _, err := w.Write(r.raw) @@ -149,25 +175,34 @@ func (r Record) EncodeRLP(w io.Writer) error { // DecodeRLP implements rlp.Decoder. Decoding verifies the signature. func (r *Record) DecodeRLP(s *rlp.Stream) error { - raw, err := s.Raw() + dec, raw, err := decodeRecord(s) if err != nil { return err } + *r = dec + r.raw = raw + return nil +} + +func decodeRecord(s *rlp.Stream) (dec Record, raw []byte, err error) { + raw, err = s.Raw() + if err != nil { + return dec, raw, err + } if len(raw) > SizeLimit { - return errTooBig + return dec, raw, errTooBig } // Decode the RLP container. - dec := Record{raw: raw} s = rlp.NewStream(bytes.NewReader(raw), 0) if _, err := s.List(); err != nil { - return err + return dec, raw, err } if err = s.Decode(&dec.signature); err != nil { - return err + return dec, raw, err } if err = s.Decode(&dec.seq); err != nil { - return err + return dec, raw, err } // The rest of the record contains sorted k/v pairs. var prevkey string @@ -177,73 +212,68 @@ func (r *Record) DecodeRLP(s *rlp.Stream) error { if err == rlp.EOL { break } - return err + return dec, raw, err } if err := s.Decode(&kv.v); err != nil { if err == rlp.EOL { - return errIncompletePair + return dec, raw, errIncompletePair } - return err + return dec, raw, err } if i > 0 { if kv.k == prevkey { - return errDuplicateKey + return dec, raw, errDuplicateKey } if kv.k < prevkey { - return errNotSorted + return dec, raw, errNotSorted } } dec.pairs = append(dec.pairs, kv) prevkey = kv.k } - if err := s.ListEnd(); err != nil { - return err - } + return dec, raw, s.ListEnd() +} - _, scheme := dec.idScheme() - if scheme == nil { - return errNoID - } - if err := scheme.Verify(&dec, dec.signature); err != nil { - return err - } - *r = dec - return nil +// IdentityScheme returns the name of the identity scheme in the record. +func (r *Record) IdentityScheme() string { + var id ID + r.Load(&id) + return string(id) } -// NodeAddr returns the node address. The return value will be nil if the record is -// unsigned. -func (r *Record) NodeAddr() []byte { - _, scheme := r.idScheme() - if scheme == nil { - return nil - } - return scheme.NodeAddr(r) +// VerifySignature checks whether the record is signed using the given identity scheme. +func (r *Record) VerifySignature(s IdentityScheme) error { + return s.Verify(r, r.signature) } // SetSig sets the record signature. It returns an error if the encoded record is larger // than the size limit or if the signature is invalid according to the passed scheme. -func (r *Record) SetSig(idscheme string, sig []byte) error { - // Check that "id" is set and matches the given scheme. This panics because - // inconsitencies here are always implementation bugs in the signing function calling - // this method. - id, s := r.idScheme() - if s == nil { - panic(errNoID) - } - if id != idscheme { - panic(fmt.Errorf("identity scheme mismatch in Sign: record has %s, want %s", id, idscheme)) - } - - // Verify against the scheme. - if err := s.Verify(r, sig); err != nil { - return err - } - raw, err := r.encode(sig) - if err != nil { - return err +// +// You can also use SetSig to remove the signature explicitly by passing a nil scheme +// and signature. +// +// SetSig panics when either the scheme or the signature (but not both) are nil. +func (r *Record) SetSig(s IdentityScheme, sig []byte) error { + switch { + // Prevent storing invalid data. + case s == nil && sig != nil: + panic("enr: invalid call to SetSig with non-nil signature but nil scheme") + case s != nil && sig == nil: + panic("enr: invalid call to SetSig with nil signature but non-nil scheme") + // Verify if we have a scheme. + case s != nil: + if err := s.Verify(r, sig); err != nil { + return err + } + raw, err := r.encode(sig) + if err != nil { + return err + } + r.signature, r.raw = sig, raw + // Reset otherwise. + default: + r.signature, r.raw = nil, nil } - r.signature, r.raw = sig, raw return nil } @@ -268,11 +298,3 @@ func (r *Record) encode(sig []byte) (raw []byte, err error) { } return raw, nil } - -func (r *Record) idScheme() (string, IdentityScheme) { - var id ID - if err := r.Load(&id); err != nil { - return "", nil - } - return string(id), FindIdentityScheme(string(id)) -} diff --git a/p2p/enr/enr_test.go b/p2p/enr/enr_test.go index b8cd6dc1d443..ef7c01ed04c6 100644 --- a/p2p/enr/enr_test.go +++ b/p2p/enr/enr_test.go @@ -18,23 +18,17 @@ package enr import ( "bytes" - "encoding/hex" + "encoding/binary" "fmt" "math/rand" "testing" "time" - "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/rlp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var ( - privkey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - pubkey = &privkey.PublicKey -) - var rnd = rand.New(rand.NewSource(time.Now().UnixNano())) func randomString(strlen int) string { @@ -76,7 +70,7 @@ func TestGetSetIP6(t *testing.T) { assert.Equal(t, ip, ip2) } -// TestGetSetUDP tests encoding/decoding and setting/getting of the DiscPort key. +// TestGetSetDiscPort tests encoding/decoding and setting/getting of the DiscPort key. func TestGetSetUDP(t *testing.T) { port := UDP(30309) var r Record @@ -87,18 +81,6 @@ func TestGetSetUDP(t *testing.T) { assert.Equal(t, port, port2) } -// TestGetSetSecp256k1 tests encoding/decoding and setting/getting of the Secp256k1 key. -func TestGetSetSecp256k1(t *testing.T) { - var r Record - if err := SignV4(&r, privkey); err != nil { - t.Fatal(err) - } - - var pk Secp256k1 - require.NoError(t, r.Load(&pk)) - assert.EqualValues(t, pubkey, &pk) -} - func TestLoadErrors(t *testing.T) { var r Record ip4 := IP{127, 0, 0, 1} @@ -167,29 +149,38 @@ func TestSortedGetAndSet(t *testing.T) { func TestDirty(t *testing.T) { var r Record - if r.Signed() { - t.Error("Signed returned true for zero record") - } if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned { t.Errorf("expected errEncodeUnsigned, got %#v", err) } - require.NoError(t, SignV4(&r, privkey)) - if !r.Signed() { - t.Error("Signed return false for signed record") + require.NoError(t, signTest([]byte{5}, &r)) + if len(r.signature) == 0 { + t.Error("record is not signed") } _, err := rlp.EncodeToBytes(r) assert.NoError(t, err) r.SetSeq(3) - if r.Signed() { - t.Error("Signed returned true for modified record") + if len(r.signature) != 0 { + t.Error("signature still set after modification") } if _, err := rlp.EncodeToBytes(r); err != errEncodeUnsigned { t.Errorf("expected errEncodeUnsigned, got %#v", err) } } +func TestSeq(t *testing.T) { + var r Record + + assert.Equal(t, uint64(0), r.Seq()) + r.Set(UDP(1)) + assert.Equal(t, uint64(0), r.Seq()) + signTest([]byte{5}, &r) + assert.Equal(t, uint64(0), r.Seq()) + r.Set(UDP(2)) + assert.Equal(t, uint64(1), r.Seq()) +} + // TestGetSetOverwrite tests value overwrite when setting a new value with an existing key in record. func TestGetSetOverwrite(t *testing.T) { var r Record @@ -210,7 +201,7 @@ func TestSignEncodeAndDecode(t *testing.T) { var r Record r.Set(UDP(30303)) r.Set(IP{127, 0, 0, 1}) - require.NoError(t, SignV4(&r, privkey)) + require.NoError(t, signTest([]byte{5}, &r)) blob, err := rlp.EncodeToBytes(r) require.NoError(t, err) @@ -224,48 +215,6 @@ func TestSignEncodeAndDecode(t *testing.T) { assert.Equal(t, blob, blob2) } -func TestNodeAddr(t *testing.T) { - var r Record - if addr := r.NodeAddr(); addr != nil { - t.Errorf("wrong address on empty record: got %v, want %v", addr, nil) - } - - require.NoError(t, SignV4(&r, privkey)) - expected := "a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7" - assert.Equal(t, expected, hex.EncodeToString(r.NodeAddr())) -} - -var pyRecord, _ = hex.DecodeString("f884b8407098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c01826964827634826970847f00000189736563703235366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31388375647082765f") - -// TestPythonInterop checks that we can decode and verify a record produced by the Python -// implementation. -func TestPythonInterop(t *testing.T) { - var r Record - if err := rlp.DecodeBytes(pyRecord, &r); err != nil { - t.Fatalf("can't decode: %v", err) - } - - var ( - wantAddr, _ = hex.DecodeString("a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7") - wantSeq = uint64(1) - wantIP = IP{127, 0, 0, 1} - wantUDP = UDP(30303) - ) - if r.Seq() != wantSeq { - t.Errorf("wrong seq: got %d, want %d", r.Seq(), wantSeq) - } - if addr := r.NodeAddr(); !bytes.Equal(addr, wantAddr) { - t.Errorf("wrong addr: got %x, want %x", addr, wantAddr) - } - want := map[Entry]interface{}{new(IP): &wantIP, new(UDP): &wantUDP} - for k, v := range want { - desc := fmt.Sprintf("loading key %q", k.ENRKey()) - if assert.NoError(t, r.Load(k), desc) { - assert.Equal(t, k, v, desc) - } - } -} - // TestRecordTooBig tests that records bigger than SizeLimit bytes cannot be signed. func TestRecordTooBig(t *testing.T) { var r Record @@ -273,13 +222,13 @@ func TestRecordTooBig(t *testing.T) { // set a big value for random key, expect error r.Set(WithEntry(key, randomString(SizeLimit))) - if err := SignV4(&r, privkey); err != errTooBig { + if err := signTest([]byte{5}, &r); err != errTooBig { t.Fatalf("expected to get errTooBig, got %#v", err) } // set an acceptable value for random key, expect no error r.Set(WithEntry(key, randomString(100))) - require.NoError(t, SignV4(&r, privkey)) + require.NoError(t, signTest([]byte{5}, &r)) } // TestSignEncodeAndDecodeRandom tests encoding/decoding of records containing random key/value pairs. @@ -295,7 +244,7 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) { r.Set(WithEntry(key, &value)) } - require.NoError(t, SignV4(&r, privkey)) + require.NoError(t, signTest([]byte{5}, &r)) _, err := rlp.EncodeToBytes(r) require.NoError(t, err) @@ -308,10 +257,40 @@ func TestSignEncodeAndDecodeRandom(t *testing.T) { } } -func BenchmarkDecode(b *testing.B) { - var r Record - for b.Loop() { - rlp.DecodeBytes(pyRecord, &r) +type testSig struct{} + +type testID []byte + +func (id testID) ENRKey() string { return "testid" } + +func signTest(id []byte, r *Record) error { + r.Set(ID("test")) + r.Set(testID(id)) + return r.SetSig(testSig{}, makeTestSig(id, r.Seq())) +} + +func makeTestSig(id []byte, seq uint64) []byte { + sig := make([]byte, 8, len(id)+8) + binary.BigEndian.PutUint64(sig[:8], seq) + sig = append(sig, id...) + return sig +} + +func (testSig) Verify(r *Record, sig []byte) error { + var id []byte + if err := r.Load((*testID)(&id)); err != nil { + return err + } + if !bytes.Equal(sig, makeTestSig(id, r.Seq())) { + return ErrInvalidSig + } + return nil +} + +func (testSig) NodeAddr(r *Record) []byte { + var id []byte + if err := r.Load((*testID)(&id)); err != nil { + return nil } - r.NodeAddr() + return id } diff --git a/p2p/enr/entries.go b/p2p/enr/entries.go index fc524dc405d8..22f839d836ec 100644 --- a/p2p/enr/entries.go +++ b/p2p/enr/entries.go @@ -17,12 +17,10 @@ package enr import ( - "crypto/ecdsa" "fmt" "io" "net" - "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/rlp" ) @@ -98,30 +96,6 @@ func (v *IP) DecodeRLP(s *rlp.Stream) error { return nil } -// Secp256k1 is the "secp256k1" key, which holds a public key. -type Secp256k1 ecdsa.PublicKey - -func (v Secp256k1) ENRKey() string { return "secp256k1" } - -// EncodeRLP implements rlp.Encoder. -func (v Secp256k1) EncodeRLP(w io.Writer) error { - return rlp.Encode(w, crypto.CompressPubkey((*ecdsa.PublicKey)(&v))) -} - -// DecodeRLP implements rlp.Decoder. -func (v *Secp256k1) DecodeRLP(s *rlp.Stream) error { - buf, err := s.Bytes() - if err != nil { - return err - } - pk, err := crypto.DecompressPubkey(buf) - if err != nil { - return err - } - *v = (Secp256k1)(*pk) - return nil -} - // KeyError is an error related to a key. type KeyError struct { Key string diff --git a/p2p/enr/idscheme.go b/p2p/enr/idscheme.go deleted file mode 100644 index 8180cd832a1e..000000000000 --- a/p2p/enr/idscheme.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package enr - -import ( - "crypto/ecdsa" - "errors" - "sync" - - "github.com/XinFinOrg/XDPoSChain/common/math" - "github.com/XinFinOrg/XDPoSChain/crypto" - "github.com/XinFinOrg/XDPoSChain/crypto/keccak" - "github.com/XinFinOrg/XDPoSChain/rlp" -) - -// Registry of known identity schemes. -var schemes sync.Map - -// An IdentityScheme is capable of verifying record signatures and -// deriving node addresses. -type IdentityScheme interface { - Verify(r *Record, sig []byte) error - NodeAddr(r *Record) []byte -} - -// RegisterIdentityScheme adds an identity scheme to the global registry. -func RegisterIdentityScheme(name string, scheme IdentityScheme) { - if _, loaded := schemes.LoadOrStore(name, scheme); loaded { - panic("identity scheme " + name + " already registered") - } -} - -// FindIdentityScheme resolves name to an identity scheme in the global registry. -func FindIdentityScheme(name string) IdentityScheme { - s, ok := schemes.Load(name) - if !ok { - return nil - } - return s.(IdentityScheme) -} - -// v4ID is the "v4" identity scheme. -type v4ID struct{} - -func init() { - RegisterIdentityScheme("v4", v4ID{}) -} - -// SignV4 signs a record using the v4 scheme. -func SignV4(r *Record, privkey *ecdsa.PrivateKey) error { - // Copy r to avoid modifying it if signing fails. - cpy := *r - cpy.Set(ID("v4")) - cpy.Set(Secp256k1(privkey.PublicKey)) - - h := keccak.NewLegacyKeccak256() - rlp.Encode(h, cpy.AppendElements(nil)) - sig, err := crypto.Sign(h.Sum(nil), privkey) - if err != nil { - return err - } - sig = sig[:len(sig)-1] // remove v - if err = cpy.SetSig("v4", sig); err == nil { - *r = cpy - } - return err -} - -// s256raw is an unparsed secp256k1 public key entry. -type s256raw []byte - -func (s256raw) ENRKey() string { return "secp256k1" } - -func (v4ID) Verify(r *Record, sig []byte) error { - var entry s256raw - if err := r.Load(&entry); err != nil { - return err - } else if len(entry) != 33 { - return errors.New("invalid public key") - } - - h := keccak.NewLegacyKeccak256() - rlp.Encode(h, r.AppendElements(nil)) - if !crypto.VerifySignature(entry, h.Sum(nil), sig) { - return errInvalidSig - } - return nil -} - -func (v4ID) NodeAddr(r *Record) []byte { - var pubkey Secp256k1 - err := r.Load(&pubkey) - if err != nil { - return nil - } - buf := make([]byte, 64) - math.ReadBits(pubkey.X, buf[:32]) - math.ReadBits(pubkey.Y, buf[32:]) - return crypto.Keccak256(buf) -} diff --git a/p2p/enr/idscheme_test.go b/p2p/enr/idscheme_test.go deleted file mode 100644 index d790e12f142c..000000000000 --- a/p2p/enr/idscheme_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package enr - -import ( - "crypto/ecdsa" - "math/big" - "testing" -) - -// Checks that failure to sign leaves the record unmodified. -func TestSignError(t *testing.T) { - invalidKey := &ecdsa.PrivateKey{D: new(big.Int), PublicKey: *pubkey} - - var r Record - if err := SignV4(&r, invalidKey); err == nil { - t.Fatal("expected error from SignV4") - } - if len(r.pairs) > 0 { - t.Fatal("expected empty record, have", r.pairs) - } -} diff --git a/p2p/message.go b/p2p/message.go index 29bdf162b5aa..6da0434bfef1 100644 --- a/p2p/message.go +++ b/p2p/message.go @@ -25,7 +25,7 @@ import ( "time" "github.com/XinFinOrg/XDPoSChain/event" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/rlp" ) @@ -38,7 +38,7 @@ import ( // separate Msg with a bytes.Reader as Payload for each send. type Msg struct { Code uint64 - Size uint32 // Size of the raw payload + Size uint32 // size of the payload Payload io.Reader ReceivedAt time.Time @@ -51,21 +51,21 @@ type Msg struct { // the given value, which must be a pointer. // // For the decoding rules, please see package rlp. -func (m Msg) Decode(val interface{}) error { - s := rlp.NewStream(m.Payload, uint64(m.Size)) +func (msg Msg) Decode(val interface{}) error { + s := rlp.NewStream(msg.Payload, uint64(msg.Size)) if err := s.Decode(val); err != nil { - return newPeerError(errInvalidMsg, "(code %x) (size %d) %v", m.Code, m.Size, err) + return newPeerError(errInvalidMsg, "(code %x) (size %d) %v", msg.Code, msg.Size, err) } return nil } -func (m Msg) String() string { - return fmt.Sprintf("msg #%v (%v bytes)", m.Code, m.Size) +func (msg Msg) String() string { + return fmt.Sprintf("msg #%v (%v bytes)", msg.Code, msg.Size) } // Discard reads any remaining payload data into a black hole. -func (m Msg) Discard() error { - _, err := io.Copy(io.Discard, m.Payload) +func (msg Msg) Discard() error { + _, err := io.Copy(io.Discard, msg.Payload) return err } @@ -100,6 +100,23 @@ func Send(w MsgWriter, msgcode uint64, data interface{}) error { return w.WriteMsg(Msg{Code: msgcode, Size: uint32(size), Payload: r}) } +// SendPriority is like Send, but if w implements PriorityMsgWriter the message +// is routed to the high-priority lane. SendPriority always requests the high +// lane; callers that want the normal lane should use Send. Writers without +// priority support fall back to the regular Send path so that callers do not +// have to know whether the wrapper chain preserves priority. +func SendPriority(w MsgWriter, msgcode uint64, data interface{}) error { + size, r, err := rlp.EncodeToReader(data) + if err != nil { + return err + } + msg := Msg{Code: msgcode, Size: uint32(size), Payload: r} + if pw, ok := w.(PriorityMsgWriter); ok { + return pw.WriteMsgPriority(msg, true) + } + return w.WriteMsg(msg) +} + // SendItems writes an RLP with the given code and data elements. // For a call such as: // @@ -123,24 +140,24 @@ type eofSignal struct { // note: when using eofSignal to detect whether a message payload // has been read, Read might not be called for zero sized messages. -func (s *eofSignal) Read(buf []byte) (int, error) { - if s.count == 0 { - if s.eof != nil { - s.eof <- struct{}{} - s.eof = nil +func (r *eofSignal) Read(buf []byte) (int, error) { + if r.count == 0 { + if r.eof != nil { + r.eof <- struct{}{} + r.eof = nil } return 0, io.EOF } max := len(buf) - if int(s.count) < len(buf) { - max = int(s.count) + if int(r.count) < len(buf) { + max = int(r.count) } - n, err := s.wrapped.Read(buf[:max]) - s.count -= uint32(n) - if (err != nil || s.count == 0) && s.eof != nil { - s.eof <- struct{}{} // tell Peer that msg has been consumed - s.eof = nil + n, err := r.wrapped.Read(buf[:max]) + r.count -= uint32(n) + if (err != nil || r.count == 0) && r.eof != nil { + r.eof <- struct{}{} // tell Peer that msg has been consumed + r.eof = nil } return n, err } @@ -255,13 +272,13 @@ type msgEventer struct { MsgReadWriter feed *event.Feed - peerID discover.NodeID + peerID enode.ID Protocol string } // newMsgEventer returns a msgEventer which sends message events to the given // feed -func newMsgEventer(rw MsgReadWriter, feed *event.Feed, peerID discover.NodeID, proto string) *msgEventer { +func newMsgEventer(rw MsgReadWriter, feed *event.Feed, peerID enode.ID, proto string) *msgEventer { return &msgEventer{ MsgReadWriter: rw, feed: feed, @@ -304,6 +321,33 @@ func (ev *msgEventer) WriteMsg(msg Msg) error { return nil } +// WriteMsgPriority forwards a priority-tagged write to the underlying writer +// if it implements PriorityMsgWriter, falling back to WriteMsg otherwise. It +// emits a "message sent" event identical to WriteMsg so that priority and +// non-priority sends are observable through the same feed. +func (ev *msgEventer) WriteMsgPriority(msg Msg, high bool) error { + var err error + if pw, ok := ev.MsgReadWriter.(PriorityMsgWriter); ok { + err = pw.WriteMsgPriority(msg, high) + } else { + err = ev.MsgReadWriter.WriteMsg(msg) + } + if err != nil { + return err + } + ev.feed.Send(&PeerEvent{ + Type: PeerEventTypeMsgSend, + Peer: ev.peerID, + Protocol: ev.Protocol, + MsgCode: &msg.Code, + MsgSize: &msg.Size, + }) + return nil +} + +// Compile-time check that msgEventer forwards the PriorityMsgWriter contract. +var _ PriorityMsgWriter = (*msgEventer)(nil) + // Close closes the underlying MsgReadWriter if it implements the io.Closer // interface func (ev *msgEventer) Close() error { diff --git a/p2p/metrics.go b/p2p/metrics.go index c02c90207af8..e8389f9e3687 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -49,6 +49,17 @@ var ( dialUnexpectedIdentity = metrics.NewRegisteredMeter("p2p/dials/error/id/unexpected", nil) dialEncHandshakeError = metrics.NewRegisteredMeter("p2p/dials/error/rlpx/enc", nil) dialProtoHandshakeError = metrics.NewRegisteredMeter("p2p/dials/error/rlpx/proto", nil) + + // Per-Peer write queue depth, sampled on every enqueue (depth measured + // after the slot is added). Useful for tuning writeReqQueueSize and for + // spotting peers whose downstream transport is back-pressuring. + writeQueueHiDepth = metrics.NewRegisteredHistogram("p2p/peer/writeq/hi/depth", nil, metrics.NewExpDecaySample(1028, 0.015)) + writeQueueLoDepth = metrics.NewRegisteredHistogram("p2p/peer/writeq/lo/depth", nil, metrics.NewExpDecaySample(1028, 0.015)) + + // Number of enqueue attempts that found the queue already full and had + // to block (back-pressure events). + writeQueueHiBlocked = metrics.NewRegisteredMeter("p2p/peer/writeq/hi/blocked", nil) + writeQueueLoBlocked = metrics.NewRegisteredMeter("p2p/peer/writeq/lo/blocked", nil) ) func markDialError(err error) { @@ -82,9 +93,9 @@ type meteredConn struct { net.Conn } -// newMeteredConn creates a new metered connection, also bumping the ingress or -// egress connection meter. If the metrics system is disabled, this function -// returns the original object. +// newMeteredConn creates a new metered connection, bumps the ingress or egress +// connection meter and also increases the metered peer count. If the metrics +// system is disabled, function returns the original connection. func newMeteredConn(conn net.Conn) net.Conn { // Short circuit if metrics are disabled if !metrics.Enabled() { diff --git a/p2p/nat/nat.go b/p2p/nat/nat.go index 1ee4edf155af..8fb83cca8429 100644 --- a/p2p/nat/nat.go +++ b/p2p/nat/nat.go @@ -129,21 +129,15 @@ func Map(m Interface, c chan struct{}, protocol string, extport, intport int, na // ExtIP assumes that the local machine is reachable on the given // external IP address, and that any required ports were mapped manually. // Mapping operations will not return an error but won't actually do anything. -func ExtIP(ip net.IP) Interface { - if ip == nil { - panic("IP must not be nil") - } - return extIP(ip) -} +type ExtIP net.IP -type extIP net.IP - -func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } -func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } +func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } +func (n ExtIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } // These do nothing. -func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil } -func (extIP) DeleteMapping(string, int, int) error { return nil } + +func (ExtIP) AddMapping(string, int, int, string, time.Duration) error { return nil } +func (ExtIP) DeleteMapping(string, int, int) error { return nil } // Any returns a port mapper that tries to discover any supported // mechanism on the local network. diff --git a/p2p/nat/nat_test.go b/p2p/nat/nat_test.go index 469101e997e9..814e6d9e14c8 100644 --- a/p2p/nat/nat_test.go +++ b/p2p/nat/nat_test.go @@ -28,7 +28,7 @@ import ( func TestAutoDiscRace(t *testing.T) { ad := startautodisc("thing", func() Interface { time.Sleep(500 * time.Millisecond) - return extIP{33, 44, 55, 66} + return ExtIP{33, 44, 55, 66} }) // Spawn a few concurrent calls to ad.ExternalIP. diff --git a/p2p/netutil/iptrack.go b/p2p/netutil/iptrack.go new file mode 100644 index 000000000000..1a34ec81f5c9 --- /dev/null +++ b/p2p/netutil/iptrack.go @@ -0,0 +1,130 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package netutil + +import ( + "time" + + "github.com/XinFinOrg/XDPoSChain/common/mclock" +) + +// IPTracker predicts the external endpoint, i.e. IP address and port, of the local host +// based on statements made by other hosts. +type IPTracker struct { + window time.Duration + contactWindow time.Duration + minStatements int + clock mclock.Clock + statements map[string]ipStatement + contact map[string]mclock.AbsTime + lastStatementGC mclock.AbsTime + lastContactGC mclock.AbsTime +} + +type ipStatement struct { + endpoint string + time mclock.AbsTime +} + +// NewIPTracker creates an IP tracker. +// +// The window parameters configure the amount of past network events which are kept. The +// minStatements parameter enforces a minimum number of statements which must be recorded +// before any prediction is made. Higher values for these parameters decrease 'flapping' of +// predictions as network conditions change. Window duration values should typically be in +// the range of minutes. +func NewIPTracker(window, contactWindow time.Duration, minStatements int) *IPTracker { + return &IPTracker{ + window: window, + contactWindow: contactWindow, + statements: make(map[string]ipStatement), + minStatements: minStatements, + contact: make(map[string]mclock.AbsTime), + clock: mclock.System{}, + } +} + +// PredictFullConeNAT checks whether the local host is behind full cone NAT. It predicts by +// checking whether any statement has been received from a node we didn't contact before +// the statement was made. +func (it *IPTracker) PredictFullConeNAT() bool { + now := it.clock.Now() + it.gcContact(now) + it.gcStatements(now) + for host, st := range it.statements { + if c, ok := it.contact[host]; !ok || c > st.time { + return true + } + } + return false +} + +// PredictEndpoint returns the current prediction of the external endpoint. +func (it *IPTracker) PredictEndpoint() string { + it.gcStatements(it.clock.Now()) + + // The current strategy is simple: find the endpoint with most statements. + counts := make(map[string]int) + maxcount, max := 0, "" + for _, s := range it.statements { + c := counts[s.endpoint] + 1 + counts[s.endpoint] = c + if c > maxcount && c >= it.minStatements { + maxcount, max = c, s.endpoint + } + } + return max +} + +// AddStatement records that a certain host thinks our external endpoint is the one given. +func (it *IPTracker) AddStatement(host, endpoint string) { + now := it.clock.Now() + it.statements[host] = ipStatement{endpoint, now} + if time.Duration(now-it.lastStatementGC) >= it.window { + it.gcStatements(now) + } +} + +// AddContact records that a packet containing our endpoint information has been sent to a +// certain host. +func (it *IPTracker) AddContact(host string) { + now := it.clock.Now() + it.contact[host] = now + if time.Duration(now-it.lastContactGC) >= it.contactWindow { + it.gcContact(now) + } +} + +func (it *IPTracker) gcStatements(now mclock.AbsTime) { + it.lastStatementGC = now + cutoff := now.Add(-it.window) + for host, s := range it.statements { + if s.time < cutoff { + delete(it.statements, host) + } + } +} + +func (it *IPTracker) gcContact(now mclock.AbsTime) { + it.lastContactGC = now + cutoff := now.Add(-it.contactWindow) + for host, ct := range it.contact { + if ct < cutoff { + delete(it.contact, host) + } + } +} diff --git a/p2p/netutil/iptrack_test.go b/p2p/netutil/iptrack_test.go new file mode 100644 index 000000000000..1d70b8ab83c3 --- /dev/null +++ b/p2p/netutil/iptrack_test.go @@ -0,0 +1,138 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package netutil + +import ( + "fmt" + mrand "math/rand" + "testing" + "time" + + "github.com/XinFinOrg/XDPoSChain/common/mclock" +) + +const ( + opStatement = iota + opContact + opPredict + opCheckFullCone +) + +type iptrackTestEvent struct { + op int + time int // absolute, in milliseconds + ip, from string +} + +func TestIPTracker(t *testing.T) { + tests := map[string][]iptrackTestEvent{ + "minStatements": { + {opPredict, 0, "", ""}, + {opStatement, 0, "127.0.0.1", "127.0.0.2"}, + {opPredict, 1000, "", ""}, + {opStatement, 1000, "127.0.0.1", "127.0.0.3"}, + {opPredict, 1000, "", ""}, + {opStatement, 1000, "127.0.0.1", "127.0.0.4"}, + {opPredict, 1000, "127.0.0.1", ""}, + }, + "window": { + {opStatement, 0, "127.0.0.1", "127.0.0.2"}, + {opStatement, 2000, "127.0.0.1", "127.0.0.3"}, + {opStatement, 3000, "127.0.0.1", "127.0.0.4"}, + {opPredict, 10000, "127.0.0.1", ""}, + {opPredict, 10001, "", ""}, // first statement expired + {opStatement, 10100, "127.0.0.1", "127.0.0.2"}, + {opPredict, 10200, "127.0.0.1", ""}, + }, + "fullcone": { + {opContact, 0, "", "127.0.0.2"}, + {opStatement, 10, "127.0.0.1", "127.0.0.2"}, + {opContact, 2000, "", "127.0.0.3"}, + {opStatement, 2010, "127.0.0.1", "127.0.0.3"}, + {opContact, 3000, "", "127.0.0.4"}, + {opStatement, 3010, "127.0.0.1", "127.0.0.4"}, + {opCheckFullCone, 3500, "false", ""}, + }, + "fullcone_2": { + {opContact, 0, "", "127.0.0.2"}, + {opStatement, 10, "127.0.0.1", "127.0.0.2"}, + {opContact, 2000, "", "127.0.0.3"}, + {opStatement, 2010, "127.0.0.1", "127.0.0.3"}, + {opStatement, 3000, "127.0.0.1", "127.0.0.4"}, + {opContact, 3010, "", "127.0.0.4"}, + {opCheckFullCone, 3500, "true", ""}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { runIPTrackerTest(t, test) }) + } +} + +func runIPTrackerTest(t *testing.T, evs []iptrackTestEvent) { + var ( + clock mclock.Simulated + it = NewIPTracker(10*time.Second, 10*time.Second, 3) + ) + it.clock = &clock + for i, ev := range evs { + evtime := time.Duration(ev.time) * time.Millisecond + clock.Run(evtime - time.Duration(clock.Now())) + switch ev.op { + case opStatement: + it.AddStatement(ev.from, ev.ip) + case opContact: + it.AddContact(ev.from) + case opPredict: + if pred := it.PredictEndpoint(); pred != ev.ip { + t.Errorf("op %d: wrong prediction %q, want %q", i, pred, ev.ip) + } + case opCheckFullCone: + pred := fmt.Sprintf("%t", it.PredictFullConeNAT()) + if pred != ev.ip { + t.Errorf("op %d: wrong prediction %s, want %s", i, pred, ev.ip) + } + } + } +} + +// This checks that old statements and contacts are GCed even if Predict* isn't called. +func TestIPTrackerForceGC(t *testing.T) { + var ( + clock mclock.Simulated + window = 10 * time.Second + rate = 50 * time.Millisecond + max = int(window/rate) + 1 + it = NewIPTracker(window, window, 3) + ) + it.clock = &clock + + for i := 0; i < 5*max; i++ { + e1 := make([]byte, 4) + e2 := make([]byte, 4) + mrand.Read(e1) + mrand.Read(e2) + it.AddStatement(string(e1), string(e2)) + it.AddContact(string(e1)) + clock.Run(rate) + } + if len(it.contact) > 2*max { + t.Errorf("contacts not GCed, have %d", len(it.contact)) + } + if len(it.statements) > 2*max { + t.Errorf("statements not GCed, have %d", len(it.statements)) + } +} diff --git a/p2p/peer.go b/p2p/peer.go index b8b257f56dbc..05fc3d2b6a68 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -29,7 +29,8 @@ import ( "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/metrics" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" "github.com/XinFinOrg/XDPoSChain/rlp" ) @@ -61,7 +62,7 @@ type protoHandshake struct { Name string Caps []Cap ListenPort uint64 - ID discover.NodeID + ID []byte // secp256k1 public key // Ignore additional fields (for forward compatibility). Rest []rlp.RawValue `rlp:"tail"` @@ -91,12 +92,65 @@ const ( // PeerEvent is an event emitted when peers are either added or dropped from // a p2p.Server or when a message is sent or received on a peer connection type PeerEvent struct { - Type PeerEventType `json:"type"` - Peer discover.NodeID `json:"peer"` - Error string `json:"error,omitempty"` - Protocol string `json:"protocol,omitempty"` - MsgCode *uint64 `json:"msg_code,omitempty"` - MsgSize *uint32 `json:"msg_size,omitempty"` + Type PeerEventType `json:"type"` + Peer enode.ID `json:"peer"` + Error string `json:"error,omitempty"` + Protocol string `json:"protocol,omitempty"` + MsgCode *uint64 `json:"msg_code,omitempty"` + MsgSize *uint32 `json:"msg_size,omitempty"` +} + +// PriorityMsgWriter is an optional interface implemented by MsgWriters that +// support two-level write priority. Messages marked as high priority preempt +// normal-priority messages at the next write boundary on the underlying +// transport (in-flight writes are never interrupted). +// +// Any MsgReadWriter that wraps another writer (for example msgEventer or +// eth.meteredMsgReadWriter) MUST also implement PriorityMsgWriter and forward +// the priority flag to the inner writer. Otherwise SendPriority will silently +// fall back to the normal-priority lane on connections that go through the +// wrapper, defeating the BFT latency guarantee for consensus messages. +type PriorityMsgWriter interface { + MsgWriter + // WriteMsgPriority writes msg through the underlying transport. If high + // is true, the message is queued on the high-priority lane and will be + // served before any pending low-priority writes. + WriteMsgPriority(msg Msg, high bool) error +} + +// writePriorityStarveLimit bounds the number of consecutive high-priority +// writes that may be served before a pending low-priority write is forced +// through. This prevents low-priority traffic (block bodies, transactions) +// from being starved by a constant stream of high-priority messages. +const writePriorityStarveLimit = 16 + +// writeReqQueueSize is the capacity of the per-priority write request queues. +// A buffered queue lets concurrent writers enqueue while the arbiter is busy +// with an in-flight transport write, which is required for the priority bias +// to take effect. When the queue is full, additional writers block (yielding +// back-pressure equivalent to the single-token scheduler). +// +// The 128 default is sized to absorb a burst from all eth-protocol writers +// on a single peer without blocking: a small constant set of consensus +// senders (VoteMsg / TimeoutMsg / SyncInfoMsg) on the hi lane, plus the +// tx-broadcast loop, block-propagation loop, header/body fetcher responses, +// and downloader requests on the lo lane. In practice steady-state depth +// should stay in single digits; sustained depths above a few dozen indicate +// a slow downstream transport rather than producer bursts. +// +// The depth distribution and back-pressure rate are exposed as metrics +// (p2p/peer/writeq/{hi,lo}/{depth,blocked}); use them to verify whether this +// constant needs tuning under real load. +const writeReqQueueSize = 128 + +// writeSlot is a single-use handshake between a protoRW writer and the +// per-Peer write arbiter in Peer.run. The writer enqueues a slot on either +// the high- or low-priority request channel and waits for the arbiter to +// grant the slot by closing proceed. The writer then performs the actual +// transport write and reports the result on done. +type writeSlot struct { + proceed chan struct{} // arbiter closes this to grant the write slot + done chan error // writer reports the write result (buffered, len 1) } // Peer represents a connected remote node. @@ -112,25 +166,35 @@ type Peer struct { pingRecv chan struct{} disc chan DiscReason + // Write arbitration: protoRW writers submit writeSlot requests on hiReq + // (high priority) or loReq (low priority). Peer.run picks one, grants + // it, waits for completion, then schedules the next. These channels are + // created in newPeer and never reassigned afterwards. + hiReq chan *writeSlot + loReq chan *writeSlot + // events receives message send / receive events if set events *event.Feed - - pairPeerMu sync.RWMutex - pairPeer *Peer } // NewPeer returns a peer for testing purposes. -func NewPeer(id discover.NodeID, name string, caps []Cap) *Peer { +func NewPeer(id enode.ID, name string, caps []Cap) *Peer { pipe, _ := net.Pipe() - conn := &conn{fd: pipe, transport: nil, id: id, caps: caps, name: name} + node := enode.SignNull(new(enr.Record), id) + conn := &conn{fd: pipe, transport: nil, node: node, caps: caps, name: name} peer := newPeer(conn, nil) close(peer.closed) // ensures Disconnect doesn't block return peer } // ID returns the node's public key. -func (p *Peer) ID() discover.NodeID { - return p.rw.id +func (p *Peer) ID() enode.ID { + return p.rw.node.ID() +} + +// Node returns the peer's node descriptor. +func (p *Peer) Node() *enode.Node { + return p.rw.node } // Name returns the node name that the remote node advertised. @@ -165,7 +229,8 @@ func (p *Peer) Disconnect(reason DiscReason) { // String implements fmt.Stringer. func (p *Peer) String() string { - return fmt.Sprintf("Peer %x %v", p.rw.id[:8], p.RemoteAddr()) + id := p.ID() + return fmt.Sprintf("Peer %x %v", id[:8], p.RemoteAddr()) } // Inbound returns true if the peer is an inbound connection @@ -183,7 +248,9 @@ func newPeer(conn *conn, protocols []Protocol) *Peer { protoErr: make(chan error, len(protomap)+1), // protocols + pingLoop closed: make(chan struct{}), pingRecv: make(chan struct{}, 16), - log: log.New("id", conn.id, "conn", conn.flags), + hiReq: make(chan *writeSlot, writeReqQueueSize), + loReq: make(chan *writeSlot, writeReqQueueSize), + log: log.New("id", conn.node.ID(), "conn", conn.flags), } return p } @@ -192,56 +259,84 @@ func (p *Peer) Log() log.Logger { return p.log } -func (p *Peer) PairPeer() *Peer { - p.pairPeerMu.RLock() - defer p.pairPeerMu.RUnlock() - - return p.pairPeer -} - -func (p *Peer) SetPairPeer(pair *Peer) { - p.pairPeerMu.Lock() - p.pairPeer = pair - p.pairPeerMu.Unlock() -} - -func (p *Peer) ClearPairPeer(pair *Peer) bool { - p.pairPeerMu.Lock() - defer p.pairPeerMu.Unlock() - - if p.pairPeer != pair { - return false - } - p.pairPeer = nil - return true -} - func (p *Peer) run() (remoteRequested bool, err error) { var ( - writeStart = make(chan struct{}, 1) - writeErr = make(chan error, 1) readErr = make(chan error, 1) reason DiscReason // sent to the peer + activeDone chan error // non-nil while a write is in flight + hiStreak int // consecutive hi-priority writes since the last lo ) p.wg.Go(func() { p.readLoop(readErr) }) p.wg.Go(p.pingLoop) // Start all protocol handlers. - writeStart <- struct{}{} - p.startProtocols(writeStart, writeErr) - - // Wait for an error or disconnect. + p.startProtocols() + + // Write arbiter loop, folded into the main select. The arbiter accepts + // writeSlot requests only when no write is currently in flight + // (activeDone == nil), prefers hi over lo, and enforces a starvation + // guard that biases lo every writePriorityStarveLimit consecutive hi. + // + // Note: the hi-over-lo preference is best-effort, not strict. It is + // strictly honoured only by the non-blocking probe at the top of the + // loop body. In the blocking select below, if both hiReq and loReq + // become ready simultaneously the Go runtime picks one at random; the + // next iteration will re-apply the hi bias, so a sustained hi load is + // still served ahead of lo on average. loop: for { + // When no write is in flight, try to pick the next request with a + // priority bias before falling into the blocking select. + if activeDone == nil { + if hiStreak >= writePriorityStarveLimit { + // Bias toward lo: try lo non-blocking first; if no lo is + // pending, fall through to the blocking select that + // accepts either. + select { + case slot := <-p.loReq: + close(slot.proceed) + activeDone = slot.done + hiStreak = 0 + continue + default: + } + } else { + // Bias toward hi: try hi non-blocking first; if no hi is + // pending, fall through to the blocking select that + // accepts either. + select { + case slot := <-p.hiReq: + close(slot.proceed) + activeDone = slot.done + hiStreak++ + continue + default: + } + } + } + + // Only enable the request channels when no write is in flight. + var hiReq, loReq <-chan *writeSlot + if activeDone == nil { + hiReq, loReq = p.hiReq, p.loReq + } + select { - case err = <-writeErr: - // A write finished. Allow the next write to start if - // there was no error. + case slot := <-hiReq: + close(slot.proceed) + activeDone = slot.done + hiStreak++ + case slot := <-loReq: + close(slot.proceed) + activeDone = slot.done + hiStreak = 0 + case err = <-activeDone: + // Active write finished. Allow the next one to be scheduled. + activeDone = nil if err != nil { reason = DiscNetworkError break loop } - writeStart <- struct{}{} case err = <-readErr: if r, ok := err.(DiscReason); ok { remoteRequested = true @@ -261,9 +356,6 @@ loop: close(p.closed) p.rw.close(reason) p.wg.Wait() - if pairPeer := p.PairPeer(); pairPeer != nil { - go pairPeer.Disconnect(DiscPairPeerStop) - } return remoteRequested, err } @@ -377,11 +469,11 @@ outer: return result } -func (p *Peer) startProtocols(writeStart <-chan struct{}, writeErr chan<- error) { +func (p *Peer) startProtocols() { for _, proto := range p.running { proto.closed = p.closed - proto.wstart = writeStart - proto.werr = writeErr + proto.hiReq = p.hiReq + proto.loReq = p.loReq var rw MsgReadWriter = proto if p.events != nil { rw = newMsgEventer(rw, p.events, p.ID(), proto.Name) @@ -413,15 +505,35 @@ func (p *Peer) getProto(code uint64) (*protoRW, error) { type protoRW struct { Protocol - in chan Msg // receices read messages + in chan Msg // receives read messages closed <-chan struct{} // receives when peer is shutting down - wstart <-chan struct{} // receives when write may start - werr chan<- error // for write results + // hiReq/loReq are written once by startProtocols before any concurrent + // writer is started and are read-only thereafter. Do not reassign them + // from goroutines other than the one that called startProtocols. + hiReq chan<- *writeSlot // high-priority write request queue + loReq chan<- *writeSlot // normal-priority write request queue offset uint64 w MsgWriter } -func (rw *protoRW) WriteMsg(msg Msg) (err error) { +// Compile-time check that protoRW honours the PriorityMsgWriter contract. +// Wrappers in the chain (msgEventer, eth.meteredMsgReadWriter, ...) MUST do +// the same; otherwise SendPriority silently falls back to the normal lane. +var _ PriorityMsgWriter = (*protoRW)(nil) + +// WriteMsg writes msg through the underlying transport at normal priority. +// It implements MsgWriter. +func (rw *protoRW) WriteMsg(msg Msg) error { + return rw.writeMsg(msg, false) +} + +// WriteMsgPriority writes msg through the underlying transport, optionally +// at high priority. It implements PriorityMsgWriter. +func (rw *protoRW) WriteMsgPriority(msg Msg, high bool) error { + return rw.writeMsg(msg, high) +} + +func (rw *protoRW) writeMsg(msg Msg, high bool) (err error) { if msg.Code >= rw.Length { return newPeerError(errInvalidMsgCode, "not handled") } @@ -429,17 +541,46 @@ func (rw *protoRW) WriteMsg(msg Msg) (err error) { msg.meterCode = msg.Code msg.Code += rw.offset + + slot := &writeSlot{ + proceed: make(chan struct{}), + done: make(chan error, 1), + } + reqCh := rw.loReq + depthMetric, blockedMetric := writeQueueLoDepth, writeQueueLoBlocked + if high { + reqCh = rw.hiReq + depthMetric, blockedMetric = writeQueueHiDepth, writeQueueHiBlocked + } + // Sample queue depth before enqueue so the histogram reflects the + // back-pressure observed by this writer (slots ahead of it), independent + // of how quickly the arbiter drains afterwards. This is still a racy + // snapshot under concurrent producers, but it is a meaningful upper + // bound on what this writer waited behind, whereas sampling after the + // send can be drained to ~0 by the arbiter and underreports congestion. + depthMetric.Update(int64(len(reqCh))) + // Enqueue the request. Try non-blocking first so we can observe + // saturation events; on fallback the writer waits like before. + select { + case reqCh <- slot: + default: + blockedMetric.Mark(1) + select { + case reqCh <- slot: + case <-rw.closed: + return ErrShuttingDown + } + } + // Wait for the arbiter to grant the slot. select { - case <-rw.wstart: - err = rw.w.WriteMsg(msg) - // Report write status back to Peer.run. It will initiate - // shutdown if the error is non-nil and unblock the next write - // otherwise. The calling protocol code should exit for errors - // as well but we don't want to rely on that. - rw.werr <- err + case <-slot.proceed: case <-rw.closed: - err = ErrShuttingDown + return ErrShuttingDown } + // Perform the actual write and report the result. done is buffered, so + // this send never blocks; the arbiter consumes it asynchronously. + err = rw.w.WriteMsg(msg) + slot.done <- err return err } diff --git a/p2p/peer_error.go b/p2p/peer_error.go index 9c3e58261e45..e5698c6eb7a4 100644 --- a/p2p/peer_error.go +++ b/p2p/peer_error.go @@ -54,8 +54,6 @@ func (pe *peerError) Error() string { var errProtocolReturned = errors.New("protocol returned") -var ErrAddPairPeer = errors.New("add a pair peer") - type DiscReason uint const ( @@ -71,6 +69,9 @@ const ( DiscUnexpectedIdentity DiscSelf DiscReadTimeout + // DiscPairPeerStop is kept only as an iota slot for wire compatibility + // with legacy XDPoSChain peers that still send this reason code. This + // node never sends it anymore; do not remove or reorder. DiscPairPeerStop DiscNonAllowlistedPeer DiscDenylistedPeer diff --git a/p2p/peer_priority_test.go b/p2p/peer_priority_test.go new file mode 100644 index 000000000000..bf87d41fe922 --- /dev/null +++ b/p2p/peer_priority_test.go @@ -0,0 +1,489 @@ +// Copyright 2025 The XDPoSChain Authors +// This file is part of the XDPoSChain library. +// +// The XDPoSChain library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The XDPoSChain library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the XDPoSChain library. If not, see . + +package p2p + +import ( + "io" + "sync/atomic" + "testing" + "time" + + "github.com/XinFinOrg/XDPoSChain/event" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" +) + +// readCode reads one message from rw, discards its body and returns the code. +func readCode(t *testing.T, rw MsgReader) uint64 { + t.Helper() + msg, err := rw.ReadMsg() + if err != nil { + t.Fatalf("ReadMsg: %v", err) + } + if err := msg.Discard(); err != nil { + t.Fatalf("Discard: %v", err) + } + return msg.Code +} + +func awaitSignal(t *testing.T, ch <-chan struct{}, label string) { + t.Helper() + select { + case <-ch: + case <-time.After(2 * time.Second): + t.Fatalf("timed out waiting for %s", label) + } +} + +type writeGateTransport struct { + transport + started chan struct{} + release chan struct{} + blocked int32 +} + +func newWriteGateTransport(inner transport) *writeGateTransport { + return &writeGateTransport{ + transport: inner, + started: make(chan struct{}), + release: make(chan struct{}), + } +} + +func (t *writeGateTransport) WriteMsg(msg Msg) error { + if atomic.CompareAndSwapInt32(&t.blocked, 0, 1) { + close(t.started) + <-t.release + } + return t.transport.WriteMsg(msg) +} + +func (t *writeGateTransport) unblockFirstWrite() { + close(t.release) +} + +type writeQueueHooks struct { + hiQueued chan struct{} + loQueued chan struct{} + stop chan struct{} + proto *protoRW + origHi chan<- *writeSlot + origLo chan<- *writeSlot +} + +// hookProtoWriteQueues replaces a protoRW's hi/lo request channels with +// observable proxies, so tests can detect the exact moment a write request +// is enqueued. It mutates proto.hiReq/loReq directly; this is safe only +// because the swap happens before any concurrent writer is started in the +// test (production code writes these fields once in startProtocols and +// treats them as read-only thereafter). +func hookProtoWriteQueues(t *testing.T, rw MsgReadWriter) *writeQueueHooks { + t.Helper() + + proto, ok := rw.(*protoRW) + if !ok { + t.Fatalf("unexpected MsgReadWriter type %T", rw) + } + hooks := &writeQueueHooks{ + hiQueued: make(chan struct{}, writeReqQueueSize), + loQueued: make(chan struct{}, writeReqQueueSize), + stop: make(chan struct{}), + proto: proto, + origHi: proto.hiReq, + origLo: proto.loReq, + } + hiProxy := make(chan *writeSlot, writeReqQueueSize) + loProxy := make(chan *writeSlot, writeReqQueueSize) + proto.hiReq = hiProxy + proto.loReq = loProxy + + go forwardWriteSlots(hiProxy, hooks.origHi, hooks.hiQueued, hooks.stop) + go forwardWriteSlots(loProxy, hooks.origLo, hooks.loQueued, hooks.stop) + + return hooks +} + +func forwardWriteSlots(in <-chan *writeSlot, out chan<- *writeSlot, ack chan<- struct{}, stop <-chan struct{}) { + for { + select { + case slot := <-in: + select { + case out <- slot: + ack <- struct{}{} + case <-stop: + return + } + case <-stop: + return + } + } +} + +func (h *writeQueueHooks) close() { + h.proto.hiReq = h.origHi + h.proto.loReq = h.origLo + close(h.stop) +} + +// TestPeerWritePriorityPreemption verifies that a high-priority write is +// served before a low-priority write that was enqueued first, as long as +// both are pending when the previous write finishes. +func TestPeerWritePriorityPreemption(t *testing.T) { + rwc := make(chan MsgReadWriter, 1) + stop := make(chan struct{}) + proto := Protocol{ + Name: "a", + Length: 64, + Run: func(p *Peer, rw MsgReadWriter) error { + rwc <- rw + <-stop + return nil + }, + } + var gate *writeGateTransport + closer, remote, _, _ := testPeerWithTransport([]Protocol{proto}, func(inner transport) transport { + gate = newWriteGateTransport(inner) + return gate + }) + defer closer() + defer close(stop) + + rw := <-rwc + hooks := hookProtoWriteQueues(t, rw) + defer hooks.close() + + // Slot 1: low priority. The send blocks at the transport because nobody + // is reading from `remote` yet. While it blocks, the arbiter is parked + // on activeDone. + first := make(chan error, 1) + go func() { first <- SendItems(rw, 1) }() + awaitSignal(t, gate.started, "first write to reach the transport") + + // Enqueue a low-priority write, then a high-priority write. The hi/lo + // request channels are buffered, so both enqueue immediately and wait + // on their proceed signal. + loCh := make(chan error, 1) + hiCh := make(chan error, 1) + go func() { loCh <- SendItems(rw, 2) }() + awaitSignal(t, hooks.loQueued, "low-priority request to enqueue") + go func() { hiCh <- SendPriority(rw, 3, []uint{}) }() + awaitSignal(t, hooks.hiQueued, "high-priority request to enqueue") + gate.unblockFirstWrite() + + // Drain the first message. After this, the arbiter releases slot 1 + // (no error) and picks the next pending request, preferring hi. + if got, want := readCode(t, remote), uint64(baseProtocolLength+1); got != want { + t.Fatalf("first message code: got %d, want %d", got, want) + } + if err := <-first; err != nil { + t.Fatalf("first write: %v", err) + } + + // The next message on the wire must be the high-priority one (code 3), + // even though the low-priority one (code 2) was enqueued first. + if got, want := readCode(t, remote), uint64(baseProtocolLength+3); got != want { + t.Fatalf("preemption failed: next code = %d, want hi=%d", got, want) + } + if err := <-hiCh; err != nil { + t.Fatalf("hi write: %v", err) + } + + // Finally the low-priority one is served. + if got, want := readCode(t, remote), uint64(baseProtocolLength+2); got != want { + t.Fatalf("lo not served last: code = %d, want lo=%d", got, want) + } + if err := <-loCh; err != nil { + t.Fatalf("lo write: %v", err) + } +} + +// TestPeerWriteStarvationGuard verifies that after writePriorityStarveLimit +// consecutive high-priority writes, a pending low-priority write is forced +// through ahead of the next high-priority one. +func TestPeerWriteStarvationGuard(t *testing.T) { + rwc := make(chan MsgReadWriter, 1) + stop := make(chan struct{}) + proto := Protocol{ + Name: "a", + Length: 64, + Run: func(p *Peer, rw MsgReadWriter) error { + rwc <- rw + <-stop + return nil + }, + } + var gate *writeGateTransport + closer, remote, _, _ := testPeerWithTransport([]Protocol{proto}, func(inner transport) transport { + gate = newWriteGateTransport(inner) + return gate + }) + defer closer() + defer close(stop) + + rw := <-rwc + hooks := hookProtoWriteQueues(t, rw) + defer hooks.close() + + // Block the arbiter on an initial low-priority in-flight write so we + // can pre-load both queues deterministically. + first := make(chan error, 1) + go func() { first <- SendItems(rw, 0) }() + awaitSignal(t, gate.started, "first write to reach the transport") + + // Enqueue one low-priority write (code 1) ahead of any high-priority + // ones to make the starvation case observable. + loCh := make(chan error, 1) + go func() { loCh <- SendItems(rw, 1) }() + awaitSignal(t, hooks.loQueued, "low-priority request to enqueue") + + // Then enqueue writePriorityStarveLimit+1 high-priority writes with + // codes 2..N. The first writePriorityStarveLimit of them must be + // served before the pending low-priority one is forced through. + const extraHi = writePriorityStarveLimit + 1 + hiCh := make(chan error, extraHi) + for i := 0; i < extraHi; i++ { + code := uint64(2 + i) + go func() { hiCh <- SendPriority(rw, code, []uint{}) }() + } + for i := 0; i < extraHi; i++ { + awaitSignal(t, hooks.hiQueued, "high-priority request to enqueue") + } + gate.unblockFirstWrite() + + // Drain the in-flight initial write (code 0). + if got, want := readCode(t, remote), uint64(baseProtocolLength+0); got != want { + t.Fatalf("initial code: got %d, want %d", got, want) + } + if err := <-first; err != nil { + t.Fatalf("initial write: %v", err) + } + + // Read writePriorityStarveLimit messages: all should be high-priority + // (codes in [2, 2+extraHi)). + for i := 0; i < writePriorityStarveLimit; i++ { + got := readCode(t, remote) + if got < baseProtocolLength+2 || got >= baseProtocolLength+2+extraHi { + t.Fatalf("write %d: expected high-priority code, got %d", i, got) + } + } + + // The next message must be the low-priority one (code 1), forced + // through by the starvation guard. + if got, want := readCode(t, remote), uint64(baseProtocolLength+1); got != want { + t.Fatalf("starvation guard failed: next code = %d, want lo=%d", got, want) + } + if err := <-loCh; err != nil { + t.Fatalf("lo write: %v", err) + } + + // Drain the remaining high-priority write so the test goroutines exit + // cleanly. + _ = readCode(t, remote) + for i := 0; i < extraHi; i++ { + if err := <-hiCh; err != nil { + t.Fatalf("hi write %d: %v", i, err) + } + } +} + +// TestSendPriorityFallback verifies that SendPriority falls back to the +// regular WriteMsg path on writers that do not implement PriorityMsgWriter. +func TestSendPriorityFallback(t *testing.T) { + mw := &capturingWriter{} + if err := SendPriority(mw, 42, []uint{1, 2}); err != nil { + t.Fatalf("SendPriority: %v", err) + } + if !mw.called { + t.Fatal("WriteMsg was not called on fallback writer") + } + if mw.code != 42 { + t.Fatalf("code: got %d, want 42", mw.code) + } +} + +type capturingWriter struct { + called bool + code uint64 +} + +func (w *capturingWriter) WriteMsg(msg Msg) error { + w.called = true + w.code = msg.Code + return msg.Discard() +} + +// priorityCapturingRW is a MsgReadWriter that records whether the high or low +// lane was used for the last write. It is used to verify that a wrapping +// MsgReadWriter (such as msgEventer) preserves the PriorityMsgWriter +// interface to the underlying transport. +type priorityCapturingRW struct { + last Msg + high bool + used bool // true if WriteMsgPriority was used +} + +func (rw *priorityCapturingRW) ReadMsg() (Msg, error) { return Msg{}, io.EOF } +func (rw *priorityCapturingRW) WriteMsg(msg Msg) error { rw.last = msg; rw.used = false; return nil } +func (rw *priorityCapturingRW) WriteMsgPriority(msg Msg, high bool) error { + rw.last = msg + rw.high = high + rw.used = true + return nil +} + +// TestMsgEventerForwardsPriority verifies that msgEventer preserves the +// PriorityMsgWriter contract: a SendPriority call through the wrapper still +// reaches the underlying writer's high-priority lane, and the corresponding +// PeerEventTypeMsgSend event is emitted. +func TestMsgEventerForwardsPriority(t *testing.T) { + inner := &priorityCapturingRW{} + feed := new(event.Feed) + ch := make(chan *PeerEvent, 1) + sub := feed.Subscribe(ch) + defer sub.Unsubscribe() + + ev := newMsgEventer(inner, feed, enode.ID{}, "test") + + if _, ok := MsgReadWriter(ev).(PriorityMsgWriter); !ok { + t.Fatal("msgEventer does not implement PriorityMsgWriter") + } + if err := SendPriority(ev, 7, []uint{1}); err != nil { + t.Fatalf("SendPriority: %v", err) + } + if !inner.used { + t.Fatal("priority lane was not used on inner writer") + } + if !inner.high { + t.Fatal("high flag was not propagated to inner writer") + } + if inner.last.Code != 7 { + t.Fatalf("code: got %d, want 7", inner.last.Code) + } + select { + case e := <-ch: + if e.Type != PeerEventTypeMsgSend { + t.Fatalf("event type: got %v, want %v", e.Type, PeerEventTypeMsgSend) + } + case <-time.After(time.Second): + t.Fatal("no PeerEventTypeMsgSend emitted") + } +} + +// TestPeerWriteLoEventuallyServedUnderSustainedHiPressure verifies the +// strict starvation guard: even when high-priority writes are arriving +// continuously (both preloaded and produced in the background while the +// peer is draining), a single low-priority write is forced through after +// at most writePriorityStarveLimit consecutive high-priority writes. +// +// The deterministic upper bound is writePriorityStarveLimit; we allow a +// small slack of 2 to absorb any single-iteration race between the +// arbiter's blocking select and the lo writer's loPending increment. +func TestPeerWriteLoEventuallyServedUnderSustainedHiPressure(t *testing.T) { + rwc := make(chan MsgReadWriter, 1) + stop := make(chan struct{}) + proto := Protocol{ + Name: "a", + Length: 4096, + Run: func(p *Peer, rw MsgReadWriter) error { + rwc <- rw + <-stop + return nil + }, + } + var gate *writeGateTransport + closer, remote, _, _ := testPeerWithTransport([]Protocol{proto}, func(inner transport) transport { + gate = newWriteGateTransport(inner) + return gate + }) + defer closer() + defer close(stop) + + rw := <-rwc + hooks := hookProtoWriteQueues(t, rw) + defer hooks.close() + + // Park the arbiter on an initial in-flight write so we can pre-load + // both lanes deterministically before any traffic reaches the wire. + first := make(chan error, 1) + go func() { first <- SendItems(rw, 0) }() + awaitSignal(t, gate.started, "first write to reach the transport") + + // Enqueue the single low-priority write (code 1) that the test will + // look for on the wire. + loCh := make(chan error, 1) + go func() { loCh <- SendItems(rw, 1) }() + awaitSignal(t, hooks.loQueued, "low-priority request to enqueue") + + // Pre-load a burst of high-priority writes so the hi lane is already + // saturated before draining begins. + const preHi = writePriorityStarveLimit * 3 + hiCh := make(chan error, preHi) + for i := 0; i < preHi; i++ { + code := uint64(100 + i) + go func() { hiCh <- SendPriority(rw, code, []uint{}) }() + } + for i := 0; i < preHi; i++ { + awaitSignal(t, hooks.hiQueued, "preloaded hi to enqueue") + } + + // Start a producer that keeps issuing high-priority writes while the + // test drains the wire, modelling unbounded sustained hi pressure. + // It exits on producerStop or when the peer closes (deferred teardown + // unblocks any in-flight SendPriority call with ErrShuttingDown). + producerStop := make(chan struct{}) + go func() { + code := uint64(10000) + for { + select { + case <-producerStop: + return + default: + } + if err := SendPriority(rw, code, []uint{}); err != nil { + return + } + code++ + } + }() + + gate.unblockFirstWrite() + + // Drain the initial in-flight write (code 0). + if got, want := readCode(t, remote), uint64(baseProtocolLength+0); got != want { + t.Fatalf("initial code: got %d, want %d", got, want) + } + if err := <-first; err != nil { + t.Fatalf("initial write: %v", err) + } + + // Read messages until we see the lo (code 1). The strict guard caps + // the wait at writePriorityStarveLimit hi writes; allow +2 slack. + const bound = writePriorityStarveLimit + 2 + hiBefore := 0 + for i := 0; i < bound; i++ { + got := readCode(t, remote) + if got == baseProtocolLength+1 { + close(producerStop) + if err := <-loCh; err != nil { + t.Fatalf("lo write: %v", err) + } + return + } + hiBefore++ + } + close(producerStop) + t.Fatalf("lo write not served within %d hi writes under sustained hi pressure (saw %d hi)", bound, hiBefore) +} diff --git a/p2p/peer_test.go b/p2p/peer_test.go index 09907bbb341c..08f8b795517c 100644 --- a/p2p/peer_test.go +++ b/p2p/peer_test.go @@ -44,9 +44,16 @@ var discard = Protocol{ } func testPeer(protos []Protocol) (func(), *conn, *Peer, <-chan error) { + return testPeerWithTransport(protos, nil) +} + +func testPeerWithTransport(protos []Protocol, wrap func(transport) transport) (func(), *conn, *Peer, <-chan error) { fd1, fd2 := net.Pipe() - c1 := &conn{fd: fd1, transport: newTestTransport(randomID(), fd1)} - c2 := &conn{fd: fd2, transport: newTestTransport(randomID(), fd2)} + c1 := &conn{fd: fd1, node: newNode(randomID(), nil), transport: newTestTransport(&newkey().PublicKey, fd1)} + c2 := &conn{fd: fd2, node: newNode(randomID(), nil), transport: newTestTransport(&newkey().PublicKey, fd2)} + if wrap != nil { + c1.transport = wrap(c1.transport) + } for _, p := range protos { c1.caps = append(c1.caps, p.cap()) c2.caps = append(c2.caps, p.cap()) @@ -197,34 +204,6 @@ func TestPeerDisconnectRace(t *testing.T) { } } -func TestPeerRunDisconnectsPairPeer(t *testing.T) { - closer, _, peer, errc := testPeer(nil) - defer closer() - - pairPeer := &Peer{ - disc: make(chan DiscReason, 1), - closed: make(chan struct{}), - } - peer.SetPairPeer(pairPeer) - - closer() - - select { - case <-errc: - case <-time.After(2 * time.Second): - t.Fatal("peer did not stop") - } - - select { - case reason := <-pairPeer.disc: - if reason != DiscPairPeerStop { - t.Fatalf("unexpected pair disconnect reason: got %v want %v", reason, DiscPairPeerStop) - } - case <-time.After(2 * time.Second): - t.Fatal("pair peer was not disconnected") - } -} - func TestNewPeer(t *testing.T) { name := "nodename" caps := []Cap{{"foo", 2}, {"bar", 3}} diff --git a/p2p/protocol.go b/p2p/protocol.go index f924028cce31..b2c33c8f2892 100644 --- a/p2p/protocol.go +++ b/p2p/protocol.go @@ -21,7 +21,8 @@ import ( "fmt" "strings" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" ) // Protocol represents a P2P subprotocol implementation. @@ -53,7 +54,10 @@ type Protocol struct { // PeerInfo is an optional helper method to retrieve protocol specific metadata // about a certain peer in the network. If an info retrieval function is set, // but returns nil, it is assumed that the protocol handshake is still running. - PeerInfo func(id discover.NodeID) interface{} + PeerInfo func(id enode.ID) interface{} + + // Attributes contains protocol specific information for the node record. + Attributes []enr.Entry } func (p Protocol) cap() Cap { @@ -66,10 +70,6 @@ type Cap struct { Version uint } -func (cap Cap) RlpData() interface{} { - return []interface{}{cap.Name, cap.Version} -} - func (cap Cap) String() string { return fmt.Sprintf("%s/%d", cap.Name, cap.Version) } @@ -81,3 +81,13 @@ func (cap Cap) Cmp(other Cap) int { } return strings.Compare(cap.Name, other.Name) } + +type capsByNameAndVersion []Cap + +func (cs capsByNameAndVersion) Len() int { return len(cs) } +func (cs capsByNameAndVersion) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] } +func (cs capsByNameAndVersion) Less(i, j int) bool { + return cs[i].Name < cs[j].Name || (cs[i].Name == cs[j].Name && cs[i].Version < cs[j].Version) +} + +func (capsByNameAndVersion) ENRKey() string { return "cap" } diff --git a/p2p/protocols/protocol_test.go b/p2p/protocols/protocol_test.go deleted file mode 100644 index 72d96f41ea25..000000000000 --- a/p2p/protocols/protocol_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package protocols - -import ( - "fmt" - - "github.com/XinFinOrg/XDPoSChain/p2p/discover" -) - -// handshake message type -type hs0 struct { - C uint -} - -// message to kill/drop the peer with nodeID -type kill struct { - C discover.NodeID -} - -// message to drop connection -type drop struct { -} - -// / protoHandshake represents module-independent aspects of the protocol and is -// the first message peers send and receive as part the initial exchange -type protoHandshake struct { - Version uint // local and remote peer should have identical version - NetworkID string // local and remote peer should have identical network id -} - -// checkProtoHandshake verifies local and remote protoHandshakes match -func checkProtoHandshake(testVersion uint, testNetworkID string) func(interface{}) error { - return func(rhs interface{}) error { - remote := rhs.(*protoHandshake) - if remote.NetworkID != testNetworkID { - return fmt.Errorf("%s (!= %s)", remote.NetworkID, testNetworkID) - } - - if remote.Version != testVersion { - return fmt.Errorf("%d (!= %d)", remote.Version, testVersion) - } - return nil - } -} diff --git a/p2p/rlpx.go b/p2p/rlpx.go index f2de9ae29cfe..3c3339e5ce0c 100644 --- a/p2p/rlpx.go +++ b/p2p/rlpx.go @@ -34,11 +34,12 @@ import ( "sync" "time" + "github.com/XinFinOrg/XDPoSChain/common/bitutil" "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/crypto/ecies" "github.com/XinFinOrg/XDPoSChain/crypto/keccak" + "github.com/XinFinOrg/XDPoSChain/crypto/secp256k1" "github.com/XinFinOrg/XDPoSChain/metrics" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/rlp" "github.com/golang/snappy" ) @@ -150,7 +151,7 @@ func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*protoHandshake, } if msg.Code == discMsg { // Disconnect before protocol handshake is valid according to the - // spec and we send it ourself if the posthanshake checks fail. + // spec and we send it ourself if the post-handshake checks fail. // We can't return the reason directly, though, because it is echoed // back otherwise. Wrap it in a string instead. var reason [1]DiscReason @@ -164,7 +165,7 @@ func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*protoHandshake, if err := msg.Decode(&hs); err != nil { return nil, err } - if (hs.ID == discover.NodeID{}) { + if len(hs.ID) != 64 || !bitutil.TestBytes(hs.ID) { return nil, DiscInvalidIdentity } return &hs, nil @@ -174,31 +175,29 @@ func readProtocolHandshake(rw MsgReader, our *protoHandshake) (*protoHandshake, // messages. the protocol handshake is the first authenticated message // and also verifies whether the encryption handshake 'worked' and the // remote side actually provided the right public key. -func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) { +func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *ecdsa.PublicKey) (*ecdsa.PublicKey, error) { var ( sec secrets err error ) if dial == nil { - sec, err = receiverEncHandshake(t.fd, prv, nil) + sec, err = receiverEncHandshake(t.fd, prv) } else { - sec, err = initiatorEncHandshake(t.fd, prv, dial.ID) + sec, err = initiatorEncHandshake(t.fd, prv, dial) } if err != nil { - return discover.NodeID{}, err + return nil, err } t.wmu.Lock() t.rw = newRLPXFrameRW(t.fd, sec) t.wmu.Unlock() - return sec.RemoteID, nil + return sec.Remote.ExportECDSA(), nil } // encHandshake contains the state of the encryption handshake. type encHandshake struct { - initiator bool - remoteID discover.NodeID - - remotePub *ecies.PublicKey // remote-pubk + initiator bool + remote *ecies.PublicKey // remote-pubk initNonce, respNonce []byte // nonce randomPrivKey *ecies.PrivateKey // ecdhe-random remoteRandomPub *ecies.PublicKey // ecdhe-random-pubk @@ -207,7 +206,7 @@ type encHandshake struct { // secrets represents the connection secrets // which are negotiated during the encryption handshake. type secrets struct { - RemoteID discover.NodeID + Remote *ecies.PublicKey AES, MAC []byte EgressMAC, IngressMAC hash.Hash Token []byte @@ -248,9 +247,9 @@ func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) { sharedSecret := crypto.Keccak256(ecdheSecret, crypto.Keccak256(h.respNonce, h.initNonce)) aesSecret := crypto.Keccak256(ecdheSecret, sharedSecret) s := secrets{ - RemoteID: h.remoteID, - AES: aesSecret, - MAC: crypto.Keccak256(ecdheSecret, aesSecret), + Remote: h.remote, + AES: aesSecret, + MAC: crypto.Keccak256(ecdheSecret, aesSecret), } // setup sha3 instances for the MACs @@ -272,15 +271,15 @@ func (h *encHandshake) secrets(auth, authResp []byte) (secrets, error) { // staticSharedSecret returns the static shared secret, the result // of key agreement between the local and remote static node key. func (h *encHandshake) staticSharedSecret(prv *ecdsa.PrivateKey) ([]byte, error) { - return ecies.ImportECDSA(prv).GenerateShared(h.remotePub, sskLen, sskLen) + return ecies.ImportECDSA(prv).GenerateShared(h.remote, sskLen, sskLen) } // initiatorEncHandshake negotiates a session token on conn. // it should be called on the dialing side of the connection. // // prv is the local client's private key. -func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remoteID discover.NodeID) (s secrets, err error) { - h := &encHandshake{initiator: true, remoteID: remoteID} +func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remote *ecdsa.PublicKey) (s secrets, err error) { + h := &encHandshake{initiator: true, remote: ecies.ImportECDSAPublic(remote)} authMsg, err := h.makeAuthMsg(prv) if err != nil { return s, err @@ -306,14 +305,10 @@ func initiatorEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, remoteID d // makeAuthMsg creates the initiator handshake message. func (h *encHandshake) makeAuthMsg(prv *ecdsa.PrivateKey) (*authMsgV4, error) { - rpub, err := h.remoteID.Pubkey() - if err != nil { - return nil, fmt.Errorf("bad remoteID: %v", err) - } - h.remotePub = ecies.ImportECDSAPublic(rpub) // Generate random initiator nonce. h.initNonce = make([]byte, shaLen) - if _, err := rand.Read(h.initNonce); err != nil { + _, err := rand.Read(h.initNonce) + if err != nil { return nil, err } // Generate random keypair to for ECDH. @@ -351,8 +346,7 @@ func (h *encHandshake) handleAuthResp(msg *authRespV4) (err error) { // it should be called on the listening side of the connection. // // prv is the local client's private key. -// token is the token from a previous session with this node. -func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) { +func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey) (s secrets, err error) { authMsg := new(authMsgV4) authPacket, err := readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn) if err != nil { @@ -384,13 +378,12 @@ func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byt func (h *encHandshake) handleAuthMsg(msg *authMsgV4, prv *ecdsa.PrivateKey) error { // Import the remote identity. - h.initNonce = msg.Nonce[:] - h.remoteID = msg.InitiatorPubkey - rpub, err := h.remoteID.Pubkey() + rpub, err := importPublicKey(msg.InitiatorPubkey[:]) if err != nil { - return fmt.Errorf("bad remoteID: %#v", err) + return err } - h.remotePub = ecies.ImportECDSAPublic(rpub) + h.initNonce = msg.Nonce[:] + h.remote = rpub // Generate random keypair for ECDH. // If a private key is already set, use it instead of generating one (for testing). @@ -407,7 +400,7 @@ func (h *encHandshake) handleAuthMsg(msg *authMsgV4, prv *ecdsa.PrivateKey) erro return err } signedMsg := xor(token, h.initNonce) - remoteRandomPub, err := crypto.Ecrecover(signedMsg, msg.Signature[:]) + remoteRandomPub, err := secp256k1.RecoverPubkey(signedMsg, msg.Signature[:]) if err != nil { return err } @@ -429,6 +422,16 @@ func (h *encHandshake) makeAuthResp() (msg *authRespV4, err error) { return msg, nil } +func (msg *authMsgV4) sealPlain(h *encHandshake) ([]byte, error) { + buf := make([]byte, authMsgLen) + n := copy(buf, msg.Signature[:]) + n += copy(buf[n:], crypto.Keccak256(exportPubkey(&h.randomPrivKey.PublicKey))) + n += copy(buf[n:], msg.InitiatorPubkey[:]) + n += copy(buf[n:], msg.Nonce[:]) + buf[n] = 0 // token-flag + return ecies.Encrypt(rand.Reader, h.remote, buf, nil, nil) +} + func (msg *authMsgV4) decodePlain(input []byte) { n := copy(msg.Signature[:], input) n += shaLen // skip sha3(initiator-ephemeral-pubk) @@ -442,7 +445,7 @@ func (msg *authRespV4) sealPlain(hs *encHandshake) ([]byte, error) { buf := make([]byte, authRespLen) n := copy(buf, msg.RandomPubkey[:]) copy(buf[n:], msg.Nonce[:]) - return ecies.Encrypt(rand.Reader, hs.remotePub, buf, nil, nil) + return ecies.Encrypt(rand.Reader, hs.remote, buf, nil, nil) } func (msg *authRespV4) decodePlain(input []byte) { @@ -465,7 +468,7 @@ func sealEIP8(msg interface{}, h *encHandshake) ([]byte, error) { prefix := make([]byte, 2) binary.BigEndian.PutUint16(prefix, uint16(buf.Len()+eciesOverhead)) - enc, err := ecies.Encrypt(rand.Reader, h.remotePub, buf.Bytes(), nil, prefix) + enc, err := ecies.Encrypt(rand.Reader, h.remote, buf.Bytes(), nil, prefix) return append(prefix, enc...), err } diff --git a/p2p/rlpx_test.go b/p2p/rlpx_test.go index d4daa7db81cf..71224284e0c9 100644 --- a/p2p/rlpx_test.go +++ b/p2p/rlpx_test.go @@ -18,6 +18,7 @@ package p2p import ( "bytes" + "crypto/ecdsa" "crypto/rand" "errors" "fmt" @@ -31,11 +32,10 @@ import ( "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/crypto/ecies" - "github.com/XinFinOrg/XDPoSChain/crypto/keccak" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/pipes" "github.com/XinFinOrg/XDPoSChain/rlp" "github.com/davecgh/go-spew/spew" + "golang.org/x/crypto/sha3" ) func TestSharedSecret(t *testing.T) { @@ -79,9 +79,9 @@ func TestEncHandshake(t *testing.T) { func testEncHandshake(token []byte) error { type result struct { - side string - id discover.NodeID - err error + side string + pubkey *ecdsa.PublicKey + err error } var ( prv0, _ = crypto.GenerateKey() @@ -96,14 +96,12 @@ func testEncHandshake(token []byte) error { defer func() { output <- r }() defer fd0.Close() - dest := &discover.Node{ID: discover.PubkeyID(&prv1.PublicKey)} - r.id, r.err = c0.doEncHandshake(prv0, dest) + r.pubkey, r.err = c0.doEncHandshake(prv0, &prv1.PublicKey) if r.err != nil { return } - id1 := discover.PubkeyID(&prv1.PublicKey) - if r.id != id1 { - r.err = fmt.Errorf("remote ID mismatch: got %v, want: %v", r.id, id1) + if !reflect.DeepEqual(r.pubkey, &prv1.PublicKey) { + r.err = fmt.Errorf("remote pubkey mismatch: got %v, want: %v", r.pubkey, &prv1.PublicKey) } }() go func() { @@ -111,13 +109,12 @@ func testEncHandshake(token []byte) error { defer func() { output <- r }() defer fd1.Close() - r.id, r.err = c1.doEncHandshake(prv1, nil) + r.pubkey, r.err = c1.doEncHandshake(prv1, nil) if r.err != nil { return } - id0 := discover.PubkeyID(&prv0.PublicKey) - if r.id != id0 { - r.err = fmt.Errorf("remote ID mismatch: got %v, want: %v", r.id, id0) + if !reflect.DeepEqual(r.pubkey, &prv0.PublicKey) { + r.err = fmt.Errorf("remote ID mismatch: got %v, want: %v", r.pubkey, &prv0.PublicKey) } }() @@ -149,12 +146,12 @@ func testEncHandshake(token []byte) error { func TestProtocolHandshake(t *testing.T) { var ( prv0, _ = crypto.GenerateKey() - node0 = &discover.Node{ID: discover.PubkeyID(&prv0.PublicKey), IP: net.IP{1, 2, 3, 4}, TCP: 33} - hs0 = &protoHandshake{Version: 3, ID: node0.ID, Caps: []Cap{{"a", 0}, {"b", 2}}} + pub0 = crypto.FromECDSAPub(&prv0.PublicKey)[1:] + hs0 = &protoHandshake{Version: 3, ID: pub0, Caps: []Cap{{"a", 0}, {"b", 2}}} prv1, _ = crypto.GenerateKey() - node1 = &discover.Node{ID: discover.PubkeyID(&prv1.PublicKey), IP: net.IP{5, 6, 7, 8}, TCP: 44} - hs1 = &protoHandshake{Version: 3, ID: node1.ID, Caps: []Cap{{"c", 1}, {"d", 3}}} + pub1 = crypto.FromECDSAPub(&prv1.PublicKey)[1:] + hs1 = &protoHandshake{Version: 3, ID: pub1, Caps: []Cap{{"c", 1}, {"d", 3}}} wg sync.WaitGroup ) @@ -164,16 +161,18 @@ func TestProtocolHandshake(t *testing.T) { t.Fatal(err) } - wg.Go(func() { + wg.Add(2) + go func() { + defer wg.Done() defer fd0.Close() rlpx := newRLPX(fd0) - remid, err := rlpx.doEncHandshake(prv0, node1) + rpubkey, err := rlpx.doEncHandshake(prv0, &prv1.PublicKey) if err != nil { t.Errorf("dial side enc handshake failed: %v", err) return } - if remid != node1.ID { - t.Errorf("dial side remote id mismatch: got %v, want %v", remid, node1.ID) + if !reflect.DeepEqual(rpubkey, &prv1.PublicKey) { + t.Errorf("dial side remote pubkey mismatch: got %v, want %v", rpubkey, &prv1.PublicKey) return } @@ -188,17 +187,18 @@ func TestProtocolHandshake(t *testing.T) { return } rlpx.close(DiscQuitting) - }) - wg.Go(func() { + }() + go func() { + defer wg.Done() defer fd1.Close() rlpx := newRLPX(fd1) - remid, err := rlpx.doEncHandshake(prv1, nil) + rpubkey, err := rlpx.doEncHandshake(prv1, nil) if err != nil { t.Errorf("listen side enc handshake failed: %v", err) return } - if remid != node0.ID { - t.Errorf("listen side remote id mismatch: got %v, want %v", remid, node0.ID) + if !reflect.DeepEqual(rpubkey, &prv0.PublicKey) { + t.Errorf("listen side remote pubkey mismatch: got %v, want %v", rpubkey, &prv0.PublicKey) return } @@ -216,7 +216,7 @@ func TestProtocolHandshake(t *testing.T) { if err := ExpectMsg(rlpx, discMsg, []DiscReason{DiscQuitting}); err != nil { t.Errorf("error receiving disconnect: %v", err) } - }) + }() wg.Wait() } @@ -333,8 +333,8 @@ func TestRLPXFrameRW(t *testing.T) { s1 := secrets{ AES: aesSecret, MAC: macSecret, - EgressMAC: keccak.NewLegacyKeccak256(), - IngressMAC: keccak.NewLegacyKeccak256(), + EgressMAC: sha3.NewLegacyKeccak256(), + IngressMAC: sha3.NewLegacyKeccak256(), } s1.EgressMAC.Write(egressMACinit) s1.IngressMAC.Write(ingressMACinit) @@ -343,8 +343,8 @@ func TestRLPXFrameRW(t *testing.T) { s2 := secrets{ AES: aesSecret, MAC: macSecret, - EgressMAC: keccak.NewLegacyKeccak256(), - IngressMAC: keccak.NewLegacyKeccak256(), + EgressMAC: sha3.NewLegacyKeccak256(), + IngressMAC: sha3.NewLegacyKeccak256(), } s2.EgressMAC.Write(ingressMACinit) s2.IngressMAC.Write(egressMACinit) diff --git a/p2p/server.go b/p2p/server.go index 72df20dade2d..04d658c480da 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -18,23 +18,28 @@ package p2p import ( - "cmp" + "bytes" "crypto/ecdsa" + "encoding/hex" "errors" "net" - "slices" + "sort" "sync" "sync/atomic" "time" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common/mclock" + "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p/discover" "github.com/XinFinOrg/XDPoSChain/p2p/discv5" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" "github.com/XinFinOrg/XDPoSChain/p2p/nat" "github.com/XinFinOrg/XDPoSChain/p2p/netutil" + "github.com/XinFinOrg/XDPoSChain/rlp" ) const ( @@ -87,17 +92,18 @@ type Config struct { DiscoveryV5 bool `toml:",omitempty"` // Name sets the node name of this server. + // Use common.MakeName to create a name that follows existing conventions. Name string `toml:"-"` // Allowlist for peers - AllowPeers map[discover.NodeID]struct{} + AllowPeers map[enode.ID]struct{} // Denylist for peers. - DenyPeers map[discover.NodeID]struct{} + DenyPeers map[enode.ID]struct{} // BootstrapNodes are used to establish connectivity // with the rest of the network. - BootstrapNodes []*discover.Node + BootstrapNodes []*enode.Node // BootstrapNodesV5 are used to establish connectivity // with the rest of the network using the V5 discovery @@ -106,11 +112,11 @@ type Config struct { // Static nodes are used as pre-configured connections which are always // maintained and re-connected on disconnects. - StaticNodes []*discover.Node + StaticNodes []*enode.Node // Trusted nodes are used as pre-configured connections which are always // allowed to connect, even above the peer limit. - TrustedNodes []*discover.Node + TrustedNodes []*enode.Node // Connectivity can be restricted to certain IP networks. // If this option is set to a non-nil value, only hosts which match one of the @@ -165,8 +171,10 @@ type Server struct { newPeerHook func(*Peer) lock sync.Mutex // protects running - Running bool + running bool + nodedb *enode.DB + localnode *enode.LocalNode ntab discoverTable listener net.Listener ourHandshake *protoHandshake @@ -178,10 +186,10 @@ type Server struct { peerOpDone chan struct{} quit chan struct{} - addstatic chan *discover.Node - removestatic chan *discover.Node - addtrusted chan *discover.Node - removetrusted chan *discover.Node + addstatic chan *enode.Node + removestatic chan *enode.Node + addtrusted chan *enode.Node + removetrusted chan *enode.Node posthandshake chan *conn addpeer chan *conn delpeer chan peerDrop @@ -190,7 +198,7 @@ type Server struct { log log.Logger } -type peerOpFunc func(map[discover.NodeID]*Peer) +type peerOpFunc func(map[enode.ID]*Peer) type peerDrop struct { *Peer @@ -212,16 +220,16 @@ const ( type conn struct { fd net.Conn transport + node *enode.Node flags connFlag - cont chan error // The run loop uses cont to signal errors to SetupConn. - id discover.NodeID // valid after the encryption handshake - caps []Cap // valid after the protocol handshake - name string // valid after the protocol handshake + cont chan error // The run loop uses cont to signal errors to SetupConn. + caps []Cap // valid after the protocol handshake + name string // valid after the protocol handshake } type transport interface { // The two handshakes. - doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) + doEncHandshake(prv *ecdsa.PrivateKey, dialDest *ecdsa.PublicKey) (*ecdsa.PublicKey, error) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) // The MsgReadWriter can only be used after the encryption // handshake has completed. The code uses conn.id to track this @@ -229,14 +237,14 @@ type transport interface { MsgReadWriter // transports must provide Close because we use MsgPipe in some of // the tests. Closing the actual network connection doesn't do - // anything in those tests because NsgPipe doesn't use it. + // anything in those tests because MsgPipe doesn't use it. close(err error) } func (c *conn) String() string { s := c.flags.String() - if (c.id != discover.NodeID{}) { - s += " " + c.id.String() + if (c.node.ID() != enode.ID{}) { + s += " " + c.node.ID().String() } s += " " + c.fd.RemoteAddr().String() return s @@ -268,26 +276,28 @@ func (c *conn) is(f connFlag) bool { } func (c *conn) set(f connFlag, val bool) { - flags := connFlag(atomic.LoadInt32((*int32)(&c.flags))) - if val { - flags |= f - } else { - flags &= ^f + for { + oldFlags := connFlag(atomic.LoadInt32((*int32)(&c.flags))) + flags := oldFlags + if val { + flags |= f + } else { + flags &= ^f + } + if atomic.CompareAndSwapInt32((*int32)(&c.flags), int32(oldFlags), int32(flags)) { + return + } } - atomic.StoreInt32((*int32)(&c.flags), int32(flags)) } -// Peers returns the public view of connected remote nodes. -// -// The returned slice contains one entry per remote NodeID, so multiple physical -// connections associated with the same node are represented by a single entry. +// Peers returns all connected peers. func (srv *Server) Peers() []*Peer { var ps []*Peer select { // Note: We'd love to put this function into a variable but // that seems to cause a weird compiler error in some // environments. - case srv.peerOp <- func(peers map[discover.NodeID]*Peer) { + case srv.peerOp <- func(peers map[enode.ID]*Peer) { for _, p := range peers { ps = append(ps, p) } @@ -298,15 +308,11 @@ func (srv *Server) Peers() []*Peer { return ps } -// PeerCount returns the number of connected remote nodes. -// -// Multiple physical connections associated with the same remote NodeID -// (for example pair peers) are counted once because the public peer view is -// keyed by NodeID. +// PeerCount returns the number of connected peers. func (srv *Server) PeerCount() int { var count int select { - case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }: + case srv.peerOp <- func(ps map[enode.ID]*Peer) { count = len(ps) }: <-srv.peerOpDone case <-srv.quit: } @@ -316,7 +322,7 @@ func (srv *Server) PeerCount() int { // AddPeer connects to the given node and maintains the connection until the // server is shut down. If the connection fails for any reason, the server will // attempt to reconnect the peer. -func (srv *Server) AddPeer(node *discover.Node) { +func (srv *Server) AddPeer(node *enode.Node) { select { case srv.addstatic <- node: case <-srv.quit: @@ -324,16 +330,16 @@ func (srv *Server) AddPeer(node *discover.Node) { } // RemovePeer disconnects from the given node -func (srv *Server) RemovePeer(node *discover.Node) { +func (srv *Server) RemovePeer(node *enode.Node) { select { case srv.removestatic <- node: case <-srv.quit: } } -// AddTrustedPeer adds the given node to a reserved allowlist which allows the -// node to always connect, even if the slots are full. -func (srv *Server) AddTrustedPeer(node *discover.Node) { +// AddTrustedPeer adds the given node to a reserved whitelist which allows the +// node to always connect, even if the slot are full. +func (srv *Server) AddTrustedPeer(node *enode.Node) { select { case srv.addtrusted <- node: case <-srv.quit: @@ -341,7 +347,7 @@ func (srv *Server) AddTrustedPeer(node *discover.Node) { } // RemoveTrustedPeer removes the given node from the trusted peer set. -func (srv *Server) RemoveTrustedPeer(node *discover.Node) { +func (srv *Server) RemoveTrustedPeer(node *enode.Node) { select { case srv.removetrusted <- node: case <-srv.quit: @@ -354,50 +360,32 @@ func (srv *Server) SubscribeEvents(ch chan *PeerEvent) event.Subscription { } // Self returns the local node's endpoint information. -func (srv *Server) Self() *discover.Node { +func (srv *Server) Self() *enode.Node { srv.lock.Lock() - defer srv.lock.Unlock() - - if !srv.Running { - return &discover.Node{IP: net.ParseIP("0.0.0.0")} - } - return srv.makeSelf(srv.listener, srv.ntab) -} + ln := srv.localnode + srv.lock.Unlock() -func (srv *Server) makeSelf(listener net.Listener, ntab discoverTable) *discover.Node { - // If the server's not running, return an empty node. - // If the node is running but discovery is off, manually assemble the node infos. - if ntab == nil { - // Inbound connections disabled, use zero address. - if listener == nil { - return &discover.Node{IP: net.ParseIP("0.0.0.0"), ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)} - } - // Otherwise inject the listener address too - addr := listener.Addr().(*net.TCPAddr) - return &discover.Node{ - ID: discover.PubkeyID(&srv.PrivateKey.PublicKey), - IP: addr.IP, - TCP: uint16(addr.Port), - } + if ln == nil { + return enode.NewV4(&srv.PrivateKey.PublicKey, net.ParseIP("0.0.0.0"), 0, 0) } - // Otherwise return the discovery node. - return ntab.Self() + return ln.Node() } // Stop terminates the server and all active peer connections. // It blocks until all active connections have been closed. func (srv *Server) Stop() { srv.lock.Lock() - defer srv.lock.Unlock() - if !srv.Running { + if !srv.running { + srv.lock.Unlock() return } - srv.Running = false + srv.running = false if srv.listener != nil { // this unblocks listener Accept srv.listener.Close() } close(srv.quit) + srv.lock.Unlock() srv.loopWG.Wait() } @@ -432,15 +420,17 @@ func (s *sharedUDPConn) Close() error { func (srv *Server) Start() (err error) { srv.lock.Lock() defer srv.lock.Unlock() - if srv.Running { + if srv.running { return errors.New("server already running") } - srv.Running = true + srv.running = true srv.log = srv.Config.Logger if srv.log == nil { srv.log = log.New() } - srv.log.Info("Starting P2P networking") + if srv.NoDial && srv.ListenAddr == "" { + srv.log.Warn("P2P server will be useless, neither dialing nor listening") + } // static fields if srv.PrivateKey == nil { @@ -456,72 +446,127 @@ func (srv *Server) Start() (err error) { srv.addpeer = make(chan *conn) srv.delpeer = make(chan peerDrop) srv.posthandshake = make(chan *conn) - srv.addstatic = make(chan *discover.Node) - srv.removestatic = make(chan *discover.Node) - srv.addtrusted = make(chan *discover.Node) - srv.removetrusted = make(chan *discover.Node) + srv.addstatic = make(chan *enode.Node) + srv.removestatic = make(chan *enode.Node) + srv.addtrusted = make(chan *enode.Node) + srv.removetrusted = make(chan *enode.Node) srv.peerOp = make(chan peerOpFunc) srv.peerOpDone = make(chan struct{}) - var ( - conn *net.UDPConn - sconn *sharedUDPConn - realaddr *net.UDPAddr - unhandled chan discover.ReadPacket - ) - - if !srv.NoDiscovery || srv.DiscoveryV5 { - addr, err := net.ResolveUDPAddr("udp", srv.ListenAddr) - if err != nil { + if err := srv.setupLocalNode(); err != nil { + return err + } + if srv.ListenAddr != "" { + if err := srv.setupListening(); err != nil { return err } - conn, err = net.ListenUDP("udp", addr) - if err != nil { - return err + } + if err := srv.setupDiscovery(); err != nil { + return err + } + + dynPeers := srv.maxDialedConns() + dialer := newDialState(srv.localnode.ID(), srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict) + srv.loopWG.Add(1) + go srv.run(dialer) + return nil +} + +func (srv *Server) setupLocalNode() error { + // Create the devp2p handshake. + pubkey := crypto.FromECDSAPub(&srv.PrivateKey.PublicKey) + srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: pubkey[1:]} + for _, p := range srv.Protocols { + srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap()) + } + sort.Sort(capsByNameAndVersion(srv.ourHandshake.Caps)) + + // Create the local node. + db, err := enode.OpenDB(srv.Config.NodeDatabase) + if err != nil { + return err + } + srv.nodedb = db + srv.localnode = enode.NewLocalNode(db, srv.PrivateKey) + srv.localnode.SetFallbackIP(net.IP{127, 0, 0, 1}) + srv.localnode.Set(capsByNameAndVersion(srv.ourHandshake.Caps)) + // TODO: check conflicts + for _, p := range srv.Protocols { + for _, e := range p.Attributes { + srv.localnode.Set(e) } - realaddr = conn.LocalAddr().(*net.UDPAddr) - if srv.NAT != nil { - if !realaddr.IP.IsLoopback() { - go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") - } - // TODO: react to external IP changes over time. - if ext, err := srv.NAT.ExternalIP(); err == nil { - realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port} + } + switch srv.NAT.(type) { + case nil: + // No NAT interface, do nothing. + case nat.ExtIP: + // ExtIP doesn't block, set the IP right away. + ip, _ := srv.NAT.ExternalIP() + srv.localnode.SetStaticIP(ip) + default: + // Ask the router about the IP. This takes a while and blocks startup, + // do it in the background. + srv.loopWG.Add(1) + go func() { + defer srv.loopWG.Done() + if ip, err := srv.NAT.ExternalIP(); err == nil { + srv.localnode.SetStaticIP(ip) } - } + }() } + return nil +} - if !srv.NoDiscovery && srv.DiscoveryV5 { - unhandled = make(chan discover.ReadPacket, 100) - sconn = &sharedUDPConn{conn, unhandled} +func (srv *Server) setupDiscovery() error { + if srv.NoDiscovery && !srv.DiscoveryV5 { + return nil } - // node table + addr, err := net.ResolveUDPAddr("udp", srv.ListenAddr) + if err != nil { + return err + } + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return err + } + realaddr := conn.LocalAddr().(*net.UDPAddr) + srv.log.Debug("UDP listener up", "addr", realaddr) + if srv.NAT != nil { + if !realaddr.IP.IsLoopback() { + go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery") + } + } + srv.localnode.SetFallbackUDP(realaddr.Port) + + // Discovery V4 + var unhandled chan discover.ReadPacket + var sconn *sharedUDPConn if !srv.NoDiscovery { + if srv.DiscoveryV5 { + unhandled = make(chan discover.ReadPacket, 100) + sconn = &sharedUDPConn{conn, unhandled} + } cfg := discover.Config{ - PrivateKey: srv.PrivateKey, - AnnounceAddr: realaddr, - NodeDBPath: srv.NodeDatabase, - NetRestrict: srv.NetRestrict, - Bootnodes: srv.BootstrapNodes, - Unhandled: unhandled, + PrivateKey: srv.PrivateKey, + NetRestrict: srv.NetRestrict, + Bootnodes: srv.BootstrapNodes, + Unhandled: unhandled, } - ntab, err := discover.ListenUDP(conn, cfg) + ntab, err := discover.ListenUDP(conn, srv.localnode, cfg) if err != nil { return err } srv.ntab = ntab } - + // Discovery V5 if srv.DiscoveryV5 { - var ( - ntab *discv5.Network - err error - ) + var ntab *discv5.Network + var err error if sconn != nil { - ntab, err = discv5.ListenUDP(srv.PrivateKey, sconn, realaddr, "", srv.NetRestrict) //srv.NodeDatabase) + ntab, err = discv5.ListenUDP(srv.PrivateKey, sconn, "", srv.NetRestrict) } else { - ntab, err = discv5.ListenUDP(srv.PrivateKey, conn, realaddr, "", srv.NetRestrict) //srv.NodeDatabase) + ntab, err = discv5.ListenUDP(srv.PrivateKey, conn, "", srv.NetRestrict) } if err != nil { return err @@ -531,32 +576,10 @@ func (srv *Server) Start() (err error) { } srv.DiscV5 = ntab } - - dynPeers := srv.maxDialedConns() - dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict) - - // handshake - srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)} - for _, p := range srv.Protocols { - srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap()) - } - // listen/dial - if srv.ListenAddr != "" { - if err := srv.startListening(); err != nil { - return err - } - } - if srv.NoDial && srv.ListenAddr == "" { - srv.log.Warn("P2P server will be useless, neither dialing nor listening") - } - - srv.loopWG.Add(1) - go srv.run(dialer) - srv.Running = true return nil } -func (srv *Server) startListening() error { +func (srv *Server) setupListening() error { // Launch the TCP listener. listener, err := net.Listen("tcp", srv.ListenAddr) if err != nil { @@ -565,8 +588,11 @@ func (srv *Server) startListening() error { laddr := listener.Addr().(*net.TCPAddr) srv.ListenAddr = laddr.String() srv.listener = listener + srv.localnode.Set(enr.TCP(laddr.Port)) + srv.loopWG.Add(1) go srv.listenLoop() + // Map the TCP listening port if NAT is configured. if !laddr.IP.IsLoopback() && srv.NAT != nil { srv.loopWG.Add(1) @@ -579,19 +605,21 @@ func (srv *Server) startListening() error { } type dialer interface { - newTasks(running int, peers map[discover.NodeID]*Peer, now time.Time) []task + newTasks(running int, peers map[enode.ID]*Peer, now time.Time) []task taskDone(task, time.Time) - addStatic(*discover.Node) - removeStatic(*discover.Node) + addStatic(*enode.Node) + removeStatic(*enode.Node) } func (srv *Server) run(dialstate dialer) { + srv.log.Info("Started P2P networking", "self", srv.localnode.Node()) defer srv.loopWG.Done() + defer srv.nodedb.Close() + var ( - peers = make(map[discover.NodeID]*Peer) - connCount = 0 + peers = make(map[enode.ID]*Peer) inboundCount = 0 - trusted = make(map[discover.NodeID]bool, len(srv.TrustedNodes)) + trusted = make(map[enode.ID]bool, len(srv.TrustedNodes)) taskdone = make(chan task, maxActiveDialTasks) runningTasks []task queuedTasks []task // tasks that can't run yet @@ -599,7 +627,7 @@ func (srv *Server) run(dialstate dialer) { // Put trusted nodes into a map to speed up checks. // Trusted peers are loaded on startup or added via AddTrustedPeer RPC. for _, n := range srv.TrustedNodes { - trusted[n.ID] = true + trusted[n.ID()] = true } // removes t from runningTasks @@ -644,33 +672,35 @@ running: // This channel is used by AddPeer to add to the // ephemeral static peer list. Add it to the dialer, // it will keep the node connected. - srv.log.Debug("Adding static node", "node", n) + srv.log.Trace("Adding static node", "node", n) dialstate.addStatic(n) case n := <-srv.removestatic: // This channel is used by RemovePeer to send a // disconnect request to a peer and begin the // stop keeping the node connected. - srv.log.Debug("Removing static node", "node", n) + srv.log.Trace("Removing static node", "node", n) dialstate.removeStatic(n) - if p, ok := peers[n.ID]; ok { + if p, ok := peers[n.ID()]; ok { p.Disconnect(DiscRequested) } case n := <-srv.addtrusted: // This channel is used by AddTrustedPeer to add an enode // to the trusted node set. srv.log.Trace("Adding trusted node", "node", n) - trusted[n.ID] = true + trusted[n.ID()] = true // Mark any already-connected peer as trusted - if p, ok := peers[n.ID]; ok { + if p, ok := peers[n.ID()]; ok { p.rw.set(trustedConn, true) } case n := <-srv.removetrusted: // This channel is used by RemoveTrustedPeer to remove an enode // from the trusted node set. srv.log.Trace("Removing trusted node", "node", n) - delete(trusted, n.ID) + if _, ok := trusted[n.ID()]; ok { + delete(trusted, n.ID()) + } // Unmark any already-connected peer as trusted - if p, ok := peers[n.ID]; ok { + if p, ok := peers[n.ID()]; ok { p.rw.set(trustedConn, false) } case op := <-srv.peerOp: @@ -687,7 +717,7 @@ running: case c := <-srv.posthandshake: // A connection has passed the encryption handshake so // the remote identity is known (but hasn't been verified yet). - if trusted[c.id] { + if trusted[c.node.ID()] { // Ensure that the trusted flag is set before checking against MaxPeers. c.flags |= trustedConn } @@ -710,16 +740,9 @@ running: p.events = &srv.peerFeed } name := truncateName(c.name) - connCount++ - + srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "peers", len(peers)+1) go srv.runPeer(p) - if peers[c.id] != nil { - peers[c.id].SetPairPeer(p) - srv.log.Debug("Adding p2p pair peer", "name", name, "addr", c.fd.RemoteAddr(), "connections", connCount) - } else { - peers[c.id] = p - srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "connections", connCount) - } + peers[c.node.ID()] = p if p.Inbound() { inboundCount++ serveSuccessMeter.Mark(1) @@ -739,8 +762,8 @@ running: case pd := <-srv.delpeer: // A peer disconnected. d := common.PrettyDuration(mclock.Now() - pd.created) - connCount = removePeerTracking(peers, pd, connCount) - pd.log.Debug("Removing p2p peer", "duration", d, "connections", connCount, "req", pd.requested, "err", pd.err) + pd.log.Debug("Removing p2p peer", "duration", d, "peers", len(peers)-1, "req", pd.requested, "err", pd.err) + delete(peers, pd.ID()) if pd.Inbound() { inboundCount-- } @@ -764,25 +787,14 @@ running: // Wait for peers to shut down. Pending connections and tasks are // not handled here and will terminate soon-ish because srv.quit // is closed. - for connCount > 0 { - pd := <-srv.delpeer - pd.log.Trace("<-delpeer (spindown)", "remainingTasks", len(runningTasks)) - connCount = removePeerTracking(peers, pd, connCount) + for len(peers) > 0 { + p := <-srv.delpeer + p.log.Trace("<-delpeer (spindown)", "remainingTasks", len(runningTasks)) + delete(peers, p.ID()) } } -func removePeerTracking(peers map[discover.NodeID]*Peer, pd peerDrop, connCount int) int { - if connCount > 0 { - connCount-- - } - if current := peers[pd.ID()]; current == pd.Peer { - delete(peers, pd.ID()) - } else if current != nil && current.ClearPairPeer(pd.Peer) { - } - return connCount -} - -func (srv *Server) protoHandshakeChecks(peers map[discover.NodeID]*Peer, inboundCount int, c *conn) error { +func (srv *Server) protoHandshakeChecks(peers map[enode.ID]*Peer, inboundCount int, c *conn) error { // Drop connections with no matching protocols. if len(srv.Protocols) > 0 && countMatchingProtocols(srv.Protocols, c.caps) == 0 { return DiscUselessPeer @@ -792,19 +804,15 @@ func (srv *Server) protoHandshakeChecks(peers map[discover.NodeID]*Peer, inbound return srv.encHandshakeChecks(peers, inboundCount, c) } -func (srv *Server) encHandshakeChecks(peers map[discover.NodeID]*Peer, inboundCount int, c *conn) error { +func (srv *Server) encHandshakeChecks(peers map[enode.ID]*Peer, inboundCount int, c *conn) error { switch { case !c.is(trustedConn|staticDialedConn) && len(peers) >= srv.MaxPeers: return DiscTooManyPeers case !c.is(trustedConn) && c.is(inboundConn) && inboundCount >= srv.maxInboundConns(): return DiscTooManyPeers - case peers[c.id] != nil: - exitPeer := peers[c.id] - if exitPeer.PairPeer() != nil { - return DiscAlreadyConnected - } - return nil - case c.id == srv.Self().ID: + case peers[c.node.ID()] != nil: + return DiscAlreadyConnected + case c.node.ID() == srv.localnode.ID(): return DiscSelf default: return nil @@ -826,15 +834,11 @@ func (srv *Server) maxDialedConns() int { return srv.MaxPeers / r } -type tempError interface { - Temporary() bool -} - // listenLoop runs in its own goroutine and accepts // inbound connections. func (srv *Server) listenLoop() { defer srv.loopWG.Done() - srv.log.Info("RLPx listener up", "self", srv.makeSelf(srv.listener, srv.ntab)) + srv.log.Debug("TCP listener up", "addr", srv.listener.Addr()) tokens := defaultMaxPendingPeers if srv.MaxPendingPeers > 0 { @@ -855,7 +859,7 @@ func (srv *Server) listenLoop() { ) for { fd, err = srv.listener.Accept() - if tempErr, ok := err.(tempError); ok && tempErr.Temporary() { + if netutil.IsTemporaryError(err) { srv.log.Debug("Temporary read error", "err", err) continue } else if err != nil { @@ -868,7 +872,7 @@ func (srv *Server) listenLoop() { // Reject connections that do not match NetRestrict. if srv.NetRestrict != nil { if tcp, ok := fd.RemoteAddr().(*net.TCPAddr); ok && !srv.NetRestrict.Contains(tcp.IP) { - srv.log.Debug("Rejected conn (not allowlisted in NetRestrict)", "addr", fd.RemoteAddr()) + srv.log.Debug("Rejected conn (not whitelisted in NetRestrict)", "addr", fd.RemoteAddr()) fd.Close() slots <- struct{}{} continue @@ -876,7 +880,6 @@ func (srv *Server) listenLoop() { } fd = newMeteredConn(fd) - serveMeter.Mark(1) srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr()) go func() { srv.SetupConn(fd, inboundConn, nil) @@ -888,53 +891,58 @@ func (srv *Server) listenLoop() { // SetupConn runs the handshakes and attempts to add the connection // as a peer. It returns when the connection has been added as a peer // or the handshakes have failed. -func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error { - self := srv.Self() - if self == nil { - return errors.New("shutdown") - } +func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *enode.Node) error { c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)} err := srv.setupConn(c, flags, dialDest) if err != nil { - if !c.is(inboundConn) { - markDialError(err) - } c.close(err) - srv.log.Trace("Setting up connection failed", "id", c.id, "err", err) + srv.log.Trace("Setting up connection failed", "addr", fd.RemoteAddr(), "err", err) } return err } -func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) error { +func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *enode.Node) error { // Prevent leftover pending conns from entering the handshake. srv.lock.Lock() - running := srv.Running + running := srv.running srv.lock.Unlock() if !running { return errServerStopped } + // If dialing, figure out the remote public key. + var dialPubkey *ecdsa.PublicKey + if dialDest != nil { + dialPubkey = new(ecdsa.PublicKey) + if err := dialDest.Load((*enode.Secp256k1)(dialPubkey)); err != nil { + return errors.New("dial destination doesn't have a secp256k1 public key") + } + } // Run the encryption handshake. - var err error - if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil { + remotePubkey, err := c.doEncHandshake(srv.PrivateKey, dialPubkey) + if err != nil { srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err) return err } - clog := srv.log.New("id", c.id.String(), "addr", c.fd.RemoteAddr(), "conn", c.flags) + if dialDest != nil { + // For dialed connections, check that the remote public key matches. + if dialPubkey.X.Cmp(remotePubkey.X) != 0 || dialPubkey.Y.Cmp(remotePubkey.Y) != 0 { + return DiscUnexpectedIdentity + } + c.node = dialDest + } else { + c.node = nodeFromConn(remotePubkey, c.fd) + } + clog := srv.log.New("id", c.node.ID(), "addr", c.fd.RemoteAddr(), "conn", c.flags) if len(srv.AllowPeers) > 0 { - if _, ok := srv.AllowPeers[c.id]; !ok { + if _, ok := srv.AllowPeers[c.node.ID()]; !ok { clog.Debug("Reject non-allowlisted peer") return DiscNonAllowlistedPeer } } - if _, ok := srv.DenyPeers[c.id]; ok { + if _, ok := srv.DenyPeers[c.node.ID()]; ok { clog.Debug("Reject blacklisted peer") return DiscDenylistedPeer } - // For dialed connections, check that the remote public key matches. - if dialDest != nil && c.id != dialDest.ID { - clog.Trace("Dialed identity mismatch", "want", c, dialDest.ID) - return DiscUnexpectedIdentity - } err = srv.checkpoint(c, srv.posthandshake) if err != nil { clog.Trace("Rejected peer before protocol handshake", "err", err) @@ -946,8 +954,8 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) e clog.Trace("Failed proto handshake", "err", err) return err } - if phs.ID != c.id { - clog.Trace("Wrong devp2p handshake identity", "err", phs.ID) + if id := c.node.ID(); !bytes.Equal(crypto.Keccak256(phs.ID), id[:]) { + clog.Trace("Wrong devp2p handshake identity", "phsid", hex.EncodeToString(phs.ID)) return DiscUnexpectedIdentity } c.caps, c.name = phs.Caps, phs.Name @@ -958,10 +966,20 @@ func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) e } // If the checks completed successfully, runPeer has now been // launched by run. - clog.Debug("Setup connection") + clog.Trace("connection set up", "inbound", dialDest == nil) return nil } +func nodeFromConn(pubkey *ecdsa.PublicKey, conn net.Conn) *enode.Node { + var ip net.IP + var port int + if tcp, ok := conn.RemoteAddr().(*net.TCPAddr); ok { + ip = tcp.IP + port = tcp.Port + } + return enode.NewV4(pubkey, ip, port, port) +} + func truncateName(s string) string { if len(s) > 20 { return s[:20] + "..." @@ -1019,6 +1037,7 @@ type NodeInfo struct { ID string `json:"id"` // Unique node identifier (also the encryption key) Name string `json:"name"` // Name of the node, including client type, version, OS, custom data Enode string `json:"enode"` // Enode URL for adding this peer from remote peers + ENR string `json:"enr"` // Ethereum Node Record IP string `json:"ip"` // IP address of the node Ports struct { Discovery int `json:"discovery"` // UDP listening port for discovery protocol @@ -1030,19 +1049,21 @@ type NodeInfo struct { // NodeInfo gathers and returns a collection of metadata known about the host. func (srv *Server) NodeInfo() *NodeInfo { - node := srv.Self() - // Gather and assemble the generic node infos + node := srv.Self() info := &NodeInfo{ Name: srv.Name, Enode: node.String(), - ID: node.ID.String(), - IP: node.IP.String(), + ID: node.ID().String(), + IP: node.IP().String(), ListenAddr: srv.ListenAddr, Protocols: make(map[string]interface{}), } - info.Ports.Discovery = int(node.UDP) - info.Ports.Listener = int(node.TCP) + info.Ports.Discovery = node.UDP() + info.Ports.Listener = node.TCP() + if enc, err := rlp.EncodeToBytes(node.Record()); err == nil { + info.ENR = "0x" + hex.EncodeToString(enc) + } // Gather all the running protocol infos (only once per protocol type) for _, proto := range srv.Protocols { @@ -1067,9 +1088,12 @@ func (srv *Server) PeersInfo() []*PeerInfo { } } // Sort the result array alphabetically by node identifier - slices.SortFunc(infos, func(a, b *PeerInfo) int { - return cmp.Compare(a.ID, b.ID) - }) - + for i := 0; i < len(infos); i++ { + for j := i + 1; j < len(infos); j++ { + if infos[i].ID > infos[j].ID { + infos[i], infos[j] = infos[j], infos[i] + } + } + } return infos } diff --git a/p2p/server_test.go b/p2p/server_test.go index 92933d2f43f1..15c5dcb1820f 100644 --- a/p2p/server_test.go +++ b/p2p/server_test.go @@ -26,39 +26,37 @@ import ( "time" "github.com/XinFinOrg/XDPoSChain/crypto" - "github.com/XinFinOrg/XDPoSChain/crypto/keccak" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "golang.org/x/crypto/sha3" ) -func init() { - // log.SetDefault(log.LvlFilterHandler(log.LvlError, log.StreamHandler(os.Stderr, log.TerminalFormat(false)))) -} - type testTransport struct { - id discover.NodeID + rpub *ecdsa.PublicKey *rlpx closeErr error } -func newTestTransport(id discover.NodeID, fd net.Conn) transport { +func newTestTransport(rpub *ecdsa.PublicKey, fd net.Conn) transport { wrapped := newRLPX(fd).(*rlpx) wrapped.rw = newRLPXFrameRW(fd, secrets{ MAC: zero16, AES: zero16, - IngressMAC: keccak.NewLegacyKeccak256(), - EgressMAC: keccak.NewLegacyKeccak256(), + IngressMAC: sha3.NewLegacyKeccak256(), + EgressMAC: sha3.NewLegacyKeccak256(), }) - return &testTransport{id: id, rlpx: wrapped} + return &testTransport{rpub: rpub, rlpx: wrapped} } -func (c *testTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) { - return c.id, nil +func (c *testTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *ecdsa.PublicKey) (*ecdsa.PublicKey, error) { + return c.rpub, nil } func (c *testTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { - return &protoHandshake{ID: c.id, Name: "test"}, nil + pubkey := crypto.FromECDSAPub(c.rpub)[1:] + return &protoHandshake{ID: pubkey, Name: "test"}, nil } func (c *testTransport) close(err error) { @@ -66,7 +64,7 @@ func (c *testTransport) close(err error) { c.closeErr = err } -func startTestServer(t *testing.T, id discover.NodeID, pf func(*Peer)) *Server { +func startTestServer(t *testing.T, remoteKey *ecdsa.PublicKey, pf func(*Peer)) *Server { config := Config{ Name: "test", MaxPeers: 10, @@ -76,7 +74,7 @@ func startTestServer(t *testing.T, id discover.NodeID, pf func(*Peer)) *Server { server := &Server{ Config: config, newPeerHook: pf, - newTransport: func(fd net.Conn) transport { return newTestTransport(id, fd) }, + newTransport: func(fd net.Conn) transport { return newTestTransport(remoteKey, fd) }, } if err := server.Start(); err != nil { t.Fatalf("Could not start server: %v", err) @@ -87,14 +85,11 @@ func startTestServer(t *testing.T, id discover.NodeID, pf func(*Peer)) *Server { func TestServerListen(t *testing.T) { // start the test server connected := make(chan *Peer) - remid := randomID() + remid := &newkey().PublicKey srv := startTestServer(t, remid, func(p *Peer) { - if p.ID() != remid { + if p.ID() != enode.PubkeyToIDV4(remid) { t.Error("peer func called with wrong node id") } - if p == nil { - t.Error("peer func called with nil conn") - } connected <- p }) defer close(connected) @@ -129,7 +124,7 @@ func TestServerDial(t *testing.T) { t.Fatalf("could not setup listener: %v", err) } defer listener.Close() - accepted := make(chan net.Conn, 1) + accepted := make(chan net.Conn) go func() { conn, err := listener.Accept() if err != nil { @@ -140,23 +135,24 @@ func TestServerDial(t *testing.T) { }() // start the server - connected := make(chan *Peer) - remid := randomID() + connected := make(chan *Peer, 2) + remid := &newkey().PublicKey srv := startTestServer(t, remid, func(p *Peer) { connected <- p }) defer close(connected) defer srv.Stop() // tell the server to connect tcpAddr := listener.Addr().(*net.TCPAddr) - node := &discover.Node{ID: remid, IP: tcpAddr.IP, TCP: uint16(tcpAddr.Port)} + node := enode.NewV4(remid, tcpAddr.IP, tcpAddr.Port, 0) srv.AddPeer(node) select { case conn := <-accepted: defer conn.Close() + select { case peer := <-connected: - if peer.ID() != remid { + if peer.ID() != enode.PubkeyToIDV4(remid) { t.Errorf("peer has wrong id") } if peer.Name() != "test" { @@ -197,21 +193,6 @@ func TestServerDial(t *testing.T) { t.Error("server did not launch peer within one second") } - select { - case peer := <-connected: - if peer.ID() != remid { - t.Errorf("peer has wrong id") - } - if peer.Name() != "test" { - t.Errorf("peer has wrong name") - } - if peer.RemoteAddr().String() != conn.LocalAddr().String() { - t.Errorf("peer started with wrong conn: got %v, want %v", - peer.RemoteAddr(), conn.LocalAddr()) - } - case <-time.After(1 * time.Second): - t.Error("server did not launch peer within one second") - } case <-time.After(1 * time.Second): t.Error("server did not connect within one second") } @@ -225,7 +206,7 @@ func TestServerTaskScheduling(t *testing.T) { quit, returned = make(chan struct{}), make(chan struct{}) tc = 0 tg = taskgen{ - newFunc: func(running int, peers map[discover.NodeID]*Peer) []task { + newFunc: func(running int, peers map[enode.ID]*Peer) []task { tc++ return []task{&testTask{index: tc - 1}} }, @@ -240,12 +221,15 @@ func TestServerTaskScheduling(t *testing.T) { // The Server in this test isn't actually running // because we're only interested in what run does. + db, _ := enode.OpenDB("") srv := &Server{ - Config: Config{MaxPeers: 10}, - quit: make(chan struct{}), - ntab: fakeTable{}, - Running: true, - log: log.New(), + Config: Config{MaxPeers: 10}, + localnode: enode.NewLocalNode(db, newkey()), + nodedb: db, + quit: make(chan struct{}), + ntab: fakeTable{}, + running: true, + log: log.New(), } srv.loopWG.Add(1) go func() { @@ -286,11 +270,14 @@ func TestServerManyTasks(t *testing.T) { } var ( - srv = &Server{ - quit: make(chan struct{}), - ntab: fakeTable{}, - Running: true, - log: log.New(), + db, _ = enode.OpenDB("") + srv = &Server{ + quit: make(chan struct{}), + localnode: enode.NewLocalNode(db, newkey()), + nodedb: db, + ntab: fakeTable{}, + running: true, + log: log.New(), } done = make(chan *testTask) start, end = 0, 0 @@ -298,7 +285,7 @@ func TestServerManyTasks(t *testing.T) { defer srv.Stop() srv.loopWG.Add(1) go srv.run(taskgen{ - newFunc: func(running int, peers map[discover.NodeID]*Peer) []task { + newFunc: func(running int, peers map[enode.ID]*Peer) []task { start, end = end, end+maxActiveDialTasks+10 if end > len(alltasks) { end = len(alltasks) @@ -333,19 +320,19 @@ func TestServerManyTasks(t *testing.T) { } type taskgen struct { - newFunc func(running int, peers map[discover.NodeID]*Peer) []task + newFunc func(running int, peers map[enode.ID]*Peer) []task doneFunc func(task) } -func (tg taskgen) newTasks(running int, peers map[discover.NodeID]*Peer, now time.Time) []task { +func (tg taskgen) newTasks(running int, peers map[enode.ID]*Peer, now time.Time) []task { return tg.newFunc(running, peers) } func (tg taskgen) taskDone(t task, now time.Time) { tg.doneFunc(t) } -func (tg taskgen) addStatic(*discover.Node) { +func (tg taskgen) addStatic(*enode.Node) { } -func (tg taskgen) removeStatic(*discover.Node) { +func (tg taskgen) removeStatic(*enode.Node) { } type testTask struct { @@ -361,13 +348,14 @@ func (t *testTask) Do(srv *Server) { // just after the encryption handshake when the server is // at capacity. Trusted connections should still be accepted. func TestServerAtCap(t *testing.T) { - trustedID := randomID() + trustedNode := newkey() + trustedID := enode.PubkeyToIDV4(&trustedNode.PublicKey) srv := &Server{ Config: Config{ PrivateKey: newkey(), MaxPeers: 10, NoDial: true, - TrustedNodes: []*discover.Node{{ID: trustedID}}, + TrustedNodes: []*enode.Node{newNode(trustedID, nil)}, }, } if err := srv.Start(); err != nil { @@ -375,10 +363,11 @@ func TestServerAtCap(t *testing.T) { } defer srv.Stop() - newconn := func(id discover.NodeID) *conn { + newconn := func(id enode.ID) *conn { fd, _ := net.Pipe() - tx := newTestTransport(id, fd) - return &conn{fd: fd, transport: tx, flags: inboundConn, id: id, cont: make(chan error)} + tx := newTestTransport(&trustedNode.PublicKey, fd) + node := enode.SignNull(new(enr.Record), id) + return &conn{fd: fd, transport: tx, flags: inboundConn, node: node, cont: make(chan error)} } // Inject a few connections to fill up the peer set. @@ -404,14 +393,14 @@ func TestServerAtCap(t *testing.T) { } // Remove from trusted set and try again - srv.RemoveTrustedPeer(&discover.Node{ID: trustedID}) + srv.RemoveTrustedPeer(newNode(trustedID, nil)) c = newconn(trustedID) if err := srv.checkpoint(c, srv.posthandshake); err != DiscTooManyPeers { t.Error("wrong error for insert:", err) } // Add anotherID to trusted set and try again - srv.AddTrustedPeer(&discover.Node{ID: anotherID}) + srv.AddTrustedPeer(newNode(anotherID, nil)) c = newconn(anotherID) if err := srv.checkpoint(c, srv.posthandshake); err != nil { t.Error("unexpected error for trusted conn @posthandshake:", err) @@ -420,23 +409,19 @@ func TestServerAtCap(t *testing.T) { t.Error("Server did not set trusted flag") } } - func TestServerPeerLimits(t *testing.T) { srvkey := newkey() + clientkey := newkey() + clientnode := enode.NewV4(&clientkey.PublicKey, nil, 0, 0) - clientid := randomID() - clientnode := &discover.Node{ID: clientid} - - var tp *setupTransport = &setupTransport{ - id: clientid, - phs: &protoHandshake{ - ID: clientid, + var tp = &setupTransport{ + pubkey: &clientkey.PublicKey, + phs: protoHandshake{ + ID: crypto.FromECDSAPub(&clientkey.PublicKey)[1:], // Force "DiscUselessPeer" due to unmatching caps // Caps: []Cap{discard.cap()}, }, } - var flags connFlag = dynDialedConn - var dialDest *discover.Node = &discover.Node{ID: clientid} srv := &Server{ Config: Config{ @@ -454,6 +439,8 @@ func TestServerPeerLimits(t *testing.T) { defer srv.Stop() // Check that server is full (MaxPeers=0) + flags := dynDialedConn + dialDest := clientnode conn, _ := net.Pipe() srv.SetupConn(conn, flags, dialDest) if tp.closeErr != DiscTooManyPeers { @@ -486,80 +473,85 @@ func TestServerPeerLimits(t *testing.T) { conn.Close() } -func TestRemovePeerTrackingKeepsPrimaryOnPairDrop(t *testing.T) { +func removePeerTracking(peers map[enode.ID]*Peer, pd peerDrop, connCount int) int { + if _, exists := peers[pd.ID()]; exists { + delete(peers, pd.ID()) + connCount-- + } + return connCount +} + +func TestRemovePeerTracking(t *testing.T) { id := randomID() - primary := newPeer(&conn{id: id}, nil) - pair := newPeer(&conn{id: id}, nil) - primary.SetPairPeer(pair) + primary := newPeer(&conn{node: newNode(id, nil)}, nil) - peers := map[discover.NodeID]*Peer{id: primary} - connCount := removePeerTracking(peers, peerDrop{Peer: pair}, 2) + peers := map[enode.ID]*Peer{id: primary} + connCount := removePeerTracking(peers, peerDrop{Peer: primary}, 1) - if connCount != 1 { - t.Fatalf("unexpected connection count: got %d want %d", connCount, 1) - } - if peers[id] != primary { - t.Fatal("primary peer was removed while dropping pair peer") + if connCount != 0 { + t.Fatalf("unexpected connection count: got %d want %d", connCount, 0) } - if primary.PairPeer() != nil { - t.Fatal("primary peer still references dropped pair peer") + if _, exists := peers[id]; exists { + t.Fatal("primary peer was not removed on drop") } } func TestServerSetupConn(t *testing.T) { - id := randomID() - srvkey := newkey() - srvid := discover.PubkeyID(&srvkey.PublicKey) + var ( + clientkey, srvkey = newkey(), newkey() + clientpub = &clientkey.PublicKey + srvpub = &srvkey.PublicKey + ) tests := []struct { dontstart bool tt *setupTransport flags connFlag - dialDest *discover.Node + dialDest *enode.Node wantCloseErr error wantCalls string }{ { dontstart: true, - tt: &setupTransport{id: id}, + tt: &setupTransport{pubkey: clientpub}, wantCalls: "close,", wantCloseErr: errServerStopped, }, { - tt: &setupTransport{id: id, encHandshakeErr: errEncHandshakeError}, + tt: &setupTransport{pubkey: clientpub, encHandshakeErr: errors.New("read error")}, flags: inboundConn, wantCalls: "doEncHandshake,close,", - wantCloseErr: errEncHandshakeError, + wantCloseErr: errors.New("read error"), }, { - tt: &setupTransport{id: id}, - dialDest: &discover.Node{ID: randomID()}, + tt: &setupTransport{pubkey: clientpub}, + dialDest: enode.NewV4(&newkey().PublicKey, nil, 0, 0), flags: dynDialedConn, wantCalls: "doEncHandshake,close,", wantCloseErr: DiscUnexpectedIdentity, }, { - tt: &setupTransport{id: id, phs: &protoHandshake{ID: randomID()}}, - dialDest: &discover.Node{ID: id}, + tt: &setupTransport{pubkey: clientpub, phs: protoHandshake{ID: randomID().Bytes()}}, + dialDest: enode.NewV4(clientpub, nil, 0, 0), flags: dynDialedConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", wantCloseErr: DiscUnexpectedIdentity, }, { - tt: &setupTransport{id: id, protoHandshakeErr: errProtoHandshakeError}, - dialDest: &discover.Node{ID: id}, + tt: &setupTransport{pubkey: clientpub, protoHandshakeErr: errors.New("foo")}, + dialDest: enode.NewV4(clientpub, nil, 0, 0), flags: dynDialedConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", - wantCloseErr: errProtoHandshakeError, + wantCloseErr: errors.New("foo"), }, { - tt: &setupTransport{id: srvid, phs: &protoHandshake{ID: srvid}}, + tt: &setupTransport{pubkey: srvpub, phs: protoHandshake{ID: crypto.FromECDSAPub(srvpub)[1:]}}, flags: inboundConn, wantCalls: "doEncHandshake,close,", wantCloseErr: DiscSelf, }, { - tt: &setupTransport{id: id, phs: &protoHandshake{ID: id}}, + tt: &setupTransport{pubkey: clientpub, phs: protoHandshake{ID: crypto.FromECDSAPub(clientpub)[1:]}}, flags: inboundConn, wantCalls: "doEncHandshake,doProtoHandshake,close,", wantCloseErr: DiscUselessPeer, @@ -584,7 +576,7 @@ func TestServerSetupConn(t *testing.T) { } p1, _ := net.Pipe() srv.SetupConn(p1, test.flags, test.dialDest) - if !errors.Is(test.tt.closeErr, test.wantCloseErr) { + if !reflect.DeepEqual(test.tt.closeErr, test.wantCloseErr) { t.Errorf("test %d: close error mismatch: got %q, want %q", i, test.tt.closeErr, test.wantCloseErr) } if test.tt.calls != test.wantCalls { @@ -594,26 +586,26 @@ func TestServerSetupConn(t *testing.T) { } type setupTransport struct { - id discover.NodeID - encHandshakeErr error - - phs *protoHandshake + pubkey *ecdsa.PublicKey + encHandshakeErr error + phs protoHandshake protoHandshakeErr error calls string closeErr error } -func (c *setupTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node) (discover.NodeID, error) { +func (c *setupTransport) doEncHandshake(prv *ecdsa.PrivateKey, dialDest *ecdsa.PublicKey) (*ecdsa.PublicKey, error) { c.calls += "doEncHandshake," - return c.id, c.encHandshakeErr + return c.pubkey, c.encHandshakeErr } + func (c *setupTransport) doProtoHandshake(our *protoHandshake) (*protoHandshake, error) { c.calls += "doProtoHandshake," if c.protoHandshakeErr != nil { return nil, c.protoHandshakeErr } - return c.phs, nil + return &c.phs, nil } func (c *setupTransport) close(err error) { c.calls += "close," @@ -636,9 +628,41 @@ func newkey() *ecdsa.PrivateKey { return key } -func randomID() (id discover.NodeID) { +func randomID() (id enode.ID) { for i := range id { id[i] = byte(rand.Intn(255)) } return id } + +func TestEncHandshakeChecksRejectsAlreadyConnectedPeer(t *testing.T) { + db, _ := enode.OpenDB("") + srv := &Server{ + Config: Config{MaxPeers: 10}, + localnode: enode.NewLocalNode(db, newkey()), + } + defer db.Close() + + id := randomID() + existing := &Peer{rw: &conn{node: newNode(id, nil)}} + c := &conn{node: newNode(id, nil)} + + err := srv.encHandshakeChecks(map[enode.ID]*Peer{id: existing}, 0, c) + if err != DiscAlreadyConnected { + t.Fatalf("expected %v, got %v", DiscAlreadyConnected, err) + } +} + +func TestRemovePeerTrackingDeletesPrimaryPeer(t *testing.T) { + id := randomID() + primary := &Peer{rw: &conn{node: newNode(id, nil)}} + peers := map[enode.ID]*Peer{id: primary} + + remaining := removePeerTracking(peers, peerDrop{Peer: primary}, 1) + if remaining != 0 { + t.Fatalf("unexpected connection count: got %d want 0", remaining) + } + if peers[id] != nil { + t.Fatal("expected primary peer to be removed from tracking") + } +} diff --git a/p2p/simulations/README.md b/p2p/simulations/README.md index d1f8649eaed2..871d71b2c714 100644 --- a/p2p/simulations/README.md +++ b/p2p/simulations/README.md @@ -63,18 +63,6 @@ using the devp2p node stack rather than executing `main()`. The nodes listen for devp2p connections and WebSocket RPC clients on random localhost ports. -### DockerAdapter - -The `DockerAdapter` is similar to the `ExecAdapter` but executes `docker run` -to run the node in a Docker container using a Docker image containing the -simulation binary at `/bin/p2p-node`. - -The Docker image is built using `docker build` when the adapter is initialised, -meaning no prior setup is necessary other than having a working Docker client. - -Each node listens on the external IP of the container and the default p2p and -RPC ports (`30303` and `8546` respectively). - ## Network A simulation network is created with an ID and default service (which is used diff --git a/p2p/simulations/adapters/docker.go b/p2p/simulations/adapters/docker.go deleted file mode 100644 index dc67f1f8a279..000000000000 --- a/p2p/simulations/adapters/docker.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package adapters - -import ( - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/node" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" - "github.com/docker/docker/pkg/reexec" -) - -var ( - ErrLinuxOnly = errors.New("DockerAdapter can only be used on Linux as it uses the current binary (which must be a Linux binary)") -) - -// DockerAdapter is a NodeAdapter which runs simulation nodes inside Docker -// containers. -// -// A Docker image is built which contains the current binary at /bin/p2p-node -// which when executed runs the underlying service (see the description -// of the execP2PNode function for more details) -type DockerAdapter struct { - ExecAdapter -} - -// NewDockerAdapter builds the p2p-node Docker image containing the current -// binary and returns a DockerAdapter -func NewDockerAdapter() (*DockerAdapter, error) { - // Since Docker containers run on Linux and this adapter runs the - // current binary in the container, it must be compiled for Linux. - // - // It is reasonable to require this because the caller can just - // compile the current binary in a Docker container. - if runtime.GOOS != "linux" { - return nil, ErrLinuxOnly - } - - if err := buildDockerImage(); err != nil { - return nil, err - } - - return &DockerAdapter{ - ExecAdapter{ - nodes: make(map[discover.NodeID]*ExecNode), - }, - }, nil -} - -// Name returns the name of the adapter for logging purposes -func (d *DockerAdapter) Name() string { - return "docker-adapter" -} - -// NewNode returns a new DockerNode using the given config -func (d *DockerAdapter) NewNode(config *NodeConfig) (Node, error) { - if len(config.Lifecycles) == 0 { - return nil, errors.New("node must have at least one lifecycle") - } - for _, service := range config.Lifecycles { - if _, exists := lifecycleConstructorFuncs[service]; !exists { - return nil, fmt.Errorf("unknown node service %q", service) - } - } - - // generate the config - conf := &execNodeConfig{ - Stack: node.DefaultConfig, - Node: config, - } - conf.Stack.DataDir = "/data" - conf.Stack.WSHost = "0.0.0.0" - conf.Stack.WSOrigins = []string{"*"} - conf.Stack.WSExposeAll = true - conf.Stack.P2P.EnableMsgEvents = false - conf.Stack.P2P.NoDiscovery = true - conf.Stack.P2P.NAT = nil - conf.Stack.Logger = log.New("node.id", config.ID.String()) - - // listen on all interfaces on a given port, which we set when we - // initialise NodeConfig (usually a random port) - conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port) - - node := &DockerNode{ - ExecNode: ExecNode{ - ID: config.ID, - Config: conf, - adapter: &d.ExecAdapter, - }, - } - node.newCmd = node.dockerCommand - d.ExecAdapter.nodes[node.ID] = &node.ExecNode - return node, nil -} - -// DockerNode wraps an ExecNode but exec's the current binary in a docker -// container rather than locally -type DockerNode struct { - ExecNode -} - -// dockerCommand returns a command which exec's the binary in a Docker -// container. -// -// It uses a shell so that we can pass the _P2P_NODE_CONFIG environment -// variable to the container using the --env flag. -func (n *DockerNode) dockerCommand() *exec.Cmd { - return exec.Command( - "sh", "-c", - fmt.Sprintf( - `exec docker run --interactive --env _P2P_NODE_CONFIG="${_P2P_NODE_CONFIG}" %s p2p-node %s %s`, - dockerImage, strings.Join(n.Config.Node.Lifecycles, ","), n.ID.String(), - ), - ) -} - -// dockerImage is the name of the Docker image which gets built to run the -// simulation node -const dockerImage = "p2p-node" - -// buildDockerImage builds the Docker image which is used to run the simulation -// node in a Docker container. -// -// It adds the current binary as "p2p-node" so that it runs execP2PNode -// when executed. -func buildDockerImage() error { - // create a directory to use as the build context - dir, err := os.MkdirTemp("", "p2p-docker") - if err != nil { - return err - } - defer os.RemoveAll(dir) - - // copy the current binary into the build context - bin, err := os.Open(reexec.Self()) - if err != nil { - return err - } - defer bin.Close() - dst, err := os.OpenFile(filepath.Join(dir, "self.bin"), os.O_WRONLY|os.O_CREATE, 0755) - if err != nil { - return err - } - defer dst.Close() - if _, err := io.Copy(dst, bin); err != nil { - return err - } - - // create the Dockerfile - dockerfile := []byte(` -FROM ubuntu:16.04 -RUN mkdir /data -ADD self.bin /bin/p2p-node - `) - if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), dockerfile, 0644); err != nil { - return err - } - - // run 'docker build' - cmd := exec.Command("docker", "build", "-t", dockerImage, dir) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("error building docker image: %s", err) - } - - return nil -} diff --git a/p2p/simulations/adapters/exec.go b/p2p/simulations/adapters/exec.go index 8b342e725603..7071cf7a5171 100644 --- a/p2p/simulations/adapters/exec.go +++ b/p2p/simulations/adapters/exec.go @@ -17,15 +17,14 @@ package adapters import ( - "bufio" + "bytes" "context" - "crypto/ecdsa" "encoding/json" "errors" "fmt" "io" - "log/slog" "net" + "net/http" "os" "os/exec" "os/signal" @@ -38,24 +37,26 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/rpc" "github.com/docker/docker/pkg/reexec" "github.com/gorilla/websocket" ) -// ExecAdapter is a NodeAdapter which runs simulation nodes by executing the -// current binary as a child process. -// -// An init hook is used so that the child process executes the node services -// (rather than whatever the main() function would normally do), see the -// execP2PNode function for more information. +func init() { + // Register a reexec function to start a simulation node when the current binary is + // executed as "p2p-node" (rather than whatever the main() function would normally do). + reexec.Register("p2p-node", execP2PNode) +} + +// ExecAdapter is a NodeAdapter which runs simulation nodes by executing the current binary +// as a child process. type ExecAdapter struct { // BaseDir is the directory under which the data directories for each // simulation node are created. BaseDir string - nodes map[discover.NodeID]*ExecNode + nodes map[enode.ID]*ExecNode } // NewExecAdapter returns an ExecAdapter which stores node data in @@ -63,7 +64,7 @@ type ExecAdapter struct { func NewExecAdapter(baseDir string) *ExecAdapter { return &ExecAdapter{ BaseDir: baseDir, - nodes: make(map[discover.NodeID]*ExecNode), + nodes: make(map[enode.ID]*ExecNode), } } @@ -90,21 +91,33 @@ func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) { return nil, fmt.Errorf("error creating node directory: %s", err) } + err := config.initDummyEnode() + if err != nil { + return nil, err + } + // generate the config conf := &execNodeConfig{ Stack: node.DefaultConfig, Node: config, } - conf.Stack.DataDir = filepath.Join(dir, "data") + if config.DataDir != "" { + conf.Stack.DataDir = config.DataDir + } else { + conf.Stack.DataDir = filepath.Join(dir, "data") + } + + // these parameters are crucial for execadapter node to run correctly conf.Stack.WSHost = "127.0.0.1" conf.Stack.WSPort = 0 conf.Stack.WSOrigins = []string{"*"} conf.Stack.WSExposeAll = true - conf.Stack.P2P.EnableMsgEvents = false + conf.Stack.P2P.EnableMsgEvents = config.EnableMsgEvents conf.Stack.P2P.NoDiscovery = true conf.Stack.P2P.NAT = nil + conf.Stack.NoUSB = true - // listen on a localhost port, which we set when we + // Listen on a localhost port, which we set when we // initialise NodeConfig (usually a random port) conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port) @@ -122,7 +135,7 @@ func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) { // ExecNode starts a simulation node by exec'ing the current binary and // running the configured services type ExecNode struct { - ID discover.NodeID + ID enode.ID Dir string Config *execNodeConfig Cmd *exec.Cmd @@ -132,7 +145,6 @@ type ExecNode struct { client *rpc.Client wsAddr string newCmd func() *exec.Cmd - key *ecdsa.PrivateKey } // Addr returns the node's enode URL @@ -157,7 +169,6 @@ func (n *ExecNode) Start(snapshots map[string][]byte) (err error) { } defer func() { if err != nil { - log.Error("node failed to start", "err", err) n.Stop() } }() @@ -174,59 +185,78 @@ func (n *ExecNode) Start(snapshots map[string][]byte) (err error) { return fmt.Errorf("error generating node config: %s", err) } - // use a pipe for stderr so we can both copy the node's stderr to - // os.Stderr and read the WebSocket address from the logs - stderrR, stderrW := io.Pipe() - stderr := io.MultiWriter(os.Stderr, stderrW) + // start the one-shot server that waits for startup information + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + statusURL, statusC := n.waitForStartupJSON(ctx) // start the node cmd := n.newCmd() cmd.Stdout = os.Stdout - cmd.Stderr = stderr - cmd.Env = append(os.Environ(), envNodeConfig+"="+string(confData)) + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), + envStatusURL+"="+statusURL, + envNodeConfig+"="+string(confData), + ) if err := cmd.Start(); err != nil { return fmt.Errorf("error starting node: %s", err) } n.Cmd = cmd - // read the WebSocket address from the stderr logs - var wsAddr string - wsAddrC := make(chan string) - go func() { - s := bufio.NewScanner(stderrR) - for s.Scan() { - if strings.Contains(s.Text(), "WebSocket endpoint opened") { - wsAddrC <- wsAddrPattern.FindString(s.Text()) - } - } - }() - select { - case wsAddr = <-wsAddrC: - if wsAddr == "" { - return errors.New("failed to read WebSocket address from stderr") - } - case <-time.After(10 * time.Second): - return errors.New("timed out waiting for WebSocket address on stderr") + // Wait for the node to start. + status := <-statusC + if status.Err != "" { + return errors.New(status.Err) } - - // create the RPC client and load the node info - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - client, err := rpc.DialWebsocket(ctx, wsAddr, "") + client, err := rpc.DialWebsocket(ctx, status.WSEndpoint, "") if err != nil { - return fmt.Errorf("error dialing rpc websocket: %s", err) + return fmt.Errorf("can't connect to RPC server: %v", err) } - var info p2p.NodeInfo - if err := client.CallContext(ctx, &info, "admin_nodeInfo"); err != nil { - return fmt.Errorf("error getting node info: %s", err) - } - n.client = client - n.wsAddr = wsAddr - n.Info = &info + // Node ready :) + n.client = client + n.wsAddr = status.WSEndpoint + n.Info = status.NodeInfo return nil } +// waitForStartupJSON runs a one-shot HTTP server to receive a startup report. +func (n *ExecNode) waitForStartupJSON(ctx context.Context) (string, chan nodeStartupJSON) { + var ( + ch = make(chan nodeStartupJSON, 1) + quitOnce sync.Once + srv http.Server + ) + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + ch <- nodeStartupJSON{Err: err.Error()} + return "", ch + } + quit := func(status nodeStartupJSON) { + quitOnce.Do(func() { + l.Close() + ch <- status + }) + } + srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var status nodeStartupJSON + if err := json.NewDecoder(r.Body).Decode(&status); err != nil { + status.Err = fmt.Sprintf("can't decode startup report: %v", err) + } + quit(status) + }) + // Run the HTTP server, but don't wait forever and shut it down + // if the context is canceled. + go srv.Serve(l) + go func() { + <-ctx.Done() + quit(nodeStartupJSON{Err: "didn't get startup report"}) + }() + + url := "http://" + l.Addr().String() + return url, ch +} + // execCommand returns a command which runs the node locally by exec'ing // the current binary but setting argv[0] to "p2p-node" so that the child // runs execP2PNode @@ -288,14 +318,16 @@ func (n *ExecNode) ServeRPC(clientConn *websocket.Conn) error { return err } var wg sync.WaitGroup - wg.Go(func() { wsCopy(conn, clientConn) }) - wg.Go(func() { wsCopy(clientConn, conn) }) + wg.Add(2) + go wsCopy(&wg, conn, clientConn) + go wsCopy(&wg, clientConn, conn) wg.Wait() conn.Close() return nil } -func wsCopy(src, dst *websocket.Conn) { +func wsCopy(wg *sync.WaitGroup, src, dst *websocket.Conn) { + defer wg.Done() for { msgType, r, err := src.NextReader() if err != nil { @@ -321,12 +353,6 @@ func (n *ExecNode) Snapshots() (map[string][]byte, error) { return snapshots, n.client.Call(&snapshots, "simulation_snapshot") } -func init() { - // register a reexec function to start a devp2p node when the current - // binary is executed as "p2p-node" - reexec.Register("p2p-node", execP2PNode) -} - // execNodeConfig is used to serialize the node configuration so it can be // passed to the child process as a JSON encoded environment variable type execNodeConfig struct { @@ -336,88 +362,73 @@ type execNodeConfig struct { PeerAddrs map[string]string `json:"peer_addrs,omitempty"` } -// ExternalIP gets an external IP address so that Enode URL is usable -func ExternalIP() net.IP { - addrs, err := net.InterfaceAddrs() - if err != nil { - log.Crit("error getting IP address", "err", err) - } - for _, addr := range addrs { - if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() { - return ip.IP - } +// execP2PNode starts a simulation node when the current binary is executed with +// argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2] +// and the node config from an environment variable. +func execP2PNode() { + statusURL := os.Getenv(envStatusURL) + if statusURL == "" { + log.Crit("missing " + envStatusURL) } - log.Warn("unable to determine explicit IP address, falling back to loopback") - return net.IP{127, 0, 0, 1} -} -func initLogging() { - // Initialize the logging by default first. - var innerHandler slog.Handler - innerHandler = slog.NewTextHandler(os.Stderr, nil) - glogger := log.NewGlogHandler(innerHandler) - glogger.Verbosity(log.LevelInfo) - log.SetDefault(log.NewLogger(glogger)) - - confEnv := os.Getenv(envNodeConfig) - if confEnv == "" { - return - } - var conf execNodeConfig - if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { - return + // Start the node and gather startup report. + var status nodeStartupJSON + stack, stackErr := startExecNodeStack() + if stackErr != nil { + status.Err = stackErr.Error() + } else { + status.WSEndpoint = "ws://" + stack.WSEndpoint() + status.NodeInfo = stack.Server().NodeInfo() } - var writer = os.Stderr - if conf.Node.LogFile != "" { - logWriter, err := os.Create(conf.Node.LogFile) - if err != nil { - return - } - writer = logWriter + + // Send status to the host. + statusJSON, _ := json.Marshal(status) + if _, err := http.Post(statusURL, "application/json", bytes.NewReader(statusJSON)); err != nil { + log.Crit("Can't post startup info", "url", statusURL, "err", err) } - var verbosity = log.LevelInfo - if conf.Node.LogVerbosity <= log.LevelTrace && conf.Node.LogVerbosity >= log.LevelCrit { - verbosity = log.FromLegacyLevel(int(conf.Node.LogVerbosity)) + if stackErr != nil { + os.Exit(1) } - // Reinitialize the logger - innerHandler = log.NewTerminalHandler(writer, true) - glogger = log.NewGlogHandler(innerHandler) - glogger.Verbosity(verbosity) - log.SetDefault(log.NewLogger(glogger)) -} -// execP2PNode starts a simulation node when the current binary is executed with -// argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2] -// and the node config from an environment variable. -func execP2PNode() { - initLogging() + // Stop the stack if we get a SIGTERM signal. + go func() { + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGTERM) + defer signal.Stop(sigc) + <-sigc + log.Info("Received SIGTERM, shutting down...") + stack.Close() + }() + stack.Wait() // Wait for the stack to exit. +} +func startExecNodeStack() (*node.Node, error) { // read the services from argv serviceNames := strings.Split(os.Args[1], ",") // decode the config confEnv := os.Getenv(envNodeConfig) if confEnv == "" { - log.Crit("missing " + envNodeConfig) + return nil, fmt.Errorf("missing " + envNodeConfig) } var conf execNodeConfig if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { - log.Crit("error decoding "+envNodeConfig, "err", err) + return nil, fmt.Errorf("error decoding %s: %v", envNodeConfig, err) } - conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey - conf.Stack.Logger = log.New("node.id", conf.Node.ID.String()) - if strings.HasPrefix(conf.Stack.P2P.ListenAddr, ":") { - conf.Stack.P2P.ListenAddr = ExternalIP().String() + conf.Stack.P2P.ListenAddr - } - if conf.Stack.WSHost == "0.0.0.0" { - conf.Stack.WSHost = ExternalIP().String() + // create enode record + nodeTcpConn, _ := net.ResolveTCPAddr("tcp", conf.Stack.P2P.ListenAddr) + if nodeTcpConn.IP == nil { + nodeTcpConn.IP = net.IPv4(127, 0, 0, 1) } + conf.Node.initEnode(nodeTcpConn.IP, nodeTcpConn.Port, nodeTcpConn.Port) + conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey + conf.Stack.Logger = log.New("node.id", conf.Node.ID.String()) // initialize the devp2p stack stack, err := node.New(&conf.Stack) if err != nil { - log.Crit("error creating node stack", "err", err) + return nil, fmt.Errorf("error creating node stack: %v", err) } // Register the services, collecting them into a map so they can @@ -426,7 +437,7 @@ func execP2PNode() { for _, name := range serviceNames { lifecycleFunc, exists := lifecycleConstructorFuncs[name] if !exists { - log.Crit("unknown node service", "name", name) + return nil, fmt.Errorf("unknown node service %q", err) } ctx := &ServiceContext{ RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs}, @@ -437,7 +448,7 @@ func execP2PNode() { } service, err := lifecycleFunc(ctx, stack) if err != nil { - log.Crit("error starting service", "name", name, "err", err) + return nil, err } services[name] = service stack.RegisterLifecycle(service) @@ -446,31 +457,28 @@ func execP2PNode() { // Add the snapshot API. stack.RegisterAPIs([]rpc.API{{ Namespace: "simulation", + Version: "1.0", Service: SnapshotAPI{services}, }}) - if err := stack.Start(); err != nil { - log.Crit("error stating node stack", "err", err) + if err = stack.Start(); err != nil { + err = fmt.Errorf("error starting stack: %v", err) } - - // stop the stack if we get a SIGTERM signal - go func() { - sigc := make(chan os.Signal, 1) - signal.Notify(sigc, syscall.SIGTERM) - defer signal.Stop(sigc) - <-sigc - log.Info("Received SIGTERM, shutting down...") - stack.Close() - }() - - // wait for the stack to exit - stack.Wait() + return stack, err } const ( + envStatusURL = "_P2P_STATUS_URL" envNodeConfig = "_P2P_NODE_CONFIG" ) +// nodeStartupJSON is sent to the simulation host after startup. +type nodeStartupJSON struct { + Err string + WSEndpoint string + NodeInfo *p2p.NodeInfo +} + // SnapshotAPI provides an RPC method to create snapshots of services type SnapshotAPI struct { services map[string]node.Lifecycle @@ -498,7 +506,7 @@ type wsRPCDialer struct { // DialRPC implements the RPCDialer interface by creating a WebSocket RPC // client of the given node -func (w *wsRPCDialer) DialRPC(id discover.NodeID) (*rpc.Client, error) { +func (w *wsRPCDialer) DialRPC(id enode.ID) (*rpc.Client, error) { addr, ok := w.addrs[id.String()] if !ok { return nil, fmt.Errorf("unknown node: %s", id) diff --git a/p2p/simulations/adapters/inproc.go b/p2p/simulations/adapters/inproc.go index ecbfc6e191fd..aa0de196b711 100644 --- a/p2p/simulations/adapters/inproc.go +++ b/p2p/simulations/adapters/inproc.go @@ -27,7 +27,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/pipes" "github.com/XinFinOrg/XDPoSChain/rpc" "github.com/gorilla/websocket" @@ -38,7 +38,7 @@ import ( type SimAdapter struct { pipe func() (net.Conn, net.Conn, error) mtx sync.RWMutex - nodes map[discover.NodeID]*SimNode + nodes map[enode.ID]*SimNode lifecycles LifecycleConstructors } @@ -49,32 +49,29 @@ type SimAdapter struct { func NewSimAdapter(services LifecycleConstructors) *SimAdapter { return &SimAdapter{ pipe: pipes.NetPipe, - nodes: make(map[discover.NodeID]*SimNode), - lifecycles: services, - } -} - -func NewTCPAdapter(services LifecycleConstructors) *SimAdapter { - return &SimAdapter{ - pipe: pipes.TCPPipe, - nodes: make(map[discover.NodeID]*SimNode), + nodes: make(map[enode.ID]*SimNode), lifecycles: services, } } // Name returns the name of the adapter for logging purposes -func (sa *SimAdapter) Name() string { +func (s *SimAdapter) Name() string { return "sim-adapter" } // NewNode returns a new SimNode using the given config -func (sa *SimAdapter) NewNode(config *NodeConfig) (Node, error) { - sa.mtx.Lock() - defer sa.mtx.Unlock() +func (s *SimAdapter) NewNode(config *NodeConfig) (Node, error) { + s.mtx.Lock() + defer s.mtx.Unlock() - // check a node with the ID doesn't already exist id := config.ID - if _, exists := sa.nodes[id]; exists { + // verify that the node has a private key in the config + if config.PrivateKey == nil { + return nil, fmt.Errorf("node is missing private key: %s", id) + } + + // check a node with the ID doesn't already exist + if _, exists := s.nodes[id]; exists { return nil, fmt.Errorf("node already exists: %s", id) } @@ -83,68 +80,69 @@ func (sa *SimAdapter) NewNode(config *NodeConfig) (Node, error) { return nil, errors.New("node must have at least one service") } for _, service := range config.Lifecycles { - if _, exists := sa.lifecycles[service]; !exists { + if _, exists := s.lifecycles[service]; !exists { return nil, fmt.Errorf("unknown node service %q", service) } } + err := config.initDummyEnode() + if err != nil { + return nil, err + } + n, err := node.New(&node.Config{ P2P: p2p.Config{ PrivateKey: config.PrivateKey, MaxPeers: math.MaxInt32, NoDiscovery: true, - Dialer: sa, + Dialer: s, EnableMsgEvents: config.EnableMsgEvents, }, - Logger: log.New("node.id", id), + NoUSB: true, + Logger: log.New("node.id", id.String()), }) if err != nil { return nil, err } simNode := &SimNode{ - ID: id, - config: config, - node: n, - adapter: sa, - running: make(map[string]node.Lifecycle), - connected: make(map[discover.NodeID]bool), + ID: id, + config: config, + node: n, + adapter: s, + running: make(map[string]node.Lifecycle), } - sa.nodes[id] = simNode + s.nodes[id] = simNode return simNode, nil } // Dial implements the p2p.NodeDialer interface by connecting to the node using // an in-memory net.Pipe -func (sa *SimAdapter) Dial(dest *discover.Node) (conn net.Conn, err error) { - node, ok := sa.GetNode(dest.ID) +func (s *SimAdapter) Dial(dest *enode.Node) (conn net.Conn, err error) { + node, ok := s.GetNode(dest.ID()) if !ok { - return nil, fmt.Errorf("unknown node: %s", dest.ID) - } - if node.connected[dest.ID] { - return nil, fmt.Errorf("dialed node: %s", dest.ID) + return nil, fmt.Errorf("unknown node: %s", dest.ID()) } srv := node.Server() if srv == nil { - return nil, fmt.Errorf("node not running: %s", dest.ID) + return nil, fmt.Errorf("node not running: %s", dest.ID()) } // SimAdapter.pipe is net.Pipe (NewSimAdapter) - pipe1, pipe2, err := sa.pipe() + pipe1, pipe2, err := s.pipe() if err != nil { return nil, err } // this is simulated 'listening' - // asynchronously call the dialed destintion node's p2p server + // asynchronously call the dialed destination node's p2p server // to set up connection on the 'listening' side go srv.SetupConn(pipe1, 0, nil) - node.connected[dest.ID] = true return pipe2, nil } // DialRPC implements the RPCDialer interface by creating an in-memory RPC // client of the given node -func (sa *SimAdapter) DialRPC(id discover.NodeID) (*rpc.Client, error) { - node, ok := sa.GetNode(id) +func (s *SimAdapter) DialRPC(id enode.ID) (*rpc.Client, error) { + node, ok := s.GetNode(id) if !ok { return nil, fmt.Errorf("unknown node: %s", id) } @@ -152,10 +150,10 @@ func (sa *SimAdapter) DialRPC(id discover.NodeID) (*rpc.Client, error) { } // GetNode returns the node with the given ID if it exists -func (sa *SimAdapter) GetNode(id discover.NodeID) (*SimNode, bool) { - sa.mtx.RLock() - defer sa.mtx.RUnlock() - node, ok := sa.nodes[id] +func (s *SimAdapter) GetNode(id enode.ID) (*SimNode, bool) { + s.mtx.RLock() + defer s.mtx.RUnlock() + node, ok := s.nodes[id] return node, ok } @@ -164,14 +162,13 @@ func (sa *SimAdapter) GetNode(id discover.NodeID) (*SimNode, bool) { // pipe type SimNode struct { lock sync.RWMutex - ID discover.NodeID + ID enode.ID config *NodeConfig adapter *SimAdapter node *node.Node running map[string]node.Lifecycle client *rpc.Client registerOnce sync.Once - connected map[discover.NodeID]bool } // Close closes the underlaying node.Node to release @@ -185,9 +182,9 @@ func (sn *SimNode) Addr() []byte { return []byte(sn.Node().String()) } -// Node returns a discover.Node representing the SimNode -func (sn *SimNode) Node() *discover.Node { - return discover.NewNode(sn.ID, net.IP{127, 0, 0, 1}, 30303, 30303) +// Node returns a node descriptor representing the SimNode +func (sn *SimNode) Node() *enode.Node { + return sn.config.Node() } // Client returns an rpc.Client which can be used to communicate with the @@ -202,7 +199,7 @@ func (sn *SimNode) Client() (*rpc.Client, error) { } // ServeRPC serves RPC requests over the given connection by creating an -// in-memory client to the node's RPC server +// in-memory client to the node's RPC server. func (sn *SimNode) ServeRPC(conn *websocket.Conn) error { handler, err := sn.node.RPCHandler() if err != nil { @@ -278,7 +275,6 @@ func (sn *SimNode) Start(snapshots map[string][]byte) error { // create an in-process RPC client client := sn.node.Attach() - sn.lock.Lock() sn.client = client sn.lock.Unlock() @@ -297,6 +293,13 @@ func (sn *SimNode) Stop() error { return sn.node.Close() } +// Service returns a running service by name +func (sn *SimNode) Service(name string) node.Lifecycle { + sn.lock.RLock() + defer sn.lock.RUnlock() + return sn.running[name] +} + // Services returns a copy of the underlying services func (sn *SimNode) Services() []node.Lifecycle { sn.lock.RLock() @@ -308,6 +311,17 @@ func (sn *SimNode) Services() []node.Lifecycle { return services } +// ServiceMap returns a map by names of the underlying services +func (sn *SimNode) ServiceMap() map[string]node.Lifecycle { + sn.lock.RLock() + defer sn.lock.RUnlock() + services := make(map[string]node.Lifecycle, len(sn.running)) + for name, service := range sn.running { + services[name] = service + } + return services +} + // Server returns the underlying p2p.Server func (sn *SimNode) Server() *p2p.Server { return sn.node.Server() @@ -326,7 +340,7 @@ func (sn *SimNode) SubscribeEvents(ch chan *p2p.PeerEvent) event.Subscription { // NodeInfo returns information about the node func (sn *SimNode) NodeInfo() *p2p.NodeInfo { server := sn.Server() - if server.Running == false { + if server == nil { return &p2p.NodeInfo{ ID: sn.ID.String(), Enode: sn.Node().String(), diff --git a/p2p/simulations/adapters/inproc_test.go b/p2p/simulations/adapters/inproc_test.go index bc0dc32b8f19..0a3284f4778c 100644 --- a/p2p/simulations/adapters/inproc_test.go +++ b/p2p/simulations/adapters/inproc_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The go-ethereum Authors +// Copyright 2018 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify @@ -20,8 +20,8 @@ import ( "bytes" "encoding/binary" "fmt" + "sync" "testing" - "time" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/pipes" ) @@ -32,42 +32,26 @@ func TestTCPPipe(t *testing.T) { t.Fatal(err) } - done := make(chan struct{}) - - go func() { - msgs := 50 - size := 1024 - for i := 0; i < msgs; i++ { - msg := make([]byte, size) - _ = binary.PutUvarint(msg, uint64(i)) - - _, err := c1.Write(msg) - if err != nil { - t.Fatal(err) - } + msgs := 50 + size := 1024 + for i := 0; i < msgs; i++ { + msg := make([]byte, size) + binary.PutUvarint(msg, uint64(i)) + if _, err := c1.Write(msg); err != nil { + t.Fatal(err) } + } - for i := 0; i < msgs; i++ { - msg := make([]byte, size) - _ = binary.PutUvarint(msg, uint64(i)) - - out := make([]byte, size) - _, err := c2.Read(out) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(msg, out) { - t.Fatalf("expected %#v, got %#v", msg, out) - } + for i := 0; i < msgs; i++ { + msg := make([]byte, size) + binary.PutUvarint(msg, uint64(i)) + out := make([]byte, size) + if _, err := c2.Read(out); err != nil { + t.Fatal(err) + } + if !bytes.Equal(msg, out) { + t.Fatalf("expected %#v, got %#v", msg, out) } - done <- struct{}{} - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("test timeout") } } @@ -77,60 +61,41 @@ func TestTCPPipeBidirections(t *testing.T) { t.Fatal(err) } - done := make(chan struct{}) - - go func() { - msgs := 50 - size := 7 - for i := 0; i < msgs; i++ { - msg := []byte(fmt.Sprintf("ping %02d", i)) - - _, err := c1.Write(msg) - if err != nil { - t.Fatal(err) - } + msgs := 50 + size := 7 + for i := 0; i < msgs; i++ { + msg := []byte(fmt.Sprintf("ping %02d", i)) + if _, err := c1.Write(msg); err != nil { + t.Fatal(err) } + } - for i := 0; i < msgs; i++ { - expected := []byte(fmt.Sprintf("ping %02d", i)) - - out := make([]byte, size) - _, err := c2.Read(out) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(expected, out) { - t.Fatalf("expected %#v, got %#v", out, expected) - } else { - msg := []byte(fmt.Sprintf("pong %02d", i)) - _, err := c2.Write(msg) - if err != nil { - t.Fatal(err) - } - } + for i := 0; i < msgs; i++ { + expected := []byte(fmt.Sprintf("ping %02d", i)) + out := make([]byte, size) + if _, err := c2.Read(out); err != nil { + t.Fatal(err) } - for i := 0; i < msgs; i++ { - expected := []byte(fmt.Sprintf("pong %02d", i)) - - out := make([]byte, size) - _, err := c1.Read(out) - if err != nil { + if !bytes.Equal(expected, out) { + t.Fatalf("expected %#v, got %#v", out, expected) + } else { + msg := []byte(fmt.Sprintf("pong %02d", i)) + if _, err := c2.Write(msg); err != nil { t.Fatal(err) } - - if !bytes.Equal(expected, out) { - t.Fatalf("expected %#v, got %#v", out, expected) - } } - done <- struct{}{} - }() + } - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("test timeout") + for i := 0; i < msgs; i++ { + expected := []byte(fmt.Sprintf("pong %02d", i)) + out := make([]byte, size) + if _, err := c1.Read(out); err != nil { + t.Fatal(err) + } + if !bytes.Equal(expected, out) { + t.Fatalf("expected %#v, got %#v", out, expected) + } } } @@ -140,46 +105,35 @@ func TestNetPipe(t *testing.T) { t.Fatal(err) } - done := make(chan struct{}) + msgs := 50 + size := 1024 + var wg sync.WaitGroup + defer wg.Wait() + // netPipe is blocking, so writes are emitted asynchronously + wg.Add(1) go func() { - msgs := 50 - size := 1024 - // netPipe is blocking, so writes are emitted asynchronously - go func() { - for i := 0; i < msgs; i++ { - msg := make([]byte, size) - _ = binary.PutUvarint(msg, uint64(i)) - - _, err := c1.Write(msg) - if err != nil { - t.Fatal(err) - } - } - }() + defer wg.Done() for i := 0; i < msgs; i++ { msg := make([]byte, size) - _ = binary.PutUvarint(msg, uint64(i)) - - out := make([]byte, size) - _, err := c2.Read(out) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(msg, out) { - t.Fatalf("expected %#v, got %#v", msg, out) + binary.PutUvarint(msg, uint64(i)) + if _, err := c1.Write(msg); err != nil { + t.Error(err) } } - - done <- struct{}{} }() - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("test timeout") + for i := 0; i < msgs; i++ { + msg := make([]byte, size) + binary.PutUvarint(msg, uint64(i)) + out := make([]byte, size) + if _, err := c2.Read(out); err != nil { + t.Error(err) + } + if !bytes.Equal(msg, out) { + t.Errorf("expected %#v, got %#v", msg, out) + } } } @@ -189,71 +143,60 @@ func TestNetPipeBidirections(t *testing.T) { t.Fatal(err) } - done := make(chan struct{}) + msgs := 1000 + size := 8 + pingTemplate := "ping %03d" + pongTemplate := "pong %03d" + var wg sync.WaitGroup + defer wg.Wait() + // netPipe is blocking, so writes are emitted asynchronously + wg.Add(1) go func() { - msgs := 1000 - size := 8 - pingTemplate := "ping %03d" - pongTemplate := "pong %03d" - - // netPipe is blocking, so writes are emitted asynchronously - go func() { - for i := 0; i < msgs; i++ { - msg := []byte(fmt.Sprintf(pingTemplate, i)) + defer wg.Done() - _, err := c1.Write(msg) - if err != nil { - t.Fatal(err) - } - } - }() - - // netPipe is blocking, so reads for pong are emitted asynchronously - go func() { - for i := 0; i < msgs; i++ { - expected := []byte(fmt.Sprintf(pongTemplate, i)) - - out := make([]byte, size) - _, err := c1.Read(out) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(expected, out) { - t.Fatalf("expected %#v, got %#v", expected, out) - } + for i := 0; i < msgs; i++ { + msg := []byte(fmt.Sprintf(pingTemplate, i)) + if _, err := c1.Write(msg); err != nil { + t.Error(err) } + } + }() - done <- struct{}{} - }() + // netPipe is blocking, so reads for pong are emitted asynchronously + wg.Add(1) + go func() { + defer wg.Done() - // expect to read pings, and respond with pongs to the alternate connection for i := 0; i < msgs; i++ { - expected := []byte(fmt.Sprintf(pingTemplate, i)) - + expected := []byte(fmt.Sprintf(pongTemplate, i)) out := make([]byte, size) - _, err := c2.Read(out) - if err != nil { - t.Fatal(err) + if _, err := c1.Read(out); err != nil { + t.Error(err) } - if !bytes.Equal(expected, out) { - t.Fatalf("expected %#v, got %#v", expected, out) - } else { - msg := []byte(fmt.Sprintf(pongTemplate, i)) - - _, err := c2.Write(msg) - if err != nil { - t.Fatal(err) - } + t.Errorf("expected %#v, got %#v", expected, out) } } }() - select { - case <-done: - case <-time.After(5 * time.Second): - t.Fatal("test timeout") + // expect to read pings, and respond with pongs to the alternate connection + for i := 0; i < msgs; i++ { + expected := []byte(fmt.Sprintf(pingTemplate, i)) + + out := make([]byte, size) + _, err := c2.Read(out) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(expected, out) { + t.Errorf("expected %#v, got %#v", expected, out) + } else { + msg := []byte(fmt.Sprintf(pongTemplate, i)) + if _, err := c2.Write(msg); err != nil { + t.Fatal(err) + } + } } } diff --git a/p2p/simulations/adapters/state.go b/p2p/simulations/adapters/state.go deleted file mode 100644 index 78dfb11f950b..000000000000 --- a/p2p/simulations/adapters/state.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package adapters - -type SimStateStore struct { - m map[string][]byte -} - -func (st *SimStateStore) Load(s string) ([]byte, error) { - return st.m[s], nil -} - -func (st *SimStateStore) Save(s string, data []byte) error { - st.m[s] = data - return nil -} - -func NewSimStateStore() *SimStateStore { - return &SimStateStore{ - make(map[string][]byte), - } -} diff --git a/p2p/simulations/adapters/types.go b/p2p/simulations/adapters/types.go index 37c7c51b8b47..dc670edf3cce 100644 --- a/p2p/simulations/adapters/types.go +++ b/p2p/simulations/adapters/types.go @@ -21,15 +21,16 @@ import ( "encoding/hex" "encoding/json" "fmt" - "log/slog" "net" "os" "strconv" "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" "github.com/XinFinOrg/XDPoSChain/rpc" "github.com/docker/docker/pkg/reexec" "github.com/gorilla/websocket" @@ -79,7 +80,7 @@ type NodeAdapter interface { type NodeConfig struct { // ID is the node's ID which is used to identify the node in the // simulation network - ID discover.NodeID + ID enode.ID // PrivateKey is the node's private key which is used by the devp2p // stack to encrypt communications @@ -91,27 +92,30 @@ type NodeConfig struct { // Name is a human friendly name for the node like "node01" Name string + // Use an existing database instead of a temporary one if non-empty + DataDir string + // Lifecycles are the names of the service lifecycles which should be run when // starting the node (for SimNodes it should be the names of service lifecycles // contained in SimAdapter.lifecycles, for other nodes it should be // service lifecycles registered by calling the RegisterLifecycle function) Lifecycles []string - // function to sanction or prevent suggesting a peer - Reachable func(id discover.NodeID) bool + // Properties are the names of the properties this node should hold + // within running services (e.g. "bootnode", "lightnode" or any custom values) + // These values need to be checked and acted upon by node Services + Properties []string - Port uint16 + // Enode + node *enode.Node - // LogFile is the log file name of the p2p node at runtime. - // - // The default value is empty so that the default log writer - // is the system standard output. - LogFile string + // ENR Record with entries to overwrite + Record enr.Record + + // function to sanction or prevent suggesting a peer + Reachable func(id enode.ID) bool - // LogVerbosity is the log verbosity of the p2p node at runtime. - // - // The default verbosity is INFO. - LogVerbosity slog.Level + Port uint16 } // nodeConfigJSON is used to encode and decode NodeConfig as JSON by encoding @@ -121,10 +125,9 @@ type nodeConfigJSON struct { PrivateKey string `json:"private_key"` Name string `json:"name"` Services []string `json:"services"` + Properties []string `json:"properties"` EnableMsgEvents bool `json:"enable_msg_events"` Port uint16 `json:"port"` - LogFile string `json:"logfile"` - LogVerbosity int `json:"log_verbosity"` } // MarshalJSON implements the json.Marshaler interface by encoding the config @@ -134,10 +137,9 @@ func (n *NodeConfig) MarshalJSON() ([]byte, error) { ID: n.ID.String(), Name: n.Name, Services: n.Lifecycles, + Properties: n.Properties, Port: n.Port, EnableMsgEvents: n.EnableMsgEvents, - LogFile: n.LogFile, - LogVerbosity: int(n.LogVerbosity), } if n.PrivateKey != nil { confJSON.PrivateKey = hex.EncodeToString(crypto.FromECDSA(n.PrivateKey)) @@ -154,11 +156,9 @@ func (n *NodeConfig) UnmarshalJSON(data []byte) error { } if confJSON.ID != "" { - nodeID, err := discover.HexID(confJSON.ID) - if err != nil { + if err := n.ID.UnmarshalText([]byte(confJSON.ID)); err != nil { return err } - n.ID = nodeID } if confJSON.PrivateKey != "" { @@ -175,31 +175,36 @@ func (n *NodeConfig) UnmarshalJSON(data []byte) error { n.Name = confJSON.Name n.Lifecycles = confJSON.Services + n.Properties = confJSON.Properties n.Port = confJSON.Port n.EnableMsgEvents = confJSON.EnableMsgEvents - n.LogFile = confJSON.LogFile - n.LogVerbosity = slog.Level(confJSON.LogVerbosity) return nil } +// Node returns the node descriptor represented by the config. +func (n *NodeConfig) Node() *enode.Node { + return n.node +} + // RandomNodeConfig returns node configuration with a randomly generated ID and // PrivateKey func RandomNodeConfig() *NodeConfig { - key, err := crypto.GenerateKey() + prvkey, err := crypto.GenerateKey() if err != nil { panic("unable to generate key") } - id := discover.PubkeyID(&key.PublicKey) port, err := assignTCPPort() if err != nil { panic("unable to assign tcp port") } + + enodId := enode.PubkeyToIDV4(&prvkey.PublicKey) return &NodeConfig{ - ID: id, - Name: fmt.Sprintf("node_%s", id.String()), - PrivateKey: key, + PrivateKey: prvkey, + ID: enodId, + Name: fmt.Sprintf("node_%s", enodId.String()), Port: port, EnableMsgEvents: true, } @@ -235,7 +240,7 @@ type ServiceContext struct { // other nodes in the network (for example a simulated Swarm node which needs // to connect to a Geth node to resolve ENS names) type RPCDialer interface { - DialRPC(id discover.NodeID) (*rpc.Client, error) + DialRPC(id enode.ID) (*rpc.Client, error) } // LifecycleConstructor allows a Lifecycle to be constructed during node start-up. @@ -270,3 +275,30 @@ func RegisterLifecycles(lifecycles LifecycleConstructors) { os.Exit(0) } } + +// adds the host part to the configuration's ENR, signs it +// creates and the corresponding enode object to the configuration +func (n *NodeConfig) initEnode(ip net.IP, tcpport int, udpport int) error { + enrIp := enr.IP(ip) + n.Record.Set(&enrIp) + enrTcpPort := enr.TCP(tcpport) + n.Record.Set(&enrTcpPort) + enrUdpPort := enr.UDP(udpport) + n.Record.Set(&enrUdpPort) + + err := enode.SignV4(&n.Record, n.PrivateKey) + if err != nil { + return fmt.Errorf("unable to generate ENR: %v", err) + } + nod, err := enode.New(enode.V4ID{}, &n.Record) + if err != nil { + return fmt.Errorf("unable to create enode: %v", err) + } + log.Trace("simnode new", "record", n.Record) + n.node = nod + return nil +} + +func (n *NodeConfig) initDummyEnode() error { + return n.initEnode(net.IPv4(127, 0, 0, 1), int(n.Port), 0) +} diff --git a/p2p/simulations/adapters/ws.go b/p2p/simulations/adapters/ws.go deleted file mode 100644 index 979a21709e49..000000000000 --- a/p2p/simulations/adapters/ws.go +++ /dev/null @@ -1,51 +0,0 @@ -package adapters - -import ( - "bufio" - "errors" - "io" - "regexp" - "strings" - "time" -) - -// wsAddrPattern is a regex used to read the WebSocket address from the node's -// log -var wsAddrPattern = regexp.MustCompile(`ws://[\d.:]+`) - -func matchWSAddr(str string) (string, bool) { - if !strings.Contains(str, "WebSocket endpoint opened") { - return "", false - } - - return wsAddrPattern.FindString(str), true -} - -// findWSAddr scans through reader r, looking for the log entry with -// WebSocket address information. -func findWSAddr(r io.Reader, timeout time.Duration) (string, error) { - ch := make(chan string) - - go func() { - s := bufio.NewScanner(r) - for s.Scan() { - addr, ok := matchWSAddr(s.Text()) - if ok { - ch <- addr - } - } - close(ch) - }() - - var wsAddr string - select { - case wsAddr = <-ch: - if wsAddr == "" { - return "", errors.New("empty result") - } - case <-time.After(timeout): - return "", errors.New("timed out") - } - - return wsAddr, nil -} diff --git a/p2p/simulations/adapters/ws_test.go b/p2p/simulations/adapters/ws_test.go deleted file mode 100644 index 0bb9ed2b2b0d..000000000000 --- a/p2p/simulations/adapters/ws_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package adapters - -import ( - "bytes" - "testing" - "time" -) - -func TestFindWSAddr(t *testing.T) { - line := `t=2018-05-02T19:00:45+0200 lvl=info msg="WebSocket endpoint opened" node.id=26c65a606d1125a44695bc08573190d047152b6b9a776ccbbe593e90f91444d9c1ebdadac6a775ad9fdd0923468a1d698ed3a842c1fb89c1bc0f9d4801f8c39c url=ws://127.0.0.1:59975` - buf := bytes.NewBufferString(line) - got, err := findWSAddr(buf, 10*time.Second) - if err != nil { - t.Fatalf("Failed to find addr: %v", err) - } - expected := `ws://127.0.0.1:59975` - - if got != expected { - t.Fatalf("Expected to get '%s', but got '%s'", expected, got) - } -} diff --git a/p2p/simulations/connect.go b/p2p/simulations/connect.go new file mode 100644 index 000000000000..351a1c93d768 --- /dev/null +++ b/p2p/simulations/connect.go @@ -0,0 +1,153 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package simulations + +import ( + "errors" + "strings" + + "github.com/XinFinOrg/XDPoSChain/p2p/enode" +) + +var ( + ErrNodeNotFound = errors.New("node not found") +) + +// ConnectToLastNode connects the node with provided NodeID +// to the last node that is up, and avoiding connection to self. +// It is useful when constructing a chain network topology +// when Network adds and removes nodes dynamically. +func (net *Network) ConnectToLastNode(id enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + ids := net.getUpNodeIDs() + l := len(ids) + if l < 2 { + return nil + } + last := ids[l-1] + if last == id { + last = ids[l-2] + } + return net.connectNotConnected(last, id) +} + +// ConnectToRandomNode connects the node with provided NodeID +// to a random node that is up. +func (net *Network) ConnectToRandomNode(id enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + selected := net.getRandomUpNode(id) + if selected == nil { + return ErrNodeNotFound + } + return net.connectNotConnected(selected.ID(), id) +} + +// ConnectNodesFull connects all nodes one to another. +// It provides a complete connectivity in the network +// which should be rarely needed. +func (net *Network) ConnectNodesFull(ids []enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + if ids == nil { + ids = net.getUpNodeIDs() + } + for i, lid := range ids { + for _, rid := range ids[i+1:] { + if err = net.connectNotConnected(lid, rid); err != nil { + return err + } + } + } + return nil +} + +// ConnectNodesChain connects all nodes in a chain topology. +// If ids argument is nil, all nodes that are up will be connected. +func (net *Network) ConnectNodesChain(ids []enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + return net.connectNodesChain(ids) +} + +func (net *Network) connectNodesChain(ids []enode.ID) (err error) { + if ids == nil { + ids = net.getUpNodeIDs() + } + l := len(ids) + for i := 0; i < l-1; i++ { + if err := net.connectNotConnected(ids[i], ids[i+1]); err != nil { + return err + } + } + return nil +} + +// ConnectNodesRing connects all nodes in a ring topology. +// If ids argument is nil, all nodes that are up will be connected. +func (net *Network) ConnectNodesRing(ids []enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + if ids == nil { + ids = net.getUpNodeIDs() + } + l := len(ids) + if l < 2 { + return nil + } + if err := net.connectNodesChain(ids); err != nil { + return err + } + return net.connectNotConnected(ids[l-1], ids[0]) +} + +// ConnectNodesStar connects all nodes into a star topology +// If ids argument is nil, all nodes that are up will be connected. +func (net *Network) ConnectNodesStar(ids []enode.ID, center enode.ID) (err error) { + net.lock.Lock() + defer net.lock.Unlock() + + if ids == nil { + ids = net.getUpNodeIDs() + } + for _, id := range ids { + if center == id { + continue + } + if err := net.connectNotConnected(center, id); err != nil { + return err + } + } + return nil +} + +func (net *Network) connectNotConnected(oneID, otherID enode.ID) error { + return ignoreAlreadyConnectedErr(net.connect(oneID, otherID)) +} + +func ignoreAlreadyConnectedErr(err error) error { + if err == nil || strings.Contains(err.Error(), "already connected") { + return nil + } + return err +} diff --git a/p2p/simulations/connect_test.go b/p2p/simulations/connect_test.go new file mode 100644 index 000000000000..1f636ed75757 --- /dev/null +++ b/p2p/simulations/connect_test.go @@ -0,0 +1,172 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package simulations + +import ( + "testing" + + "github.com/XinFinOrg/XDPoSChain/node" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" +) + +func newTestNetwork(t *testing.T, nodeCount int) (*Network, []enode.ID) { + t.Helper() + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { + return NewNoopService(nil), nil + }, + }) + + // create network + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "noopwoop", + }) + + // create and start nodes + ids := make([]enode.ID, nodeCount) + for i := range ids { + conf := adapters.RandomNodeConfig() + node, err := network.NewNodeWithConfig(conf) + if err != nil { + t.Fatalf("error creating node: %s", err) + } + if err := network.Start(node.ID()); err != nil { + t.Fatalf("error starting node: %s", err) + } + ids[i] = node.ID() + } + + if len(network.Conns) > 0 { + t.Fatal("no connections should exist after just adding nodes") + } + + return network, ids +} + +func TestConnectToLastNode(t *testing.T) { + net, ids := newTestNetwork(t, 10) + defer net.Shutdown() + + first := ids[0] + if err := net.ConnectToLastNode(first); err != nil { + t.Fatal(err) + } + + last := ids[len(ids)-1] + for i, id := range ids { + if id == first || id == last { + continue + } + + if net.GetConn(first, id) != nil { + t.Errorf("connection must not exist with node(ind: %v, id: %v)", i, id) + } + } + + if net.GetConn(first, last) == nil { + t.Error("first and last node must be connected") + } +} + +func TestConnectToRandomNode(t *testing.T) { + net, ids := newTestNetwork(t, 10) + defer net.Shutdown() + + err := net.ConnectToRandomNode(ids[0]) + if err != nil { + t.Fatal(err) + } + + var cc int + for i, a := range ids { + for _, b := range ids[i:] { + if net.GetConn(a, b) != nil { + cc++ + } + } + } + + if cc != 1 { + t.Errorf("expected one connection, got %v", cc) + } +} + +func TestConnectNodesFull(t *testing.T) { + tests := []struct { + name string + nodeCount int + }{ + {name: "no node", nodeCount: 0}, + {name: "single node", nodeCount: 1}, + {name: "2 nodes", nodeCount: 2}, + {name: "3 nodes", nodeCount: 3}, + {name: "even number of nodes", nodeCount: 12}, + {name: "odd number of nodes", nodeCount: 13}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + net, ids := newTestNetwork(t, test.nodeCount) + defer net.Shutdown() + + err := net.ConnectNodesFull(ids) + if err != nil { + t.Fatal(err) + } + + VerifyFull(t, net, ids) + }) + } +} + +func TestConnectNodesChain(t *testing.T) { + net, ids := newTestNetwork(t, 10) + defer net.Shutdown() + + err := net.ConnectNodesChain(ids) + if err != nil { + t.Fatal(err) + } + + VerifyChain(t, net, ids) +} + +func TestConnectNodesRing(t *testing.T) { + net, ids := newTestNetwork(t, 10) + defer net.Shutdown() + + err := net.ConnectNodesRing(ids) + if err != nil { + t.Fatal(err) + } + + VerifyRing(t, net, ids) +} + +func TestConnectNodesStar(t *testing.T) { + net, ids := newTestNetwork(t, 10) + defer net.Shutdown() + + pivotIndex := 2 + + err := net.ConnectNodesStar(ids, ids[pivotIndex]) + if err != nil { + t.Fatal(err) + } + + VerifyStar(t, net, ids, pivotIndex) +} diff --git a/p2p/simulations/events.go b/p2p/simulations/events.go index be249af16339..d0d03794edf7 100644 --- a/p2p/simulations/events.go +++ b/p2p/simulations/events.go @@ -30,7 +30,7 @@ const ( EventTypeNode EventType = "node" // EventTypeConn is the type of event emitted when a connection is - // either established or dropped between two nodes + // is either established or dropped between two nodes EventTypeConn EventType = "conn" // EventTypeMsg is the type of event emitted when a p2p message it @@ -58,6 +58,9 @@ type Event struct { // Msg is set if the type is EventTypeMsg Msg *Msg `json:"msg,omitempty"` + + //Optionally provide data (currently for simulation frontends only) + Data interface{} `json:"data"` } // NewEvent creates a new event for the given object which should be either a @@ -70,8 +73,7 @@ func NewEvent(v interface{}) *Event { switch v := v.(type) { case *Node: event.Type = EventTypeNode - node := *v - event.Node = &node + event.Node = v.copy() case *Conn: event.Type = EventTypeConn conn := *v @@ -97,7 +99,7 @@ func ControlEvent(v interface{}) *Event { func (e *Event) String() string { switch e.Type { case EventTypeNode: - return fmt.Sprintf(" id: %s up: %t", e.Node.ID().TerminalString(), e.Node.Up) + return fmt.Sprintf(" id: %s up: %t", e.Node.ID().TerminalString(), e.Node.Up()) case EventTypeConn: return fmt.Sprintf(" nodes: %s->%s up: %t", e.Conn.One.TerminalString(), e.Conn.Other.TerminalString(), e.Conn.Up) case EventTypeMsg: diff --git a/p2p/simulations/examples/ping-pong.go b/p2p/simulations/examples/ping-pong.go index 422a653cf931..ee9f786b3c6e 100644 --- a/p2p/simulations/examples/ping-pong.go +++ b/p2p/simulations/examples/ping-pong.go @@ -28,7 +28,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" ) @@ -57,6 +57,7 @@ func main() { var adapter adapters.NodeAdapter switch *adapterType { + case "sim": log.Info("using sim adapter") adapter = adapters.NewSimAdapter(services) @@ -70,14 +71,6 @@ func main() { log.Info("using exec adapter", "tmpdir", tmpdir) adapter = adapters.NewExecAdapter(tmpdir) - case "docker": - log.Info("using docker adapter") - var err error - adapter, err = adapters.NewDockerAdapter() - if err != nil { - log.Crit("error creating docker adapter", "err", err) - } - default: log.Crit(fmt.Sprintf("unknown node adapter %q", *adapterType)) } @@ -96,12 +89,12 @@ func main() { // sends a ping to all its connected peers every 10s and receives a pong in // return type pingPongService struct { - id discover.NodeID + id enode.ID log log.Logger received int64 } -func newPingPongService(id discover.NodeID) *pingPongService { +func newPingPongService(id enode.ID) *pingPongService { return &pingPongService{ id: id, log: log.New("node.id", id), diff --git a/p2p/simulations/http.go b/p2p/simulations/http.go index 0817866ad569..79d41cd93042 100644 --- a/p2p/simulations/http.go +++ b/p2p/simulations/http.go @@ -30,7 +30,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" "github.com/XinFinOrg/XDPoSChain/rpc" "github.com/gorilla/websocket" @@ -100,7 +100,7 @@ type SubscribeOpts struct { // nodes and connections and filtering message events func (c *Client) SubscribeNetwork(events chan *Event, opts SubscribeOpts) (event.Subscription, error) { url := fmt.Sprintf("%s/events?current=%t&filter=%s", c.URL, opts.Current, opts.Filter) - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } @@ -213,18 +213,18 @@ func (c *Client) RPCClient(ctx context.Context, nodeID string) (*rpc.Client, err // Get performs a HTTP GET request decoding the resulting JSON response // into "out" func (c *Client) Get(path string, out interface{}) error { - return c.Send(http.MethodGet, path, nil, out) + return c.Send("GET", path, nil, out) } // Post performs a HTTP POST request sending "in" as the JSON body and // decoding the resulting JSON response into "out" func (c *Client) Post(path string, in, out interface{}) error { - return c.Send(http.MethodPost, path, in, out) + return c.Send("POST", path, in, out) } // Delete performs a HTTP DELETE request func (c *Client) Delete(path string) error { - return c.Send(http.MethodDelete, path, nil, nil) + return c.Send("DELETE", path, nil, nil) } // Send performs a HTTP request, sending "in" as the JSON request body and @@ -365,6 +365,7 @@ func (s *Server) StopMocker(w http.ResponseWriter, req *http.Request) { // GetMockerList returns a list of available mockers func (s *Server) GetMockers(w http.ResponseWriter, req *http.Request) { + list := GetMockerList() s.JSON(w, http.StatusOK, list) } @@ -382,12 +383,6 @@ func (s *Server) StreamNetworkEvents(w http.ResponseWriter, req *http.Request) { sub := s.network.events.Subscribe(events) defer sub.Unsubscribe() - // stop the stream if the client goes away - var clientGone <-chan bool - if cn, ok := w.(http.CloseNotifier); ok { - clientGone = cn.CloseNotify() - } - // write writes the given event and data to the stream like: // // event: @@ -453,6 +448,7 @@ func (s *Server) StreamNetworkEvents(w http.ResponseWriter, req *http.Request) { } } + clientGone := req.Context().Done() for { select { case event := <-events: @@ -480,13 +476,13 @@ func (s *Server) StreamNetworkEvents(w http.ResponseWriter, req *http.Request) { // A message code of '*' or '-1' is considered a wildcard and matches any code. func NewMsgFilters(filterParam string) (MsgFilters, error) { filters := make(MsgFilters) - for filter := range strings.SplitSeq(filterParam, "-") { - proto, codes, found := strings.Cut(filter, ":") - if !found || proto == "" || codes == "" { + for _, filter := range strings.Split(filterParam, "-") { + protoCodes := strings.SplitN(filter, ":", 2) + if len(protoCodes) != 2 || protoCodes[0] == "" || protoCodes[1] == "" { return nil, fmt.Errorf("invalid message filter: %s", filter) } - - for code := range strings.SplitSeq(codes, ",") { + proto := protoCodes[0] + for _, code := range strings.Split(protoCodes[1], ",") { if code == "*" || code == "-1" { filters[MsgFilter{Proto: proto, Code: -1}] = struct{}{} continue @@ -701,18 +697,19 @@ func (s *Server) JSON(w http.ResponseWriter, status int, data interface{}) { json.NewEncoder(w).Encode(data) } -// wrapHandler returns a httprouter.Handle which wraps a http.HandlerFunc by +// wrapHandler returns an httprouter.Handle which wraps an http.HandlerFunc by // populating request.Context with any objects from the URL params func (s *Server) wrapHandler(handler http.HandlerFunc) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - ctx := context.Background() + ctx := req.Context() if id := params.ByName("nodeid"); id != "" { + var nodeID enode.ID var node *Node - if nodeID, err := discover.HexID(id); err == nil { + if nodeID.UnmarshalText([]byte(id)) == nil { node = s.network.GetNode(nodeID) } else { node = s.network.GetNodeByName(id) @@ -726,8 +723,9 @@ func (s *Server) wrapHandler(handler http.HandlerFunc) httprouter.Handle { } if id := params.ByName("peerid"); id != "" { + var peerID enode.ID var peer *Node - if peerID, err := discover.HexID(id); err == nil { + if peerID.UnmarshalText([]byte(id)) == nil { peer = s.network.GetNode(peerID) } else { peer = s.network.GetNodeByName(id) diff --git a/p2p/simulations/http_test.go b/p2p/simulations/http_test.go index 689d496316d2..e9fed738e07b 100644 --- a/p2p/simulations/http_test.go +++ b/p2p/simulations/http_test.go @@ -18,9 +18,11 @@ package simulations import ( "context" + "flag" "fmt" "math/rand" "net/http/httptest" + "os" "reflect" "sync" "sync/atomic" @@ -30,20 +32,25 @@ import ( "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/node" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" "github.com/XinFinOrg/XDPoSChain/rpc" ) +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + // testService implements the node.Service interface and provides protocols // and APIs which are useful for testing nodes in a simulation network type testService struct { - id discover.NodeID + id enode.ID // peerCount is incremented once a peer handshake has been performed peerCount int64 - peers map[discover.NodeID]*testPeer + peers map[enode.ID]*testPeer peersMtx sync.Mutex // state stores []byte which is used to test creating and loading @@ -54,7 +61,7 @@ type testService struct { func newTestService(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { svc := &testService{ id: ctx.Config.ID, - peers: make(map[discover.NodeID]*testPeer), + peers: make(map[enode.ID]*testPeer), } svc.state.Store(ctx.Snapshot) @@ -68,7 +75,7 @@ type testPeer struct { dumReady chan struct{} } -func (t *testService) peer(id discover.NodeID) *testPeer { +func (t *testService) peer(id enode.ID) *testPeer { t.peersMtx.Lock() defer t.peersMtx.Unlock() if peer, ok := t.peers[id]; ok { @@ -255,16 +262,18 @@ func (t *TestAPI) Events(ctx context.Context) (*rpc.Subscription, error) { } rpcSub := notifier.CreateSubscription() + events := make(chan int64, 1) + sub := t.feed.Subscribe(events) go func() { - events := make(chan int64) - sub := t.feed.Subscribe(events) defer sub.Unsubscribe() for { select { case event := <-events: - notifier.Notify(rpcSub.ID, event) + if err := notifier.Notify(rpcSub.ID, event); err != nil { + return + } case <-sub.Err(): return case <-rpcSub.Err(): @@ -408,23 +417,16 @@ type expectEvents struct { } func (t *expectEvents) nodeEvent(id string, up bool) *Event { - return &Event{ - Type: EventTypeNode, - Node: &Node{ - Config: &adapters.NodeConfig{ - ID: discover.MustHexID(id), - }, - Up: up, - }, - } + config := &adapters.NodeConfig{ID: enode.HexID(id)} + return &Event{Type: EventTypeNode, Node: newNode(nil, config, up)} } func (t *expectEvents) connEvent(one, other string, up bool) *Event { return &Event{ Type: EventTypeConn, Conn: &Conn{ - One: discover.MustHexID(one), - Other: discover.MustHexID(other), + One: enode.HexID(one), + Other: enode.HexID(other), Up: up, }, } @@ -437,7 +439,7 @@ loop: for { select { case event := <-t.events: - t.Logf("received %s event: %s", event.Type, event) + t.Logf("received %s event: %v", event.Type, event) if event.Type != EventTypeMsg || event.Msg.Received { continue loop @@ -467,12 +469,13 @@ loop: } func (t *expectEvents) expect(events ...*Event) { + t.Helper() timeout := time.After(10 * time.Second) i := 0 for { select { case event := <-t.events: - t.Logf("received %s event: %s", event.Type, event) + t.Logf("received %s event: %v", event.Type, event) expected := events[i] if event.Type != expected.Type { @@ -480,6 +483,7 @@ func (t *expectEvents) expect(events ...*Event) { } switch expected.Type { + case EventTypeNode: if event.Node == nil { t.Fatal("expected event.Node to be set") @@ -487,8 +491,8 @@ func (t *expectEvents) expect(events ...*Event) { if event.Node.ID() != expected.Node.ID() { t.Fatalf("expected node event %d to have id %q, got %q", i, expected.Node.ID().TerminalString(), event.Node.ID().TerminalString()) } - if event.Node.Up != expected.Node.Up { - t.Fatalf("expected node event %d to have up=%t, got up=%t", i, expected.Node.Up, event.Node.Up) + if event.Node.Up() != expected.Node.Up() { + t.Fatalf("expected node event %d to have up=%t, got up=%t", i, expected.Node.Up(), event.Node.Up()) } case EventTypeConn: @@ -504,6 +508,7 @@ func (t *expectEvents) expect(events ...*Event) { if event.Conn.Up != expected.Conn.Up { t.Fatalf("expected conn event %d to have up=%t, got up=%t", i, expected.Conn.Up, event.Conn.Up) } + } i++ @@ -584,9 +589,26 @@ func TestHTTPNodeRPC(t *testing.T) { // TestHTTPSnapshot tests creating and loading network snapshots func TestHTTPSnapshot(t *testing.T) { // start the server - _, s := testHTTPServer(t) + network, s := testHTTPServer(t) defer s.Close() + var eventsDone = make(chan struct{}) + count := 1 + eventsDoneChan := make(chan *Event) + eventSub := network.Events().Subscribe(eventsDoneChan) + go func() { + defer eventSub.Unsubscribe() + for event := range eventsDoneChan { + if event.Type == EventTypeConn && !event.Control { + count-- + if count == 0 { + eventsDone <- struct{}{} + return + } + } + } + }() + // create a two-node network client := NewClient(s.URL) nodeCount := 2 @@ -620,7 +642,7 @@ func TestHTTPSnapshot(t *testing.T) { } states[i] = state } - + <-eventsDone // create a snapshot snap, err := client.CreateSnapshot() if err != nil { @@ -634,9 +656,23 @@ func TestHTTPSnapshot(t *testing.T) { } // create another network - _, s = testHTTPServer(t) + network2, s := testHTTPServer(t) defer s.Close() client = NewClient(s.URL) + count = 1 + eventSub = network2.Events().Subscribe(eventsDoneChan) + go func() { + defer eventSub.Unsubscribe() + for event := range eventsDoneChan { + if event.Type == EventTypeConn && !event.Control { + count-- + if count == 0 { + eventsDone <- struct{}{} + return + } + } + } + }() // subscribe to events so we can check them later events := make(chan *Event, 100) @@ -651,6 +687,7 @@ func TestHTTPSnapshot(t *testing.T) { if err := client.LoadSnapshot(snap); err != nil { t.Fatalf("error loading snapshot: %s", err) } + <-eventsDone // check the nodes and connection exists net, err := client.GetNetwork() @@ -676,6 +713,9 @@ func TestHTTPSnapshot(t *testing.T) { if conn.Other.String() != nodes[1].ID { t.Fatalf("expected connection to have other=%q, got other=%q", nodes[1].ID, conn.Other) } + if !conn.Up { + t.Fatal("should be up") + } // check the node states were restored for i, node := range nodes { diff --git a/p2p/simulations/mocker.go b/p2p/simulations/mocker.go index 91cf123dae09..608845c1905e 100644 --- a/p2p/simulations/mocker.go +++ b/p2p/simulations/mocker.go @@ -25,7 +25,7 @@ import ( "time" "github.com/XinFinOrg/XDPoSChain/log" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" ) @@ -138,6 +138,8 @@ func probabilistic(net *Network, quit chan struct{}, nodeCount int) { lowid = rand1 highid = rand2 } + var steps = highid - lowid + wg.Add(steps) for i := lowid; i < highid; i++ { select { case <-quit: @@ -149,24 +151,26 @@ func probabilistic(net *Network, quit chan struct{}, nodeCount int) { err := net.Stop(nodes[i]) if err != nil { log.Error("Error stopping node", "node", nodes[i], "err", err) + wg.Done() continue } - wg.Go(func() { - id := nodes[i] + go func(id enode.ID) { time.Sleep(randWait) err := net.Start(id) if err != nil { log.Error("Error starting node", "node", id, "err", err) } - }) + wg.Done() + }(nodes[i]) } wg.Wait() } + } // connect nodeCount number of nodes in a ring -func connectNodesInRing(net *Network, nodeCount int) ([]discover.NodeID, error) { - ids := make([]discover.NodeID, nodeCount) +func connectNodesInRing(net *Network, nodeCount int) ([]enode.ID, error) { + ids := make([]enode.ID, nodeCount) for i := 0; i < nodeCount; i++ { conf := adapters.RandomNodeConfig() node, err := net.NewNodeWithConfig(conf) diff --git a/p2p/simulations/mocker_test.go b/p2p/simulations/mocker_test.go index 5a90d445ecd7..110782089ecf 100644 --- a/p2p/simulations/mocker_test.go +++ b/p2p/simulations/mocker_test.go @@ -27,23 +27,23 @@ import ( "testing" "time" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" ) func TestMocker(t *testing.T) { - //start the simulation HTTP server + // start the simulation HTTP server _, s := testHTTPServer(t) defer s.Close() - //create a client + // create a client client := NewClient(s.URL) - //start the network + // start the network err := client.StartNetwork() if err != nil { t.Fatalf("Could not start test network: %s", err) } - //stop the network to terminate + // stop the network to terminate defer func() { err = client.StopNetwork() if err != nil { @@ -51,7 +51,7 @@ func TestMocker(t *testing.T) { } }() - //get the list of available mocker types + // get the list of available mocker types resp, err := http.Get(s.URL + "/mocker") if err != nil { t.Fatalf("Could not get mocker list: %s", err) @@ -62,7 +62,7 @@ func TestMocker(t *testing.T) { t.Fatalf("Invalid Status Code received, expected 200, got %d", resp.StatusCode) } - //check the list is at least 1 in size + // check the list is at least 1 in size var mockerlist []string err = json.NewDecoder(resp.Body).Decode(&mockerlist) if err != nil { @@ -83,22 +83,22 @@ func TestMocker(t *testing.T) { // wait until all nodes are started and connected // store every node up event in a map (value is irrelevant, mimic Set datatype) - nodemap := make(map[discover.NodeID]bool) + nodemap := make(map[enode.ID]bool) nodesComplete := false connCount := 0 - wg.Go(func() { + wg.Add(1) + go func() { + defer wg.Done() + for connCount < (nodeCount-1)*2 { select { case event := <-events: - //if the event is a node Up event only - if event.Node != nil && event.Node.Up { + if isNodeUp(event) { //add the correspondent node ID to the map nodemap[event.Node.Config.ID] = true //this means all nodes got a nodeUp event, so we can continue the test if len(nodemap) == nodeCount { nodesComplete = true - //wait for 3s as the mocker will need time to connect the nodes - //time.Sleep( 3 *time.Second) } } else if event.Conn != nil && nodesComplete { connCount += 1 @@ -108,18 +108,18 @@ func TestMocker(t *testing.T) { return } } - }) + }() - //take the last element of the mockerlist as the default mocker-type to ensure one is enabled + // take the last element of the mockerlist as the default mocker-type to ensure one is enabled mockertype := mockerlist[len(mockerlist)-1] - //still, use hardcoded "probabilistic" one if available ;) + // still, use hardcoded "probabilistic" one if available ;) for _, m := range mockerlist { if m == "probabilistic" { mockertype = m break } } - //start the mocker with nodeCount number of nodes + // start the mocker with nodeCount number of nodes resp, err = http.PostForm(s.URL+"/mocker/start", url.Values{"mocker-type": {mockertype}, "node-count": {strconv.Itoa(nodeCount)}}) if err != nil { t.Fatalf("Could not start mocker: %s", err) @@ -131,7 +131,7 @@ func TestMocker(t *testing.T) { wg.Wait() - //check there are nodeCount number of nodes in the network + // check there are nodeCount number of nodes in the network nodesInfo, err := client.GetNodes() if err != nil { t.Fatalf("Could not get nodes list: %s", err) @@ -141,7 +141,7 @@ func TestMocker(t *testing.T) { t.Fatalf("Expected %d number of nodes, got: %d", nodeCount, len(nodesInfo)) } - //stop the mocker + // stop the mocker resp, err = http.Post(s.URL+"/mocker/stop", "", nil) if err != nil { t.Fatalf("Could not stop mocker: %s", err) @@ -151,14 +151,14 @@ func TestMocker(t *testing.T) { t.Fatalf("Invalid Status Code received for stopping mocker, expected 200, got %d", resp.StatusCode) } - //reset the network + // reset the network resp, err = http.Post(s.URL+"/reset", "", nil) if err != nil { t.Fatalf("Could not reset network: %s", err) } resp.Body.Close() - //now the number of nodes in the network should be zero + // now the number of nodes in the network should be zero nodesInfo, err = client.GetNodes() if err != nil { t.Fatalf("Could not get nodes list: %s", err) @@ -168,3 +168,7 @@ func TestMocker(t *testing.T) { t.Fatalf("Expected empty list of nodes, got: %d", len(nodesInfo)) } } + +func isNodeUp(event *Event) bool { + return event.Node != nil && event.Node.Up() +} diff --git a/p2p/simulations/network.go b/p2p/simulations/network.go index 5a925ce12d0a..1005f32e0e1c 100644 --- a/p2p/simulations/network.go +++ b/p2p/simulations/network.go @@ -20,15 +20,17 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "math/rand" "sync" "time" "github.com/XinFinOrg/XDPoSChain/event" "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" ) @@ -52,7 +54,10 @@ type Network struct { NetworkConfig Nodes []*Node `json:"nodes"` - nodeMap map[discover.NodeID]int + nodeMap map[enode.ID]int + + // Maps a node property string to node indexes of all nodes that hold this property + propertyMap map[string][]int Conns []*Conn `json:"conns"` connMap map[string]int @@ -68,7 +73,8 @@ func NewNetwork(nodeAdapter adapters.NodeAdapter, conf *NetworkConfig) *Network return &Network{ NetworkConfig: *conf, nodeAdapter: nodeAdapter, - nodeMap: make(map[discover.NodeID]int), + nodeMap: make(map[enode.ID]int), + propertyMap: make(map[string][]int), connMap: make(map[string]int), quitc: make(chan struct{}), } @@ -86,7 +92,7 @@ func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) defer net.lock.Unlock() if conf.Reachable == nil { - conf.Reachable = func(otherID discover.NodeID) bool { + conf.Reachable = func(otherID enode.ID) bool { _, err := net.InitConn(conf.ID, otherID) if err != nil && bytes.Compare(conf.ID.Bytes(), otherID.Bytes()) < 0 { return false @@ -113,14 +119,18 @@ func (net *Network) NewNodeWithConfig(conf *adapters.NodeConfig) (*Node, error) if err != nil { return nil, err } - node := &Node{ - Node: adapterNode, - Config: conf, - } - log.Trace(fmt.Sprintf("node %v created", conf.ID)) - net.nodeMap[conf.ID] = len(net.Nodes) + node := newNode(adapterNode, conf, false) + log.Trace("Node created", "id", conf.ID) + + nodeIndex := len(net.Nodes) + net.nodeMap[conf.ID] = nodeIndex net.Nodes = append(net.Nodes, node) + // Register any node properties with the network-level propertyMap + for _, property := range conf.Properties { + net.propertyMap[property] = append(net.propertyMap[property], nodeIndex) + } + // emit a "control" event net.events.Send(ControlEvent(node)) @@ -135,7 +145,7 @@ func (net *Network) Config() *NetworkConfig { // StartAll starts all nodes in the network func (net *Network) StartAll() error { for _, node := range net.Nodes { - if node.Up { + if node.Up() { continue } if err := net.Start(node.ID()); err != nil { @@ -148,7 +158,7 @@ func (net *Network) StartAll() error { // StopAll stops all nodes in the network func (net *Network) StopAll() error { for _, node := range net.Nodes { - if !node.Up { + if !node.Up() { continue } if err := net.Stop(node.ID()); err != nil { @@ -159,35 +169,31 @@ func (net *Network) StopAll() error { } // Start starts the node with the given ID -func (net *Network) Start(id discover.NodeID) error { +func (net *Network) Start(id enode.ID) error { return net.startWithSnapshots(id, nil) } // startWithSnapshots starts the node with the given ID using the give // snapshots -func (net *Network) startWithSnapshots(id discover.NodeID, snapshots map[string][]byte) error { +func (net *Network) startWithSnapshots(id enode.ID, snapshots map[string][]byte) error { net.lock.Lock() + defer net.lock.Unlock() node := net.getNode(id) if node == nil { - net.lock.Unlock() return fmt.Errorf("node %v does not exist", id) } - if node.Up { - net.lock.Unlock() + if node.Up() { return fmt.Errorf("node %v already up", id) } log.Trace("Starting node", "id", id, "adapter", net.nodeAdapter.Name()) if err := node.Start(snapshots); err != nil { - net.lock.Unlock() log.Warn("Node startup failed", "id", id, "err", err) return err } - node.Up = true + node.SetUp(true) log.Info("Started node", "id", id) ev := NewEvent(node) - net.lock.Unlock() - net.events.Send(ev) // subscribe to peer events @@ -206,7 +212,7 @@ func (net *Network) startWithSnapshots(id discover.NodeID, snapshots map[string] // watchPeerEvents reads peer events from the given channel and emits // corresponding network events -func (net *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEvent, sub event.Subscription) { +func (net *Network) watchPeerEvents(id enode.ID, events chan *p2p.PeerEvent, sub event.Subscription) { defer func() { sub.Unsubscribe() @@ -218,7 +224,7 @@ func (net *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEve if node == nil { return } - node.Up = false + node.SetUp(false) ev := NewEvent(node) net.events.Send(ev) }() @@ -230,19 +236,24 @@ func (net *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEve } peer := event.Peer switch event.Type { + case p2p.PeerEventTypeAdd: net.DidConnect(id, peer) + case p2p.PeerEventTypeDrop: net.DidDisconnect(id, peer) + case p2p.PeerEventTypeMsgSend: net.DidSend(id, peer, event.Protocol, *event.MsgCode) + case p2p.PeerEventTypeMsgRecv: net.DidReceive(peer, id, event.Protocol, *event.MsgCode) + } case err := <-sub.Err(): if err != nil { - log.Error(fmt.Sprintf("error getting peer events for node %v", id), "err", err) + log.Error("Error in peer event subscription", "id", id, "err", err) } return } @@ -250,40 +261,58 @@ func (net *Network) watchPeerEvents(id discover.NodeID, events chan *p2p.PeerEve } // Stop stops the node with the given ID -func (net *Network) Stop(id discover.NodeID) error { - net.lock.Lock() - node := net.getNode(id) - if node == nil { - net.lock.Unlock() - return fmt.Errorf("node %v does not exist", id) - } - if !node.Up { - net.lock.Unlock() - return fmt.Errorf("node %v already down", id) +func (net *Network) Stop(id enode.ID) error { + // IMPORTANT: node.Stop() must NOT be called under net.lock as + // node.Reachable() closure has a reference to the network and + // calls net.InitConn() what also locks the network. => DEADLOCK + // That holds until the following ticket is not resolved: + + var err error + + node, err := func() (*Node, error) { + net.lock.Lock() + defer net.lock.Unlock() + + node := net.getNode(id) + if node == nil { + return nil, fmt.Errorf("node %v does not exist", id) + } + if !node.Up() { + return nil, fmt.Errorf("node %v already down", id) + } + node.SetUp(false) + return node, nil + }() + if err != nil { + return err } - node.Up = false - net.lock.Unlock() - err := node.Stop() + err = node.Stop() // must be called without net.lock + + net.lock.Lock() + defer net.lock.Unlock() + if err != nil { - net.lock.Lock() - node.Up = true - net.lock.Unlock() + node.SetUp(true) return err } log.Info("Stopped node", "id", id, "err", err) - net.lock.Lock() ev := ControlEvent(node) - net.lock.Unlock() net.events.Send(ev) return nil } // Connect connects two nodes together by calling the "admin_addPeer" RPC // method on the "one" node so that it connects to the "other" node -func (net *Network) Connect(oneID, otherID discover.NodeID) error { - log.Debug(fmt.Sprintf("connecting %s to %s", oneID, otherID)) - conn, err := net.InitConn(oneID, otherID) +func (net *Network) Connect(oneID, otherID enode.ID) error { + net.lock.Lock() + defer net.lock.Unlock() + return net.connect(oneID, otherID) +} + +func (net *Network) connect(oneID, otherID enode.ID) error { + log.Debug("Connecting nodes with addPeer", "id", oneID, "other", otherID) + conn, err := net.initConn(oneID, otherID) if err != nil { return err } @@ -297,7 +326,7 @@ func (net *Network) Connect(oneID, otherID discover.NodeID) error { // Disconnect disconnects two nodes by calling the "admin_removePeer" RPC // method on the "one" node so that it disconnects from the "other" node -func (net *Network) Disconnect(oneID, otherID discover.NodeID) error { +func (net *Network) Disconnect(oneID, otherID enode.ID) error { conn := net.GetConn(oneID, otherID) if conn == nil { return fmt.Errorf("connection between %v and %v does not exist", oneID, otherID) @@ -314,7 +343,7 @@ func (net *Network) Disconnect(oneID, otherID discover.NodeID) error { } // DidConnect tracks the fact that the "one" node connected to the "other" node -func (net *Network) DidConnect(one, other discover.NodeID) error { +func (net *Network) DidConnect(one, other enode.ID) error { net.lock.Lock() defer net.lock.Unlock() conn, err := net.getOrCreateConn(one, other) @@ -331,7 +360,7 @@ func (net *Network) DidConnect(one, other discover.NodeID) error { // DidDisconnect tracks the fact that the "one" node disconnected from the // "other" node -func (net *Network) DidDisconnect(one, other discover.NodeID) error { +func (net *Network) DidDisconnect(one, other enode.ID) error { net.lock.Lock() defer net.lock.Unlock() conn := net.getConn(one, other) @@ -348,7 +377,7 @@ func (net *Network) DidDisconnect(one, other discover.NodeID) error { } // DidSend tracks the fact that "sender" sent a message to "receiver" -func (net *Network) DidSend(sender, receiver discover.NodeID, proto string, code uint64) error { +func (net *Network) DidSend(sender, receiver enode.ID, proto string, code uint64) error { msg := &Msg{ One: sender, Other: receiver, @@ -361,7 +390,7 @@ func (net *Network) DidSend(sender, receiver discover.NodeID, proto string, code } // DidReceive tracks the fact that "receiver" received a message from "sender" -func (net *Network) DidReceive(sender, receiver discover.NodeID, proto string, code uint64) error { +func (net *Network) DidReceive(sender, receiver enode.ID, proto string, code uint64) error { msg := &Msg{ One: sender, Other: receiver, @@ -375,63 +404,220 @@ func (net *Network) DidReceive(sender, receiver discover.NodeID, proto string, c // GetNode gets the node with the given ID, returning nil if the node does not // exist -func (net *Network) GetNode(id discover.NodeID) *Node { - net.lock.Lock() - defer net.lock.Unlock() +func (net *Network) GetNode(id enode.ID) *Node { + net.lock.RLock() + defer net.lock.RUnlock() return net.getNode(id) } -// GetNode gets the node with the given name, returning nil if the node does +func (net *Network) getNode(id enode.ID) *Node { + i, found := net.nodeMap[id] + if !found { + return nil + } + return net.Nodes[i] +} + +// GetNodeByName gets the node with the given name, returning nil if the node does // not exist func (net *Network) GetNodeByName(name string) *Node { - net.lock.Lock() - defer net.lock.Unlock() + net.lock.RLock() + defer net.lock.RUnlock() return net.getNodeByName(name) } -// GetNodes returns the existing nodes -func (net *Network) GetNodes() (nodes []*Node) { - net.lock.Lock() - defer net.lock.Unlock() +func (net *Network) getNodeByName(name string) *Node { + for _, node := range net.Nodes { + if node.Config.Name == name { + return node + } + } + return nil +} + +// GetNodeIDs returns the IDs of all existing nodes +// Nodes can optionally be excluded by specifying their enode.ID. +func (net *Network) GetNodeIDs(excludeIDs ...enode.ID) []enode.ID { + net.lock.RLock() + defer net.lock.RUnlock() + + return net.getNodeIDs(excludeIDs) +} + +func (net *Network) getNodeIDs(excludeIDs []enode.ID) []enode.ID { + // Get all current nodeIDs + nodeIDs := make([]enode.ID, 0, len(net.nodeMap)) + for id := range net.nodeMap { + nodeIDs = append(nodeIDs, id) + } + + if len(excludeIDs) > 0 { + // Return the difference of nodeIDs and excludeIDs + return filterIDs(nodeIDs, excludeIDs) + } else { + return nodeIDs + } +} + +// GetNodes returns the existing nodes. +// Nodes can optionally be excluded by specifying their enode.ID. +func (net *Network) GetNodes(excludeIDs ...enode.ID) []*Node { + net.lock.RLock() + defer net.lock.RUnlock() + + return net.getNodes(excludeIDs) +} + +func (net *Network) getNodes(excludeIDs []enode.ID) []*Node { + if len(excludeIDs) > 0 { + nodeIDs := net.getNodeIDs(excludeIDs) + return net.getNodesByID(nodeIDs) + } else { + return net.Nodes + } +} + +// GetNodesByID returns existing nodes with the given enode.IDs. +// If a node doesn't exist with a given enode.ID, it is ignored. +func (net *Network) GetNodesByID(nodeIDs []enode.ID) []*Node { + net.lock.RLock() + defer net.lock.RUnlock() + + return net.getNodesByID(nodeIDs) +} + +func (net *Network) getNodesByID(nodeIDs []enode.ID) []*Node { + nodes := make([]*Node, 0, len(nodeIDs)) + for _, id := range nodeIDs { + node := net.getNode(id) + if node != nil { + nodes = append(nodes, node) + } + } - nodes = append(nodes, net.Nodes...) return nodes } -func (net *Network) getNode(id discover.NodeID) *Node { - i, found := net.nodeMap[id] - if !found { - return nil +// GetNodesByProperty returns existing nodes that have the given property string registered in their NodeConfig +func (net *Network) GetNodesByProperty(property string) []*Node { + net.lock.RLock() + defer net.lock.RUnlock() + + return net.getNodesByProperty(property) +} + +func (net *Network) getNodesByProperty(property string) []*Node { + nodes := make([]*Node, 0, len(net.propertyMap[property])) + for _, nodeIndex := range net.propertyMap[property] { + nodes = append(nodes, net.Nodes[nodeIndex]) } - return net.Nodes[i] + + return nodes } -func (net *Network) getNodeByName(name string) *Node { +// GetNodeIDsByProperty returns existing node's enode IDs that have the given property string registered in the NodeConfig +func (net *Network) GetNodeIDsByProperty(property string) []enode.ID { + net.lock.RLock() + defer net.lock.RUnlock() + + return net.getNodeIDsByProperty(property) +} + +func (net *Network) getNodeIDsByProperty(property string) []enode.ID { + nodeIDs := make([]enode.ID, 0, len(net.propertyMap[property])) + for _, nodeIndex := range net.propertyMap[property] { + node := net.Nodes[nodeIndex] + nodeIDs = append(nodeIDs, node.ID()) + } + + return nodeIDs +} + +// GetRandomUpNode returns a random node on the network, which is running. +func (net *Network) GetRandomUpNode(excludeIDs ...enode.ID) *Node { + net.lock.RLock() + defer net.lock.RUnlock() + return net.getRandomUpNode(excludeIDs...) +} + +// GetRandomUpNode returns a random node on the network, which is running. +func (net *Network) getRandomUpNode(excludeIDs ...enode.ID) *Node { + return net.getRandomNode(net.getUpNodeIDs(), excludeIDs) +} + +func (net *Network) getUpNodeIDs() (ids []enode.ID) { for _, node := range net.Nodes { - if node.Config.Name == name { - return node + if node.Up() { + ids = append(ids, node.ID()) } } - return nil + return ids +} + +// GetRandomDownNode returns a random node on the network, which is stopped. +func (net *Network) GetRandomDownNode(excludeIDs ...enode.ID) *Node { + net.lock.RLock() + defer net.lock.RUnlock() + return net.getRandomNode(net.getDownNodeIDs(), excludeIDs) +} + +func (net *Network) getDownNodeIDs() (ids []enode.ID) { + for _, node := range net.Nodes { + if !node.Up() { + ids = append(ids, node.ID()) + } + } + return ids +} + +// GetRandomNode returns a random node on the network, regardless of whether it is running or not +func (net *Network) GetRandomNode(excludeIDs ...enode.ID) *Node { + net.lock.RLock() + defer net.lock.RUnlock() + return net.getRandomNode(net.getNodeIDs(nil), excludeIDs) // no need to exclude twice +} + +func (net *Network) getRandomNode(ids []enode.ID, excludeIDs []enode.ID) *Node { + filtered := filterIDs(ids, excludeIDs) + + l := len(filtered) + if l == 0 { + return nil + } + return net.getNode(filtered[rand.Intn(l)]) +} + +func filterIDs(ids []enode.ID, excludeIDs []enode.ID) []enode.ID { + exclude := make(map[enode.ID]bool) + for _, id := range excludeIDs { + exclude[id] = true + } + var filtered []enode.ID + for _, id := range ids { + if _, found := exclude[id]; !found { + filtered = append(filtered, id) + } + } + return filtered } // GetConn returns the connection which exists between "one" and "other" // regardless of which node initiated the connection -func (net *Network) GetConn(oneID, otherID discover.NodeID) *Conn { - net.lock.Lock() - defer net.lock.Unlock() +func (net *Network) GetConn(oneID, otherID enode.ID) *Conn { + net.lock.RLock() + defer net.lock.RUnlock() return net.getConn(oneID, otherID) } // GetOrCreateConn is like GetConn but creates the connection if it doesn't // already exist -func (net *Network) GetOrCreateConn(oneID, otherID discover.NodeID) (*Conn, error) { +func (net *Network) GetOrCreateConn(oneID, otherID enode.ID) (*Conn, error) { net.lock.Lock() defer net.lock.Unlock() return net.getOrCreateConn(oneID, otherID) } -func (net *Network) getOrCreateConn(oneID, otherID discover.NodeID) (*Conn, error) { +func (net *Network) getOrCreateConn(oneID, otherID enode.ID) (*Conn, error) { if conn := net.getConn(oneID, otherID); conn != nil { return conn, nil } @@ -456,7 +642,7 @@ func (net *Network) getOrCreateConn(oneID, otherID discover.NodeID) (*Conn, erro return conn, nil } -func (net *Network) getConn(oneID, otherID discover.NodeID) *Conn { +func (net *Network) getConn(oneID, otherID enode.ID) *Conn { label := ConnLabel(oneID, otherID) i, found := net.connMap[label] if !found { @@ -473,9 +659,13 @@ func (net *Network) getConn(oneID, otherID discover.NodeID) *Conn { // it also checks whether there has been recent attempt to connect the peers // this is cheating as the simulation is used as an oracle and know about // remote peers attempt to connect to a node which will then not initiate the connection -func (net *Network) InitConn(oneID, otherID discover.NodeID) (*Conn, error) { +func (net *Network) InitConn(oneID, otherID enode.ID) (*Conn, error) { net.lock.Lock() defer net.lock.Unlock() + return net.initConn(oneID, otherID) +} + +func (net *Network) initConn(oneID, otherID enode.ID) (*Conn, error) { if oneID == otherID { return nil, fmt.Errorf("refusing to connect to self %v", oneID) } @@ -492,10 +682,10 @@ func (net *Network) InitConn(oneID, otherID discover.NodeID) (*Conn, error) { err = conn.nodesUp() if err != nil { - log.Trace(fmt.Sprintf("nodes not up: %v", err)) + log.Trace("Nodes not up", "err", err) return nil, fmt.Errorf("nodes not up: %v", err) } - log.Debug("InitConn - connection initiated") + log.Debug("Connection initiated", "id", oneID, "other", otherID) conn.initiated = time.Now() return conn, nil } @@ -503,9 +693,9 @@ func (net *Network) InitConn(oneID, otherID discover.NodeID) (*Conn, error) { // Shutdown stops all nodes in the network and closes the quit channel func (net *Network) Shutdown() { for _, node := range net.Nodes { - log.Debug(fmt.Sprintf("stopping node %s", node.ID().TerminalString())) + log.Debug("Stopping node", "id", node.ID()) if err := node.Stop(); err != nil { - log.Warn(fmt.Sprintf("error stopping node %s", node.ID().TerminalString()), "err", err) + log.Warn("Can't stop node", "id", node.ID(), "err", err) } // If the node has the close method, call it. if closer, ok := node.Node.(io.Closer); ok { @@ -518,14 +708,15 @@ func (net *Network) Shutdown() { } // Reset resets all network properties: -// emtpies the nodes and the connection list +// empties the nodes and the connection list func (net *Network) Reset() { net.lock.Lock() defer net.lock.Unlock() //re-initialize the maps net.connMap = make(map[string]int) - net.nodeMap = make(map[discover.NodeID]int) + net.nodeMap = make(map[enode.ID]int) + net.propertyMap = make(map[string][]int) net.Nodes = nil net.Conns = nil @@ -539,12 +730,36 @@ type Node struct { // Config if the config used to created the node Config *adapters.NodeConfig `json:"config"` - // Up tracks whether or not the node is running - Up bool `json:"up"` + // up tracks whether or not the node is running + up bool + upMu *sync.RWMutex +} + +func newNode(an adapters.Node, ac *adapters.NodeConfig, up bool) *Node { + return &Node{Node: an, Config: ac, up: up, upMu: new(sync.RWMutex)} +} + +func (n *Node) copy() *Node { + configCpy := *n.Config + return newNode(n.Node, &configCpy, n.Up()) +} + +// Up returns whether the node is currently up (online) +func (n *Node) Up() bool { + n.upMu.RLock() + defer n.upMu.RUnlock() + return n.up +} + +// SetUp sets the up (online) status of the nodes with the given value +func (n *Node) SetUp(up bool) { + n.upMu.Lock() + defer n.upMu.Unlock() + n.up = up } // ID returns the ID of the node -func (n *Node) ID() discover.NodeID { +func (n *Node) ID() enode.ID { return n.Config.ID } @@ -560,10 +775,6 @@ func (n *Node) NodeInfo() *p2p.NodeInfo { return nil } info := n.Node.NodeInfo() - if info == nil { - return nil - } - info.Name = n.Config.Name return info } @@ -578,17 +789,33 @@ func (n *Node) MarshalJSON() ([]byte, error) { }{ Info: n.NodeInfo(), Config: n.Config, - Up: n.Up, + Up: n.Up(), }) } +// UnmarshalJSON implements json.Unmarshaler interface so that we don't lose Node.up +// status. IMPORTANT: The implementation is incomplete; we lose p2p.NodeInfo. +func (n *Node) UnmarshalJSON(raw []byte) error { + // TODO: How should we turn back NodeInfo into n.Node? + // Ticket: https://github.com/ethersphere/go-ethereum/issues/1177 + var node struct { + Config *adapters.NodeConfig `json:"config,omitempty"` + Up bool `json:"up"` + } + if err := json.Unmarshal(raw, &node); err != nil { + return err + } + *n = *newNode(nil, node.Config, node.Up) + return nil +} + // Conn represents a connection between two nodes in the network type Conn struct { // One is the node which initiated the connection - One discover.NodeID `json:"one"` + One enode.ID `json:"one"` // Other is the node which the connection was made to - Other discover.NodeID `json:"other"` + Other enode.ID `json:"other"` // Up tracks whether or not the connection is active Up bool `json:"up"` @@ -601,10 +828,10 @@ type Conn struct { // nodesUp returns whether both nodes are currently up func (c *Conn) nodesUp() error { - if !c.one.Up { + if !c.one.Up() { return fmt.Errorf("one %v is not up", c.One) } - if !c.other.Up { + if !c.other.Up() { return fmt.Errorf("other %v is not up", c.Other) } return nil @@ -617,11 +844,11 @@ func (c *Conn) String() string { // Msg represents a p2p message sent between two nodes in the network type Msg struct { - One discover.NodeID `json:"one"` - Other discover.NodeID `json:"other"` - Protocol string `json:"protocol"` - Code uint64 `json:"code"` - Received bool `json:"received"` + One enode.ID `json:"one"` + Other enode.ID `json:"other"` + Protocol string `json:"protocol"` + Code uint64 `json:"code"` + Received bool `json:"received"` } // String returns a log-friendly string @@ -632,8 +859,8 @@ func (m *Msg) String() string { // ConnLabel generates a deterministic string which represents a connection // between two nodes, used to compare if two connections are between the same // nodes -func ConnLabel(source, target discover.NodeID) string { - var first, second discover.NodeID +func ConnLabel(source, target enode.ID) string { + var first, second enode.ID if bytes.Compare(source.Bytes(), target.Bytes()) > 0 { first = target second = source @@ -661,15 +888,22 @@ type NodeSnapshot struct { // Snapshot creates a network snapshot func (net *Network) Snapshot() (*Snapshot, error) { + return net.snapshot(nil, nil) +} + +func (net *Network) SnapshotWithServices(addServices []string, removeServices []string) (*Snapshot, error) { + return net.snapshot(addServices, removeServices) +} + +func (net *Network) snapshot(addServices []string, removeServices []string) (*Snapshot, error) { net.lock.Lock() defer net.lock.Unlock() snap := &Snapshot{ Nodes: make([]NodeSnapshot, len(net.Nodes)), - Conns: make([]Conn, len(net.Conns)), } for i, node := range net.Nodes { - snap.Nodes[i] = NodeSnapshot{Node: *node} - if !node.Up { + snap.Nodes[i] = NodeSnapshot{Node: *node.copy()} + if !node.Up() { continue } snapshots, err := node.Snapshots() @@ -677,28 +911,127 @@ func (net *Network) Snapshot() (*Snapshot, error) { return nil, err } snap.Nodes[i].Snapshots = snapshots + for _, addSvc := range addServices { + haveSvc := false + for _, svc := range snap.Nodes[i].Node.Config.Lifecycles { + if svc == addSvc { + haveSvc = true + break + } + } + if !haveSvc { + snap.Nodes[i].Node.Config.Lifecycles = append(snap.Nodes[i].Node.Config.Lifecycles, addSvc) + } + } + if len(removeServices) > 0 { + var cleanedServices []string + for _, svc := range snap.Nodes[i].Node.Config.Lifecycles { + haveSvc := false + for _, rmSvc := range removeServices { + if rmSvc == svc { + haveSvc = true + break + } + } + if !haveSvc { + cleanedServices = append(cleanedServices, svc) + } + + } + snap.Nodes[i].Node.Config.Lifecycles = cleanedServices + } } - for i, conn := range net.Conns { - snap.Conns[i] = *conn + for _, conn := range net.Conns { + if conn.Up { + snap.Conns = append(snap.Conns, *conn) + } } return snap, nil } +// longrunning tests may need a longer timeout +var snapshotLoadTimeout = 900 * time.Second + // Load loads a network snapshot func (net *Network) Load(snap *Snapshot) error { + // Start nodes. for _, n := range snap.Nodes { if _, err := net.NewNodeWithConfig(n.Node.Config); err != nil { return err } - if !n.Node.Up { + if !n.Node.Up() { continue } if err := net.startWithSnapshots(n.Node.Config.ID, n.Snapshots); err != nil { return err } } + + // Prepare connection events counter. + allConnected := make(chan struct{}) // closed when all connections are established + done := make(chan struct{}) // ensures that the event loop goroutine is terminated + defer close(done) + + // Subscribe to event channel. + // It needs to be done outside of the event loop goroutine (created below) + // to ensure that the event channel is blocking before connect calls are made. + events := make(chan *Event) + sub := net.Events().Subscribe(events) + defer sub.Unsubscribe() + + go func() { + // Expected number of connections. + total := len(snap.Conns) + // Set of all established connections from the snapshot, not other connections. + // Key array element 0 is the connection One field value, and element 1 connection Other field. + connections := make(map[[2]enode.ID]struct{}, total) + + for { + select { + case e := <-events: + // Ignore control events as they do not represent + // connect or disconnect (Up) state change. + if e.Control { + continue + } + // Detect only connection events. + if e.Type != EventTypeConn { + continue + } + connection := [2]enode.ID{e.Conn.One, e.Conn.Other} + // Nodes are still not connected or have been disconnected. + if !e.Conn.Up { + // Delete the connection from the set of established connections. + // This will prevent false positive in case disconnections happen. + delete(connections, connection) + log.Warn("load snapshot: unexpected disconnection", "one", e.Conn.One, "other", e.Conn.Other) + continue + } + // Check that the connection is from the snapshot. + for _, conn := range snap.Conns { + if conn.One == e.Conn.One && conn.Other == e.Conn.Other { + // Add the connection to the set of established connections. + connections[connection] = struct{}{} + if len(connections) == total { + // Signal that all nodes are connected. + close(allConnected) + return + } + + break + } + } + case <-done: + // Load function returned, terminate this goroutine. + return + } + } + }() + + // Start connecting. for _, conn := range snap.Conns { - if !net.GetNode(conn.One).Up || !net.GetNode(conn.Other).Up { + + if !net.GetNode(conn.One).Up() || !net.GetNode(conn.Other).Up() { //in this case, at least one of the nodes of a connection is not up, //so it would result in the snapshot `Load` to fail continue @@ -707,6 +1040,14 @@ func (net *Network) Load(snap *Snapshot) error { return err } } + + select { + // Wait until all connections from the snapshot are established. + case <-allConnected: + // Make sure that we do not wait forever. + case <-time.After(snapshotLoadTimeout): + return errors.New("snapshot connections not established") + } return nil } @@ -728,23 +1069,23 @@ func (net *Network) Subscribe(events chan *Event) { } func (net *Network) executeControlEvent(event *Event) { - log.Trace("execute control event", "type", event.Type, "event", event) + log.Trace("Executing control event", "type", event.Type, "event", event) switch event.Type { case EventTypeNode: if err := net.executeNodeEvent(event); err != nil { - log.Error("error executing node event", "event", event, "err", err) + log.Error("Error executing node event", "event", event, "err", err) } case EventTypeConn: if err := net.executeConnEvent(event); err != nil { - log.Error("error executing conn event", "event", event, "err", err) + log.Error("Error executing conn event", "event", event, "err", err) } case EventTypeMsg: - log.Warn("ignoring control msg event") + log.Warn("Ignoring control msg event") } } func (net *Network) executeNodeEvent(e *Event) error { - if !e.Node.Up { + if !e.Node.Up() { return net.Stop(e.Node.ID()) } @@ -757,6 +1098,7 @@ func (net *Network) executeNodeEvent(e *Event) error { func (net *Network) executeConnEvent(e *Event) error { if e.Conn.Up { return net.Connect(e.Conn.One, e.Conn.Other) + } else { + return net.Disconnect(e.Conn.One, e.Conn.Other) } - return net.Disconnect(e.Conn.One, e.Conn.Other) } diff --git a/p2p/simulations/network_test.go b/p2p/simulations/network_test.go index 63a761d8f15d..b622556f4cf8 100644 --- a/p2p/simulations/network_test.go +++ b/p2p/simulations/network_test.go @@ -17,15 +17,272 @@ package simulations import ( + "bytes" "context" + "encoding/json" "fmt" + "reflect" + "strconv" + "strings" "testing" "time" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/node" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" "github.com/XinFinOrg/XDPoSChain/p2p/simulations/adapters" ) +// Tests that a created snapshot with a minimal service only contains the expected connections +// and that a network when loaded with this snapshot only contains those same connections +func TestSnapshot(t *testing.T) { + + // PART I + // create snapshot from ring network + + // this is a minimal service, whose protocol will take exactly one message OR close of connection before quitting + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { + return NewNoopService(nil), nil + }, + }) + + // create network + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "noopwoop", + }) + // \todo consider making a member of network, set to true threadsafe when shutdown + runningOne := true + defer func() { + if runningOne { + network.Shutdown() + } + }() + + // create and start nodes + nodeCount := 20 + ids := make([]enode.ID, nodeCount) + for i := 0; i < nodeCount; i++ { + conf := adapters.RandomNodeConfig() + node, err := network.NewNodeWithConfig(conf) + if err != nil { + t.Fatalf("error creating node: %s", err) + } + if err := network.Start(node.ID()); err != nil { + t.Fatalf("error starting node: %s", err) + } + ids[i] = node.ID() + } + + // subscribe to peer events + evC := make(chan *Event) + sub := network.Events().Subscribe(evC) + defer sub.Unsubscribe() + + // connect nodes in a ring + // spawn separate thread to avoid deadlock in the event listeners + connectErr := make(chan error, 1) + go func() { + for i, id := range ids { + peerID := ids[(i+1)%len(ids)] + if err := network.Connect(id, peerID); err != nil { + connectErr <- err + return + } + } + }() + + // collect connection events up to expected number + ctx, cancel := context.WithTimeout(context.TODO(), time.Second) + defer cancel() + checkIds := make(map[enode.ID][]enode.ID) + connEventCount := nodeCount +OUTER: + for { + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case err := <-connectErr: + t.Fatal(err) + case ev := <-evC: + if ev.Type == EventTypeConn && !ev.Control { + // fail on any disconnect + if !ev.Conn.Up { + t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) + } + checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) + checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) + connEventCount-- + log.Debug("ev", "count", connEventCount) + if connEventCount == 0 { + break OUTER + } + } + } + } + + // create snapshot of current network + snap, err := network.Snapshot() + if err != nil { + t.Fatal(err) + } + j, err := json.Marshal(snap) + if err != nil { + t.Fatal(err) + } + log.Debug("snapshot taken", "nodes", len(snap.Nodes), "conns", len(snap.Conns), "json", string(j)) + + // verify that the snap element numbers check out + if len(checkIds) != len(snap.Conns) || len(checkIds) != len(snap.Nodes) { + t.Fatalf("snapshot wrong node,conn counts %d,%d != %d", len(snap.Nodes), len(snap.Conns), len(checkIds)) + } + + // shut down sim network + runningOne = false + sub.Unsubscribe() + network.Shutdown() + + // check that we have all the expected connections in the snapshot + for nodid, nodConns := range checkIds { + for _, nodConn := range nodConns { + var match bool + for _, snapConn := range snap.Conns { + if snapConn.One == nodid && snapConn.Other == nodConn { + match = true + break + } else if snapConn.Other == nodid && snapConn.One == nodConn { + match = true + break + } + } + if !match { + t.Fatalf("snapshot missing conn %v -> %v", nodid, nodConn) + } + } + } + log.Info("snapshot checked") + + // PART II + // load snapshot and verify that exactly same connections are formed + + adapter = adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { + return NewNoopService(nil), nil + }, + }) + network = NewNetwork(adapter, &NetworkConfig{ + DefaultService: "noopwoop", + }) + defer func() { + network.Shutdown() + }() + + // subscribe to peer events + // every node up and conn up event will generate one additional control event + // therefore multiply the count by two + evC = make(chan *Event, (len(snap.Conns)*2)+(len(snap.Nodes)*2)) + sub = network.Events().Subscribe(evC) + defer sub.Unsubscribe() + + // load the snapshot + // spawn separate thread to avoid deadlock in the event listeners + err = network.Load(snap) + if err != nil { + t.Fatal(err) + } + + // collect connection events up to expected number + ctx, cancel = context.WithTimeout(context.TODO(), time.Second*3) + defer cancel() + + connEventCount = nodeCount + +OuterTwo: + for { + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case ev := <-evC: + if ev.Type == EventTypeConn && !ev.Control { + + // fail on any disconnect + if !ev.Conn.Up { + t.Fatalf("unexpected disconnect: %v -> %v", ev.Conn.One, ev.Conn.Other) + } + log.Debug("conn", "on", ev.Conn.One, "other", ev.Conn.Other) + checkIds[ev.Conn.One] = append(checkIds[ev.Conn.One], ev.Conn.Other) + checkIds[ev.Conn.Other] = append(checkIds[ev.Conn.Other], ev.Conn.One) + connEventCount-- + log.Debug("ev", "count", connEventCount) + if connEventCount == 0 { + break OuterTwo + } + } + } + } + + // check that we have all expected connections in the network + for _, snapConn := range snap.Conns { + var match bool + for nodid, nodConns := range checkIds { + for _, nodConn := range nodConns { + if snapConn.One == nodid && snapConn.Other == nodConn { + match = true + break + } else if snapConn.Other == nodid && snapConn.One == nodConn { + match = true + break + } + } + } + if !match { + t.Fatalf("network missing conn %v -> %v", snapConn.One, snapConn.Other) + } + } + + // verify that network didn't generate any other additional connection events after the ones we have collected within a reasonable period of time + ctx, cancel = context.WithTimeout(context.TODO(), time.Second) + defer cancel() + select { + case <-ctx.Done(): + case ev := <-evC: + if ev.Type == EventTypeConn { + t.Fatalf("Superfluous conn found %v -> %v", ev.Conn.One, ev.Conn.Other) + } + } + + // This test validates if all connections from the snapshot + // are created in the network. + t.Run("conns after load", func(t *testing.T) { + // Create new network. + n := NewNetwork( + adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { + return NewNoopService(nil), nil + }, + }), + &NetworkConfig{ + DefaultService: "noopwoop", + }, + ) + defer n.Shutdown() + + // Load the same snapshot. + err := n.Load(snap) + if err != nil { + t.Fatal(err) + } + + // Check every connection from the snapshot + // if it is in the network, too. + for _, c := range snap.Conns { + if n.GetConn(c.One, c.Other) == nil { + t.Errorf("missing connection: %s -> %s", c.One, c.Other) + } + } + }) +} + // TestNetworkSimulation creates a multi-node simulation network with each node // connected in a ring topology, checks that all nodes successfully handshake // with each other and that a snapshot fully represents the desired topology @@ -39,7 +296,7 @@ func TestNetworkSimulation(t *testing.T) { }) defer network.Shutdown() nodeCount := 20 - ids := make([]discover.NodeID, nodeCount) + ids := make([]enode.ID, nodeCount) for i := 0; i < nodeCount; i++ { conf := adapters.RandomNodeConfig() node, err := network.NewNodeWithConfig(conf) @@ -64,7 +321,7 @@ func TestNetworkSimulation(t *testing.T) { } return nil } - check := func(ctx context.Context, id discover.NodeID) (bool, error) { + check := func(ctx context.Context, id enode.ID) (bool, error) { // check we haven't run out of time select { case <-ctx.Done(): @@ -102,7 +359,7 @@ func TestNetworkSimulation(t *testing.T) { defer cancel() // trigger a check every 100ms - trigger := make(chan discover.NodeID) + trigger := make(chan enode.ID) go triggerChecks(ctx, ids, trigger, 100*time.Millisecond) result := NewSimulation(network).Run(ctx, &Step{ @@ -140,7 +397,276 @@ func TestNetworkSimulation(t *testing.T) { } } -func triggerChecks(ctx context.Context, ids []discover.NodeID, trigger chan discover.NodeID, interval time.Duration) { +func createTestNodes(count int, network *Network) (nodes []*Node, err error) { + for i := 0; i < count; i++ { + nodeConf := adapters.RandomNodeConfig() + node, err := network.NewNodeWithConfig(nodeConf) + if err != nil { + return nil, err + } + if err := network.Start(node.ID()); err != nil { + return nil, err + } + + nodes = append(nodes, node) + } + + return nodes, nil +} + +func createTestNodesWithProperty(property string, count int, network *Network) (propertyNodes []*Node, err error) { + for i := 0; i < count; i++ { + nodeConf := adapters.RandomNodeConfig() + nodeConf.Properties = append(nodeConf.Properties, property) + + node, err := network.NewNodeWithConfig(nodeConf) + if err != nil { + return nil, err + } + if err := network.Start(node.ID()); err != nil { + return nil, err + } + + propertyNodes = append(propertyNodes, node) + } + + return propertyNodes, nil +} + +// TestGetNodeIDs creates a set of nodes and attempts to retrieve their IDs,. +// It then tests again whilst excluding a node ID from being returned. +// If a node ID is not returned, or more node IDs than expected are returned, the test fails. +func TestGetNodeIDs(t *testing.T) { + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "test": newTestService, + }) + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "test", + }) + defer network.Shutdown() + + numNodes := 5 + nodes, err := createTestNodes(numNodes, network) + if err != nil { + t.Fatalf("Could not creat test nodes %v", err) + } + + gotNodeIDs := network.GetNodeIDs() + if len(gotNodeIDs) != numNodes { + t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodeIDs)) + } + + for _, node1 := range nodes { + match := false + for _, node2ID := range gotNodeIDs { + if bytes.Equal(node1.ID().Bytes(), node2ID.Bytes()) { + match = true + break + } + } + + if !match { + t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) + } + } + + excludeNodeID := nodes[3].ID() + gotNodeIDsExcl := network.GetNodeIDs(excludeNodeID) + if len(gotNodeIDsExcl) != numNodes-1 { + t.Fatalf("Expected one less node ID to be returned") + } + for _, nodeID := range gotNodeIDsExcl { + if bytes.Equal(excludeNodeID.Bytes(), nodeID.Bytes()) { + t.Fatalf("GetNodeIDs returned the node ID we excluded, ID: %s", nodeID.String()) + } + } +} + +// TestGetNodes creates a set of nodes and attempts to retrieve them again. +// It then tests again whilst excluding a node from being returned. +// If a node is not returned, or more nodes than expected are returned, the test fails. +func TestGetNodes(t *testing.T) { + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "test": newTestService, + }) + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "test", + }) + defer network.Shutdown() + + numNodes := 5 + nodes, err := createTestNodes(numNodes, network) + if err != nil { + t.Fatalf("Could not creat test nodes %v", err) + } + + gotNodes := network.GetNodes() + if len(gotNodes) != numNodes { + t.Fatalf("Expected %d nodes, got %d", numNodes, len(gotNodes)) + } + + for _, node1 := range nodes { + match := false + for _, node2 := range gotNodes { + if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { + match = true + break + } + } + + if !match { + t.Fatalf("A created node was not returned by GetNodes(), ID: %s", node1.ID().String()) + } + } + + excludeNodeID := nodes[3].ID() + gotNodesExcl := network.GetNodes(excludeNodeID) + if len(gotNodesExcl) != numNodes-1 { + t.Fatalf("Expected one less node to be returned") + } + for _, node := range gotNodesExcl { + if bytes.Equal(excludeNodeID.Bytes(), node.ID().Bytes()) { + t.Fatalf("GetNodes returned the node we excluded, ID: %s", node.ID().String()) + } + } +} + +// TestGetNodesByID creates a set of nodes and attempts to retrieve a subset of them by ID +// If a node is not returned, or more nodes than expected are returned, the test fails. +func TestGetNodesByID(t *testing.T) { + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "test": newTestService, + }) + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "test", + }) + defer network.Shutdown() + + numNodes := 5 + nodes, err := createTestNodes(numNodes, network) + if err != nil { + t.Fatalf("Could not create test nodes: %v", err) + } + + numSubsetNodes := 2 + subsetNodes := nodes[0:numSubsetNodes] + var subsetNodeIDs []enode.ID + for _, node := range subsetNodes { + subsetNodeIDs = append(subsetNodeIDs, node.ID()) + } + + gotNodesByID := network.GetNodesByID(subsetNodeIDs) + if len(gotNodesByID) != numSubsetNodes { + t.Fatalf("Expected %d nodes, got %d", numSubsetNodes, len(gotNodesByID)) + } + + for _, node1 := range subsetNodes { + match := false + for _, node2 := range gotNodesByID { + if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { + match = true + break + } + } + + if !match { + t.Fatalf("A created node was not returned by GetNodesByID(), ID: %s", node1.ID().String()) + } + } +} + +// TestGetNodesByProperty creates a subset of nodes with a property assigned. +// GetNodesByProperty is then checked for correctness by comparing the nodes returned to those initially created. +// If a node with a property is not found, or more nodes than expected are returned, the test fails. +func TestGetNodesByProperty(t *testing.T) { + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "test": newTestService, + }) + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "test", + }) + defer network.Shutdown() + + numNodes := 3 + _, err := createTestNodes(numNodes, network) + if err != nil { + t.Fatalf("Failed to create nodes: %v", err) + } + + numPropertyNodes := 3 + propertyTest := "test" + propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) + if err != nil { + t.Fatalf("Failed to create nodes with property: %v", err) + } + + gotNodesByProperty := network.GetNodesByProperty(propertyTest) + if len(gotNodesByProperty) != numPropertyNodes { + t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodesByProperty)) + } + + for _, node1 := range propertyNodes { + match := false + for _, node2 := range gotNodesByProperty { + if bytes.Equal(node1.ID().Bytes(), node2.ID().Bytes()) { + match = true + break + } + } + + if !match { + t.Fatalf("A created node with property was not returned by GetNodesByProperty(), ID: %s", node1.ID().String()) + } + } +} + +// TestGetNodeIDsByProperty creates a subset of nodes with a property assigned. +// GetNodeIDsByProperty is then checked for correctness by comparing the node IDs returned to those initially created. +// If a node ID with a property is not found, or more nodes IDs than expected are returned, the test fails. +func TestGetNodeIDsByProperty(t *testing.T) { + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "test": newTestService, + }) + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "test", + }) + defer network.Shutdown() + + numNodes := 3 + _, err := createTestNodes(numNodes, network) + if err != nil { + t.Fatalf("Failed to create nodes: %v", err) + } + + numPropertyNodes := 3 + propertyTest := "test" + propertyNodes, err := createTestNodesWithProperty(propertyTest, numPropertyNodes, network) + if err != nil { + t.Fatalf("Failed to created nodes with property: %v", err) + } + + gotNodeIDsByProperty := network.GetNodeIDsByProperty(propertyTest) + if len(gotNodeIDsByProperty) != numPropertyNodes { + t.Fatalf("Expected %d nodes with a property, got %d", numPropertyNodes, len(gotNodeIDsByProperty)) + } + + for _, node1 := range propertyNodes { + match := false + id1 := node1.ID() + for _, id2 := range gotNodeIDsByProperty { + if bytes.Equal(id1.Bytes(), id2.Bytes()) { + match = true + break + } + } + + if !match { + t.Fatalf("Not all nodes IDs were returned by GetNodeIDsByProperty(), ID: %s", id1.String()) + } + } +} + +func triggerChecks(ctx context.Context, ids []enode.ID, trigger chan enode.ID, interval time.Duration) { tick := time.NewTicker(interval) defer tick.Stop() for { @@ -158,3 +684,192 @@ func triggerChecks(ctx context.Context, ids []discover.NodeID, trigger chan disc } } } + +// \todo: refactor to implement shapshots +// and connect configuration methods once these are moved from +// swarm/network/simulations/connect.go +func BenchmarkMinimalService(b *testing.B) { + b.Run("ring/32", benchmarkMinimalServiceTmp) +} + +func benchmarkMinimalServiceTmp(b *testing.B) { + + // stop timer to discard setup time pollution + args := strings.Split(b.Name(), "/") + nodeCount, err := strconv.ParseInt(args[2], 10, 16) + if err != nil { + b.Fatal(err) + } + + for i := 0; i < b.N; i++ { + // this is a minimal service, whose protocol will close a channel upon run of protocol + // making it possible to bench the time it takes for the service to start and protocol actually to be run + protoCMap := make(map[enode.ID]map[enode.ID]chan struct{}) + adapter := adapters.NewSimAdapter(adapters.LifecycleConstructors{ + "noopwoop": func(ctx *adapters.ServiceContext, stack *node.Node) (node.Lifecycle, error) { + protoCMap[ctx.Config.ID] = make(map[enode.ID]chan struct{}) + svc := NewNoopService(protoCMap[ctx.Config.ID]) + return svc, nil + }, + }) + + // create network + network := NewNetwork(adapter, &NetworkConfig{ + DefaultService: "noopwoop", + }) + defer network.Shutdown() + + // create and start nodes + ids := make([]enode.ID, nodeCount) + for i := 0; i < int(nodeCount); i++ { + conf := adapters.RandomNodeConfig() + node, err := network.NewNodeWithConfig(conf) + if err != nil { + b.Fatalf("error creating node: %s", err) + } + if err := network.Start(node.ID()); err != nil { + b.Fatalf("error starting node: %s", err) + } + ids[i] = node.ID() + } + + // ready, set, go + b.ResetTimer() + + // connect nodes in a ring + for i, id := range ids { + peerID := ids[(i+1)%len(ids)] + if err := network.Connect(id, peerID); err != nil { + b.Fatal(err) + } + } + + // wait for all protocols to signal to close down + ctx, cancel := context.WithTimeout(context.TODO(), time.Second) + defer cancel() + for nodid, peers := range protoCMap { + for peerid, peerC := range peers { + log.Debug("getting ", "node", nodid, "peer", peerid) + select { + case <-ctx.Done(): + b.Fatal(ctx.Err()) + case <-peerC: + } + } + } + } +} + +func TestNode_UnmarshalJSON(t *testing.T) { + t.Run("up_field", func(t *testing.T) { + runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONUpField()) + }) + t.Run("config_field", func(t *testing.T) { + runNodeUnmarshalJSON(t, casesNodeUnmarshalJSONConfigField()) + }) +} + +func runNodeUnmarshalJSON(t *testing.T, tests []nodeUnmarshalTestCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got *Node + if err := json.Unmarshal([]byte(tt.marshaled), &got); err != nil { + expectErrorMessageToContain(t, err, tt.wantErr) + got = nil + } + expectNodeEquality(t, got, tt.want) + }) + } +} + +type nodeUnmarshalTestCase struct { + name string + marshaled string + want *Node + wantErr string +} + +func expectErrorMessageToContain(t *testing.T, got error, want string) { + t.Helper() + if got == nil && want == "" { + return + } + + if got == nil && want != "" { + t.Errorf("error was expected, got: nil, want: %v", want) + return + } + + if !strings.Contains(got.Error(), want) { + t.Errorf( + "unexpected error message, got %v, want: %v", + want, + got, + ) + } +} + +func expectNodeEquality(t *testing.T, got, want *Node) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("Node.UnmarshalJSON() = %v, want %v", got, want) + } +} + +func casesNodeUnmarshalJSONUpField() []nodeUnmarshalTestCase { + return []nodeUnmarshalTestCase{ + { + name: "empty json", + marshaled: "{}", + want: newNode(nil, nil, false), + }, + { + name: "a stopped node", + marshaled: "{\"up\": false}", + want: newNode(nil, nil, false), + }, + { + name: "a running node", + marshaled: "{\"up\": true}", + want: newNode(nil, nil, true), + }, + { + name: "invalid JSON value on valid key", + marshaled: "{\"up\": foo}", + wantErr: "invalid character", + }, + { + name: "invalid JSON key and value", + marshaled: "{foo: bar}", + wantErr: "invalid character", + }, + { + name: "bool value expected but got something else (string)", + marshaled: "{\"up\": \"true\"}", + wantErr: "cannot unmarshal string into Go struct", + }, + } +} + +func casesNodeUnmarshalJSONConfigField() []nodeUnmarshalTestCase { + // Don't do a big fuss around testing, as adapters.NodeConfig should + // handle it's own serialization. Just do a sanity check. + return []nodeUnmarshalTestCase{ + { + name: "Config field is omitted", + marshaled: "{}", + want: newNode(nil, nil, false), + }, + { + name: "Config field is nil", + marshaled: "{\"config\": null}", + want: newNode(nil, nil, false), + }, + { + name: "a non default Config field", + marshaled: "{\"config\":{\"name\":\"node_ecdd0\",\"port\":44665}}", + want: newNode(nil, &adapters.NodeConfig{Name: "node_ecdd0", Port: 44665}, false), + }, + } +} diff --git a/p2p/simulations/pipes/pipes.go b/p2p/simulations/pipes/pipes.go index 8532c1bcf0e9..ec277c0d147c 100644 --- a/p2p/simulations/pipes/pipes.go +++ b/p2p/simulations/pipes/pipes.go @@ -1,4 +1,4 @@ -// Copyright 2017 The go-ethereum Authors +// Copyright 2018 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify diff --git a/p2p/simulations/simulation.go b/p2p/simulations/simulation.go index 30faf988bca8..533dcb1a3c1a 100644 --- a/p2p/simulations/simulation.go +++ b/p2p/simulations/simulation.go @@ -20,7 +20,7 @@ import ( "context" "time" - "github.com/XinFinOrg/XDPoSChain/p2p/discover" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" ) // Simulation provides a framework for running actions in a simulated network @@ -55,7 +55,7 @@ func (s *Simulation) Run(ctx context.Context, step *Step) (result *StepResult) { } // wait for all node expectations to either pass, error or timeout - nodes := make(map[discover.NodeID]struct{}, len(step.Expect.Nodes)) + nodes := make(map[enode.ID]struct{}, len(step.Expect.Nodes)) for _, id := range step.Expect.Nodes { nodes[id] = struct{}{} } @@ -119,7 +119,7 @@ type Step struct { // Trigger is a channel which receives node ids and triggers an // expectation check for that node - Trigger chan discover.NodeID + Trigger chan enode.ID // Expect is the expectation to wait for when performing this step Expect *Expectation @@ -127,15 +127,15 @@ type Step struct { type Expectation struct { // Nodes is a list of nodes to check - Nodes []discover.NodeID + Nodes []enode.ID // Check checks whether a given node meets the expectation - Check func(context.Context, discover.NodeID) (bool, error) + Check func(context.Context, enode.ID) (bool, error) } func newStepResult() *StepResult { return &StepResult{ - Passes: make(map[discover.NodeID]time.Time), + Passes: make(map[enode.ID]time.Time), } } @@ -150,7 +150,7 @@ type StepResult struct { FinishedAt time.Time // Passes are the timestamps of the successful node expectations - Passes map[discover.NodeID]time.Time + Passes map[enode.ID]time.Time // NetworkEvents are the network events which occurred during the step NetworkEvents []*Event diff --git a/p2p/simulations/test.go b/p2p/simulations/test.go new file mode 100644 index 000000000000..6f67b8bdd3bb --- /dev/null +++ b/p2p/simulations/test.go @@ -0,0 +1,150 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package simulations + +import ( + "testing" + + "github.com/XinFinOrg/XDPoSChain/p2p" + "github.com/XinFinOrg/XDPoSChain/p2p/enode" + "github.com/XinFinOrg/XDPoSChain/p2p/enr" + "github.com/XinFinOrg/XDPoSChain/rpc" +) + +// NoopService is the service that does not do anything +// but implements node.Service interface. +type NoopService struct { + c map[enode.ID]chan struct{} +} + +func NewNoopService(ackC map[enode.ID]chan struct{}) *NoopService { + return &NoopService{ + c: ackC, + } +} + +func (t *NoopService) Protocols() []p2p.Protocol { + return []p2p.Protocol{ + { + Name: "noop", + Version: 666, + Length: 0, + Run: func(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + if t.c != nil { + t.c[peer.ID()] = make(chan struct{}) + close(t.c[peer.ID()]) + } + rw.ReadMsg() + return nil + }, + NodeInfo: func() interface{} { + return struct{}{} + }, + PeerInfo: func(id enode.ID) interface{} { + return struct{}{} + }, + Attributes: []enr.Entry{}, + }, + } +} + +func (t *NoopService) APIs() []rpc.API { + return []rpc.API{} +} + +func (t *NoopService) Start() error { + return nil +} + +func (t *NoopService) Stop() error { + return nil +} + +func VerifyRing(t *testing.T, net *Network, ids []enode.ID) { + t.Helper() + n := len(ids) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + c := net.GetConn(ids[i], ids[j]) + if i == j-1 || (i == 0 && j == n-1) { + if c == nil { + t.Errorf("nodes %v and %v are not connected, but they should be", i, j) + } + } else { + if c != nil { + t.Errorf("nodes %v and %v are connected, but they should not be", i, j) + } + } + } + } +} + +func VerifyChain(t *testing.T, net *Network, ids []enode.ID) { + t.Helper() + n := len(ids) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + c := net.GetConn(ids[i], ids[j]) + if i == j-1 { + if c == nil { + t.Errorf("nodes %v and %v are not connected, but they should be", i, j) + } + } else { + if c != nil { + t.Errorf("nodes %v and %v are connected, but they should not be", i, j) + } + } + } + } +} + +func VerifyFull(t *testing.T, net *Network, ids []enode.ID) { + t.Helper() + n := len(ids) + var connections int + for i, lid := range ids { + for _, rid := range ids[i+1:] { + if net.GetConn(lid, rid) != nil { + connections++ + } + } + } + + want := n * (n - 1) / 2 + if connections != want { + t.Errorf("wrong number of connections, got: %v, want: %v", connections, want) + } +} + +func VerifyStar(t *testing.T, net *Network, ids []enode.ID, centerIndex int) { + t.Helper() + n := len(ids) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + c := net.GetConn(ids[i], ids[j]) + if i == centerIndex || j == centerIndex { + if c == nil { + t.Errorf("nodes %v and %v are not connected, but they should be", i, j) + } + } else { + if c != nil { + t.Errorf("nodes %v and %v are connected, but they should not be", i, j) + } + } + } + } +}