Command-line and data storage handling for testnets

This commit is contained in:
Zahary Karadjov 2019-03-18 05:54:08 +02:00 committed by zah
parent 798197879b
commit 6bb38095c9
10 changed files with 272 additions and 94 deletions

View File

@ -202,7 +202,7 @@ proc add*(pool: var AttestationPool,
# TODO inefficient data structures..
let
attestationSlot = attestation.data.slot.Slot
attestationSlot = attestation.data.slot
idx = pool.slotIndex(state, attestationSlot)
slotData = addr pool.slots[idx]
validation = Validation(
@ -350,4 +350,4 @@ proc resolve*(pool: var AttestationPool, state: BeaconState) =
proc latestAttestation*(
pool: AttestationPool, pubKey: ValidatorPubKey): BlockRef =
pool.latestAttestations.getOrDefault(pubKey)
pool.latestAttestations.getOrDefault(pubKey)

View File

@ -1,10 +1,10 @@
import
std_shims/[os_shims, objects], net, sequtils, options, tables,
std_shims/[os_shims, objects], net, sequtils, options, tables, osproc, random,
chronos, chronicles, confutils,
spec/[datatypes, digest, crypto, beaconstate, helpers, validator], conf, time,
state_transition, fork_choice, ssz, beacon_chain_db, validator_pool, extras,
attestation_pool, block_pool, eth2_network, beacon_node_types,
mainchain_monitor, trusted_state_snapshots,
mainchain_monitor, trusted_state_snapshots, version,
eth/trie/db, eth/trie/backends/rocksdb_backend
const
@ -12,6 +12,11 @@ const
topicAttestations = "ethereum/2.1/beacon_chain/attestations"
topicfetchBlocks = "ethereum/2.1/beacon_chain/fetch"
dataDirValidators = "validators"
networkMetadataFile = "network.json"
genesisFile = "genesis.json"
testnetsBaseUrl = "http://node-01.do-ams3.nimbus.misc.statusim.net:8000/nimbus_testnets"
# #################################################
# Careful handling of beacon_node <-> sync_protocol
# to avoid recursive dependencies
@ -23,17 +28,66 @@ import sync_protocol
func shortValidatorKey(node: BeaconNode, validatorIdx: int): string =
($node.state.data.validator_registry[validatorIdx].pubkey)[0..7]
func localValidatorsDir(conf: BeaconNodeConf): string =
conf.dataDir / "validators"
func databaseDir(conf: BeaconNodeConf): string =
conf.dataDir / "db"
func slotStart(node: BeaconNode, slot: Slot): Timestamp =
node.state.data.slotStart(slot)
template `//`(url, fragment: string): string =
url & "/" & fragment
proc downloadFile(url: string): Future[string] {.async.} =
let (fileContents, errorCode) = execCmdEx("curl --fail " & url)
if errorCode != 0:
raise newException(IOError, "Failed to download URL: " & url)
return fileContents
proc doUpdateTestnet(conf: BeaconNodeConf) {.async.} =
let latestMetadata = await downloadFile(testnetsBaseUrl // $conf.network // networkMetadataFile)
let localMetadataFile = conf.dataDir / networkMetadataFile
if fileExists(localMetadataFile) and readFile(localMetadataFile).string == latestMetadata:
return
removeDir conf.databaseDir
writeFile localMetadataFile, latestMetadata
let newGenesis = await downloadFile(testnetsBaseUrl // $conf.network // genesisFile)
writeFile conf.dataDir / genesisFile, newGenesis
info "New testnet genesis data received. Starting with a fresh database."
proc loadTestnetMetadata(conf: BeaconNodeConf): TestnetMetadata =
Json.loadFile(conf.dataDir / networkMetadataFile, TestnetMetadata)
proc obtainTestnetKey(conf: BeaconNodeConf): Future[ValidatorPrivKey] {.async.} =
await doUpdateTestnet(conf)
let
metadata = loadTestnetMetadata(conf)
privKeyName = validatorFileBaseName(rand(metadata.userValidatorsRange)) & ".privkey"
privKeyContent = await downloadFile(testnetsBaseUrl // $conf.network // privKeyName)
return ValidatorPrivKey.init(privKeyContent)
proc saveValidatorKey(key: ValidatorPrivKey, conf: BeaconNodeConf) =
let filename = conf.dataDir / dataDirValidators / $key.pubKey
writeFile(filename, $key)
proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async.} =
new result
result.config = conf
if conf.network in {testnet0, testnet1}:
await doUpdateTestnet(conf)
result.attachedValidators = ValidatorPool.init
init result.mainchainMonitor, "", Port(0) # TODO: specify geth address and port
let trieDB = trieDB newChainDb(string conf.dataDir)
let trieDB = trieDB newChainDb(string conf.databaseDir)
result.db = BeaconChainDB.init(trieDB)
# TODO this is problably not the right place to ensure that db is sane..
@ -43,8 +97,15 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
let headBlock = result.db.getHeadBlock()
if headBlock.isNone():
var snapshotFile = conf.dataDir / genesisFile
if conf.stateSnapshot.isSome:
snapshotFile = conf.stateSnapshot.get.string
elif not fileExists(snapshotFile):
error "Nimbus database not initialized. Please specify the initial state snapshot file."
quit 1
let
tailState = result.config.stateSnapshot.get()
tailState = Json.loadFile(snapshotFile, BeaconState)
tailBlock = get_initial_beacon_block(tailState)
blockRoot = hash_tree_root_final(tailBlock)
@ -62,11 +123,15 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
result.blockPool = BlockPool.init(result.db)
result.attestationPool = AttestationPool.init(result.blockPool)
result.network = await createEth2Node(Port conf.tcpPort, Port conf.udpPort)
result.network = await createEth2Node(conf)
let state = result.network.protocolState(BeaconSync)
state.node = result
state.db = result.db
let sync = result.network.protocolState(BeaconSync)
sync.networkId = case conf.network
of mainnet: 1.uint64
of ephemeralNetwork: 1000.uint64
of testnet0, testnet1: loadTestnetMetadata(conf).networkId
sync.node = result
sync.db = result.db
let head = result.blockPool.get(result.db.getHeadBlock().get())
@ -86,6 +151,10 @@ proc connectToNetwork(node: BeaconNode) {.async.} =
for ln in lines(bootstrapFile):
bootstrapNodes.add BootstrapAddr.init(string ln)
if node.config.network in {testnet0, testnet1}:
let metadata = loadTestnetMetadata(node.config)
bootstrapNodes.add metadata.bootstrapNodes
if bootstrapNodes.len > 0:
info "Connecting to bootstrap nodes", bootstrapNodes
else:
@ -147,18 +216,24 @@ template findIt(s: openarray, predicate: untyped): int =
break
res
proc addLocalValidators*(node: BeaconNode) =
for privKey in node.config.validators:
let
pubKey = privKey.pubKey()
proc addLocalValidator(node: BeaconNode, validatorKey: ValidatorPrivKey) =
let pubKey = validatorKey.pubKey()
let idx = node.state.data.validator_registry.findIt(it.pubKey == pubKey)
if idx == -1:
warn "Validator not in registry", pubKey
else:
debug "Attaching validator", validator = shortValidatorKey(node, idx),
idx, pubKey
node.attachedValidators.addLocalValidator(idx, pubKey, privKey)
let idx = node.state.data.validator_registry.findIt(it.pubKey == pubKey)
if idx == -1:
warn "Validator not in registry", pubKey
else:
debug "Attaching validator", validator = shortValidatorKey(node, idx),
idx, pubKey
node.attachedValidators.addLocalValidator(idx, pubKey, validatorKey)
proc addLocalValidators(node: BeaconNode) =
for validatorKeyFile in node.config.validators:
node.addLocalValidator validatorKeyFile.load
for kind, file in walkDir(node.config.localValidatorsDir):
if kind in {pcFile, pcLinkToFile}:
node.addLocalValidator ValidatorPrivKey.init(readFile(file).string)
info "Local validators attached ", count = node.attachedValidators.count
@ -584,7 +659,8 @@ proc createPidFile(filename: string) =
addQuitProc proc {.noconv.} = removeFile gPidFile
when isMainModule:
let config = load BeaconNodeConf
let config = BeaconNodeConf.load(version = fullVersionStr())
if config.logLevel != LogLevel.NONE:
setLogLevel(config.logLevel)
@ -595,6 +671,41 @@ when isMainModule:
config.genesisOffset, config.outputStateFile.string)
quit 0
of importValidator:
template reportFailureFor(keyExpr) =
error "Failed to import validator key", key = keyExpr
programResult = 1
var downloadKey = true
if config.key.isSome:
downloadKey = false
try:
ValidatorPrivKey.init(config.key.get).saveValidatorKey(config)
except:
reportFailureFor config.key.get
if config.keyFile.isSome:
downloadKey = false
try:
config.keyFile.get.load.saveValidatorKey(config)
except:
reportFailureFor config.keyFile.get.string
if downloadKey:
if config.network in {testnet0, testnet1}:
try:
(waitFor obtainTestnetKey(config)).saveValidatorKey(config)
except:
error "Failed to download key", err = getCurrentExceptionMsg()
quit 1
else:
echo "Validator keys can be downloaded only for testnets"
quit 1
of updateTestnet:
waitFor doUpdateTestnet(config)
of noCommand:
waitFor synchronizeClock()
createPidFile(config.dataDir.string / "beacon_node.pid")

View File

@ -229,4 +229,4 @@ type
connection*: ValidatorConnection
ValidatorPool* = object
validators*: Table[ValidatorPubKey, AttachedValidator]
validators*: Table[ValidatorPubKey, AttachedValidator]

View File

@ -1,24 +1,36 @@
import
os, options,
os, options, strformat,
confutils/defs, chronicles/options as chroniclesOptions,
json_serialization,
spec/[crypto, datatypes], time
spec/[crypto, datatypes], time, version
export
json_serialization
defs
type
ValidatorKeyPath* = distinct string
ValidatorKeyPath* = TypedInputFile[ValidatorPrivKey, Txt, "privkey"]
StartUpCommand* = enum
noCommand
createChain
importValidator
updateTestnet
Network* = enum
ephemeralNetwork
testnet0
testnet1
mainnet
BeaconNodeConf* = object
logLevel* {.
desc: "Sets the log level",
defaultValue: enabledLogLevel
.}: LogLevel
defaultValue: enabledLogLevel.}: LogLevel
network* {.
desc: "The network Nimbus should connect to"
longform: "network"
shortform: "n"
defaultValue: testnet0.}: Network
case cmd* {.
command
@ -28,7 +40,7 @@ type
dataDir* {.
desc: "The directory where nimbus will store all blockchain data."
shortform: "d"
defaultValue: getConfigDir() / "nimbus".}: DirPath
defaultValue: config.defaultDataDir().}: OutDir
bootstrapNodes* {.
desc: "Specifies one or more bootstrap nodes to use when connecting to the network."
@ -38,36 +50,36 @@ type
bootstrapNodesFile* {.
desc: "Specifies a line-delimited file of bootsrap Ethereum network addresses"
shortform: "f"
defaultValue: "".}: FilePath
defaultValue: "".}: InputFile
tcpPort* {.
desc: "TCP listening port".}: int
desc: "TCP listening port"
defaultValue: config.defaultPort().}: int
udpPort* {.
desc: "UDP listening port".}: int
desc: "UDP listening port",
defaultValue: config.defaultPort().}: int
validators* {.
required
desc: "Path to a validator private key, as generated by validator_keygen"
longform: "validator"
shortform: "v".}: seq[ValidatorPrivKey]
shortform: "v".}: seq[ValidatorKeyPath]
stateSnapshot* {.
desc: "Json file specifying a recent state snapshot"
shortform: "s".}: Option[BeaconState]
shortform: "s".}: Option[TypedInputFile[BeaconState, Json, "json"]]
of createChain:
validatorsDir* {.
desc: "Directory containing validator descriptors named vXXXXXXX.deposit.json"
shortform: "d".}: DirPath
shortform: "d".}: InputDir
numValidators* {.
desc: ""
shortform: "n".}: int
desc: "The number of validators in the newly created chain".}: int
firstValidator* {.
desc: "index of first validator to add to validator list"
shortform: "o"
defaultValue: 0.}: int
genesisOffset* {.
@ -78,27 +90,36 @@ type
outputStateFile* {.
desc: "Output file where to write the initial state snapshot"
longform: "out"
shortform: "o".}: OutFilePath
shortform: "o".}: OutFile
proc readFileBytes(path: string): seq[byte] =
cast[seq[byte]](readFile(path))
of importValidator:
keyFile* {.
desc: "File with validator key to be imported (in hex form)".}: Option[ValidatorKeyPath]
proc loadPrivKey*(p: ValidatorKeyPath): ValidatorPrivKey =
ValidatorPrivKey.init(readFileBytes(string(p) & ".privkey"))
key* {.
desc: "Validator key to be imported (in hex form)".}: Option[string]
proc parseCmdArg*(T: type ValidatorKeyPath, input: TaintedString): T =
result = T(input)
discard loadPrivKey(result)
of updateTestnet:
discard
template mustBeFilePath(input: TaintedString) =
if not fileExists(string input):
raise newException(ValueError, "")
proc defaultDataDir*(conf: BeaconNodeConf): string =
if conf.network == ephemeralNetwork:
getCurrentDir() / "beacon-node-cache"
template handledAsJsonFilename(T: untyped) {.dirty.} =
proc parseCmdArg*(_: type T, input: TaintedString): T =
input.mustBeFilePath
return Json.loadFile(string(input), T)
else:
let dataDir = when defined(windows):
"AppData" / "Roaming" / "Nimbus"
elif defined(macosx):
"Library" / "Application Support" / "Nimbus"
else:
".cache" / "nimbus"
getHomeDir() / dataDir / "BeaconNode" / $conf.network
proc defaultPort*(conf: BeaconNodeConf): int =
(if conf.network == testnet0: 9630 else: 9632) + ord(useRLPx)
proc validatorFileBaseName*(validatorIdx: int): string =
# there can apparently be tops 4M validators so we use 7 digits..
fmt"v{validatorIdx:07}"
handledAsJsonFilename BeaconState
handledAsJsonFilename Deposit
handledAsJsonFilename ValidatorPrivKey

