Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy crypto [alt design #1369] #1371

Merged
merged 4 commits into from
Jul 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions beacon_chain/spec/beaconstate.nim
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,10 @@ proc initialize_beacon_state_from_eth1*(
Eth1Data(block_hash: eth1_block_hash, deposit_count: uint64(len(deposits))),
latest_block_header:
BeaconBlockHeader(
body_root: hash_tree_root(BeaconBlockBody(
# This differs from the spec intentionally.
# We must specify the default value for `ValidatorSig`
# in order to get a correct `hash_tree_root`.
randao_reveal: ValidatorSig(kind: OpaqueBlob)
))
# This differs from the spec intentionally.
# We must specify the default value for `ValidatorSig`/`BeaconBlockBody`
# in order to get a correct `hash_tree_root`.
body_root: hash_tree_root(BeaconBlockBody())
)
)

Expand Down Expand Up @@ -307,10 +305,7 @@ func is_valid_genesis_state*(preset: RuntimePreset,
func get_initial_beacon_block*(state: BeaconState): SignedBeaconBlock =
let message = BeaconBlock(
slot: GENESIS_SLOT,
state_root: hash_tree_root(state),
body: BeaconBlockBody(
# TODO: This shouldn't be necessary if OpaqueBlob is the default
randao_reveal: ValidatorSig(kind: OpaqueBlob)))
state_root: hash_tree_root(state))
# parent_root, randao_reveal, eth1_data, signature, and body automatically
# initialized to default values.
SignedBeaconBlock(message: message, root: hash_tree_root(message))
Expand Down
116 changes: 91 additions & 25 deletions beacon_chain/spec/crypto.nim
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,30 @@ const
RawPrivKeySize* = 48

type
BlsValueType* = enum
BlsValueKind* = enum
ToBeChecked
Real
OpaqueBlob
InvalidBLS
OpaqueBlob # For SSZ testing only

BlsValue*[N: static int, T: blscurve.PublicKey or blscurve.Signature] = object
# TODO This is a temporary type needed until we sort out the
# issues with invalid BLS values appearing in the SSZ test suites.
case kind*: BlsValueType
## This is a lazily initiated wrapper for the underlying cryptographic type
##
## Fields intentionally private to avoid displaying/logging the raw data
## or accessing fields without promoting them
## or trying to iterate on a case object even though the case is wrong.
## Is there a way to prevent macro from doing that? (SSZ/Chronicles)
#
# Note, since 0.20 case object transition are very restrictive
# and do not allow to preserve content (https://github.com/nim-lang/RFCs/issues/56)
# Fortunately, the content is transformed anyway if the object is valid
# but we might want to keep the invalid content at least for logging before discarding it.
# Our usage requires "-d:nimOldCaseObjects"
case kind: BlsValueKind
of Real:
blsValue*: T
of OpaqueBlob:
blob*: array[N, byte]
blsValue: T
of ToBeChecked, InvalidBLS, OpaqueBlob:
blob: array[N, byte]

ValidatorPubKey* = BlsValue[RawPubKeySize, blscurve.PublicKey]

Expand All @@ -75,7 +87,58 @@ type

SomeSig* = TrustedSig | ValidatorSig

# Lazy parsing
# ----------------------------------------------------------------------

func unsafePromote*[N, T](a: ptr BlsValue[N, T]) =
## Try promoting an opaque blob to its corresponding
## BLS value.
##
## ⚠️ Warning - unsafe.
## At a low-level we mutate the input but all API like
## bls_sign, bls_verify assume that their inputs are immutable
if a.kind != ToBeChecked:
return

# Try if valid BLS value
var buffer: T
let success = buffer.fromBytes(a.blob)

# Unsafe hidden mutation of the input
if true:
a.kind = Real # Requires "-d:nimOldCaseObjects"
a.blsValue = buffer
else:
a.kind = InvalidBLS

# Accessors
# ----------------------------------------------------------------------

func setBlob*[N, T](a: var BlsValue[N, T], data: array[N, byte]) {.inline.} =
## Set a BLS Value lazily
a.blob = data

func keyGen*(ikm: openArray[byte]): BlsResult[tuple[pub: ValidatorPubKey, priv: ValidatorPrivKey]] {.inline.} =
## Key generation using the BLS Signature draft 2 (https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature-02)
## Note: As of July-2020, the only use-case is for testing
##
## Validator key generation should use Lamport Signatures (EIP-2333)
## (https://eips.ethereum.org/EIPS/eip-2333)
## and be done in a dedicated hardened module/process.
var
sk: SecretKey
pk: PublicKey
if keyGen(ikm, pk, sk):
ok((ValidatorPubKey(kind: Real, blsValue: pk), ValidatorPrivKey(sk)))
else:
err "bls: cannot generate keypair"

# Comparison
# ----------------------------------------------------------------------

func `==`*(a, b: BlsValue): bool =
unsafePromote(a.unsafeAddr)
unsafePromote(b.unsafeAddr)
if a.kind != b.kind: return false
if a.kind == Real:
return a.blsValue == b.blsValue
Expand All @@ -96,16 +159,14 @@ func toPubKey*(privkey: ValidatorPrivKey): ValidatorPubKey =
## Create a private key from a public key
# Un-specced in either hash-to-curve or Eth2
# TODO: Test suite should use `keyGen` instead
when ValidatorPubKey is BlsValue:
ValidatorPubKey(kind: Real, blsValue: SecretKey(privkey).privToPub())
elif ValidatorPubKey is array:
privkey.getKey.getBytes
else:
privkey.getKey
ValidatorPubKey(kind: Real, blsValue: SecretKey(privkey).privToPub())

func aggregate*(x: var ValidatorSig, other: ValidatorSig) =
## Aggregate 2 Validator Signatures
## This assumes that they are real signatures
## and will crash if they are not
unsafePromote(x.addr)
unsafePromote(other.unsafeAddr)
x.blsValue.aggregate(other.blsValue)

# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#bls-signatures
Expand All @@ -119,8 +180,11 @@ func blsVerify*(
## 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.
unsafePromote(pubkey.unsafeAddr)
unsafePromote(signature.unsafeAddr)

if signature.kind != Real:
# Invalid signatures are possible in deposits (discussed with Danny)
# InvalidBLS signatures are possible in deposits (discussed with Danny)
return false
if pubkey.kind != Real:
# TODO: chronicles warning
Expand Down Expand Up @@ -164,13 +228,15 @@ func blsFastAggregateVerify*(
# in blscurve which already exists internally
# - or at network/databases/serialization boundaries we do not
# allow invalid BLS objects to pollute consensus routines
unsafePromote(signature.unsafeAddr)
if signature.kind != Real:
return false
var unwrapped: seq[PublicKey]
for pubkey in publicKeys:
if pubkey.kind != Real:
for i in 0 ..< publicKeys.len:
unsafePromote(publicKeys[i].unsafeAddr)
if publicKeys[i].kind != Real:
return false
unwrapped.add pubkey.blsValue
unwrapped.add publicKeys[i].blsValue

fastAggregateVerify(unwrapped, message, signature.blsValue)

Expand Down Expand Up @@ -224,12 +290,9 @@ func fromRaw*[N, T](BT: type BlsValue[N, T], bytes: openArray[byte]): BlsResult[
# Only for SSZ parsing tests, everything is an opaque blob
ok BT(kind: OpaqueBlob, blob: toArray(N, bytes))
else:
# Try if valid BLS value
var val: T
if fromBytes(val, bytes):
ok BT(kind: Real, blsValue: val)
else:
ok BT(kind: OpaqueBlob, blob: toArray(N, bytes))
# Lazily instantiate the value, it will be checked only on use
# TODO BlsResult is now unnecessary
ok BT(kind: ToBeChecked, blob: toArray(N, bytes))

func fromHex*(T: type BlsCurveType, hexStr: string): BlsResult[T] {.inline.} =
## Initialize a BLSValue from its hex representation
Expand Down Expand Up @@ -317,6 +380,10 @@ func shortLog*(x: ValidatorPrivKey): string =
func shortLog*(x: TrustedSig): string =
x.data[0..3].toHex()

chronicles.formatIt BlsValue: it.shortLog
chronicles.formatIt ValidatorPrivKey: it.shortLog
chronicles.formatIt TrustedSig: it.shortLog

# Initialization
# ----------------------------------------------------------------------

Expand Down Expand Up @@ -346,4 +413,3 @@ func init*(T: typedesc[ValidatorSig], data: array[RawSigSize, byte]): T {.noInit

proc burnMem*(key: var ValidatorPrivKey) =
key = default(ValidatorPrivKey)

2 changes: 1 addition & 1 deletion beacon_chain/validator_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
blockId: string) -> tuple[canonical: bool, header: SignedBeaconBlockHeader]:
let bd = node.getBlockDataFromBlockId(blockId)
let tsbb = bd.data
result.header.signature.blob = tsbb.signature.data
result.header.signature.setBlob(tsbb.signature.data)

result.header.message.slot = tsbb.message.slot
result.header.message.proposer_index = tsbb.message.proposer_index
Expand Down
30 changes: 6 additions & 24 deletions tests/helpers/debug_state.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,10 @@

import
macros,
nimcrypto/utils,
../../beacon_chain/spec/[datatypes, crypto, digest], ../../beacon_chain/ssz/types
# digest is necessary for them to be printed as hex

# Define comparison of object variants for BLSValue
# https://github.com/nim-lang/Nim/issues/6676
# (fully generic available - see also https://github.com/status-im/nim-beacon-chain/commit/993789bad684721bd7c74ea14b35c2d24dbb6e51)
# ----------------------------------------------------------------

proc `==`*(a, b: BlsValue): bool =
## We sometimes need to compare real BlsValue
## from parsed opaque blobs that are not really on the BLS curve
## and full of zeros
if a.kind == Real:
if b.kind == Real:
a.blsvalue == b.blsValue
else:
$a.blsvalue == toHex(b.blob, true)
else:
if b.kind == Real:
toHex(a.blob, true) == $b.blsValue
else:
a.blob == b.blob
export crypto.`==`

# ---------------------------------------------------------------------

Expand All @@ -48,9 +29,10 @@ proc compareStmt(xSubField, ySubField: NimNode, stmts: var NimNode) =
let xStr = $xSubField.toStrLit
let yStr = $ySubField.toStrLit

let isEqual = bindSym("==") # Bind all expose equality, in particular for BlsValue
stmts.add quote do:
doAssert(
`xSubField` == `ySubField`,
`isEqual`(`xSubField`, `ySubField`),
"\nDiff: " & `xStr` & " = " & $`xSubField` & "\n" &
"and " & `yStr` & " = " & $`ySubField` & "\n"
)
Expand All @@ -59,16 +41,16 @@ proc compareContainerStmt(xSubField, ySubField: NimNode, stmts: var NimNode) =
let xStr = $xSubField.toStrLit
let yStr = $ySubField.toStrLit


let isEqual = bindSym("==") # Bind all expose equality, in particular for BlsValue
stmts.add quote do:
doAssert(
`xSubField`.len == `ySubField`.len,
`isEqual`(`xSubField`.len, `ySubField`.len),
"\nDiff: " & `xStr` & ".len = " & $`xSubField`.len & "\n" &
"and " & `yStr` & ".len = " & $`ySubField`.len & "\n"
)
for idx in `xSubField`.low .. `xSubField`.high:
doAssert(
`xSubField`[idx] == `ySubField`[idx],
`isEqual`(`xSubField`[idx], `ySubField`[idx]),
"\nDiff: " & `xStr` & "[" & $idx & "] = " & $`xSubField`[idx] & "\n" &
"and " & `yStr` & "[" & $idx & "] = " & $`ySubField`[idx] & "\n"
)
Expand Down
10 changes: 1 addition & 9 deletions tests/mocking/mock_validator_keys.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import
bearssl, eth/keys,
blscurve/bls_signature_scheme,
../../beacon_chain/spec/[datatypes, crypto, presets]

proc newKeyPair(rng: var BrHmacDrbgContext): BlsResult[tuple[pub: ValidatorPubKey, priv: ValidatorPrivKey]] =
Expand All @@ -22,14 +21,7 @@ proc newKeyPair(rng: var BrHmacDrbgContext): BlsResult[tuple[pub: ValidatorPubKe

var ikm: array[32, byte]
brHmacDrbgGenerate(rng, ikm)

var
sk: SecretKey
pk: bls_signature_scheme.PublicKey
if keyGen(ikm, pk, sk):
ok((ValidatorPubKey(kind: Real, blsValue: pk), ValidatorPrivKey(sk)))
else:
err "bls: cannot generate keypair"
return ikm.keygen()

# this is being indexed inside "mock_deposits.nim" by a value up to `validatorCount`
# which is `num_validators` which is `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT`
Expand Down
9 changes: 6 additions & 3 deletions tests/test_zero_signature.nim
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ suiteReport "Zero signature sanity checks":
timedTest "SSZ serialization roundtrip of SignedBeaconBlockHeader":

let defaultBlockHeader = SignedBeaconBlockHeader(
signature: ValidatorSig(kind: OpaqueBlob)
signature: ValidatorSig()
)

check:
block:
let sigBytes = cast[ptr array[ValidatorSig.sizeof(), byte]](
defaultBlockHeader.signature.unsafeAddr)

var allZeros = true
for val in defaultBlockHeader.signature.blob:
allZeros = allZeros and val == 0
for i in 0 ..< ValidatorSig.sizeof():
allZeros = allZeros and sigBytes[i] == byte 0
allZeros

let sszDefaultBlockHeader = SSZ.encode(defaultBlockHeader)
Expand Down