mirror of
https://github.com/status-im/nim-dagger.git
synced 2025-01-12 07:34:08 +00:00
916e4834d0
This flag was originally used to change OnChainClock behavior when using hardhat as an ethereum chain source due to a very strange bug which would mark the timestamp of new blocks as one second off the timestamp they should have been. The issue has since been worked around in another manner, and thus this flag is no longer needed.
621 lines
18 KiB
Nim
621 lines
18 KiB
Nim
## Nim-Codex
|
|
## Copyright (c) 2021 Status Research & Development GmbH
|
|
## Licensed under either of
|
|
## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
## * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
## at your option.
|
|
## This file may not be copied, modified, or distributed except according to
|
|
## those terms.
|
|
|
|
{.push raises: [].}
|
|
|
|
import std/os
|
|
import std/terminal
|
|
import std/options
|
|
import std/strutils
|
|
import std/typetraits
|
|
|
|
import pkg/chronos
|
|
import pkg/chronicles/helpers
|
|
import pkg/chronicles/topics_registry
|
|
import pkg/confutils/defs
|
|
import pkg/confutils/std/net
|
|
import pkg/toml_serialization
|
|
import pkg/metrics
|
|
import pkg/metrics/chronos_httpserver
|
|
import pkg/stew/shims/net as stewnet
|
|
import pkg/stew/shims/parseutils
|
|
import pkg/stew/byteutils
|
|
import pkg/libp2p
|
|
import pkg/ethers
|
|
import pkg/questionable
|
|
import pkg/questionable/results
|
|
|
|
import ./codextypes
|
|
import ./discovery
|
|
import ./logutils
|
|
import ./stores
|
|
import ./units
|
|
import ./utils
|
|
|
|
export units, net, codextypes, logutils
|
|
|
|
export
|
|
DefaultQuotaBytes,
|
|
DefaultBlockTtl,
|
|
DefaultBlockMaintenanceInterval,
|
|
DefaultNumberOfBlocksToMaintainPerInterval
|
|
|
|
proc defaultDataDir*(): string =
|
|
let dataDir = when defined(windows):
|
|
"AppData" / "Roaming" / "Codex"
|
|
elif defined(macosx):
|
|
"Library" / "Application Support" / "Codex"
|
|
else:
|
|
".cache" / "codex"
|
|
|
|
getHomeDir() / dataDir
|
|
|
|
const
|
|
codex_enable_api_debug_peers* {.booldefine.} = false
|
|
codex_enable_proof_failures* {.booldefine.} = false
|
|
codex_enable_log_counter* {.booldefine.} = false
|
|
|
|
DefaultDataDir* = defaultDataDir()
|
|
|
|
type
|
|
StartUpCmd* {.pure.} = enum
|
|
noCmd
|
|
persistence
|
|
|
|
PersistenceCmd* {.pure.} = enum
|
|
noCmd
|
|
prover
|
|
|
|
LogKind* {.pure.} = enum
|
|
Auto = "auto"
|
|
Colors = "colors"
|
|
NoColors = "nocolors"
|
|
Json = "json"
|
|
None = "none"
|
|
|
|
RepoKind* = enum
|
|
repoFS = "fs"
|
|
repoSQLite = "sqlite"
|
|
|
|
CodexConf* = object
|
|
configFile* {.
|
|
desc: "Loads the configuration from a TOML file"
|
|
defaultValueDesc: "none"
|
|
defaultValue: InputFile.none
|
|
name: "config-file" }: Option[InputFile]
|
|
|
|
logLevel* {.
|
|
defaultValue: "info"
|
|
desc: "Sets the log level",
|
|
name: "log-level" }: string
|
|
|
|
logFormat* {.
|
|
hidden
|
|
desc: "Specifies what kind of logs should be written to stdout (auto, colors, nocolors, json)"
|
|
defaultValueDesc: "auto"
|
|
defaultValue: LogKind.Auto
|
|
name: "log-format" }: LogKind
|
|
|
|
metricsEnabled* {.
|
|
desc: "Enable the metrics server"
|
|
defaultValue: false
|
|
name: "metrics" }: bool
|
|
|
|
metricsAddress* {.
|
|
desc: "Listening address of the metrics server"
|
|
defaultValue: ValidIpAddress.init("127.0.0.1")
|
|
defaultValueDesc: "127.0.0.1"
|
|
name: "metrics-address" }: ValidIpAddress
|
|
|
|
metricsPort* {.
|
|
desc: "Listening HTTP port of the metrics server"
|
|
defaultValue: 8008
|
|
name: "metrics-port" }: Port
|
|
|
|
dataDir* {.
|
|
desc: "The directory where codex will store configuration and data"
|
|
defaultValue: DefaultDataDir
|
|
defaultValueDesc: $DefaultDataDir
|
|
abbr: "d"
|
|
name: "data-dir" }: OutDir
|
|
|
|
listenAddrs* {.
|
|
desc: "Multi Addresses to listen on"
|
|
defaultValue: @[
|
|
MultiAddress.init("/ip4/0.0.0.0/tcp/0")
|
|
.expect("Should init multiaddress")]
|
|
defaultValueDesc: "/ip4/0.0.0.0/tcp/0"
|
|
abbr: "i"
|
|
name: "listen-addrs" }: seq[MultiAddress]
|
|
|
|
# TODO: change this once we integrate nat support
|
|
nat* {.
|
|
desc: "IP Addresses to announce behind a NAT"
|
|
defaultValue: ValidIpAddress.init("127.0.0.1")
|
|
defaultValueDesc: "127.0.0.1"
|
|
abbr: "a"
|
|
name: "nat" }: ValidIpAddress
|
|
|
|
discoveryIp* {.
|
|
desc: "Discovery listen address"
|
|
defaultValue: ValidIpAddress.init(IPv4_any())
|
|
defaultValueDesc: "0.0.0.0"
|
|
abbr: "e"
|
|
name: "disc-ip" }: ValidIpAddress
|
|
|
|
discoveryPort* {.
|
|
desc: "Discovery (UDP) port"
|
|
defaultValue: 8090.Port
|
|
defaultValueDesc: "8090"
|
|
abbr: "u"
|
|
name: "disc-port" }: Port
|
|
|
|
netPrivKeyFile* {.
|
|
desc: "Source of network (secp256k1) private key file path or name"
|
|
defaultValue: "key"
|
|
name: "net-privkey" }: string
|
|
|
|
bootstrapNodes* {.
|
|
desc: "Specifies one or more bootstrap nodes to use when connecting to the network"
|
|
abbr: "b"
|
|
name: "bootstrap-node" }: seq[SignedPeerRecord]
|
|
|
|
maxPeers* {.
|
|
desc: "The maximum number of peers to connect to"
|
|
defaultValue: 160
|
|
name: "max-peers" }: int
|
|
|
|
agentString* {.
|
|
defaultValue: "Codex"
|
|
desc: "Node agent string which is used as identifier in network"
|
|
name: "agent-string" }: string
|
|
|
|
apiBindAddress* {.
|
|
desc: "The REST API bind address"
|
|
defaultValue: "127.0.0.1"
|
|
name: "api-bindaddr"
|
|
}: string
|
|
|
|
apiPort* {.
|
|
desc: "The REST Api port",
|
|
defaultValue: 8080.Port
|
|
defaultValueDesc: "8080"
|
|
name: "api-port"
|
|
abbr: "p" }: Port
|
|
|
|
repoKind* {.
|
|
desc: "Backend for main repo store (fs, sqlite)"
|
|
defaultValueDesc: "fs"
|
|
defaultValue: repoFS
|
|
name: "repo-kind" }: RepoKind
|
|
|
|
storageQuota* {.
|
|
desc: "The size of the total storage quota dedicated to the node"
|
|
defaultValue: DefaultQuotaBytes
|
|
defaultValueDesc: $DefaultQuotaBytes
|
|
name: "storage-quota"
|
|
abbr: "q" }: NBytes
|
|
|
|
blockTtl* {.
|
|
desc: "Default block timeout in seconds - 0 disables the ttl"
|
|
defaultValue: DefaultBlockTtl
|
|
defaultValueDesc: $DefaultBlockTtl
|
|
name: "block-ttl"
|
|
abbr: "t" }: Duration
|
|
|
|
blockMaintenanceInterval* {.
|
|
desc: "Time interval in seconds - determines frequency of block maintenance cycle: how often blocks are checked for expiration and cleanup"
|
|
defaultValue: DefaultBlockMaintenanceInterval
|
|
defaultValueDesc: $DefaultBlockMaintenanceInterval
|
|
name: "block-mi" }: Duration
|
|
|
|
blockMaintenanceNumberOfBlocks* {.
|
|
desc: "Number of blocks to check every maintenance cycle"
|
|
defaultValue: DefaultNumberOfBlocksToMaintainPerInterval
|
|
defaultValueDesc: $DefaultNumberOfBlocksToMaintainPerInterval
|
|
name: "block-mn" }: int
|
|
|
|
cacheSize* {.
|
|
desc: "The size of the block cache, 0 disables the cache - might help on slow hardrives"
|
|
defaultValue: 0
|
|
defaultValueDesc: "0"
|
|
name: "cache-size"
|
|
abbr: "c" }: NBytes
|
|
|
|
logFile* {.
|
|
desc: "Logs to file"
|
|
defaultValue: string.none
|
|
name: "log-file"
|
|
hidden
|
|
.}: Option[string]
|
|
|
|
case cmd* {.
|
|
defaultValue: noCmd
|
|
command }: StartUpCmd
|
|
of persistence:
|
|
ethProvider* {.
|
|
desc: "The URL of the JSON-RPC API of the Ethereum node"
|
|
defaultValue: "ws://localhost:8545"
|
|
name: "eth-provider"
|
|
.}: string
|
|
|
|
ethAccount* {.
|
|
desc: "The Ethereum account that is used for storage contracts"
|
|
defaultValue: EthAddress.none
|
|
defaultValueDesc: ""
|
|
name: "eth-account"
|
|
.}: Option[EthAddress]
|
|
|
|
ethPrivateKey* {.
|
|
desc: "File containing Ethereum private key for storage contracts"
|
|
defaultValue: string.none
|
|
defaultValueDesc: ""
|
|
name: "eth-private-key"
|
|
.}: Option[string]
|
|
|
|
marketplaceAddress* {.
|
|
desc: "Address of deployed Marketplace contract"
|
|
defaultValue: EthAddress.none
|
|
defaultValueDesc: ""
|
|
name: "marketplace-address"
|
|
.}: Option[EthAddress]
|
|
|
|
# TODO: should go behind a feature flag
|
|
simulateProofFailures* {.
|
|
desc: "Simulates proof failures once every N proofs. 0 = disabled."
|
|
defaultValue: 0
|
|
name: "simulate-proof-failures"
|
|
hidden
|
|
.}: int
|
|
|
|
validator* {.
|
|
desc: "Enables validator, requires an Ethereum node"
|
|
defaultValue: false
|
|
name: "validator"
|
|
.}: bool
|
|
|
|
validatorMaxSlots* {.
|
|
desc: "Maximum number of slots that the validator monitors"
|
|
defaultValue: 1000
|
|
name: "validator-max-slots"
|
|
.}: int
|
|
|
|
case persistenceCmd* {.
|
|
defaultValue: noCmd
|
|
command }: PersistenceCmd
|
|
|
|
of PersistenceCmd.prover:
|
|
circomR1cs* {.
|
|
desc: "The r1cs file for the storage circuit"
|
|
defaultValue: $DefaultDataDir / "circuits" / "proof_main.r1cs"
|
|
defaultValueDesc: $DefaultDataDir & "/circuits/proof_main.r1cs"
|
|
name: "circom-r1cs"
|
|
.}: InputFile
|
|
|
|
circomWasm* {.
|
|
desc: "The wasm file for the storage circuit"
|
|
defaultValue: $DefaultDataDir / "circuits" / "proof_main.wasm"
|
|
defaultValueDesc: $DefaultDataDir & "/circuits/proof_main.wasm"
|
|
name: "circom-wasm"
|
|
.}: InputFile
|
|
|
|
circomZkey* {.
|
|
desc: "The zkey file for the storage circuit"
|
|
defaultValue: $DefaultDataDir / "circuits" / "proof_main.zkey"
|
|
defaultValueDesc: $DefaultDataDir & "/circuits/proof_main.zkey"
|
|
name: "circom-zkey"
|
|
.}: InputFile
|
|
|
|
# TODO: should probably be hidden and behind a feature flag
|
|
circomNoZkey* {.
|
|
desc: "Ignore the zkey file - use only for testing!"
|
|
defaultValue: false
|
|
name: "circom-no-zkey"
|
|
.}: bool
|
|
|
|
numProofSamples* {.
|
|
desc: "Number of samples to prove"
|
|
defaultValue: DefaultSamplesNum
|
|
defaultValueDesc: $DefaultSamplesNum
|
|
name: "proof-samples" }: int
|
|
|
|
maxSlotDepth* {.
|
|
desc: "The maximum depth of the slot tree"
|
|
defaultValue: DefaultMaxSlotDepth
|
|
defaultValueDesc: $DefaultMaxSlotDepth
|
|
name: "max-slot-depth" }: int
|
|
|
|
maxDatasetDepth* {.
|
|
desc: "The maximum depth of the dataset tree"
|
|
defaultValue: DefaultMaxDatasetDepth
|
|
defaultValueDesc: $DefaultMaxDatasetDepth
|
|
name: "max-dataset-depth" }: int
|
|
|
|
maxBlockDepth* {.
|
|
desc: "The maximum depth of the network block merkle tree"
|
|
defaultValue: DefaultBlockDepth
|
|
defaultValueDesc: $DefaultBlockDepth
|
|
name: "max-block-depth" }: int
|
|
|
|
maxCellElms* {.
|
|
desc: "The maximum number of elements in a cell"
|
|
defaultValue: DefaultCellElms
|
|
defaultValueDesc: $DefaultCellElms
|
|
name: "max-cell-elements" }: int
|
|
of PersistenceCmd.noCmd:
|
|
discard
|
|
|
|
of StartUpCmd.noCmd:
|
|
discard # end of persistence
|
|
|
|
EthAddress* = ethers.Address
|
|
|
|
logutils.formatIt(LogFormat.textLines, EthAddress): it.short0xHexLog
|
|
logutils.formatIt(LogFormat.json, EthAddress): %it
|
|
|
|
func persistence*(self: CodexConf): bool =
|
|
self.cmd == StartUpCmd.persistence
|
|
|
|
func prover*(self: CodexConf): bool =
|
|
self.persistence and self.persistenceCmd == PersistenceCmd.prover
|
|
|
|
proc getCodexVersion(): string =
|
|
let tag = strip(staticExec("git tag"))
|
|
if tag.isEmptyOrWhitespace:
|
|
return "untagged build"
|
|
return tag
|
|
|
|
proc getCodexRevision(): string =
|
|
# using a slice in a static context breaks nimsuggest for some reason
|
|
var res = strip(staticExec("git rev-parse --short HEAD"))
|
|
return res
|
|
|
|
proc getNimBanner(): string =
|
|
staticExec("nim --version | grep Version")
|
|
|
|
const
|
|
codexVersion* = getCodexVersion()
|
|
codexRevision* = getCodexRevision()
|
|
nimBanner* = getNimBanner()
|
|
|
|
codexFullVersion* =
|
|
"Codex version: " & codexVersion & "\p" &
|
|
"Codex revision: " & codexRevision & "\p" &
|
|
nimBanner
|
|
|
|
proc parseCmdArg*(T: typedesc[MultiAddress],
|
|
input: string): MultiAddress
|
|
{.upraises: [ValueError, LPError].} =
|
|
var ma: MultiAddress
|
|
let res = MultiAddress.init(input)
|
|
if res.isOk:
|
|
ma = res.get()
|
|
else:
|
|
warn "Invalid MultiAddress", input=input, error = res.error()
|
|
quit QuitFailure
|
|
ma
|
|
|
|
proc parseCmdArg*(T: type SignedPeerRecord, uri: string): T =
|
|
var res: SignedPeerRecord
|
|
try:
|
|
if not res.fromURI(uri):
|
|
warn "Invalid SignedPeerRecord uri", uri = uri
|
|
quit QuitFailure
|
|
except CatchableError as exc:
|
|
warn "Invalid SignedPeerRecord uri", uri = uri, error = exc.msg
|
|
quit QuitFailure
|
|
res
|
|
|
|
proc parseCmdArg*(T: type EthAddress, address: string): T =
|
|
EthAddress.init($address).get()
|
|
|
|
proc parseCmdArg*(T: type NBytes, val: string): T =
|
|
var num = 0'i64
|
|
let count = parseSize(val, num, alwaysBin = true)
|
|
if count == 0:
|
|
warn "Invalid number of bytes", nbytes = val
|
|
quit QuitFailure
|
|
NBytes(num)
|
|
|
|
proc parseCmdArg*(T: type Duration, val: string): T =
|
|
var dur: Duration
|
|
let count = parseDuration(val, dur)
|
|
if count == 0:
|
|
warn "Cannot parse duration", dur = dur
|
|
quit QuitFailure
|
|
dur
|
|
|
|
proc readValue*(r: var TomlReader, val: var EthAddress)
|
|
{.upraises: [SerializationError, IOError].} =
|
|
val = EthAddress.init(r.readValue(string)).get()
|
|
|
|
proc readValue*(r: var TomlReader, val: var SignedPeerRecord) =
|
|
without uri =? r.readValue(string).catch, err:
|
|
error "invalid SignedPeerRecord configuration value", error = err.msg
|
|
quit QuitFailure
|
|
|
|
val = SignedPeerRecord.parseCmdArg(uri)
|
|
|
|
proc readValue*(r: var TomlReader, val: var MultiAddress) =
|
|
without input =? r.readValue(string).catch, err:
|
|
error "invalid MultiAddress configuration value", error = err.msg
|
|
quit QuitFailure
|
|
|
|
let res = MultiAddress.init(input)
|
|
if res.isOk:
|
|
val = res.get()
|
|
else:
|
|
warn "Invalid MultiAddress", input=input, error=res.error()
|
|
quit QuitFailure
|
|
|
|
proc readValue*(r: var TomlReader, val: var NBytes)
|
|
{.upraises: [SerializationError, IOError].} =
|
|
var value = 0'i64
|
|
var str = r.readValue(string)
|
|
let count = parseSize(str, value, alwaysBin = true)
|
|
if count == 0:
|
|
error "invalid number of bytes for configuration value", value = str
|
|
quit QuitFailure
|
|
val = NBytes(value)
|
|
|
|
proc readValue*(r: var TomlReader, val: var Duration)
|
|
{.upraises: [SerializationError, IOError].} =
|
|
var str = r.readValue(string)
|
|
var dur: Duration
|
|
let count = parseDuration(str, dur)
|
|
if count == 0:
|
|
error "Invalid duration parse", value = str
|
|
quit QuitFailure
|
|
val = dur
|
|
|
|
# no idea why confutils needs this:
|
|
proc completeCmdArg*(T: type EthAddress; val: string): seq[string] =
|
|
discard
|
|
|
|
proc completeCmdArg*(T: type NBytes; val: string): seq[string] =
|
|
discard
|
|
|
|
proc completeCmdArg*(T: type Duration; val: string): seq[string] =
|
|
discard
|
|
|
|
# silly chronicles, colors is a compile-time property
|
|
proc stripAnsi*(v: string): string =
|
|
var
|
|
res = newStringOfCap(v.len)
|
|
i: int
|
|
|
|
while i < v.len:
|
|
let c = v[i]
|
|
if c == '\x1b':
|
|
var
|
|
x = i + 1
|
|
found = false
|
|
|
|
while x < v.len: # look for [..m
|
|
let c2 = v[x]
|
|
if x == i + 1:
|
|
if c2 != '[':
|
|
break
|
|
else:
|
|
if c2 in {'0'..'9'} + {';'}:
|
|
discard # keep looking
|
|
elif c2 == 'm':
|
|
i = x + 1
|
|
found = true
|
|
break
|
|
else:
|
|
break
|
|
inc x
|
|
|
|
if found: # skip adding c
|
|
continue
|
|
res.add c
|
|
inc i
|
|
|
|
res
|
|
|
|
proc updateLogLevel*(logLevel: string) {.upraises: [ValueError].} =
|
|
# Updates log levels (without clearing old ones)
|
|
let directives = logLevel.split(";")
|
|
try:
|
|
setLogLevel(parseEnum[LogLevel](directives[0].toUpperAscii))
|
|
except ValueError:
|
|
raise (ref ValueError)(msg: "Please specify one of: trace, debug, info, notice, warn, error or fatal")
|
|
|
|
if directives.len > 1:
|
|
for topicName, settings in parseTopicDirectives(directives[1..^1]):
|
|
if not setTopicState(topicName, settings.state, settings.logLevel):
|
|
warn "Unrecognized logging topic", topic = topicName
|
|
|
|
proc setupLogging*(conf: CodexConf) =
|
|
when defaultChroniclesStream.outputs.type.arity != 3:
|
|
warn "Logging configuration options not enabled in the current build"
|
|
else:
|
|
var logFile: ?IoHandle
|
|
proc noOutput(logLevel: LogLevel, msg: LogOutputStr) = discard
|
|
proc writeAndFlush(f: File, msg: LogOutputStr) =
|
|
try:
|
|
f.write(msg)
|
|
f.flushFile()
|
|
except IOError as err:
|
|
logLoggingFailure(cstring(msg), err)
|
|
|
|
proc stdoutFlush(logLevel: LogLevel, msg: LogOutputStr) =
|
|
writeAndFlush(stdout, msg)
|
|
|
|
proc noColorsFlush(logLevel: LogLevel, msg: LogOutputStr) =
|
|
writeAndFlush(stdout, stripAnsi(msg))
|
|
|
|
proc fileFlush(logLevel: LogLevel, msg: LogOutputStr) =
|
|
if file =? logFile:
|
|
if error =? file.writeFile(stripAnsi(msg).toBytes).errorOption:
|
|
error "failed to write to log file", errorCode = $error
|
|
|
|
defaultChroniclesStream.outputs[2].writer = noOutput
|
|
if logFilePath =? conf.logFile and logFilePath.len > 0:
|
|
let logFileHandle = openFile(
|
|
logFilePath,
|
|
{OpenFlags.Write, OpenFlags.Create, OpenFlags.Truncate}
|
|
)
|
|
if logFileHandle.isErr:
|
|
error "failed to open log file",
|
|
path = logFilePath,
|
|
errorCode = $logFileHandle.error
|
|
else:
|
|
logFile = logFileHandle.option
|
|
defaultChroniclesStream.outputs[2].writer = fileFlush
|
|
|
|
defaultChroniclesStream.outputs[1].writer = noOutput
|
|
|
|
let writer =
|
|
case conf.logFormat:
|
|
of LogKind.Auto:
|
|
if isatty(stdout):
|
|
stdoutFlush
|
|
else:
|
|
noColorsFlush
|
|
of LogKind.Colors: stdoutFlush
|
|
of LogKind.NoColors: noColorsFlush
|
|
of LogKind.Json:
|
|
defaultChroniclesStream.outputs[1].writer = stdoutFlush
|
|
noOutput
|
|
of LogKind.None:
|
|
noOutput
|
|
|
|
when codex_enable_log_counter:
|
|
var counter = 0.uint64
|
|
proc numberedWriter(logLevel: LogLevel, msg: LogOutputStr) =
|
|
inc(counter)
|
|
let withoutNewLine = msg[0..^2]
|
|
writer(logLevel, withoutNewLine & " count=" & $counter & "\n")
|
|
defaultChroniclesStream.outputs[0].writer = numberedWriter
|
|
else:
|
|
defaultChroniclesStream.outputs[0].writer = writer
|
|
|
|
try:
|
|
updateLogLevel(conf.logLevel)
|
|
except ValueError as err:
|
|
try:
|
|
stderr.write "Invalid value for --log-level. " & err.msg & "\n"
|
|
except IOError:
|
|
echo "Invalid value for --log-level. " & err.msg
|
|
quit QuitFailure
|
|
|
|
proc setupMetrics*(config: CodexConf) =
|
|
if config.metricsEnabled:
|
|
let metricsAddress = config.metricsAddress
|
|
notice "Starting metrics HTTP server",
|
|
url = "http://" & $metricsAddress & ":" & $config.metricsPort & "/metrics"
|
|
try:
|
|
startMetricsHttpServer($metricsAddress, config.metricsPort)
|
|
except CatchableError as exc:
|
|
raiseAssert exc.msg
|
|
except Exception as exc:
|
|
raiseAssert exc.msg # TODO fix metrics
|