2019-03-05 22:54:08 +00:00
|
|
|
import
|
2019-05-22 07:13:15 +00:00
|
|
|
options, tables,
|
2019-10-25 17:15:12 +00:00
|
|
|
chronos, json_serialization, strutils, chronicles, metrics, eth/net/nat,
|
initial 0.9.0 spec sync (#509)
* rename compute_epoch_of_slot(...) to compute_epoch_at_slot(...)
* remove some unnecessary imports; remove some crosslink-related code and tests; complete renaming of compute_epoch_of_slot(...) to compute_epoch_at_slot(...)
* rm more transfer-related code and tests; rm more unnecessary strutils imports
* rm remaining unused imports
* remove useless get_empty_per_epoch_cache(...)/compute_start_slot_of_epoch(...) calls
* rename compute_start_slot_of_epoch(...) to compute_start_slot_at_epoch(...)
* rename ACTIVATION_EXIT_DELAY to MAX_SEED_LOOKAHEAD
* update domain types to 0.9.0
* mark AttesterSlashing, IndexedAttestation, AttestationDataAndCustodyBit, DepositData, BeaconBlockHeader, Fork, integer_squareroot(...), and process_voluntary_exit(...) as 0.9.0
* mark increase_balance(...), decrease_balance(...), get_block_root(...), CheckPoint, Deposit, PendingAttestation, HistoricalBatch, is_active_validator(...), and is_slashable_attestation_data(...) as 0.9.0
* mark compute_activation_exit_epoch(...), bls_verify(...), Validator, get_active_validator_indices(...), get_current_epoch(...), get_total_active_balance(...), and get_previous_epoch(...) as 0.9.0
* mark get_block_root_at_slot(...), ProposerSlashing, get_domain(...), VoluntaryExit, mainnet preset Gwei values, minimal preset max operations, process_block_header(...), and is_slashable_validator(...) as 0.9.0
* mark makeWithdrawalCredentials(...), get_validator_churn_limit(...), get_total_balance(...), is_valid_indexed_attestation(...), bls_aggregate_pubkeys(...), initial genesis value/constants, Attestation, get_randao_mix(...), mainnet preset max operations per block constants, minimal preset Gwei values and time parameters, process_eth1_data(...), get_shuffled_seq(...), compute_committee(...), and process_slots(...) as 0.9.0; partially update get_indexed_attestation(...) to 0.9.0 by removing crosslink refs and associated tests
* mark initiate_validator_exit(...), process_registry_updates(...), BeaconBlock, Eth1Data, compute_domain(...), process_randao(...), process_attester_slashing(...), get_base_reward(...), and process_slot(...) as 0.9.0
2019-10-30 19:41:19 +00:00
|
|
|
version, conf
|
2019-03-05 22:54:08 +00:00
|
|
|
|
|
|
|
const
|
2019-11-12 00:05:35 +00:00
|
|
|
clientId* = "Nimbus beacon node v" & fullVersionStr
|
2019-03-05 22:54:08 +00:00
|
|
|
|
2019-06-12 18:22:05 +00:00
|
|
|
export
|
|
|
|
version
|
|
|
|
|
|
|
|
let
|
|
|
|
globalListeningAddr = parseIpAddress("0.0.0.0")
|
|
|
|
|
2019-10-25 17:15:12 +00:00
|
|
|
# Metrics for tracking attestation and beacon block loss
|
|
|
|
declareCounter gossip_messages_sent,
|
|
|
|
"Number of gossip messages sent by this peer"
|
|
|
|
|
|
|
|
declareCounter gossip_messages_received,
|
|
|
|
"Number of gossip messages received by this peer"
|
|
|
|
|
2019-06-12 18:22:05 +00:00
|
|
|
proc setupNat(conf: BeaconNodeConf): tuple[ip: IpAddress,
|
|
|
|
tcpPort: Port,
|
|
|
|
udpPort: Port] =
|
|
|
|
# defaults
|
|
|
|
result.ip = globalListeningAddr
|
|
|
|
result.tcpPort = Port(conf.tcpPort)
|
|
|
|
result.udpPort = Port(conf.udpPort)
|
|
|
|
|
|
|
|
var nat: NatStrategy
|
|
|
|
case conf.nat.toLowerAscii:
|
|
|
|
of "any":
|
|
|
|
nat = NatAny
|
|
|
|
of "none":
|
|
|
|
nat = NatNone
|
|
|
|
of "upnp":
|
|
|
|
nat = NatUpnp
|
|
|
|
of "pmp":
|
|
|
|
nat = NatPmp
|
|
|
|
else:
|
|
|
|
if conf.nat.startsWith("extip:") and isIpAddress(conf.nat[6..^1]):
|
|
|
|
# any required port redirection is assumed to be done by hand
|
|
|
|
result.ip = parseIpAddress(conf.nat[6..^1])
|
|
|
|
nat = NatNone
|
|
|
|
else:
|
|
|
|
error "not a valid NAT mechanism, nor a valid IP address", value = conf.nat
|
|
|
|
quit(QuitFailure)
|
|
|
|
|
|
|
|
if nat != NatNone:
|
|
|
|
let extIP = getExternalIP(nat)
|
|
|
|
if extIP.isSome:
|
|
|
|
result.ip = extIP.get()
|
|
|
|
let extPorts = redirectPorts(tcpPort = result.tcpPort,
|
|
|
|
udpPort = result.udpPort,
|
|
|
|
description = clientId)
|
|
|
|
if extPorts.isSome:
|
|
|
|
(result.tcpPort, result.udpPort) = extPorts.get()
|
|
|
|
|
2019-05-31 18:36:32 +00:00
|
|
|
when networkBackend == rlpxBackend:
|
2019-03-05 22:54:08 +00:00
|
|
|
import
|
2019-03-18 03:54:08 +00:00
|
|
|
os,
|
2019-06-12 18:22:05 +00:00
|
|
|
eth/[rlp, p2p, keys], gossipsub_protocol,
|
2019-03-22 11:33:10 +00:00
|
|
|
eth/p2p/peer_pool # for log on connected peers
|
2019-03-05 22:54:08 +00:00
|
|
|
|
|
|
|
export
|
|
|
|
p2p, rlp, gossipsub_protocol
|
|
|
|
|
2019-03-19 19:50:22 +00:00
|
|
|
const
|
|
|
|
netBackendName* = "rlpx"
|
2019-06-03 17:07:50 +00:00
|
|
|
IrrelevantNetwork* = UselessPeer
|
2019-03-19 19:50:22 +00:00
|
|
|
|
2019-03-05 22:54:08 +00:00
|
|
|
type
|
|
|
|
Eth2Node* = EthereumNode
|
2019-05-22 07:13:15 +00:00
|
|
|
Eth2NodeIdentity* = KeyPair
|
2019-03-05 22:54:08 +00:00
|
|
|
BootstrapAddr* = ENode
|
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
proc getPersistentNetIdentity*(conf: BeaconNodeConf): Eth2NodeIdentity =
|
2019-03-18 03:54:08 +00:00
|
|
|
let privateKeyFile = conf.dataDir / "network.privkey"
|
|
|
|
var privKey: PrivateKey
|
|
|
|
if not fileExists(privateKeyFile):
|
|
|
|
privKey = newPrivateKey()
|
2019-03-19 17:22:17 +00:00
|
|
|
createDir conf.dataDir.string
|
2019-03-18 03:54:08 +00:00
|
|
|
writeFile(privateKeyFile, $privKey)
|
|
|
|
else:
|
|
|
|
privKey = initPrivateKey(readFile(privateKeyFile).string)
|
|
|
|
|
2019-03-19 17:22:17 +00:00
|
|
|
KeyPair(seckey: privKey, pubkey: privKey.getPublicKey())
|
|
|
|
|
|
|
|
proc getPersistenBootstrapAddr*(conf: BeaconNodeConf,
|
|
|
|
ip: IpAddress, port: Port): BootstrapAddr =
|
2019-03-05 22:54:08 +00:00
|
|
|
let
|
2019-05-22 07:13:15 +00:00
|
|
|
identity = getPersistentNetIdentity(conf)
|
2019-03-19 17:22:17 +00:00
|
|
|
address = Address(ip: ip, tcpPort: port, udpPort: port)
|
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
initENode(identity.pubKey, address)
|
|
|
|
|
|
|
|
proc isSameNode*(bootstrapNode: BootstrapAddr, id: Eth2NodeIdentity): bool =
|
|
|
|
bootstrapNode.pubKey == id.pubKey
|
|
|
|
|
|
|
|
proc shortForm*(id: Eth2NodeIdentity): string =
|
|
|
|
($id.pubKey)[0..5]
|
2019-03-19 17:22:17 +00:00
|
|
|
|
|
|
|
proc writeValue*(writer: var JsonWriter, value: BootstrapAddr) {.inline.} =
|
|
|
|
writer.writeValue $value
|
|
|
|
|
|
|
|
proc readValue*(reader: var JsonReader, value: var BootstrapAddr) {.inline.} =
|
|
|
|
value = initENode reader.readValue(string)
|
|
|
|
|
2019-08-05 00:00:49 +00:00
|
|
|
proc createEth2Node*(conf: BeaconNodeConf,
|
|
|
|
bootstrapNodes: seq[BootstrapAddr]): Future[EthereumNode] {.async.} =
|
2019-03-19 17:22:17 +00:00
|
|
|
let
|
2019-05-22 07:13:15 +00:00
|
|
|
keys = getPersistentNetIdentity(conf)
|
2019-04-18 00:02:14 +00:00
|
|
|
(ip, tcpPort, udpPort) = setupNat(conf)
|
|
|
|
address = Address(ip: ip,
|
|
|
|
tcpPort: tcpPort,
|
|
|
|
udpPort: udpPort)
|
2019-03-05 22:54:08 +00:00
|
|
|
|
2019-03-19 17:22:17 +00:00
|
|
|
# TODO there are more networking options to add here: local bind ip, ipv6
|
|
|
|
# etc.
|
2019-03-12 13:14:30 +00:00
|
|
|
return newEthereumNode(keys, address, 0,
|
2019-03-28 11:56:44 +00:00
|
|
|
nil, clientId)
|
2019-03-05 22:54:08 +00:00
|
|
|
|
|
|
|
proc saveConnectionAddressFile*(node: Eth2Node, filename: string) =
|
|
|
|
writeFile(filename, $node.listeningAddress)
|
|
|
|
|
2019-11-12 19:56:37 +00:00
|
|
|
proc initAddress*(T: type BootstrapAddr, str: string): T =
|
2019-03-05 22:54:08 +00:00
|
|
|
initENode(str)
|
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
func peersCount*(node: Eth2Node): int =
|
|
|
|
node.peerPool.len
|
2019-03-22 11:33:10 +00:00
|
|
|
|
2019-03-05 22:54:08 +00:00
|
|
|
else:
|
2019-06-03 17:07:50 +00:00
|
|
|
import
|
2019-10-28 23:04:52 +00:00
|
|
|
os, random,
|
|
|
|
stew/io, eth/async_utils, libp2p/crypto/crypto,
|
|
|
|
ssz
|
2019-05-31 18:36:32 +00:00
|
|
|
|
2019-10-23 10:44:31 +00:00
|
|
|
when networkBackend == libp2pBackend:
|
|
|
|
import
|
|
|
|
libp2p_backend
|
|
|
|
|
|
|
|
export
|
|
|
|
libp2p_backend
|
|
|
|
|
|
|
|
else:
|
|
|
|
import
|
2019-11-12 19:56:37 +00:00
|
|
|
libp2p/daemon/daemonapi, libp2p/multiaddress, libp2p_daemon_backend
|
2019-10-23 10:44:31 +00:00
|
|
|
|
|
|
|
export
|
|
|
|
libp2p_daemon_backend
|
2019-06-03 17:07:50 +00:00
|
|
|
|
2019-08-05 00:00:49 +00:00
|
|
|
const
|
|
|
|
netBackendName* = "libp2p"
|
|
|
|
networkKeyFilename = "privkey.protobuf"
|
2019-06-03 17:07:50 +00:00
|
|
|
|
2019-03-05 22:54:08 +00:00
|
|
|
type
|
2019-11-12 19:56:37 +00:00
|
|
|
BootstrapAddr* = MultiAddress
|
|
|
|
Eth2NodeIdentity* = KeyPair
|
|
|
|
|
|
|
|
proc initAddress*(T: type BootstrapAddr, str: string): T =
|
|
|
|
let address = MultiAddress.init(str)
|
|
|
|
if IPFS.match(address) and matchPartial(multiaddress.TCP, address):
|
|
|
|
result = address
|
2019-10-28 23:04:52 +00:00
|
|
|
else:
|
2019-11-12 19:56:37 +00:00
|
|
|
raise newException(MultiAddressError,
|
|
|
|
"Invalid bootstrap node multi-address")
|
2019-03-05 22:54:08 +00:00
|
|
|
|
2019-06-12 12:23:05 +00:00
|
|
|
proc ensureNetworkIdFile(conf: BeaconNodeConf): string =
|
|
|
|
result = conf.dataDir / networkKeyFilename
|
|
|
|
if not fileExists(result):
|
|
|
|
createDir conf.dataDir.string
|
2019-09-10 00:16:01 +00:00
|
|
|
let pk = PrivateKey.random(Secp256k1)
|
2019-06-12 12:23:05 +00:00
|
|
|
writeFile(result, pk.getBytes)
|
2019-03-05 22:54:08 +00:00
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
proc getPersistentNetIdentity*(conf: BeaconNodeConf): Eth2NodeIdentity =
|
2019-11-12 19:56:37 +00:00
|
|
|
let privateKeyFile = conf.dataDir / networkKeyFilename
|
|
|
|
var privKey: PrivateKey
|
|
|
|
if not fileExists(privateKeyFile):
|
|
|
|
createDir conf.dataDir.string
|
|
|
|
privKey = PrivateKey.random(Secp256k1)
|
|
|
|
writeFile(privateKeyFile, privKey.getBytes())
|
|
|
|
else:
|
|
|
|
let strdata = readFile(privateKeyFile)
|
|
|
|
privKey = PrivateKey.init(cast[seq[byte]](strdata))
|
|
|
|
|
|
|
|
result = KeyPair(seckey: privKey, pubkey: privKey.getKey())
|
2019-05-22 07:13:15 +00:00
|
|
|
|
2019-06-17 11:08:05 +00:00
|
|
|
template tcpEndPoint(address, port): auto =
|
2019-06-27 12:52:32 +00:00
|
|
|
MultiAddress.init(address, Protocol.IPPROTO_TCP, port)
|
2019-06-17 11:08:05 +00:00
|
|
|
|
2019-07-11 02:36:07 +00:00
|
|
|
var mainDaemon: DaemonAPI
|
|
|
|
|
2019-08-05 00:00:49 +00:00
|
|
|
proc allMultiAddresses(nodes: seq[BootstrapAddr]): seq[string] =
|
|
|
|
for node in nodes:
|
2019-11-12 19:56:37 +00:00
|
|
|
result.add $node
|
2019-08-05 00:00:49 +00:00
|
|
|
|
|
|
|
proc createEth2Node*(conf: BeaconNodeConf,
|
|
|
|
bootstrapNodes: seq[BootstrapAddr]): Future[Eth2Node] {.async.} =
|
2019-06-12 18:22:05 +00:00
|
|
|
var
|
2019-11-15 22:37:39 +00:00
|
|
|
(extIp, extTcpPort, _) = setupNat(conf)
|
2019-06-12 18:22:05 +00:00
|
|
|
hostAddress = tcpEndPoint(globalListeningAddr, Port conf.tcpPort)
|
2019-11-05 22:56:10 +00:00
|
|
|
announcedAddresses = if extIp == globalListeningAddr: @[]
|
2019-06-12 18:22:05 +00:00
|
|
|
else: @[tcpEndPoint(extIp, extTcpPort)]
|
2019-06-17 11:08:05 +00:00
|
|
|
keyFile = conf.ensureNetworkIdFile
|
2019-06-12 18:22:05 +00:00
|
|
|
|
2019-08-05 00:00:49 +00:00
|
|
|
info "Starting the LibP2P daemon", hostAddress, announcedAddresses,
|
|
|
|
keyFile, bootstrapNodes
|
|
|
|
|
|
|
|
var daemonFut = if bootstrapNodes.len == 0:
|
2019-11-05 00:41:12 +00:00
|
|
|
newDaemonApi({PSNoSign, DHTFull, PSFloodSub},
|
2019-08-05 00:00:49 +00:00
|
|
|
id = keyFile,
|
|
|
|
hostAddresses = @[hostAddress],
|
|
|
|
announcedAddresses = announcedAddresses)
|
|
|
|
else:
|
2019-11-05 00:41:12 +00:00
|
|
|
newDaemonApi({PSNoSign, DHTFull, PSFloodSub, WaitBootstrap},
|
2019-08-05 00:00:49 +00:00
|
|
|
id = keyFile,
|
|
|
|
hostAddresses = @[hostAddress],
|
|
|
|
announcedAddresses = announcedAddresses,
|
|
|
|
bootstrapNodes = allMultiAddresses(bootstrapNodes),
|
|
|
|
peersRequired = 1)
|
|
|
|
|
|
|
|
mainDaemon = await daemonFut
|
2019-11-12 19:56:37 +00:00
|
|
|
var identity = await mainDaemon.identity()
|
|
|
|
|
|
|
|
info "LibP2P daemon started", peer = identity.peer.pretty(),
|
|
|
|
addresses = identity.addresses
|
2019-07-11 02:36:07 +00:00
|
|
|
|
|
|
|
proc closeDaemon() {.noconv.} =
|
|
|
|
info "Shutting down the LibP2P daemon"
|
|
|
|
waitFor mainDaemon.close()
|
|
|
|
addQuitProc(closeDaemon)
|
2019-06-12 18:22:05 +00:00
|
|
|
|
2019-07-11 02:36:07 +00:00
|
|
|
return await Eth2Node.init(mainDaemon)
|
2019-06-12 12:23:05 +00:00
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
proc getPersistenBootstrapAddr*(conf: BeaconNodeConf,
|
|
|
|
ip: IpAddress, port: Port): BootstrapAddr =
|
2019-11-12 19:56:37 +00:00
|
|
|
let pair = getPersistentNetIdentity(conf)
|
|
|
|
let pidma = MultiAddress.init(multiCodec("p2p"), PeerID.init(pair.pubkey))
|
|
|
|
result = tcpEndPoint(ip, port) & pidma
|
2019-05-22 07:13:15 +00:00
|
|
|
|
|
|
|
proc isSameNode*(bootstrapNode: BootstrapAddr, id: Eth2NodeIdentity): bool =
|
2019-11-12 19:56:37 +00:00
|
|
|
if IPFS.match(bootstrapNode):
|
|
|
|
let pid1 = PeerID.init(bootstrapNode[2].protoAddress())
|
|
|
|
let pid2 = PeerID.init(id.pubkey)
|
|
|
|
result = (pid1 == pid2)
|
2019-05-22 07:13:15 +00:00
|
|
|
|
|
|
|
proc shortForm*(id: Eth2NodeIdentity): string =
|
2019-11-12 19:56:37 +00:00
|
|
|
$PeerID.init(id.pubkey)
|
2019-11-06 14:56:54 +00:00
|
|
|
|
2019-11-12 19:56:37 +00:00
|
|
|
proc connectToNetwork*(node: Eth2Node,
|
|
|
|
bootstrapNodes: seq[MultiAddress]) {.async.} =
|
2019-11-06 14:56:54 +00:00
|
|
|
# TODO: perhaps we should do these in parallel
|
|
|
|
var connected = false
|
|
|
|
for bootstrapNode in bootstrapNodes:
|
|
|
|
try:
|
2019-11-12 19:56:37 +00:00
|
|
|
if IPFS.match(bootstrapNode):
|
|
|
|
let pid = PeerID.init(bootstrapNode[2].protoAddress())
|
|
|
|
await node.daemon.connect(pid, @[bootstrapNode[0] & bootstrapNode[1]])
|
|
|
|
var peer = node.getPeer(pid)
|
|
|
|
peer.wasDialed = true
|
|
|
|
await initializeConnection(peer)
|
|
|
|
connected = true
|
|
|
|
else:
|
|
|
|
raise newException(CatchableError, "Incorrect bootstrap address")
|
2019-11-06 14:56:54 +00:00
|
|
|
except CatchableError as err:
|
|
|
|
error "Failed to connect to bootstrap node",
|
|
|
|
node = bootstrapNode, err = err.msg
|
|
|
|
|
|
|
|
if bootstrapNodes.len > 0 and connected == false:
|
|
|
|
fatal "Failed to connect to any bootstrap node. Quitting."
|
|
|
|
quit 1
|
2019-06-24 16:27:19 +00:00
|
|
|
|
2019-03-05 22:54:08 +00:00
|
|
|
proc saveConnectionAddressFile*(node: Eth2Node, filename: string) =
|
|
|
|
let id = waitFor node.daemon.identity()
|
2019-11-25 19:56:28 +00:00
|
|
|
writeFile(filename, $id.addresses[0] & "/p2p/" & id.peer.pretty)
|
2019-03-05 22:54:08 +00:00
|
|
|
|
|
|
|
proc loadConnectionAddressFile*(filename: string): PeerInfo =
|
|
|
|
Json.loadFile(filename, PeerInfo)
|
|
|
|
|
2019-05-22 07:13:15 +00:00
|
|
|
func peersCount*(node: Eth2Node): int =
|
|
|
|
node.peers.len
|
|
|
|
|
2019-09-29 17:42:53 +00:00
|
|
|
proc makeMessageHandler[MsgType](msgHandler: proc(msg: MsgType) {.gcsafe.}): P2PPubSubCallback =
|
2019-06-03 17:07:50 +00:00
|
|
|
result = proc(api: DaemonAPI,
|
|
|
|
ticket: PubsubTicket,
|
2019-09-29 17:42:53 +00:00
|
|
|
msg: PubSubMessage): Future[bool] {.async, gcsafe.} =
|
2019-10-25 17:15:12 +00:00
|
|
|
inc gossip_messages_received
|
2019-09-09 17:57:59 +00:00
|
|
|
trace "Incoming gossip bytes",
|
|
|
|
peer = msg.peer, len = msg.data.len, tops = msg.topics
|
2019-06-03 17:07:50 +00:00
|
|
|
msgHandler SSZ.decode(msg.data, MsgType)
|
|
|
|
return true
|
|
|
|
|
|
|
|
proc subscribe*[MsgType](node: Eth2Node,
|
|
|
|
topic: string,
|
2019-09-29 17:42:53 +00:00
|
|
|
msgHandler: proc(msg: MsgType) {.gcsafe.} ) {.async, gcsafe.} =
|
2019-06-03 17:07:50 +00:00
|
|
|
discard await node.daemon.pubsubSubscribe(topic, makeMessageHandler(msgHandler))
|
|
|
|
|
|
|
|
proc broadcast*(node: Eth2Node, topic: string, msg: auto) =
|
2019-10-25 17:15:12 +00:00
|
|
|
inc gossip_messages_sent
|
2019-06-03 17:07:50 +00:00
|
|
|
traceAsyncErrors node.daemon.pubsubPublish(topic, SSZ.encode(msg))
|
|
|
|
|
|
|
|
# TODO:
|
|
|
|
# At the moment, this is just a compatiblity shim for the existing RLPx functionality.
|
|
|
|
# The filtering is not implemented properly yet.
|
|
|
|
iterator randomPeers*(node: Eth2Node, maxPeers: int, Protocol: type): Peer =
|
|
|
|
var peers = newSeq[Peer]()
|
|
|
|
for _, peer in pairs(node.peers): peers.add peer
|
|
|
|
shuffle peers
|
|
|
|
if peers.len > maxPeers: peers.setLen(maxPeers)
|
|
|
|
for p in peers: yield p
|