Jacek Sieka efdf759cc0
avoid some slashing protection queries (#2528)
This PR reduces the number of database queries for slashing protection
from 5 reads and 1 write to 2 reads and 1 write in the optimistic case.

In the process, it removes user-level support for writing the database
in the version 1 format in order to simplify the code flow, and prevent
code rot. In particular, the v1 format was not covered by any unit tests
and has no advantages over v2. The concrete code to read and write it
remains for now, in particular to support upgrades from v1 to v2.

The branch also removes the use of concepts which doesn't work with
checked exceptions - in particular, this highlights code that both
raises exceptions and returns error codes, which could be cleaned up in
the future.

* Cache internal validator ID
* Rely on unique index to check for trivial duplicate votes
* Combine two surround vote queries into one
* Combine API for checking and registering slashing into single function

The slashing DB is normally not a bottleneck, but may become one with
high attached validator counts.
2021-05-04 15:17:28 +02:00

700 lines
24 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [Defect].}
import
strutils, os, options, unicode, uri,
chronicles, chronicles/options as chroniclesOptions,
confutils, confutils/defs, confutils/std/net, stew/shims/net as stewNet,
stew/io2, unicodedb/properties, normalize,
eth/common/eth_types as commonEthTypes, eth/net/nat,
eth/p2p/discoveryv5/enr,
json_serialization, web3/[ethtypes, confutils_defs],
spec/[crypto, keystore, digest, datatypes, network],
./networking/network_metadata,
filepath
export
uri,
defaultEth2TcpPort, enabledLogLevel, ValidIpAddress,
defs, parseCmdArg, completeCmdArg, network_metadata
const
# TODO: How should we select between IPv4 and IPv6
# Maybe there should be a config option for this.
defaultListenAddress* = (static ValidIpAddress.init("0.0.0.0"))
defaultAdminListenAddress* = (static ValidIpAddress.init("127.0.0.1"))
type
BNStartUpCmd* = enum
noCommand
createTestnet
deposits
wallets
record
web3
WalletsCmd* {.pure.} = enum
create = "Creates a new EIP-2386 wallet"
restore = "Restores a wallet from cold storage"
list = "Lists details about all wallets"
DepositsCmd* {.pure.} = enum
createTestnetDeposits = "Creates validator keystores and deposits for testnet usage"
`import` = "Imports password-protected keystores interactively"
# status = "Displays status information about all deposits"
exit = "Submits a validator voluntary exit"
VCStartUpCmd* = enum
VCNoCommand
RecordCmd* {.pure.} = enum
create = "Create a new ENR"
print = "Print the content of a given ENR"
Web3Cmd* {.pure.} = enum
test = "Test a web3 provider"
Web3Mode* {.pure.} = enum
auto # Enabled only when validators are attached
enabled # Always enabled
disabled # Always disabled
SlashingDbKind* {.pure.} = enum
v1
v2
both
StateDbKind* {.pure.} = enum
sql
file
BeaconNodeConf* = object
logLevel* {.
defaultValue: "INFO"
desc: "Sets the log level for process and topics (e.g. \"DEBUG; TRACE:discv5,libp2p; REQUIRED:none; DISABLED:none\") [=INFO]"
name: "log-level" }: string
logFile* {.
desc: "Specifies a path for the written Json log file"
name: "log-file" }: Option[OutFile]
eth2Network* {.
desc: "The Eth2 network to join [=mainnet]"
name: "network" }: Option[string]
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data"
abbr: "d"
name: "data-dir" }: OutDir
validatorsDirFlag* {.
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDirFlag* {.
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
walletsDirFlag* {.
desc: "A directory containing wallet files"
name: "wallets-dir" }: Option[InputDir]
web3Urls* {.
desc: "One or more Web3 provider URLs used for obtaining deposit contract data"
name: "web3-url" }: seq[string]
web3Mode* {.
hidden
defaultValue: Web3Mode.auto
desc: "URL of the Web3 server to observe Eth1"
name: "web3-mode" }: Web3Mode
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration"
name: "non-interactive" }: bool
netKeyFile* {.
defaultValue: "random",
desc: "Source of network (secp256k1) private key file " &
"(random|<path>) [=random]"
name: "netkey-file" }: string
netKeyInsecurePassword* {.
defaultValue: false,
desc: "Use pre-generated INSECURE password for network private key " &
"file [=false]"
name: "insecure-netkey-password" }: bool
agentString* {.
defaultValue: "nimbus",
desc: "Node agent string which is used as identifier in network"
name: "agent-string" }: string
subscribeAllSubnets* {.
defaultValue: false,
desc: "Subscribe to all attestation subnet topics when gossiping"
name: "subscribe-all-subnets" }: bool
slashingDbKind* {.
hidden
defaultValue: SlashingDbKind.v2
desc: "The slashing DB flavour to use (v2) [=v2]"
name: "slashing-db-kind" }: SlashingDbKind
stateDbKind* {.
hidden
defaultValue: StateDbKind.sql
desc: "State DB kind (sql, file) [=sql]"
name: "state-db-kind" }: StateDbKind
case cmd* {.
command
defaultValue: noCommand }: BNStartUpCmd
of noCommand:
bootstrapNodes* {.
desc: "Specifies one or more bootstrap nodes to use when connecting to the network"
abbr: "b"
name: "bootstrap-node" }: seq[string]
bootstrapNodesFile* {.
defaultValue: ""
desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses"
name: "bootstrap-file" }: InputFile
listenAddress* {.
defaultValue: defaultListenAddress
desc: "Listening address for the Ethereum LibP2P and Discovery v5 " &
"traffic [=0.0.0.0]"
name: "listen-address" }: ValidIpAddress
tcpPort* {.
defaultValue: defaultEth2TcpPort
desc: "Listening TCP port for Ethereum LibP2P traffic [=9000]"
name: "tcp-port" }: Port
udpPort* {.
defaultValue: defaultEth2TcpPort
desc: "Listening UDP port for node discovery [=9000]"
name: "udp-port" }: Port
maxPeers* {.
defaultValue: 160 # 5 (fanout) * 64 (subnets) / 2 (subs) for a heathy mesh
desc: "The maximum number of peers to connect to [=160]"
name: "max-peers" }: int
nat* {.
desc: "Specify method to use for determining public address. " &
"Must be one of: any, none, upnp, pmp, extip:<IP>"
defaultValue: NatConfig(hasExtIp: false, nat: NatAny)
name: "nat" .}: NatConfig
enrAutoUpdate* {.
defaultValue: false
desc: "Discovery can automatically update its ENR with the IP address " &
"and UDP port as seen by other nodes it communicates with. " &
"This option allows to enable/disable this functionality"
name: "enr-auto-update" .}: bool
weakSubjectivityCheckpoint* {.
desc: "Weak subjectivity checkpoint in the format block_root:epoch_number"
name: "weak-subjectivity-checkpoint" }: Option[Checkpoint]
finalizedCheckpointState* {.
desc: "SSZ file specifying a recent finalized state"
name: "finalized-checkpoint-state" }: Option[InputFile]
finalizedCheckpointBlock* {.
desc: "SSZ file specifying a recent finalized block"
name: "finalized-checkpoint-block" }: Option[InputFile]
nodeName* {.
defaultValue: ""
desc: "A name for this node that will appear in the logs. " &
"If you set this to 'auto', a persistent automatically generated ID will be selected for each --data-dir folder"
name: "node-name" }: string
graffiti* {.
desc: "The graffiti value that will appear in proposed blocks. " &
"You can use a 0x-prefixed hex encoded string to specify raw bytes"
name: "graffiti" }: Option[GraffitiBytes]
verifyFinalization* {.
defaultValue: false
desc: "Specify whether to verify finalization occurs on schedule, for testing"
name: "verify-finalization" }: bool
stopAtEpoch* {.
defaultValue: 0
desc: "A positive epoch selects the epoch at which to stop"
name: "stop-at-epoch" }: uint64
metricsEnabled* {.
defaultValue: false
desc: "Enable the metrics server [=false]"
name: "metrics" }: bool
metricsAddress* {.
defaultValue: defaultAdminListenAddress
desc: "Listening address of the metrics server [=127.0.0.1]"
name: "metrics-address" }: ValidIpAddress
metricsPort* {.
defaultValue: 8008
desc: "Listening HTTP port of the metrics server [=8008]"
name: "metrics-port" }: Port
statusBarEnabled* {.
defaultValue: true
desc: "Display a status bar at the bottom of the terminal screen"
name: "status-bar" }: bool
statusBarContents* {.
defaultValue: "peers: $connected_peers;" &
"finalized: $finalized_root:$finalized_epoch;" &
"head: $head_root:$head_epoch:$head_epoch_slot;" &
"time: $epoch:$epoch_slot ($slot);" &
"sync: $sync_status|" &
"ETH: $attached_validators_balance"
desc: "Textual template for the contents of the status bar"
name: "status-bar-contents" }: string
rpcEnabled* {.
defaultValue: false
desc: "Enable the JSON-RPC server [=false]"
name: "rpc" }: bool
rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port for the JSON-RPC service [=9190]"
name: "rpc-port" }: Port
rpcAddress* {.
defaultValue: defaultAdminListenAddress
desc: "Listening address of the RPC server [=127.0.0.1]"
name: "rpc-address" }: ValidIpAddress
restEnabled* {.
defaultValue: false
desc: "Enable the REST (BETA version) server [=false]"
name: "rest" }: bool
restPort* {.
defaultValue: DefaultEth2RestPort
desc: "Port for the REST (BETA version) server [=5052]"
name: "rest-port" }: Port
restAddress* {.
defaultValue: defaultAdminListenAddress
desc: "Listening address of the REST (BETA version) server [=127.0.0.1]"
name: "rest-address" }: ValidIpAddress
inProcessValidators* {.
defaultValue: true # the use of the nimbus_signing_process binary by default will be delayed until async I/O over stdin/stdout is developed for the child process.
desc: "Disable the push model (the beacon node tells a signing process with the private keys of the validators what to sign and when) and load the validators in the beacon node itself"
name: "in-process-validators" }: bool
discv5Enabled* {.
defaultValue: true
desc: "Enable Discovery v5 [=true]"
name: "discv5" }: bool
dumpEnabled* {.
defaultValue: false
desc: "Write SSZ dumps of blocks, attestations and states to data dir [=false]"
name: "dump" }: bool
directPeers* {.
desc: "The list of priviledged, secure and known peers to connect and maintain the connection to, this requires a not random netkey-file. In the complete multiaddress format like: /ip4/<address>/tcp/<port>/p2p/<peerId-public-key>. Peering agreements are established out of band and must be reciprocal."
name: "direct-peer" .}: seq[string]
doppelgangerDetection* {.
defaultValue: true
desc: "Whether to detect whether another validator is be running the same validator keys [=true]"
name: "doppelganger-detection"
}: bool
of createTestnet:
testnetDepositsFile* {.
desc: "A LaunchPad deposits file for the genesis state validators"
name: "deposits-file" }: InputFile
totalValidators* {.
desc: "The number of validator deposits in the newly created chain"
name: "total-validators" }: uint64
bootstrapAddress* {.
defaultValue: init(ValidIpAddress, "127.0.0.1")
desc: "The public IP address that will be advertised as a bootstrap node for the testnet"
name: "bootstrap-address" }: ValidIpAddress
bootstrapPort* {.
defaultValue: defaultEth2TcpPort
desc: "The TCP/UDP port that will be used by the bootstrap node"
name: "bootstrap-port" }: Port
genesisOffset* {.
defaultValue: 5
desc: "Seconds from now to add to genesis time"
name: "genesis-offset" }: int
outputGenesis* {.
desc: "Output file where to write the initial state snapshot"
name: "output-genesis" }: OutFile
withGenesisRoot* {.
defaultValue: false
desc: "Include a genesis root in 'network.json'"
name: "with-genesis-root" }: bool
outputBootstrapFile* {.
desc: "Output file with list of bootstrap nodes for the network"
name: "output-bootstrap-file" }: OutFile
of wallets:
case walletsCmd* {.command.}: WalletsCmd
of WalletsCmd.create:
nextAccount* {.
desc: "Initial value for the 'nextaccount' property of the wallet"
name: "next-account" }: Option[Natural]
createdWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
createdWalletFileFlag* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
of WalletsCmd.restore:
restoredWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
restoredWalletFileFlag* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
restoredDepositsCount* {.
desc: "Expected number of deposits to recover. If not specified, " &
"Nimbus will try to guess the number by inspecting the latest " &
"beacon state"
name: "deposits".}: Option[Natural]
of WalletsCmd.list:
discard
of deposits:
case depositsCmd* {.command.}: DepositsCmd
of DepositsCmd.createTestnetDeposits:
totalDeposits* {.
defaultValue: 1
desc: "Number of deposits to generate"
name: "count" }: int
existingWalletId* {.
desc: "An existing wallet ID. If not specified, a new wallet will be created"
name: "wallet" }: Option[WalletName]
outValidatorsDir* {.
defaultValue: "validators"
desc: "Output folder for validator keystores"
name: "out-validators-dir" }: string
outSecretsDir* {.
defaultValue: "secrets"
desc: "Output folder for randomly generated keystore passphrases"
name: "out-secrets-dir" }: string
outDepositsFile* {.
desc: "The name of generated deposits file"
name: "out-deposits-file" }: Option[OutFile]
newWalletNameFlag* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "new-wallet-name" }: Option[WalletName]
newWalletFileFlag* {.
desc: "Output wallet file"
name: "new-wallet-file" }: Option[OutFile]
#[
of DepositsCmd.status:
discard
]#
of DepositsCmd.`import`:
importedDepositsDir* {.
argument
desc: "A directory with keystores to import" }: Option[InputDir]
of DepositsCmd.exit:
exitedValidator* {.
name: "validator"
desc: "Validator index or a public key of the exited validator" }: string
rpcUrlForExit* {.
name: "rpc-url"
defaultValue: parseUri("http://localhost:" & $defaultEth2RpcPort)
desc: "URL of the beacon node JSON-RPC service" }: Uri
exitAtEpoch* {.
name: "epoch"
desc: "The desired exit epoch" }: Option[uint64]
of record:
case recordCmd* {.command.}: RecordCmd
of RecordCmd.create:
ipExt* {.
desc: "External IP address"
name: "ip" .}: ValidIpAddress
tcpPortExt* {.
desc: "External TCP port"
name: "tcp-port" .}: Port
udpPortExt* {.
desc: "External UDP port"
name: "udp-port" .}: Port
seqNumber* {.
defaultValue: 1,
desc: "Record sequence number"
name: "seq-number" .}: uint
fields* {.
desc: "Additional record key pairs, provide as <string>:<bytes in hex>"
name: "field" .}: seq[(string)]
of RecordCmd.print:
recordPrint* {.
argument
desc: "ENR URI of the record to print"
name: "enr" .}: Record
of web3:
case web3Cmd* {.command.}: Web3Cmd
of Web3Cmd.test:
web3TestUrl* {.
argument
desc: "The web3 provider URL to test"
name: "url" }: Uri
ValidatorClientConf* = object
logLevel* {.
defaultValue: "INFO"
desc: "Sets the log level [=INFO]"
name: "log-level" }: string
logFile* {.
desc: "Specifies a path for the written Json log file"
name: "log-file" }: Option[OutFile]
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data"
abbr: "d"
name: "data-dir" }: OutDir
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration"
name: "non-interactive" }: bool
validatorsDirFlag* {.
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDirFlag* {.
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
case cmd* {.
command
defaultValue: VCNoCommand }: VCStartUpCmd
of VCNoCommand:
graffiti* {.
desc: "The graffiti value that will appear in proposed blocks. " &
"You can use a 0x-prefixed hex encoded string to specify raw bytes"
name: "graffiti" }: Option[GraffitiBytes]
stopAtEpoch* {.
defaultValue: 0
desc: "A positive epoch selects the epoch at which to stop"
name: "stop-at-epoch" }: uint64
rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port of the server to connect to for RPC [=9190]"
name: "rpc-port" }: Port
rpcAddress* {.
defaultValue: defaultAdminListenAddress
desc: "Address of the server to connect to for RPC [=127.0.0.1]"
name: "rpc-address" }: ValidIpAddress
retryDelay* {.
defaultValue: 10
desc: "Delay in seconds between retries after unsuccessful attempts to connect to a beacon node [=10]"
name: "retry-delay" }: int
proc defaultDataDir*(config: BeaconNodeConf|ValidatorClientConf): string =
let dataDir = when defined(windows):
"AppData" / "Roaming" / "Nimbus"
elif defined(macosx):
"Library" / "Application Support" / "Nimbus"
else:
".cache" / "nimbus"
getHomeDir() / dataDir / "BeaconNode"
func dumpDir*(config: BeaconNodeConf|ValidatorClientConf): string =
config.dataDir / "dump"
func dumpDirInvalid*(config: BeaconNodeConf|ValidatorClientConf): string =
config.dumpDir / "invalid" # things that failed validation
func dumpDirIncoming*(config: BeaconNodeConf|ValidatorClientConf): string =
config.dumpDir / "incoming" # things that couldn't be validated (missingparent etc)
func dumpDirOutgoing*(config: BeaconNodeConf|ValidatorClientConf): string =
config.dumpDir / "outgoing" # things we produced
proc createDumpDirs*(config: BeaconNodeConf) =
if config.dumpEnabled:
let resInv = secureCreatePath(config.dumpDirInvalid)
if resInv.isErr():
warn "Could not create dump directory", path = config.dumpDirInvalid
let resInc = secureCreatePath(config.dumpDirIncoming)
if resInc.isErr():
warn "Could not create dump directory", path = config.dumpDirIncoming
let resOut = secureCreatePath(config.dumpDirOutgoing)
if resOut.isErr():
warn "Could not create dump directory", path = config.dumpDirOutgoing
func parseCmdArg*(T: type GraffitiBytes, input: TaintedString): T
{.raises: [ValueError, Defect].} =
GraffitiBytes.init(string input)
func completeCmdArg*(T: type GraffitiBytes, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type BlockHashOrNumber, input: TaintedString): T
{.raises: [ValueError, Defect].} =
init(BlockHashOrNumber, string input)
func completeCmdArg*(T: type BlockHashOrNumber, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type Uri, input: TaintedString): T
{.raises: [ValueError, Defect].} =
parseUri(input.string)
func completeCmdArg*(T: type Uri, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type Checkpoint, input: TaintedString): T
{.raises: [ValueError, Defect].} =
let sepIdx = find(input.string, ':')
if sepIdx == -1:
raise newException(ValueError,
"The weak subjectivity checkpoint must be provided in the `block_root:epoch_number` format")
T(root: Eth2Digest.fromHex(input[0 ..< sepIdx]),
epoch: parseBiggestUInt(input[sepIdx .. ^1]).Epoch)
func completeCmdArg*(T: type Checkpoint, input: TaintedString): seq[string] =
return @[]
proc isPrintable(rune: Rune): bool =
# This can be eventually replaced by the `unicodeplus` package, but a single
# proc does not justify the extra dependencies at the moment:
# https://github.com/nitely/nim-unicodeplus
# https://github.com/nitely/nim-segmentation
rune == Rune(0x20) or unicodeCategory(rune) notin ctgC+ctgZ
func parseCmdArg*(T: type WalletName, input: TaintedString): T
{.raises: [ValueError, Defect].} =
if input.len == 0:
raise newException(ValueError, "The wallet name should not be empty")
if input[0] == '_':
raise newException(ValueError, "The wallet name should not start with an underscore")
for rune in runes(input.string):
if not rune.isPrintable:
raise newException(ValueError, "The wallet name should consist only of printable characters")
# From the Unicode Normalization FAQ (https://unicode.org/faq/normalization.html):
# NFKC is the preferred form for identifiers, especially where there are security concerns
# (see UTR #36 http://www.unicode.org/reports/tr36/)
return T(toNFKC(input))
func completeCmdArg*(T: type WalletName, input: TaintedString): seq[string] =
return @[]
proc parseCmdArg*(T: type enr.Record, p: TaintedString): T
{.raises: [ConfigurationError, Defect].} =
if not fromURI(result, p):
raise newException(ConfigurationError, "Invalid ENR")
proc completeCmdArg*(T: type enr.Record, val: TaintedString): seq[string] =
return @[]
func validatorsDir*(config: BeaconNodeConf|ValidatorClientConf): string =
string config.validatorsDirFlag.get(InputDir(config.dataDir / "validators"))
func secretsDir*(config: BeaconNodeConf|ValidatorClientConf): string =
string config.secretsDirFlag.get(InputDir(config.dataDir / "secrets"))
func walletsDir*(config: BeaconNodeConf): string =
if config.walletsDirFlag.isSome:
config.walletsDirFlag.get.string
else:
config.dataDir / "wallets"
func outWalletName*(config: BeaconNodeConf): Option[WalletName] =
proc fail {.noReturn.} =
raiseAssert "outWalletName should be used only in the right context"
case config.cmd
of wallets:
case config.walletsCmd
of WalletsCmd.create: config.createdWalletNameFlag
of WalletsCmd.restore: config.restoredWalletNameFlag
of WalletsCmd.list: fail()
of deposits:
case config.depositsCmd
of DepositsCmd.createTestnetDeposits: config.newWalletNameFlag
else: fail()
else:
fail()
func outWalletFile*(config: BeaconNodeConf): Option[OutFile] =
proc fail {.noReturn.} =
raiseAssert "outWalletName should be used only in the right context"
case config.cmd
of wallets:
case config.walletsCmd
of WalletsCmd.create: config.createdWalletFileFlag
of WalletsCmd.restore: config.restoredWalletFileFlag
of WalletsCmd.list: fail()
of deposits:
case config.depositsCmd
of DepositsCmd.createTestnetDeposits: config.newWalletFileFlag
else: fail()
else:
fail()
func databaseDir*(config: BeaconNodeConf|ValidatorClientConf): string =
config.dataDir / "db"
template writeValue*(writer: var JsonWriter,
value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) =
writer.writeValue(string value)