nimbus-eth2/beacon_chain/spec/crypto.nim

382 lines
14 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.
# At the time of writing, the exact definitions of what should be used for
# cryptography in the spec is in flux, with sizes and test vectors still being
# hashed out. This layer helps isolate those chagnes.
# BLS signatures can be combined such that multiple signatures are aggregated.
# Each time a new signature is added, the corresponding public key must be
# added to the verification key as well - if a key signs twice, it must be added
# twice to the verification key. Aggregated signatures can be combined
# arbitrarily (like addition) as long as public keys are aggregated in the same
# way.
#
# In eth2, we use a single bit to record which keys have signed, thus we cannot
# combined overlapping aggregates - ie if we have an aggregate of signatures of
# A, B and C, and another with B, C and D, we cannot practically combine them
# even if in theory it is possible to allow this in BLS.
{.push raises: [Defect].}
import
# Standard library
std/[options, hashes, tables],
# Internal
./digest,
# Status
stew/[endians2, objects, results, byteutils],
blscurve,
chronicles,
json_serialization,
nimcrypto/utils as ncrutils
export results, json_serialization
# Type definitions
# ----------------------------------------------------------------------
const
RawSigSize* = 96
RawPubKeySize* = 48
# RawPrivKeySize* = 48 for Miracl / 32 for BLST
type
# BLS deserialization is a bit slow, so we deserialize public keys and
# signatures lazily - this helps operations like comparisons and hashes to
# be fast (which is important), makes loading blocks and states fast, and
# allows invalid values in the SSZ byte stream, which is valid from an SSZ
# point of view - the invalid values are later processed to
ValidatorPubKey* = object
blob*: array[RawPubKeySize, byte]
ValidatorSig* = object
blob*: array[RawSigSize, byte]
ValidatorPrivKey* = distinct blscurve.SecretKey
BlsCurveType* = ValidatorPrivKey | ValidatorPubKey | ValidatorSig
BlsResult*[T] = Result[T, cstring]
TrustedSig* = object
data*: array[RawSigSize, byte]
SomeSig* = TrustedSig | ValidatorSig
CookedSig* = distinct blscurve.Signature ## \
## Allows loading in an atttestation or other message's signature once across
## all its computations, rather than repeatedly re-loading it each time it is
## referenced. This primarily currently serves the attestation pool.
export AggregateSignature
# API
# ----------------------------------------------------------------------
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#bls-signatures
func toPubKey*(privkey: ValidatorPrivKey): ValidatorPubKey =
## Derive a public key from a private key
# Un-specced in either hash-to-curve or Eth2
var pubKey: blscurve.PublicKey
let ok = publicFromSecret(pubKey, SecretKey privkey)
doAssert ok, "The validator private key was a zero key. This should never happen."
ValidatorPubKey(blob: pubKey.exportRaw())
proc loadWithCache*(v: ValidatorPubKey): Option[blscurve.PublicKey] =
## Parse public key blob - this may fail - this function uses a cache to
## avoid the expensive deserialization - for now, external public keys only
## come from deposits in blocks - when more sources are added, the memory
## usage of the cache should be considered
var cache {.threadvar.}: Table[typeof(v.blob), blscurve.PublicKey]
# Try to get parse value from cache - if it's not in there, try to parse it -
# if that's not possible, it's broken
cache.withValue(v.blob, key) do:
return some key[]
do:
# Only valid keys are cached
var val: blscurve.PublicKey
return
if fromBytes(val, v.blob):
some cache.mGetOrPut(v.blob, val)
else:
none blscurve.PublicKey
proc load*(v: ValidatorSig): Option[blscurve.Signature] =
## Parse signature blob - this may fail
var parsed: blscurve.Signature
if fromBytes(parsed, v.blob):
some(parsed)
else:
none(blscurve.Signature)
func init*(agg: var AggregateSignature, sig: ValidatorSig) {.inline.}=
## Initializes an aggregate signature context
## This assumes that the signature is valid
agg.init(sig.load().get())
func init*(agg: var AggregateSignature, sig: CookedSig) {.inline.}=
## Initializes an aggregate signature context
agg.init(blscurve.Signature(sig))
proc aggregate*(agg: var AggregateSignature, sig: ValidatorSig) {.inline.}=
## Aggregate two Validator Signatures
## Both signatures must be valid
agg.aggregate(sig.load.get())
proc aggregate*(agg: var AggregateSignature, sig: CookedSig) {.inline.}=
## Aggregate two Validator Signatures
agg.aggregate(blscurve.Signature(sig))
func finish*(agg: AggregateSignature): ValidatorSig {.inline.}=
## Canonicalize an AggregateSignature into a signature
var sig: blscurve.Signature
sig.finish(agg)
ValidatorSig(blob: sig.exportRaw())
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#bls-signatures
proc blsVerify*(
pubkey: ValidatorPubKey, message: openArray[byte],
signature: ValidatorSig): bool =
## Check that a signature is valid for a message
## under the provided public key.
## returns `true` if the signature is valid, `false` otherwise.
##
## The proof-of-possession MUST be verified before calling this function.
## It is recommended to use the overload that accepts a proof-of-possession
## to enforce correct usage.
let
parsedSig = signature.load()
if parsedSig.isNone():
false
else:
let
parsedKey = pubkey.loadWithCache()
# It may happen that signatures or keys fail to parse as invalid blobs may
# be passed around - for example, the deposit contract doesn't verify
# signatures, so the loading happens lazily at verification time instead!
parsedKey.isSome() and
parsedKey.get.verify(message, parsedSig.get())
proc blsVerify*(sigSet: SignatureSet): bool =
## Unbatched verification
## of 1 SignatureSet
## tuple[pubkey: blscurve.PublicKey, message: array[32, byte], blscurve.signature: Signature]
verify(
sigSet.pubkey,
sigSet.message,
sigSet.signature
)
func blsSign*(privkey: ValidatorPrivKey, message: openArray[byte]): ValidatorSig =
## Computes a signature from a secret key and a message
let sig = SecretKey(privkey).sign(message)
ValidatorSig(blob: sig.exportRaw())
proc blsFastAggregateVerify*(
publicKeys: openArray[ValidatorPubKey],
message: openArray[byte],
signature: ValidatorSig
): bool =
## Verify the aggregate of multiple signatures on the same message
## This function is faster than AggregateVerify
##
## The proof-of-possession MUST be verified before calling this function.
## It is recommended to use the overload that accepts a proof-of-possession
## to enforce correct usage.
# TODO: Note: `invalid` in the following paragraph means invalid by construction
# The keys/signatures are not even points on the elliptic curves.
# To respect both the IETF API and the fact that
# we can have invalid public keys (as in not point on the elliptic curve),
# requiring a wrapper indirection,
# we need a first pass to extract keys from the wrapper
# and then call fastAggregateVerify.
# Instead:
# - either we expose a new API: context + init-update-finish
# in blscurve which already exists internally
# - or at network/databases/serialization boundaries we do not
# allow invalid BLS objects to pollute consensus routines
let parsedSig = signature.load()
if not parsedSig.isSome():
return false
var unwrapped: seq[PublicKey]
for pubkey in publicKeys:
let realkey = pubkey.loadWithCache()
if realkey.isNone:
return false
unwrapped.add realkey.get
fastAggregateVerify(unwrapped, message, parsedSig.get())
proc toGaugeValue*(hash: Eth2Digest): int64 =
# Only the last 8 bytes are taken into consideration in accordance
# to the ETH2 metrics spec:
# https://github.com/ethereum/eth2.0-metrics/blob/6a79914cb31f7d54858c7dd57eee75b6162ec737/metrics.md#interop-metrics
cast[int64](uint64.fromBytesLE(hash.data.toOpenArray(24, 31)))
# Codecs
# ----------------------------------------------------------------------
func `$`*(x: ValidatorPrivKey): string =
"<private key>"
func `$`*(x: ValidatorPubKey | ValidatorSig): string =
x.blob.toHex()
func toRaw*(x: ValidatorPrivKey): array[32, byte] =
# TODO: distinct type - see https://github.com/status-im/nim-blscurve/pull/67
when BLS_BACKEND == BLST:
result = SecretKey(x).exportRaw()
else:
# Miracl exports to 384-bit arrays, but Curve order is 256-bit
let raw = SecretKey(x).exportRaw()
result[0..32-1] = raw.toOpenArray(48-32, 48-1)
template toRaw*(x: ValidatorPubKey | ValidatorSig): auto =
x.blob
template toRaw*(x: TrustedSig): auto =
x.data
func toHex*(x: BlsCurveType): string =
toHex(toRaw(x))
func exportRaw*(x: CookedSig): ValidatorSig =
ValidatorSig(blob: blscurve.Signature(x).exportRaw())
func fromRaw*(T: type ValidatorPrivKey, bytes: openArray[byte]): BlsResult[T] =
var val: SecretKey
if val.fromBytes(bytes):
ok ValidatorPrivKey(val)
else:
err "bls: invalid private key"
func fromRaw*(BT: type[ValidatorPubKey | ValidatorSig], bytes: openArray[byte]): BlsResult[BT] =
# Signatures and keys are deserialized lazily
if bytes.len() != sizeof(BT):
err "bls: invalid bls length"
else:
ok BT(blob: toArray(sizeof(BT), bytes))
func fromHex*(T: type BlsCurveType, hexStr: string): BlsResult[T] {.inline.} =
## Initialize a BLSValue from its hex representation
try:
T.fromRaw(hexStr.hexToSeqByte())
except ValueError:
err "bls: cannot parse value"
func `==`*(a, b: ValidatorPubKey | ValidatorSig): bool =
equalMem(unsafeAddr a.blob[0], unsafeAddr b.blob[0], sizeof(a.blob))
# Hashing
# ----------------------------------------------------------------------
template hash*(x: ValidatorPubKey | ValidatorSig): Hash =
static: doAssert sizeof(Hash) <= x.blob.len div 2
# We use rough "middle" of blob for the hash, assuming this is where most of
# the entropy is found
cast[ptr Hash](unsafeAddr x.blob[x.blob.len div 2])[]
# Serialization
# ----------------------------------------------------------------------
{.pragma: serializationRaises, raises: [SerializationError, IOError, Defect].}
proc writeValue*(writer: var JsonWriter, value: ValidatorPubKey) {.
inline, raises: [IOError, Defect].} =
writer.writeValue(value.toHex())
proc readValue*(reader: var JsonReader, value: var ValidatorPubKey)
{.serializationRaises.} =
let key = ValidatorPubKey.fromHex(reader.readValue(string))
if key.isOk:
value = key.get
else:
# TODO: Can we provide better diagnostic?
raiseUnexpectedValue(reader, "Valid hex-encoded public key expected")
proc writeValue*(writer: var JsonWriter, value: ValidatorSig) {.
inline, raises: [IOError, Defect].} =
# Workaround: https://github.com/status-im/nimbus-eth2/issues/374
writer.writeValue(value.toHex())
proc readValue*(reader: var JsonReader, value: var ValidatorSig)
{.serializationRaises.} =
let sig = ValidatorSig.fromHex(reader.readValue(string))
if sig.isOk:
value = sig.get
else:
# TODO: Can we provide better diagnostic?
raiseUnexpectedValue(reader, "Valid hex-encoded signature expected")
proc writeValue*(writer: var JsonWriter, value: ValidatorPrivKey) {.
inline, raises: [IOError, Defect].} =
writer.writeValue(value.toHex())
proc readValue*(reader: var JsonReader, value: var ValidatorPrivKey)
{.serializationRaises.} =
let key = ValidatorPrivKey.fromHex(reader.readValue(string))
if key.isOk:
value = key.get
else:
# TODO: Can we provide better diagnostic?
raiseUnexpectedValue(reader, "Valid hex-encoded private key expected")
template fromSszBytes*(T: type[ValidatorPubKey | ValidatorSig], bytes: openArray[byte]): auto =
let v = fromRaw(T, bytes)
if v.isErr:
raise newException(MalformedSszError, $v.error)
v[]
# Logging
# ----------------------------------------------------------------------
func shortLog*(x: ValidatorPubKey | ValidatorSig): string =
## Logging for wrapped BLS types
## that may contain valid or non-validated data
byteutils.toHex(x.blob.toOpenArray(0, 3))
func shortLog*(x: ValidatorPrivKey): string =
## Logging for raw unwrapped BLS types
"<private key>"
func shortLog*(x: TrustedSig): string =
byteutils.toHex(x.data.toOpenArray(0, 3))
# Initialization
# ----------------------------------------------------------------------
# TODO more specific exceptions? don't raise?
# For confutils
func init*(T: typedesc[ValidatorPrivKey], hex: string): T {.noInit, raises: [ValueError, Defect].} =
let v = T.fromHex(hex)
if v.isErr:
raise (ref ValueError)(msg: $v.error)
v[]
# For mainchain monitor
func init*(T: typedesc[ValidatorPubKey], data: array[RawPubKeySize, byte]): T {.noInit, raises: [ValueError, Defect].} =
let v = T.fromRaw(data)
if v.isErr:
raise (ref ValueError)(msg: $v.error)
v[]
# For mainchain monitor
func init*(T: typedesc[ValidatorSig], data: array[RawSigSize, byte]): T {.noInit, raises: [ValueError, Defect].} =
let v = T.fromRaw(data)
if v.isErr:
raise (ref ValueError)(msg: $v.error)
v[]
proc burnMem*(key: var ValidatorPrivKey) =
ncrutils.burnMem(addr key, sizeof(ValidatorPrivKey))