From 52878405b728e3f622bfcd5bb800bf401f9a258c Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Fri, 13 Dec 2019 19:30:39 +0200 Subject: [PATCH] Integrate Discovery V5 and support ENR bootstrap records --- beacon_chain/beacon_node.nim | 45 +++++++++++- beacon_chain/conf.nim | 5 ++ beacon_chain/eth2_discovery.nim | 26 +++++++ beacon_chain/eth2_network.nim | 26 +++++-- beacon_chain/libp2p_backend.nim | 33 ++++++++- beacon_chain/nimquery.nim | 120 ++++++++++++++++++++++++++++++++ beacon_chain/version.nim | 2 +- vendor/nim-eth | 2 +- vendor/nim-stint | 2 +- 9 files changed, 246 insertions(+), 15 deletions(-) create mode 100644 beacon_chain/eth2_discovery.nim create mode 100644 beacon_chain/nimquery.nim diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 019eaaea4..373c2d1e3 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -6,7 +6,7 @@ import stew/[objects, bitseqs, byteutils], chronos, chronicles, confutils, metrics, json_serialization/std/[options, sets], serialization/errors, - kvstore, kvstore_lmdb, eth/async_utils, + kvstore, kvstore_lmdb, eth/async_utils, eth/p2p/discoveryv5/enr, # Local modules spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network], @@ -48,6 +48,7 @@ type networkIdentity: Eth2NodeIdentity requestManager: RequestManager bootstrapNodes: seq[BootstrapAddr] + bootstrapEnrs: seq[enr.Record] db: BeaconChainDB config: BeaconNodeConf attachedValidators: ValidatorPool @@ -130,6 +131,36 @@ proc loadBootstrapFile(bootstrapFile: string): seq[BootstrapAddr] = for line in lines(bootstrapFile): result.addBootstrapAddr(line) +proc addEnrBootstrapNode(node: BeaconNode, enrBase64: string) = + info "Adding bootsrap node", enr = enrBase64 + var enrRec: enr.Record + if enrRec.fromBase64(enrBase64): + info "Parsed ENR record", value = enrRec + try: + let + ip = IpAddress(family: IpAddressFamily.IPv4, + address_v4: cast[array[4, uint8]](enrRec.get("ip", int))) + tcpPort = Port enrRec.get("tcp", int) + # udpPort = Port enrRec.get("udp", int) + node.addBootstrapNode BootstrapAddr.initAddress(ip, tcpPort) + node.bootstrapEnrs.add enrRec + except CatchableError as err: + warn "Invalid ENR record", enrRec + +proc useEnrBootstrapFile(node: BeaconNode, bootstrapFile: string) = + let ext = splitFile(bootstrapFile).ext + if cmpIgnoreCase(ext, ".txt") == 0: + for ln in lines(bootstrapFile): + node.addEnrBootstrapNode(string ln) + elif cmpIgnoreCase(ext, ".yaml") == 0: + # TODO. This is very ugly, but let's try to negotiate the + # removal of YAML metadata. + for ln in lines(bootstrapFile): + node.addEnrBootstrapNode(string(ln[3..^2])) + else: + error "Unknown bootstrap file format", ext + quit 1 + proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async.} = let networkId = getPersistentNetIdentity(conf) @@ -184,7 +215,15 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async blockPool.headState.data.data.eth1_data.block_hash) mainchainMonitor.start() - var bootNodes: seq[BootstrapAddr] + var + bootNodes: seq[BootstrapAddr] + bootstrapEnrs: seq[enr.Record] + + # TODO: rebase this + let enrBootstrapFile = string conf.enrBootstrapNodesFile + if enrBootstrapFile.len > 0: + result.useEnrBootstrapFile(enrBootstrapFile) + for node in conf.bootstrapNodes: bootNodes.addBootstrapAddr(node) bootNodes.add(loadBootstrapFile(string conf.bootstrapNodesFile)) bootNodes.add(loadBootstrapFile(conf.dataDir / "bootstrap_nodes.txt")) @@ -192,7 +231,7 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async bootNodes = filterIt(bootNodes, not it.isSameNode(networkId)) let - network = await createEth2Node(conf, bootNodes) + network = await createEth2Node(conf, bootNodes, bootstrapEnrs) let addressFile = string(conf.dataDir) / "beacon_node.address" network.saveConnectionAddressFile(addressFile) diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index ffe17e833..c1014c863 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -90,6 +90,11 @@ type desc: "Specifies a line-delimited file of bootsrap Ethereum network addresses." name: "bootstrap-file" }: InputFile + enrBootstrapNodesFile* {. + defaultValue: "" + desc: "Specifies a line-delimited file of bootstrap ENR records" + name: "enr-bootstrap-file" }: InputFile + tcpPort* {. defaultValue: defaultPort(config) desc: "TCP listening port." diff --git a/beacon_chain/eth2_discovery.nim b/beacon_chain/eth2_discovery.nim new file mode 100644 index 000000000..bfc3d12eb --- /dev/null +++ b/beacon_chain/eth2_discovery.nim @@ -0,0 +1,26 @@ +import + net, + eth/keys, eth/trie/db, + eth/p2p/discoveryv5/[protocol, node, discovery_db, types], + conf + +type + Eth2DiscoveryProtocol* = protocol.Protocol + Eth2DiscoveryId* = NodeId + +export + Eth2DiscoveryProtocol, open, start, close + +proc new*(T: type Eth2DiscoveryProtocol, + conf: BeaconNodeConf, + rawPrivKeyBytes: openarray[byte]): T = + # TODO + # Implement more configuration options: + # * for setting up a specific key + # * for using a persistent database + var + pk = initPrivateKey(rawPrivKeyBytes) + db = DiscoveryDB.init(newMemoryDB()) + + newProtocol(pk, db, Port conf.udpPort) + diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index aed164b69..4787d742e 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -1,6 +1,7 @@ import options, tables, - chronos, json_serialization, strutils, chronicles, metrics, eth/net/nat, + chronos, json_serialization, strutils, chronicles, metrics, + eth/net/nat, eth/p2p/discoveryv5/enr, version, conf const @@ -126,6 +127,10 @@ when networkBackend == rlpx: proc initAddress*(T: type BootstrapAddr, str: string): T = initENode(str) + proc initAddress*(T: type BootstrapAddr, ip: IpAddress, tcpPort: Port): T = + # TODO + discard + func peersCount*(node: Eth2Node): int = node.peerPool.len @@ -178,6 +183,12 @@ else: raise newException(MultiAddressError, "Invalid bootstrap node multi-address") + template tcpEndPoint(address, port): auto = + MultiAddress.init(address, Protocol.IPPROTO_TCP, port) + + proc initAddress*(T: type BootstrapAddr, ip: IpAddress, tcpPort: Port): T = + tcpEndPoint(ip, tcpPort) + proc ensureNetworkIdFile(conf: BeaconNodeConf): string = result = conf.dataDir / networkKeyFilename if not fileExists(result): @@ -198,15 +209,13 @@ else: result = KeyPair(seckey: privKey, pubkey: privKey.getKey()) - template tcpEndPoint(address, port): auto = - MultiAddress.init(address, Protocol.IPPROTO_TCP, port) - proc allMultiAddresses(nodes: seq[BootstrapAddr]): seq[string] = for node in nodes: result.add $node proc createEth2Node*(conf: BeaconNodeConf, - bootstrapNodes: seq[BootstrapAddr]): Future[Eth2Node] {.async.} = + bootstrapNodes: seq[BootstrapAddr], + bootstrapEnrs: seq[enr.Record]): Future[Eth2Node] {.async.} = var (extIp, extTcpPort, _) = setupNat(conf) hostAddress = tcpEndPoint(globalListeningAddr, Port conf.tcpPort) @@ -222,8 +231,11 @@ else: # TODO nim-libp2p still doesn't have support for announcing addresses # that are different from the host address (this is relevant when we # are running behind a NAT). - result = Eth2Node.init newStandardSwitch(some keys.seckey, hostAddress, - triggerSelf = true, gossip = true) + var switch = newStandardSwitch(some keys.seckey, hostAddress, + triggerSelf = true, gossip = true) + result = Eth2Node.init(conf, switch, keys.seckey) + for enr in bootstrapEnrs: + result.addKnownPeer(enr) await result.start() else: let keyFile = conf.ensureNetworkIdFile diff --git a/beacon_chain/libp2p_backend.nim b/beacon_chain/libp2p_backend.nim index 4a93cfb3d..59a9cf6a6 100644 --- a/beacon_chain/libp2p_backend.nim +++ b/beacon_chain/libp2p_backend.nim @@ -3,6 +3,7 @@ import stew/[varints,base58], stew/shims/[macros, tables], chronos, chronicles, faststreams/output_stream, serialization, json_serialization/std/options, eth/p2p/p2p_protocol_dsl, + eth/p2p/discoveryv5/enr, # TODO: create simpler to use libp2p modules that use re-exports libp2p/[switch, multistream, connection, multiaddress, peerinfo, peer, @@ -11,7 +12,10 @@ import libp2p/protocols/secure/[secure, secio], libp2p/protocols/pubsub/[pubsub, floodsub], libp2p/transports/[transport, tcptransport], - libp2p_json_serialization, ssz + libp2p_json_serialization, eth2_discovery, conf, ssz + +import + eth/p2p/discoveryv5/protocol as discv5_protocol export p2pProtocol, libp2p_json_serialization, ssz @@ -22,7 +26,10 @@ type # TODO Is this really needed? Eth2Node* = ref object of RootObj switch*: Switch + discovery*: Eth2DiscoveryProtocol + wantedPeers*: int peers*: Table[PeerID, Peer] + peersByDiscoveryId*: Table[Eth2DiscoveryId, Peer] protocolStates*: seq[RootRef] libp2pTransportLoops*: seq[Future[void]] @@ -32,6 +39,7 @@ type network*: Eth2Node info*: PeerInfo wasDialed*: bool + discoveryId*: Eth2DiscoveryId connectionState*: ConnectionState protocolStates*: seq[RootRef] maxInactivityAllowed*: Duration @@ -139,10 +147,26 @@ include eth/p2p/p2p_backends_helpers include eth/p2p/p2p_tracing include libp2p_backends_common -proc init*(T: type Eth2Node, switch: Switch): T = +proc dialPeer*(node: Eth2Node, enr: enr.Record) {.async.} = + discard + +proc runDiscoveryLoop(node: Eth2Node) {.async.} = + while true: + if node.peersByDiscoveryId.len < node.wantedPeers: + let discoveredPeers = await node.discovery.lookupRandom() + for peer in discoveredPeers: + if peer.id notin node.peersByDiscoveryId: + # TODO do this in parallel + await node.dialPeer(peer.record) + + await sleepAsync seconds(1) + +proc init*(T: type Eth2Node, conf: BeaconNodeConf, + switch: Switch, privKey: PrivateKey): T = new result result.switch = switch result.peers = initTable[PeerID, Peer]() + result.discovery = Eth2DiscoveryProtocol.new(conf, privKey.getBytes) newSeq result.protocolStates, allProtocols.len for proto in allProtocols: @@ -153,7 +177,12 @@ proc init*(T: type Eth2Node, switch: Switch): T = if msg.protocolMounter != nil: msg.protocolMounter result +proc addKnownPeer*(node: Eth2Node, peerEnr: enr.Record) = + node.discovery.addNode peerEnr + proc start*(node: Eth2Node) {.async.} = + node.discovery.open() + node.discovery.start() node.libp2pTransportLoops = await node.switch.start() proc init*(T: type Peer, network: Eth2Node, info: PeerInfo): Peer = diff --git a/beacon_chain/nimquery.nim b/beacon_chain/nimquery.nim new file mode 100644 index 000000000..18bd2a6c7 --- /dev/null +++ b/beacon_chain/nimquery.nim @@ -0,0 +1,120 @@ +import + strutils, strformat, parseutils + +type + TokenKind* = enum + tIdent = "ident" + tNumber = "number" + tDot = "dot" + tOpenBracket = "[" + tCloseBracket = "]" + tEof = "end of file" + tError = "error" + + Token* = object + case kind*: TokenKind + of tIdent: + name*: string + of tNumber: + val*: uint64 + of tError: + errMsg: string + else: + discard + + Lexer* = object + tok*: Token + input: string + pos: int + + Parser* = object + lexer: Lexer + + NodeKind* = enum + Ident + Number + Dot + ArrayAccess + Error + + Node* = ref object {.acyclic.} + case kind*: NodeKind + of Dot: + objVal*, field*: Node + of ArrayAccess: + arrayVal*, index*: Node + of Ident: + name*: string + of Number: + numVal*: uint64 + of Error: + errMsg*: string + +func advance(lexer: var Lexer) = + if lexer.pos >= lexer.input.len: + lexer.tok = Token(kind: tEof) + else: + let nextChar = lexer.input[lexer.pos] + case nextChar + of IdentStartChars: + lexer.tok = Token(kind: tIdent) + lexer.pos = parseIdent(lexer.input, lexer.tok.name, lexer.pos) + of Whitespace: + lexer.pos = skipWhitespace(lexer.input, lexer.pos) + advance lexer + of Digits: + lexer.tok = Token(kind: tNumber) + lexer.pos = parseBiggestUInt(lexer.input, lexer.tok.val, lexer.pos) + of '[': + lexer.tok = Token(kind: tOpenBracket) + inc lexer.pos + of ']': + lexer.tok = Token(kind: tCloseBracket) + inc lexer.pos + of '.': + lexer.tok = Token(kind: tDot) + inc lexer.pos + else: + lexer.tok = Token( + kind: tError, + errMsg: &"Unexpected character '{nextChar}' at position {lexer.pos}") + +func init*(T: type Lexer, src: string): Lexer = + result.input = src + result.pos = 0 + advance result + +func init*(T: type Parser, src: string): Parser = + Parser(lexer: Lexer.init(src)) + +func expr(parser: var Parser): Node = + template unexpectedToken = + return Node(kind: Error, errMsg: &"Unexpected {parser.lexer.tok.kind} token") + + case parser.lexer.tok.kind + of tIdent: + result = Node(kind: Ident, name: parser.lexer.tok.name) + of tNumber: + return Node(kind: Number, numVal: parser.lexer.tok.val) + else: + unexpectedToken + + advance parser.lexer + case parser.lexer.tok.kind + of tOpenBracket: + advance parser.lexer + result = Node(kind: ArrayAccess, arrayVal: result, index: parser.expr) + if parser.lexer.tok.kind != tCloseBracket: + unexpectedToken + else: + advance parser.lexer + of tDot: + advance parser.lexer + return Node(kind: Dot, objVal: result, field: parser.expr) + else: + discard + +func parse*(input: string): Node = + var p = Parser.init(input) + p.expr + diff --git a/beacon_chain/version.nim b/beacon_chain/version.nim index 963408ead..8502e452d 100644 --- a/beacon_chain/version.nim +++ b/beacon_chain/version.nim @@ -5,7 +5,7 @@ type rlpx const - NETWORK_TYPE {.strdefine.} = "libp2p_daemon" + NETWORK_TYPE {.strdefine.} = "libp2p" networkBackend* = when NETWORK_TYPE == "rlpx": rlpx elif NETWORK_TYPE == "libp2p": libp2p diff --git a/vendor/nim-eth b/vendor/nim-eth index 655fc4375..3527d47cb 160000 --- a/vendor/nim-eth +++ b/vendor/nim-eth @@ -1 +1 @@ -Subproject commit 655fc43751f203acdc525bab688115043f504b87 +Subproject commit 3527d47cb5c7911741766a38d56c4b153e06e155 diff --git a/vendor/nim-stint b/vendor/nim-stint index 25c2604b4..6f665a4fa 160000 --- a/vendor/nim-stint +++ b/vendor/nim-stint @@ -1 +1 @@ -Subproject commit 25c2604b4b41d1b13f4a2740486507fe5f63086e +Subproject commit 6f665a4fa2f08a878d1b8e442293eab8218f3fb8