View File

@ -1,14 +1,13 @@
const
useDEVP2P = not defined(withLibP2P)
import
options, chronos, version
options, chronos, json_serialization,
spec/digest, version, conf
const
clientId = "Nimbus beacon node v" & versionAsStr
clientId = "Nimbus beacon node v" & fullVersionStr()
when useDEVP2P:
when useRLPx:
import
os,
eth/[rlp, p2p, keys], gossipsub_protocol
export
@ -20,12 +19,26 @@ when useDEVP2P:
template libp2pProtocol*(name, version: string) {.pragma.}
proc createEth2Node*(tcpPort, udpPort: Port): Future[EthereumNode] {.async.} =
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)
proc createEth2Node*(conf: BeaconNodeConf): Future[EthereumNode] {.async.} =
let privateKeyFile = conf.dataDir / "network.privkey"
var privKey: PrivateKey
if not fileExists(privateKeyFile):
privKey = newPrivateKey()
writeFile(privateKeyFile, $privKey)
else:
privKey = initPrivateKey(readFile(privateKeyFile).string)
let
keys = newKeyPair()
keys = KeyPair(seckey: privKey, pubkey: privKey.getPublicKey())
address = Address(ip: parseIpAddress("127.0.0.1"),
tcpPort: tcpPort,
udpPort: udpPort)
tcpPort: Port conf.tcpPort,
udpPort: Port conf.udpPort)
return newEthereumNode(keys, address, 0,
nil, clientId, minPeers = 1)
@ -38,7 +51,7 @@ when useDEVP2P:
else:
import
libp2p/daemon/daemonapi, json_serialization, chronicles,
libp2p/daemon/daemonapi, chronicles,
libp2p_backend
export
@ -62,7 +75,7 @@ else:
proc init*(T: type BootstrapAddr, str: string): T =
Json.decode(str, PeerInfo)
proc createEth2Node*(tcpPort, udpPort: Port): Future[Eth2Node] {.async.} =
proc createEth2Node*(conf: BeaconNodeConf): Future[Eth2Node] {.async.} =
var node = new Eth2Node
await node.init()
return node
@ -72,7 +85,8 @@ else:
for bootstrapNode in bootstrapNodes:
try:
await node.daemon.connect(bootstrapNode.peer, bootstrapNode.addresses)
await node.getPeer(bootstrapNode.peer).performProtocolHandshakes()
let peer = node.getPeer(bootstrapNode.peer)
await peer.performProtocolHandshakes()
except PeerDisconnected:
error "Failed to connect to bootstrap node", node = bootstrapNode
@ -83,3 +97,15 @@ else:
proc loadConnectionAddressFile*(filename: string): PeerInfo =
Json.loadFile(filename, PeerInfo)
type
TestnetMetadata* = object
networkId*: uint64
genesisRoot*: Eth2Digest
bootstrapNodes*: BootstrapAddr
totalValidators*: int
userValidatorsStart*: int
userValidatorsEnd*: int
proc userValidatorsRange*(d: TestnetMetadata): HSlice[int, int] =
d.userValidatorsStart .. d.userValidatorsEnd

