nimbus-eth2/beacon_chain/eth2_discovery.nim

188 lines
6.6 KiB
Nim

# TODO Cannot use push here becaise it gets applied to PeerID.init (!)
# probably because it's a generic proc...
# {.push raises: [Defect].}
import
os, net, strutils, strformat, parseutils,
chronicles, stew/[results, 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, start, close, results
proc toENode*(a: MultiAddress): Result[ENode, cstring] {.raises: [Defect].} =
try:
if not IPFS.match(a):
return err "Unsupported MultiAddress"
# 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: PublicKey(pubkey.skkey),
address: Address(ip: ipAddress,
tcpPort: Port tcpPort,
udpPort: Port udpPort))
except CatchableError:
# This will reach the error exit path below
discard
except Exception as e:
# TODO:
# nim-libp2p/libp2p/multiaddress.nim(616, 40) Error: can raise an unlisted exception: Exception
if e of Defect:
raise (ref Defect)(e)
return err "Invalid MultiAddress"
proc toMultiAddressStr*(enode: ENode): string =
var peerId = PeerID.init(libp2pCrypto.PublicKey(
scheme: Secp256k1, skkey: SkPublicKey(enode.pubkey)))
&"/ip4/{enode.address.ip}/tcp/{enode.address.tcpPort}/p2p/{peerId.pretty}"
proc toENode*(enrRec: enr.Record): Result[ENode, cstring] {.raises: [Defect].} =
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)
let pubkey = enrRec.get(PublicKey)
if pubkey.isNone:
return err "Failed to read public key from ENR record"
return ok ENode(pubkey: pubkey.get(),
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
proc new*(T: type Eth2DiscoveryProtocol,
conf: BeaconNodeConf,
ip: Option[IpAddress], tcpPort, udpPort: Port,
rawPrivKeyBytes: openarray[byte]): T =
# TODO
# Implement more configuration options:
# * for setting up a specific key
# * for using a persistent database
var
pk = PrivateKey.fromRaw(rawPrivKeyBytes).tryGet()
ourPubKey = pk.toPublicKey().tryGet()
db = DiscoveryDB.init(newMemoryDB())
var bootNodes: seq[ENode]
var bootEnrs: seq[enr.Record]
for node in conf.bootstrapNodes:
addBootstrapNode(node, bootNodes, bootEnrs, ourPubKey)
loadBootstrapFile(string conf.bootstrapNodesFile, bootNodes, bootEnrs, ourPubKey)
let persistentBootstrapFile = conf.dataDir / "bootstrap_nodes.txt"
if fileExists(persistentBootstrapFile):
loadBootstrapFile(persistentBootstrapFile, bootNodes, bootEnrs, ourPubKey)
newProtocol(pk, db, ip, tcpPort, udpPort, bootEnrs)