nimbus-eth2/beacon_chain/spec/keystore.nim

1404 lines
46 KiB
Nim
Raw Normal View History

# beacon_chain
# Copyright (c) 2018-2024 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: [].}
import
# Standard library
2021-11-30 01:20:21 +00:00
std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode,
2023-02-16 17:25:48 +00:00
uri, hashes],
2020-10-02 15:46:05 +00:00
# Third-party libraries
normalize,
# Status libraries
results,
stew/[bitops2, base10, io2, endians2], stew/shims/macros,
eth/keyfile/uuid, blscurve,
json_serialization, json_serialization/std/options,
2023-02-16 17:25:48 +00:00
chronos/timer,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
2020-10-02 15:46:05 +00:00
# Local modules
libp2p/crypto/crypto as lcrypto,
./datatypes/base, ./signatures
export base, uri, io2, options
# We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures.
import nimcrypto/utils as ncrutils
export
results, burnMem, writeValue, readValue
{.localPassC: "-fno-lto".} # no LTO for crypto
2020-05-20 07:01:20 +00:00
type
KeystoreMode* = enum
Secure, Fast
ChecksumFunctionKind* = enum
sha256Checksum = "sha256"
Sha256Params* = object
Sha256Digest* = MDigest[256]
ChecksumBytes* = distinct seq[byte]
Checksum* = object
case function*: ChecksumFunctionKind
of sha256Checksum:
params*: Sha256Params
message*: Sha256Digest
Aes128CtrIv* = distinct seq[byte]
Aes128CtrParams* = object
iv*: Aes128CtrIv
CipherFunctionKind* = enum
aes128CtrCipher = "aes-128-ctr"
CipherBytes* = distinct seq[byte]
Cipher* = object
case function*: CipherFunctionKind
of aes128CtrCipher:
params*: Aes128CtrParams
message*: CipherBytes
KdfKind* = enum
kdfPbkdf2 = "pbkdf2"
kdfScrypt = "scrypt"
ScryptSalt* = distinct seq[byte]
ScryptParams* = object
dklen*: uint64
n*, p*, r*: int
salt*: ScryptSalt
Pbkdf2Salt* = distinct seq[byte]
PrfKind* = enum # Pseudo-random-function Kind
HmacSha256 = "hmac-sha256"
Pbkdf2Params* = object
dklen*: uint64
c*: uint64
prf*: PrfKind
salt*: Pbkdf2Salt
DecryptionStatus* = enum
Success = "Success"
InvalidPassword = "Invalid password"
InvalidKeystore = "Invalid keystore"
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
Wallet* = object
uuid*: UUID
name*: WalletName
version*: uint
walletType* {.serializedFieldName: "type"}: string
# TODO: The use of `JsonString` can be removed once we
# solve the serialization problem for `Crypto[T]`
crypto*: Crypto
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
Kdf* = object
case function*: KdfKind
of kdfPbkdf2:
pbkdf2Params* {.serializedFieldName: "params".}: Pbkdf2Params
of kdfScrypt:
scryptParams* {.serializedFieldName: "params".}: ScryptParams
message*: string
Crypto* = object
kdf*: Kdf
checksum*: Checksum
cipher*: Cipher
Keystore* = object
crypto*: Crypto
description*: Option[string]
pubkey*: ValidatorPubKey
path*: KeyPath
uuid*: string
version*: int
KeystoreKind* = enum
Local, Remote
RemoteKeystoreFlag* {.pure.} = enum
IgnoreSSLVerification, DynamicKeystore
HttpHostUri* = distinct Uri
RemoteSignerInfo* = object
url*: HttpHostUri
id*: uint32
pubkey*: ValidatorPubKey
2022-08-07 21:53:20 +00:00
FileLockHandle* = ref object
ioHandle*: IoLockHandle
opened*: bool
RemoteSignerType* {.pure.} = enum
Web3Signer, VerifyingWeb3Signer
ProvenProperty* = object
path*: string
description*: Option[string]
capellaIndex*: Option[GeneralizedIndex]
denebIndex*: Option[GeneralizedIndex]
electraIndex*: Option[GeneralizedIndex]
KeystoreData* = object
version*: uint64
pubkey*: ValidatorPubKey
description*: Option[string]
2022-08-07 21:53:20 +00:00
handle*: FileLockHandle
case kind*: KeystoreKind
of KeystoreKind.Local:
privateKey*: ValidatorPrivKey
path*: KeyPath
uuid*: string
of KeystoreKind.Remote:
flags*: set[RemoteKeystoreFlag]
remotes*: seq[RemoteSignerInfo]
threshold*: uint32
case remoteType*: RemoteSignerType
of RemoteSignerType.Web3Signer:
discard
of RemoteSignerType.VerifyingWeb3Signer:
provenBlockProperties*: seq[ProvenProperty]
NetKeystore* = object
crypto*: Crypto
description*: Option[string]
pubkey*: lcrypto.PublicKey
uuid*: string
version*: int
2021-11-30 01:20:21 +00:00
RemoteKeystore* = object
version*: uint64
2021-11-30 01:20:21 +00:00
description*: Option[string]
case remoteType*: RemoteSignerType
of RemoteSignerType.Web3Signer:
discard
of RemoteSignerType.VerifyingWeb3Signer:
provenBlockProperties*: seq[ProvenProperty]
2021-11-30 01:20:21 +00:00
pubkey*: ValidatorPubKey
flags*: set[RemoteKeystoreFlag]
remotes*: seq[RemoteSignerInfo]
threshold*: uint32
2021-11-30 01:20:21 +00:00
KsResult*[T] = Result[T, string]
Eth2KeyKind* = enum
signingKeyKind # Also known as voting key
withdrawalKeyKind
UUID* = distinct string
WalletName* = distinct string
Mnemonic* = distinct string
KeyPath* = distinct string
KeySeed* = distinct seq[byte]
2020-10-02 15:46:05 +00:00
KeystorePass* = object
str*: string
Credentials* = object
mnemonic*: Mnemonic
keystore*: Keystore
signingKey*: ValidatorPrivKey
withdrawalKey*: ValidatorPrivKey
SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes
2023-02-16 17:25:48 +00:00
CacheItemFlag {.pure.} = enum
Missing, Present
KeystoreCacheItem = object
flag: CacheItemFlag
kdf: Kdf
cipher: Cipher
decryptionKey: seq[byte]
timestamp: Moment
KdfSaltKey* = distinct array[32, byte]
KeystoreCache* = object
expireTime*: Duration
table*: Table[KdfSaltKey, KeystoreCacheItem]
KeystoreCacheRef* = ref KeystoreCache
const
keyLen = 32
2020-05-20 07:22:21 +00:00
scryptParams = ScryptParams(
dklen: uint64 keyLen,
n: 2^18,
2020-08-02 18:46:12 +00:00
p: 1,
r: 8
)
pbkdf2Params = Pbkdf2Params(
dklen: uint64 keyLen,
c: uint64(2^18),
prf: HmacSha256
)
# https://eips.ethereum.org/EIPS/eip-2334
eth2KeyPurpose = 12381
eth2CoinType* = 3600
baseKeyPath* = [Natural eth2KeyPurpose, eth2CoinType]
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
wordListLen = 2048
maxWordLen = 16
2023-02-16 17:25:48 +00:00
KeystoreCachePruningTime* = 5.minutes
UUID.serializesAsBaseIn Json
KeyPath.serializesAsBaseIn Json
WalletName.serializesAsBaseIn Json
ChecksumFunctionKind.serializesAsTextInJson
CipherFunctionKind.serializesAsTextInJson
PrfKind.serializesAsTextInJson
KdfKind.serializesAsTextInJson
template `$`*(u: HttpHostUri): string =
`$`(Uri(u))
template `==`*(lhs, rhs: HttpHostUri): bool =
Uri(lhs) == Uri(rhs)
template `<`*(lhs, rhs: HttpHostUri): bool =
$Uri(lhs) < $Uri(rhs)
template `$`*(m: Mnemonic): string =
string(m)
template `==`*(lhs, rhs: WalletName): bool =
string(lhs) == string(rhs)
template `$`*(x: WalletName): string =
string(x)
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
template burnMem*(m: var (Mnemonic|string)) =
ncrutils.burnMem(distinctBase m)
template burnMem*(m: var KeySeed) =
ncrutils.burnMem(distinctBase m)
2020-10-02 15:46:05 +00:00
template burnMem*(m: var KeystorePass) =
ncrutils.burnMem(m.str)
func longName*(wallet: Wallet): string =
if wallet.name.string == wallet.uuid.string:
wallet.name.string
else:
wallet.name.string & " (" & wallet.uuid.string & ")"
macro wordListArray*(filename: static string,
maxWords: static int = 0,
minWordLen: static int = 0,
maxWordLen: static int = high(int)): untyped =
result = newTree(nnkBracket)
let words = slurp(filename.replace('\\', '/')).splitLines()
for word in words:
if word.len >= minWordLen and word.len <= maxWordLen:
result.add newCall("cstring", newLit(word))
if maxWords > 0 and result.len >= maxWords:
return
const
englishWords = wordListArray("english_word_list.txt",
maxWords = wordListLen,
maxWordLen = maxWordLen)
englishWordsDigest =
"AD90BF3BEB7B0EB7E5ACD74727DC0DA96E0A280A258354E7293FB7E211AC03DB".toDigest
func checkEnglishWords(): bool =
if len(englishWords) != wordListLen:
false
else:
var ctx: sha256
ctx.init()
for item in englishWords:
ctx.update($item)
ctx.finish() == englishWordsDigest
static:
doAssert(checkEnglishWords(), "English words array is corrupted!")
func validateKeyPath*(path: string): Result[KeyPath, cstring] =
var digitCount: int
var number: BiggestUInt
try:
for elem in path.split("/"):
# TODO: doesn't "m" have to be the first character and is it the only
# place where it is valid?
if elem == "m":
continue
# parseBiggestUInt can raise if overflow
digitCount = elem.parseBiggestUInt(number)
if digitCount == 0:
return err("Invalid derivation path")
except ValueError:
return err("KeyPath contains invalid number(s)")
return ok(KeyPath path)
iterator pathNodes(path: KeyPath): Natural =
# TODO: we have exceptions there
# and this iterator is used to derive secret keys
# if we fail we want to scrub secrets from memory
try:
for elem in path.string.split("/"):
if elem == "m": continue
yield parseBiggestUInt(elem)
except ValueError:
doAssert false, "Make sure you've validated the key path with `validateKeyPath`"
func makeKeyPath*(validatorIdx: Natural,
keyType: Eth2KeyKind): KeyPath =
# https://eips.ethereum.org/EIPS/eip-2334
let use = case keyType
of withdrawalKeyKind: "0"
of signingKeyKind: "0/0"
try:
KeyPath &"m/{eth2KeyPurpose}/{eth2CoinType}/{validatorIdx}/{use}"
except ValueError:
raiseAssert "All values above can be converted successfully to strings"
2020-10-02 15:46:05 +00:00
func isControlRune(r: Rune): bool =
let r = int r
(r >= 0 and r < 0x20) or (r >= 0x7F and r < 0xA0)
func init*(T: type KeystorePass, input: string): T =
2020-10-02 15:46:05 +00:00
for rune in toNFKD(input):
if not isControlRune(rune):
result.str.add rune
func getSeed*(mnemonic: Mnemonic, password: KeystorePass): KeySeed =
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
2020-10-02 16:08:57 +00:00
let salt = toNFKD("mnemonic" & password.str)
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
template add(m: var Mnemonic, s: cstring) =
m.string.add s
func generateMnemonic*(
2022-06-21 08:29:16 +00:00
rng: var HmacDrbgContext,
2020-10-28 18:35:31 +00:00
words: openArray[cstring] = englishWords,
entropyParam: openArray[byte] = @[]): Mnemonic =
## Generates a valid BIP-0039 mnenomic:
## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
2022-06-21 08:29:16 +00:00
var entropy =
if entropyParam.len == 0:
rng.generateBytes(32)
else:
doAssert entropyParam.len >= 128 and
entropyParam.len <= 256 and
entropyParam.len mod 32 == 0
@entropyParam
let
checksumBits = entropy.len div 4 # ranges from 4 to 8
mnemonicWordCount = 12 + (checksumBits - 4) * 3
checksum = sha256.digest(entropy)
entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits))
# Make sure the string won't be reallocated as this may
# leave partial copies of the mnemonic in memory:
result = Mnemonic newStringOfCap(mnemonicWordCount * maxWordLen)
result.add words[entropy.getBitsBE(0..10)]
for i in 1 ..< mnemonicWordCount:
let
firstBit = i*11
lastBit = firstBit + 10
result.add " "
result.add words[entropy.getBitsBE(firstBit..lastBit)]
func cmpIgnoreCase(lhs: cstring, rhs: string): int =
# TODO: This is a bit silly.
# Nim should have a `cmp` function for C strings.
cmpIgnoreCase($lhs, rhs)
func validateMnemonic*(inputWords: string,
outputMnemonic: var Mnemonic): bool =
## Accept a case-insensitive input string and returns `true`
## if it represents a valid mnenomic. The `outputMnemonic`
## value will be populated with a normalized lower-case
## version of the mnemonic using a single space separator.
##
## The `outputMnemonic` value may be populated partially
## with sensitive data even in case of validator failure.
## Make sure to burn the received data after usage.
# TODO consider using a SecretString type for inputWords
2024-02-07 16:51:12 +00:00
let words = strutils.strip(inputWords.toNFKD).split(Whitespace)
if words.len < 12 or words.len > 24 or words.len mod 3 != 0:
return false
# Make sure the string won't be re-allocated as this may
# leave partial copies of the mnemonic in memory:
outputMnemonic = Mnemonic newStringOfCap(words.len * maxWordLen)
for word in words:
let foundIdx = binarySearch(englishWords, word, cmpIgnoreCase)
if foundIdx == -1:
return false
if outputMnemonic.string.len > 0:
outputMnemonic.add " "
outputMnemonic.add englishWords[foundIdx]
return true
func deriveChildKey*(parentKey: ValidatorPrivKey,
index: Natural): ValidatorPrivKey =
2020-06-08 14:56:56 +00:00
let success = derive_child_secretKey(SecretKey result,
SecretKey parentKey,
uint32 index)
# TODO `derive_child_secretKey` is reporting pre-condition
# failures with return values. We should turn the checks
# into asserts inside the function.
doAssert success
func deriveMasterKey*(seed: KeySeed): ValidatorPrivKey =
2020-06-08 14:56:56 +00:00
let success = derive_master_secretKey(SecretKey result,
seq[byte] seed)
# TODO `derive_master_secretKey` is reporting pre-condition
# failures with return values. We should turn the checks
# into asserts inside the function.
doAssert success
func deriveMasterKey*(mnemonic: Mnemonic,
password: KeystorePass): ValidatorPrivKey =
deriveMasterKey(getSeed(mnemonic, password))
func deriveChildKey*(masterKey: ValidatorPrivKey,
path: KeyPath): ValidatorPrivKey =
result = masterKey
for idx in pathNodes(path):
result = deriveChildKey(result, idx)
func deriveChildKey*(masterKey: ValidatorPrivKey,
path: openArray[Natural]): ValidatorPrivKey =
result = masterKey
for idx in path:
# TODO: we have exceptions in pathNodes unless `validateKeyPath`
# was called,
# and this iterator is used to derive secret keys
# if we fail we want to scrub secrets from memory
result = deriveChildKey(result, idx)
func keyFromPath*(mnemonic: Mnemonic,
password: KeystorePass,
path: KeyPath): ValidatorPrivKey =
deriveChildKey(deriveMasterKey(mnemonic, password), path)
func shaChecksum(key, cipher: openArray[byte]): Sha256Digest =
var ctx: sha256
ctx.init()
ctx.update(key)
ctx.update(cipher)
result = ctx.finish()
2020-05-20 07:22:21 +00:00
ctx.clear()
2020-10-28 18:35:31 +00:00
proc writeJsonHexString(s: OutputStream, data: openArray[byte])
{.raises: [IOError].} =
s.write '"'
s.write ncrutils.toHex(data, {HexFlags.LowerCase})
s.write '"'
proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
{.raises: [SerializationError, IOError].} =
let s = r.readValue(string)
if s.len == 0 or s.len mod 16 != 0:
r.raiseUnexpectedValue(
"The Pbkdf2Salt salt must have a non-zero length divisible by 16")
2020-05-27 14:05:32 +00:00
value = Pbkdf2Salt ncrutils.fromHex(s)
let length = len(seq[byte](value))
if length == 0 or (length mod 8) != 0:
r.raiseUnexpectedValue(
"The Pbkdf2Salt must be a valid hex string")
proc readValue*(r: var JsonReader, value: var Aes128CtrIv)
{.raises: [SerializationError, IOError].} =
let s = r.readValue(string)
if s.len != 32:
r.raiseUnexpectedValue(
"The aes-128-ctr IV must be a string of length 32")
value = Aes128CtrIv ncrutils.fromHex(s)
if len(seq[byte](value)) != 16:
r.raiseUnexpectedValue(
"The aes-128-ctr IV must be a valid hex string")
proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T) {.
raises: [SerializationError, IOError].} =
value = T ncrutils.fromHex(r.readValue(string))
if len(seq[byte](value)) == 0:
r.raiseUnexpectedValue("Valid hex string expected")
template readValueImpl(r: var JsonReader, value: var Checksum) =
var
functionSpecified = false
paramsSpecified = false
messageSpecified = false
for fieldName in readObjectFields(r):
case fieldName
of "function":
value = Checksum(function: r.readValue(ChecksumFunctionKind))
functionSpecified = true
of "params":
if functionSpecified:
case value.function
of sha256Checksum:
r.readValue(value.params)
else:
r.raiseUnexpectedValue(
"The 'params' field must be specified after the 'function' field")
paramsSpecified = true
of "message":
if functionSpecified:
case value.function
of sha256Checksum:
r.readValue(value.message)
else:
r.raiseUnexpectedValue(
"The 'message' field must be specified after the 'function' field")
messageSpecified = true
else:
r.raiseUnexpectedField(fieldName, "Checksum")
if not (functionSpecified and paramsSpecified and messageSpecified):
r.raiseUnexpectedValue(
"The Checksum value should have sub-fields named " &
"'function', 'params', and 'message'")
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Checksum)
{.raises: [SerializationError, IOError].} =
readValueImpl(r, value)
{.pop.}
template readValueImpl(r: var JsonReader, value: var Cipher) =
var
functionSpecified = false
paramsSpecified = false
messageSpecified = false
for fieldName in readObjectFields(r):
case fieldName
of "function":
value = Cipher(
function: r.readValue(CipherFunctionKind), message: value.message)
functionSpecified = true
of "params":
if functionSpecified:
case value.function
of aes128CtrCipher:
r.readValue(value.params)
else:
r.raiseUnexpectedValue(
"The 'params' field must be specified after the 'function' field")
paramsSpecified = true
of "message":
r.readValue(value.message)
messageSpecified = true
else:
r.raiseUnexpectedField(fieldName, "Cipher")
if not (functionSpecified and paramsSpecified and messageSpecified):
r.raiseUnexpectedValue(
"The Cipher value should have sub-fields named " &
"'function', 'params', and 'message'")
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Cipher)
{.raises: [SerializationError, IOError].} =
readValueImpl(r, value)
{.pop.}
template readValueImpl(r: var JsonReader, value: var Kdf) =
var
functionSpecified = false
paramsSpecified = false
for fieldName in readObjectFields(r):
case fieldName
of "function":
value = Kdf(function: r.readValue(KdfKind), message: value.message)
functionSpecified = true
of "params":
if functionSpecified:
case value.function
of kdfPbkdf2:
r.readValue(value.pbkdf2Params)
of kdfScrypt:
r.readValue(value.scryptParams)
else:
r.raiseUnexpectedValue(
"The 'params' field must be specified after the 'function' field")
paramsSpecified = true
of "message":
r.readValue(value.message)
else:
r.raiseUnexpectedField(fieldName, "Kdf")
if not (functionSpecified and paramsSpecified):
r.raiseUnexpectedValue(
"The Kdf value should have sub-fields named 'function' and 'params'")
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Kdf)
{.raises: [SerializationError, IOError].} =
readValueImpl(r, value)
{.pop.}
func readValue*(r: var JsonReader, value: var (Checksum|Cipher|Kdf)) =
static: raiseAssert "Unknown flavor `JsonReader[" & $typeof(r).Flavor &
"]` for `readValue` of `" & $typeof(value) & "`"
# HttpHostUri
proc readValue*(reader: var JsonReader, value: var HttpHostUri) {.
raises: [IOError, SerializationError].} =
let svalue = reader.readValue(string)
let res = parseUri(svalue)
if res.scheme != "http" and res.scheme != "https":
reader.raiseUnexpectedValue("Incorrect URL scheme")
if len(res.hostname) == 0:
reader.raiseUnexpectedValue("Missing URL hostname")
value = HttpHostUri(res)
proc writeValue*(
writer: var JsonWriter, value: HttpHostUri) {.raises: [IOError].} =
writer.writeValue($distinctBase(value))
# RemoteKeystore
proc writeValue*(
writer: var JsonWriter, value: RemoteKeystore) {.raises: [IOError].} =
writer.beginRecord()
writer.writeField("version", value.version)
writer.writeField("pubkey", "0x" & value.pubkey.toHex())
writer.writeField("remotes", value.remotes)
writer.writeField("threshold", value.threshold)
case value.remoteType
of RemoteSignerType.Web3Signer:
writer.writeField("type", "web3signer")
of RemoteSignerType.VerifyingWeb3Signer:
writer.writeField("type", "verifying-web3signer")
writer.writeField("proven_block_properties", value.provenBlockProperties)
if value.description.isSome():
writer.writeField("description", value.description.get())
if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags:
writer.writeField("ignore_ssl_verification", true)
writer.endRecord()
template writeValue*(w: var JsonWriter,
value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) =
writeJsonHexString(w.stream, distinctBase value)
func parseProvenBlockProperty*(propertyPath: string): Result[ProvenProperty, string] =
if propertyPath == ".execution_payload.fee_recipient":
ok ProvenProperty(
path: propertyPath,
capellaIndex: some GeneralizedIndex(401),
denebIndex: some GeneralizedIndex(801),
electraIndex: some GeneralizedIndex(801))
elif propertyPath == ".graffiti":
ok ProvenProperty(
path: propertyPath,
capellaIndex: some GeneralizedIndex(18),
denebIndex: some GeneralizedIndex(18),
electraIndex: some GeneralizedIndex(18))
else:
err("Keystores with proven properties different than " &
"`.execution_payload.fee_recipient` and `.graffiti` " &
"require a more recent version of Nimbus")
proc readValue*(reader: var JsonReader, value: var RemoteKeystore)
{.raises: [SerializationError, IOError].} =
2021-11-30 01:20:21 +00:00
var
version: Option[uint64]
2021-11-30 01:20:21 +00:00
description: Option[string]
remote: Option[HttpHostUri]
remotes: Option[seq[RemoteSignerInfo]]
remoteType: Option[RemoteSignerType]
provenBlockProperties: Option[seq[ProvenProperty]]
2021-11-30 01:20:21 +00:00
ignoreSslVerification: Option[bool]
pubkey: Option[ValidatorPubKey]
threshold: Option[uint32]
# TODO: implementing deserializers for versioned objects
# manually is extremely error-prone. This should use
# the auto-generated deserializer from nim-json-serialization
for fieldName in readObjectFields(reader):
2021-11-30 01:20:21 +00:00
case fieldName:
of "pubkey":
if pubkey.isSome:
reader.raiseUnexpectedField("Multiple `pubkey` fields found",
"RemoteKeystore")
pubkey = some(reader.readValue(ValidatorPubKey))
2021-11-30 01:20:21 +00:00
of "remote":
if remote.isSome:
reader.raiseUnexpectedField("Multiple `remote` fields found",
"RemoteKeystore")
if remotes.isSome:
reader.raiseUnexpectedField("The `remote` field cannot be specified together with `remotes`",
"RemoteKeystore")
remote = some(reader.readValue(HttpHostUri))
of "remotes":
if remotes.isSome:
reader.raiseUnexpectedField("Multiple `remote` fields found",
"RemoteKeystore")
if remote.isSome:
reader.raiseUnexpectedField("The `remotes` field cannot be specified together with `remote`",
"RemoteKeystore")
if version.isNone:
reader.raiseUnexpectedField(
"The `remotes` field should be specified after the `version` field of the keystore",
"RemoteKeystore")
if version.get < 2:
reader.raiseUnexpectedField(
"The `remotes` field is valid only past version 2 of the remote keystore format",
"RemoteKeystore")
remotes = some(reader.readValue(seq[RemoteSignerInfo]))
2021-11-30 01:20:21 +00:00
of "version":
if version.isSome:
reader.raiseUnexpectedField("Multiple `version` fields found",
"RemoteKeystore")
version = some(reader.readValue(uint64))
if version.get > 3'u64:
reader.raiseUnexpectedValue(
"Remote keystore version " & $version.get &
" requires a more recent version of Nimbus")
2021-11-30 01:20:21 +00:00
of "description":
if description.isSome:
reader.raiseUnexpectedField("Multiple `description` fields found",
"RemoteKeystore")
description = some(reader.readValue(string))
2021-11-30 01:20:21 +00:00
of "ignore_ssl_verification":
if ignoreSslVerification.isSome:
reader.raiseUnexpectedField("Multiple conflicting options found",
"RemoteKeystore")
ignoreSslVerification = some(reader.readValue(bool))
2021-11-30 01:20:21 +00:00
of "type":
if remoteType.isSome:
reader.raiseUnexpectedField("Multiple `type` fields found",
"RemoteKeystore")
let remoteTypeValue = case reader.readValue(string).toLowerAscii()
of "web3signer":
RemoteSignerType.Web3Signer
of "verifying-web3signer":
RemoteSignerType.VerifyingWeb3Signer
else:
reader.raiseUnexpectedValue("Unsupported remote signer `type` value")
remoteType = some remoteTypeValue
of "proven_block_properties":
if provenBlockProperties.isSome:
reader.raiseUnexpectedField("Multiple `proven_block_properties` fields found",
"RemoteKeystore")
if version.isNone:
reader.raiseUnexpectedField(
"The `proven_block_properties` field should be specified after the `version` field of the keystore",
"RemoteKeystore")
if version.get < 3:
reader.raiseUnexpectedField(
"The `proven_block_properties` field is valid only past version 3 of the remote keystore format",
"RemoteKeystore")
if remoteType.isNone:
reader.raiseUnexpectedField(
"The `proven_block_properties` field should be specified after the `type` field of the keystore",
"RemoteKeystore")
if remoteType.get != RemoteSignerType.VerifyingWeb3Signer:
reader.raiseUnexpectedField(
"The `proven_block_properties` field can be specified only when the remote signer type is 'verifying-web3signer'",
"RemoteKeystore")
var provenProperties = reader.readValue(seq[ProvenProperty])
for prop in provenProperties.mitems:
if prop.path == ".execution_payload.fee_recipient":
prop.capellaIndex = some GeneralizedIndex(401)
prop.denebIndex = some GeneralizedIndex(801)
prop.electraIndex = some GeneralizedIndex(801)
elif prop.path == ".graffiti":
prop.capellaIndex = some GeneralizedIndex(18)
prop.denebIndex = some GeneralizedIndex(18)
prop.electraIndex = some GeneralizedIndex(801)
else:
reader.raiseUnexpectedValue("Keystores with proven properties different than " &
"`.execution_payload.fee_recipient` and `.graffiti` " &
"require a more recent version of Nimbus")
provenBlockProperties = some provenProperties
of "threshold":
if threshold.isSome:
reader.raiseUnexpectedField("Multiple `threshold` fields found",
"RemoteKeystore")
if version.isNone:
reader.raiseUnexpectedField(
"The `threshold` field should be specified after the `version` field of the keystore",
"RemoteKeystore")
if version.get < 2:
reader.raiseUnexpectedField(
"The `threshold` field is valid only past version 2 of the remote keystore format",
"RemoteKeystore")
threshold = some(reader.readValue(uint32))
2021-11-30 01:20:21 +00:00
else:
# Ignore unknown field names.
discard
if version.isNone():
reader.raiseUnexpectedValue("The required field `version` is missing")
if remotes.isNone():
if remote.isSome and pubkey.isSome:
remotes = some @[RemoteSignerInfo(
pubkey: pubkey.get,
id: 0,
url: remote.get
)]
else:
reader.raiseUnexpectedValue("The required field `remotes` is missing")
if threshold.isNone:
if remotes.get.len > 1:
reader.raiseUnexpectedValue("The `threshold` field must be specified when using distributed keystores")
else:
if threshold.get.uint64 > remotes.get.lenu64:
reader.raiseUnexpectedValue("The specified `threshold` must be lower than the number of remote signers")
2021-11-30 01:20:21 +00:00
if pubkey.isNone():
reader.raiseUnexpectedValue("Field `pubkey` is missing")
if version.get >= 3:
if remoteType.isNone:
reader.raiseUnexpectedValue("The required field `type` is missing")
case remoteType.get
of RemoteSignerType.Web3Signer:
discard
of RemoteSignerType.VerifyingWeb3Signer:
if provenBlockProperties.isNone:
reader.raiseUnexpectedValue("The required field `proven_block_properties` is missing")
value = case remoteType.get(RemoteSignerType.Web3Signer)
of RemoteSignerType.Web3Signer:
RemoteKeystore(
version: 2'u64,
pubkey: pubkey.get,
description: description,
remoteType: RemoteSignerType.Web3Signer,
remotes: remotes.get,
threshold: threshold.get(1))
of RemoteSignerType.VerifyingWeb3Signer:
RemoteKeystore(
version: 2'u64,
pubkey: pubkey.get,
description: description,
remoteType: RemoteSignerType.VerifyingWeb3Signer,
provenBlockProperties: provenBlockProperties.get,
remotes: remotes.get,
threshold: threshold.get(1))
2021-11-30 01:20:21 +00:00
template bytes(value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv): seq[byte] =
distinctBase value
2020-08-02 18:46:12 +00:00
func scrypt(password: openArray[char], salt: openArray[byte],
N, r, p: int; keyLen: static[int]): array[keyLen, byte] =
2020-08-02 18:46:12 +00:00
let (xyvLen, bLen) = scryptCalc(N, r, p)
var xyv = newSeq[uint32](xyvLen)
var b = newSeq[byte](bLen)
discard scrypt(password, salt, N, r, p, xyv, b, result)
func areValid(params: Pbkdf2Params): bool =
if params.c == 0 or params.dklen < 32 or params.salt.bytes.len == 0:
return false
# https://www.ietf.org/rfc/rfc2898.txt
let hLen = case params.prf
of HmacSha256: 256 / 8
params.dklen <= high(uint32).uint64 * hLen.uint64
func areValid(params: ScryptParams): bool =
static: doAssert scryptParams.dklen >= 32
params.dklen == scryptParams.dklen and
params.n == scryptParams.n and
params.r == scryptParams.r and
params.p == scryptParams.p and
params.salt.bytes.len > 0
func decryptCryptoField*(crypto: Crypto, decKey: openArray[byte],
outSecret: var seq[byte]): DecryptionStatus =
if crypto.cipher.message.bytes.len == 0:
2023-02-16 17:25:48 +00:00
return DecryptionStatus.InvalidKeystore
if len(decKey) < keyLen:
return DecryptionStatus.InvalidKeystore
let valid =
case crypto.checksum.function
of sha256Checksum:
template params: auto {.used.} = crypto.checksum.params
template message: auto = crypto.checksum.message
message == shaChecksum(decKey.toOpenArray(16, 31),
crypto.cipher.message.bytes)
if not valid:
2023-02-16 17:25:48 +00:00
return DecryptionStatus.InvalidPassword
case crypto.cipher.function
of aes128CtrCipher:
template params: auto = crypto.cipher.params
var aesCipher: CTR[aes128]
outSecret.setLen(crypto.cipher.message.bytes.len)
aesCipher.init(decKey.toOpenArray(0, 15), params.iv.bytes)
aesCipher.decrypt(crypto.cipher.message.bytes, outSecret)
aesCipher.clear()
2023-02-16 17:25:48 +00:00
DecryptionStatus.Success
func getDecryptionKey*(crypto: Crypto, password: KeystorePass,
2023-02-16 17:25:48 +00:00
decKey: var seq[byte]): DecryptionStatus =
let res =
case crypto.kdf.function
of kdfPbkdf2:
template params: auto = crypto.kdf.pbkdf2Params
if not params.areValid or params.c > high(int).uint64:
return DecryptionStatus.InvalidKeystore
Eth2DigestCtx.pbkdf2(password.str, params.salt.bytes, int(params.c),
int(params.dklen))
of kdfScrypt:
template params: auto = crypto.kdf.scryptParams
if not params.areValid:
return DecryptionStatus.InvalidKeystore
@(scrypt(password.str, params.salt.bytes, scryptParams.n,
scryptParams.r, scryptParams.p, int(scryptParams.dklen)))
decKey = res
DecryptionStatus.Success
func decryptCryptoField*(crypto: Crypto,
2023-02-16 17:25:48 +00:00
password: KeystorePass,
outSecret: var seq[byte]): DecryptionStatus =
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
var decKey: seq[byte]
if crypto.cipher.message.bytes.len == 0:
return InvalidKeystore
2023-02-16 17:25:48 +00:00
let res = getDecryptionKey(crypto, password, decKey)
if res != DecryptionStatus.Success:
return res
decryptCryptoField(crypto, decKey, outSecret)
func cstringToStr(v: cstring): string = $v
template parseKeystore*(jsonContent: string): Keystore =
Json.decode(jsonContent, Keystore,
requireAllFields = true,
allowUnknownFields = true)
template parseNetKeystore*(jsonContent: string): NetKeystore =
Json.decode(jsonContent, NetKeystore,
requireAllFields = true,
allowUnknownFields = true)
template parseRemoteKeystore*(jsonContent: string): RemoteKeystore =
Json.decode(jsonContent, RemoteKeystore,
requireAllFields = false,
allowUnknownFields = true)
func getSaltKey(keystore: Keystore, password: KeystorePass): KdfSaltKey =
2023-02-16 17:25:48 +00:00
let digest =
case keystore.crypto.kdf.function
of kdfPbkdf2:
template params: auto = keystore.crypto.kdf.pbkdf2Params
withEth2Hash:
h.update(seq[byte](params.salt))
h.update(password.str.toOpenArrayByte(0, len(password.str) - 1))
h.update(toBytesLE(params.dklen))
h.update(toBytesLE(params.c))
let prf = $params.prf
h.update(prf.toOpenArrayByte(0, len(prf) - 1))
of kdfScrypt:
template params: auto = keystore.crypto.kdf.scryptParams
withEth2Hash:
h.update(seq[byte](params.salt))
h.update(password.str.toOpenArrayByte(0, len(password.str) - 1))
h.update(toBytesLE(params.dklen))
h.update(toBytesLE(uint64(params.n)))
h.update(toBytesLE(uint64(params.p)))
h.update(toBytesLE(uint64(params.r)))
KdfSaltKey(digest.data)
func `==`*(a, b: KdfSaltKey): bool {.borrow.}
func hash*(salt: KdfSaltKey): Hash {.borrow.}
2023-02-16 17:25:48 +00:00
{.push warning[ProveField]:off.}
2023-02-16 17:25:48 +00:00
func `==`*(a, b: Kdf): bool =
# We do not care about `message` field.
if a.function != b.function:
return false
case a.function
of kdfPbkdf2:
template aparams: auto = a.pbkdf2Params
template bparams: auto = b.pbkdf2Params
(aparams.dklen == bparams.dklen) and (aparams.c == bparams.c) and
(aparams.prf == bparams.prf) and (len(seq[byte](aparams.salt)) > 0) and
(seq[byte](aparams.salt) == seq[byte](bparams.salt))
of kdfScrypt:
template aparams: auto = a.scryptParams
template bparams: auto = b.scryptParams
(aparams.dklen == bparams.dklen) and (aparams.n == bparams.n) and
(aparams.p == bparams.p) and (aparams.r == bparams.r) and
(len(seq[byte](aparams.salt)) > 0) and
(seq[byte](aparams.salt) == seq[byte](bparams.salt))
{.pop.}
2023-02-16 17:25:48 +00:00
func `==`*(a, b: Cipher): bool =
# We do not care about `params` and `message` fields.
a.function == b.function
func `==`*(a, b: KeystoreCacheItem): bool =
(a.kdf == b.kdf) and (a.cipher == b.cipher) and
(a.decryptionKey == b.decryptionKey)
func init*(t: typedesc[KeystoreCacheRef],
expireTime = KeystoreCachePruningTime): KeystoreCacheRef =
KeystoreCacheRef(
table: initTable[KdfSaltKey, KeystoreCacheItem](),
expireTime: expireTime
)
func clear*(cache: KeystoreCacheRef) =
2023-02-16 17:25:48 +00:00
cache.table.clear()
proc pruneExpiredKeys*(cache: KeystoreCacheRef) =
if cache.expireTime == InfiniteDuration:
return
let currentTime = Moment.now()
var keys: seq[KdfSaltKey]
for key, value in cache.table.mpairs():
if currentTime - value.timestamp >= cache.expireTime:
keys.add(key)
burnMem(value.decryptionKey)
for item in keys:
cache.table.del(item)
proc init*(t: typedesc[KeystoreCacheItem], keystore: Keystore,
key: openArray[byte]): KeystoreCacheItem =
KeystoreCacheItem(flag: CacheItemFlag.Present, kdf: keystore.crypto.kdf,
cipher: keystore.crypto.cipher, decryptionKey: @key,
timestamp: Moment.now())
func getCachedKey*(cache: KeystoreCacheRef,
2023-02-16 17:25:48 +00:00
keystore: Keystore, password: KeystorePass): Opt[seq[byte]] =
if isNil(cache): return Opt.none(seq[byte])
let
saltKey = keystore.getSaltKey(password)
item = cache.table.getOrDefault(saltKey)
case item.flag
of CacheItemFlag.Present:
if (item.kdf == keystore.crypto.kdf) and
(item.cipher == keystore.crypto.cipher):
Opt.some(item.decryptionKey)
else:
Opt.none(seq[byte])
else:
Opt.none(seq[byte])
proc setCachedKey*(cache: KeystoreCacheRef, keystore: Keystore,
password: KeystorePass, key: openArray[byte]) =
if isNil(cache): return
let saltKey = keystore.getSaltKey(password)
cache.table[saltKey] = KeystoreCacheItem.init(keystore, key)
func destroyCacheKey*(cache: KeystoreCacheRef,
2023-02-16 17:25:48 +00:00
keystore: Keystore, password: KeystorePass) =
if isNil(cache): return
let saltKey = keystore.getSaltKey(password)
cache.table.withValue(saltKey, item):
burnMem(item[].decryptionKey)
cache.table.del(saltKey)
proc decryptKeystore*(keystore: Keystore,
2023-02-16 17:25:48 +00:00
password: KeystorePass,
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
var secret: seq[byte]
defer: burnMem(secret)
2023-02-16 17:25:48 +00:00
while true:
let res = cache.getCachedKey(keystore, password)
if res.isNone():
var decKey: seq[byte]
defer: burnMem(decKey)
let kres = getDecryptionKey(keystore.crypto, password, decKey)
if kres != DecryptionStatus.Success:
return err($kres)
let dres = decryptCryptoField(keystore.crypto, decKey, secret)
if dres != DecryptionStatus.Success:
return err($dres)
cache.setCachedKey(keystore, password, decKey)
break
else:
var decKey = res.get()
defer: burnMem(decKey)
let dres = decryptCryptoField(keystore.crypto, decKey, secret)
if dres == DecryptionStatus.Success:
break
cache.destroyCacheKey(keystore, password)
ValidatorPrivKey.fromRaw(secret).mapErr(cstringToStr)
proc decryptKeystore*(keystore: JsonString,
2023-02-16 17:25:48 +00:00
password: KeystorePass,
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
let keystore =
try:
parseKeystore(string(keystore))
except SerializationError as e:
return err(e.formatMsg("<keystore>"))
decryptKeystore(keystore, password, cache)
proc decryptKeystore*(keystore: Keystore,
password: KeystorePass): KsResult[ValidatorPrivKey] =
2023-02-16 17:25:48 +00:00
decryptKeystore(keystore, password, nil)
proc decryptKeystore*(keystore: JsonString,
password: KeystorePass): KsResult[ValidatorPrivKey] =
decryptKeystore(keystore, password, nil)
proc writeValue*(
writer: var JsonWriter, value: lcrypto.PublicKey
) {.inline, raises: [IOError].} =
writer.writeValue(ncrutils.toHex(value.getBytes().get(),
{HexFlags.LowerCase}))
proc readValue*(reader: var JsonReader, value: var lcrypto.PublicKey) {.
raises: [SerializationError, IOError].} =
let res = init(lcrypto.PublicKey, reader.readValue(string))
if res.isOk():
value = res.get()
else:
# TODO: Can we provide better diagnostic?
raiseUnexpectedValue(reader, "Valid hex-encoded public key expected")
func decryptNetKeystore*(nkeystore: NetKeystore,
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
var secret: seq[byte]
defer: burnMem(secret)
let status = decryptCryptoField(nkeystore.crypto, password, secret)
case status
of Success:
let res = lcrypto.PrivateKey.init(secret)
if res.isOk:
ok res.get
else:
err "Invalid key"
else:
err $status
func decryptNetKeystore*(nkeystore: JsonString,
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
try:
let keystore = parseNetKeystore(string nkeystore)
return decryptNetKeystore(keystore, password)
except SerializationError as exc:
return err(exc.formatMsg("<keystore>"))
func generateKeystoreSalt*(rng: var HmacDrbgContext): seq[byte] =
2023-02-16 17:25:48 +00:00
rng.generateBytes(keyLen)
func createCryptoField(kdfKind: KdfKind,
2022-06-21 08:29:16 +00:00
rng: var HmacDrbgContext,
2020-10-28 18:35:31 +00:00
secret: openArray[byte],
2020-10-02 15:46:05 +00:00
password = KeystorePass.init "",
2020-10-28 18:35:31 +00:00
salt: openArray[byte] = @[],
iv: openArray[byte] = @[],
mode = Secure): Crypto =
type AES = aes128
2020-08-02 18:46:12 +00:00
let kdfSalt =
if salt.len > 0:
doAssert salt.len == keyLen
@salt
else:
2022-06-21 08:29:16 +00:00
rng.generateBytes(keyLen)
let aesIv = if iv.len > 0:
doAssert iv.len == AES.sizeBlock
@iv
else:
2022-06-21 08:29:16 +00:00
rng.generateBytes(AES.sizeBlock)
2020-08-02 18:46:12 +00:00
var decKey: seq[byte]
let kdf = case kdfKind
of kdfPbkdf2:
var params = pbkdf2Params
2020-08-02 18:46:12 +00:00
params.salt = Pbkdf2Salt kdfSalt
if mode == Fast: params.c = 1
decKey = sha256.pbkdf2(password.str,
kdfSalt,
int params.c,
int params.dklen)
Kdf(function: kdfPbkdf2, pbkdf2Params: params, message: "")
of kdfScrypt:
2020-08-02 18:46:12 +00:00
var params = scryptParams
params.salt = ScryptSalt kdfSalt
if mode == Fast: params.n = 1
decKey = @(scrypt(password.str, kdfSalt,
params.n, params.r, params.p, keyLen))
2020-08-02 18:46:12 +00:00
Kdf(function: kdfScrypt, scryptParams: params, message: "")
var
aesCipher: CTR[AES]
cipherMsg = newSeq[byte](secret.len)
2020-05-20 07:22:21 +00:00
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
aesCipher.encrypt(secret, cipherMsg)
aesCipher.clear()
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
Crypto(
kdf: kdf,
checksum: Checksum(
function: sha256Checksum,
message: sum),
cipher: Cipher(
function: aes128CtrCipher,
params: Aes128CtrParams(iv: Aes128CtrIv aesIv),
message: CipherBytes cipherMsg))
proc createNetKeystore*(kdfKind: KdfKind,
2022-06-21 08:29:16 +00:00
rng: var HmacDrbgContext,
privKey: lcrypto.PrivateKey,
2020-10-02 15:46:05 +00:00
password = KeystorePass.init "",
description = "",
2020-10-28 18:35:31 +00:00
salt: openArray[byte] = @[],
iv: openArray[byte] = @[]): NetKeystore =
let
secret = privKey.getBytes().get()
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv)
pubkey = privKey.getPublicKey().get()
uuid = uuidGenerate().expect("Random bytes should be available")
NetKeystore(
crypto: cryptoField,
pubkey: pubkey,
description: if len(description) > 0: some(description)
else: none[string](),
uuid: $uuid,
version: 1
)
proc createKeystore*(kdfKind: KdfKind,
2022-06-21 08:29:16 +00:00
rng: var HmacDrbgContext,
privKey: ValidatorPrivKey,
2020-10-02 15:46:05 +00:00
password = KeystorePass.init "",
path = KeyPath "",
2020-08-02 17:54:48 +00:00
description = "",
2020-10-28 18:35:31 +00:00
salt: openArray[byte] = @[],
iv: openArray[byte] = @[],
mode = Secure): Keystore =
let
secret = privKey.toRaw[^32..^1]
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv, mode)
pubkey = privKey.toPubKey()
uuid = uuidGenerate().expect("Random bytes should be available")
Keystore(
crypto: cryptoField,
pubkey: pubkey.toPubKey(),
path: path,
description: if len(description) > 0: some(description)
else: none[string](),
uuid: $uuid,
version: 4)
func createRemoteKeystore*(pubKey: ValidatorPubKey, remoteUri: HttpHostUri,
version = 1'u64, description = "",
remoteType = RemoteSignerType.Web3Signer,
flags: set[RemoteKeystoreFlag] = {}): RemoteKeystore =
let signerInfo = RemoteSignerInfo(
url: remoteUri,
pubkey: pubKey,
id: 0
)
RemoteKeystore(
version: version,
description: if len(description) > 0: some(description)
else: none[string](),
remoteType: remoteType,
pubkey: pubKey,
remotes: @[signerInfo],
flags: flags
)
proc createWallet*(kdfKind: KdfKind,
2022-06-21 08:29:16 +00:00
rng: var HmacDrbgContext,
seed: KeySeed,
name = WalletName "",
2020-10-28 18:35:31 +00:00
salt: openArray[byte] = @[],
iv: openArray[byte] = @[],
2020-10-02 15:46:05 +00:00
password = KeystorePass.init "",
nextAccount = none(Natural),
pretty = true): Wallet =
let
uuid = UUID $(uuidGenerate().expect("Random bytes should be available"))
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
password, salt, iv)
Wallet(
uuid: uuid,
name: if name.string.len > 0: name
else: WalletName(uuid),
version: 1,
walletType: "hierarchical deterministic",
crypto: crypto,
nextAccount: nextAccount.get(0))
# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/phase0/validator.md#bls_withdrawal_prefix
func makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
var bytes = eth2digest(k.toRaw())
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
bytes
# https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.2/specs/phase0/deposit-contract.md#withdrawal-credentials
func makeWithdrawalCredentials*(k: CookedPubKey): Eth2Digest =
makeWithdrawalCredentials(k.toPubKey())
func prepareDeposit*(cfg: RuntimeConfig,
withdrawalPubKey: CookedPubKey,
signingKey: ValidatorPrivKey, signingPubKey: CookedPubKey,
amount = MAX_EFFECTIVE_BALANCE.Gwei): DepositData =
var res = DepositData(
amount: amount,
pubkey: signingPubKey.toPubKey(),
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
res.signature = get_deposit_signature(cfg, res, signingKey).toValidatorSig()
return res