View File

@ -77,6 +77,7 @@ type
Bytes = seq[byte]
DisconnectionReason* = enum
UselessPeer
BreachOfProtocol
PeerDisconnected* = object of CatchableError

View File

@ -19,6 +19,7 @@ type
ValidatorSet = seq[Validator]
BeaconSyncState* = ref object
networkId*: uint64
node*: BeaconNode
db*: BeaconChainDB
@ -70,10 +71,10 @@ p2pProtocol BeaconSync(version = 1,
networkState = BeaconSyncState):
onPeerConnected do(peer: Peer):
const
let
protocolVersion = 1 # TODO: Spec doesn't specify this yet
networkId = 1
let node = peer.networkState.node
node = peer.networkState.node
networkId = peer.networkState.networkId
var
latestFinalizedRoot: Eth2Digest # TODO
@ -84,6 +85,11 @@ p2pProtocol BeaconSync(version = 1,
let m = await handshake(peer, timeout = 500,
status(networkId, latestFinalizedRoot,
latestFinalizedEpoch, bestRoot, bestSlot))
if m.networkId != networkId:
await peer.disconnect(UselessPeer)
return
let bestDiff = cmp((latestFinalizedEpoch, bestSlot), (m.latestFinalizedEpoch, m.bestSlot))
if bestDiff == 0:
# Nothing to do?
@ -116,7 +122,7 @@ p2pProtocol BeaconSync(version = 1,
proc status(
peer: Peer,
networkId: int,
networkId: uint64,
latestFinalizedRoot: Eth2Digest,
latestFinalizedEpoch: Epoch,
bestRoot: Eth2Digest,

View File

@ -1,27 +1,33 @@
import
os, ospaths, strutils, strformat,
chronos, nimcrypto, json_serialization, confutils,
chronos, blscurve, nimcrypto, json_serialization, confutils,
spec/[datatypes, digest, crypto], conf, time, ssz,
../tests/testutil
proc writeTextFile(filename: string, contents: string) =
writeFile(filename, contents)
echo "Wrote ", filename
proc writeFile(filename: string, value: auto) =
Json.saveFile(filename, value, pretty = true)
echo "Wrote ", filename
cli do (validators: int = 100000,
outputDir: string = "validators"):
cli do (validators: int = 125000,
outputDir: string = "validators",
generateFakeKeys = true):
for i in 0 ..< validators:
# there can apparently be tops 4M validators so we use 7 digits..
let
depositFn = outputDir / &"v{i:07}.deposit.json"
privKeyFn = outputDir / &"v{i:07}.privkey.json"
v = validatorFileBaseName(i)
depositFn = outputDir / v & ".deposit.json"
privKeyFn = outputDir / v & ".privkey"
if existsFile(depositFn) and existsFile(privKeyFn):
continue
let
privKey = makeFakeValidatorPrivKey(i)
privKey = if generateFakeKeys: makeFakeValidatorPrivKey(i)
else: ValidatorPrivKey.random
pubKey = privKey.pubKey()
let
@ -46,7 +52,8 @@ cli do (validators: int = 100000,
proof_of_possession: proofOfPossession,
withdrawal_credentials: withdrawalCredentials)))
writeFile(privKeyFn, privKey)
writeTextFile(privKeyFn, $privKey)
writeFile(depositFn, deposit)
echo "Keys generated by this tool are only for testing!"
if generateFakeKeys:
echo "Keys generated by this tool are only for testing!"

View File

@ -1,3 +1,6 @@
const
useRLPx* = not defined(withLibP2P)
const
versionMajor* = 0
versionMinor* = 1
@ -6,3 +9,5 @@ const
template versionAsStr*: string =
$versionMajor & "." & $versionMinor & "." & $versionBuild
proc fullVersionStr*: string =
versionAsStr & (if useRLPx: " rlpx" else: " libp2p")

View File

@ -16,17 +16,18 @@ V_PREFIX="$VALIDATORS_DIR/v$(printf '%06d' ${1})"
PORT=$(printf '5%04d' ${1})
$BEACON_NODE_BIN \
--network:ephemeralNetwork \
--dataDir:$DATA_DIR \
--validator:${V_PREFIX}0.privkey.json \
--validator:${V_PREFIX}1.privkey.json \
--validator:${V_PREFIX}2.privkey.json \
--validator:${V_PREFIX}3.privkey.json \
--validator:${V_PREFIX}4.privkey.json \
--validator:${V_PREFIX}5.privkey.json \
--validator:${V_PREFIX}6.privkey.json \
--validator:${V_PREFIX}7.privkey.json \
--validator:${V_PREFIX}8.privkey.json \
--validator:${V_PREFIX}9.privkey.json \
--validator:${V_PREFIX}0.privkey \
--validator:${V_PREFIX}1.privkey \
--validator:${V_PREFIX}2.privkey \
--validator:${V_PREFIX}3.privkey \
--validator:${V_PREFIX}4.privkey \
--validator:${V_PREFIX}5.privkey \
--validator:${V_PREFIX}6.privkey \
--validator:${V_PREFIX}7.privkey \
--validator:${V_PREFIX}8.privkey \
--validator:${V_PREFIX}9.privkey \
--tcpPort:$PORT \
--udpPort:$PORT \
--stateSnapshot:$SNAPSHOT_FILE \