nimbus-eth2/beacon_chain/eth2_discovery.nim

168 lines
5.7 KiB
Nim

import
os, net, strutils, strformat, parseutils,
chronicles, stew/[result, objects], eth/keys, eth/trie/db, eth/p2p/enode,
eth/p2p/discoveryv5/[enr, protocol, discovery_db, types],
libp2p/[multiaddress, peer],
libp2p/crypto/crypto as libp2pCrypto,
conf
type
Eth2DiscoveryProtocol* = protocol.Protocol
Eth2DiscoveryId* = NodeId
PublicKey = keys.PublicKey
export
Eth2DiscoveryProtocol, open, close, result
proc new*(T: type Eth2DiscoveryProtocol,
conf: BeaconNodeConf,
ip: IpAddress, 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, ip, Port conf.tcpPort, Port conf.udpPort)
proc toENode*(a: MultiAddress): Result[ENode, cstring] =
if not IPFS.match(a):
return err "Unsupported MultiAddress"
try:
# TODO. This code is quite messy with so much string handling.
# MultiAddress can offer a more type-safe API?
var
peerId = PeerID.init(a[2].protoAddress())
addressFragments = split($a[0], "/")
portFragments = split($a[1], "/")
tcpPort: int
if addressFragments.len != 3 or
addressFragments[1] != "ip4" or
portFragments.len != 3 or
portFragments[1] notin ["tcp", "udp"] or
parseInt(portFragments[2], tcpPort) == 0:
return err "Only IPv4 MultiAddresses are supported"
let
ipAddress = parseIpAddress(addressFragments[2])
# TODO. The multiaddress will have either a TCP or a UDP value, but
# is it reasonable to assume that a client will use the same ports?
# Probably not, but how can we bootstrap then?
udpPort = tcpPort
var pubkey: libp2pCrypto.PublicKey
if peerId.extractPublicKey(pubkey):
if pubkey.scheme == Secp256k1:
return ok ENode(pubkey: pubkey.skkey,
address: Address(ip: ipAddress,
tcpPort: Port tcpPort,
udpPort: Port udpPort))
except CatchableError:
# This will reach the error exit path below
discard
return err "Invalid MultiAddress"
proc toMultiAddressStr*(enode: ENode): string =
var peerId = PeerID.init(libp2pCrypto.PublicKey(scheme: Secp256k1,
skkey: enode.pubkey))
&"/ip4/{enode.address.ip}/tcp/{enode.address.tcpPort}/p2p/{peerId.pretty}"
proc toENode*(enrRec: enr.Record): Result[ENode, cstring] =
try:
# TODO: handle IPv6
let ipBytes = enrRec.get("ip", seq[byte])
if ipBytes.len != 4:
return err "Malformed ENR IP address"
let
ip = IpAddress(family: IpAddressFamily.IPv4,
address_v4: toArray(4, ipBytes))
tcpPort = Port enrRec.get("tcp", uint16)
udpPort = Port enrRec.get("udp", uint16)
var pubKey: keys.PublicKey
if not enrRec.get(pubKey):
return err "Failed to read public key from ENR record"
return ok ENode(pubkey: pubkey,
address: Address(ip: ip,
tcpPort: tcpPort,
udpPort: udpPort))
except CatchableError:
return err "Invalid ENR record"
# TODO
# This will be resoted to its more generalized form (returning ENode)
# once we refactor the discv5 code to be more easily bootstrapped with
# trusted, but non-signed bootstrap addresses.
proc parseBootstrapAddress*(address: TaintedString): Result[enr.Record, cstring] =
if address.len == 0:
return err "an empty string is not a valid bootstrap node"
logScope:
address = string(address)
if address[0] == '/':
return err "MultiAddress bootstrap addresses are not supported"
#[
try:
let ma = MultiAddress.init(address)
return toENode(ma)
except CatchableError:
return err "Invalid bootstrap multiaddress"
]#
else:
let lowerCaseAddress = toLowerAscii(string address)
if lowerCaseAddress.startsWith("enr:"):
var enrRec: enr.Record
if enrRec.fromURI(string address):
return ok enrRec
return err "Invalid ENR bootstrap record"
elif lowerCaseAddress.startsWith("enode:"):
return err "ENode bootstrap addresses are not supported"
#[
try:
return ok initEnode(string address)
except CatchableError as err:
return err "Ignoring invalid enode bootstrap address"
]#
else:
return err "Ignoring unrecognized bootstrap address type"
proc addBootstrapNode*(bootstrapAddr: string,
bootNodes: var seq[ENode],
bootEnrs: var seq[enr.Record],
localPubKey: PublicKey) =
let enrRes = parseBootstrapAddress(bootstrapAddr)
if enrRes.isOk:
let enodeRes = enrRes.value.toENode
if enodeRes.isOk:
if enodeRes.value.pubKey != localPubKey:
bootEnrs.add enrRes.value
else:
warn "Ignoring invalid bootstrap address",
bootstrapAddr, reason = enrRes.error
proc loadBootstrapFile*(bootstrapFile: string,
bootNodes: var seq[ENode],
bootEnrs: var seq[enr.Record],
localPubKey: PublicKey) =
if bootstrapFile.len == 0: return
let ext = splitFile(bootstrapFile).ext
if cmpIgnoreCase(ext, ".txt") == 0:
for ln in lines(bootstrapFile):
addBootstrapNode(ln, bootNodes, bootEnrs, localPubKey)
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):
addBootstrapNode(string(ln[3..^2]), bootNodes, bootEnrs, localPubKey)
else:
error "Unknown bootstrap file format", ext
quit 1