#768 Moved/re-implemented ecRecover() from Clique sources to utils/ec_recover

why:
  The same functionality was differently implemented in one or the
  other form.

details:
  Caching and non-caching variants available
This commit is contained in:
Jordan Hrycaj 2021-07-27 12:28:05 +01:00 committed by jangko
parent 9aea669363
commit 4713bd4cf4
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
12 changed files with 269 additions and 230 deletions

View File

@ -69,3 +69,11 @@ const
# EIP
MaxPrecompilesAddr* = 0xFFFF
EXTRA_SEAL* = ##\
## Fixed number of suffix bytes reserved for signer seal of the `extraData`
## header field. The 65 bytes constant value is for signatures based on the
## standard secp256k1 curve.
65
# End

View File

@ -21,7 +21,7 @@
import
std/[random, sequtils, strutils, times],
../../db/db_chain,
./clique_cfg/ec_recover,
../../utils/ec_recover,
./clique_defs,
eth/common,
ethash,
@ -117,7 +117,7 @@ proc newCliqueCfg*(db: BaseChainDB): CliqueCfg =
# clique/clique.go(145): func ecrecover(header [..]
proc ecRecover*(cfg: CliqueCfg; header: BlockHeader): auto
{.gcsafe, raises: [Defect,CatchableError].} =
cfg.signatures.getEcRecover(header)
cfg.signatures.ecRecover(header)
# ------------------------------------------------------------------------------
# Public setters

View File

@ -1,103 +0,0 @@
# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
##
## Address Cache for Clique PoA Consensus Protocol
## ===============================================
##
## For details see
## `EIP-225 <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-225.md>`_
## and
## `go-ethereum <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-225.md>`_
##
import
../../../utils/lru_cache,
../clique_defs,
../clique_helpers,
eth/[common, keys, rlp],
stint
type
# simplify Hash256 for rlp serialisation
EcKey32 = array[32, byte]
EcRecover* = LruCache[BlockHeader,EcKey32,EthAddress,CliqueError]
{.push raises: [Defect].}
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc initEcRecover*(cache: var EcRecover) =
var toKey: LruKey[BlockHeader,EcKey32] =
# Use the seal hash for cache lookup
proc(header:BlockHeader): EcKey32 =
## If the signature's already cached, return that
# clique/clique.go(148): hash := header.Hash()
header.blockHash.data
var toValue: LruValue[BlockHeader,EthAddress,CliqueError] =
# Retrieve signature from the header's extra data fields
proc(header: BlockHeader): Result[EthAddress,CliqueError] =
# Extract signature from extra data field (last 65 bytes)
let msg = header.extraData
# clique/clique.go(153): if len(header.Extra) < extraSeal {
if msg.len < EXTRA_SEAL:
return err((errMissingSignature,""))
let sig = Signature.fromRaw(
msg.toOpenArray(msg.len - EXTRA_SEAL, msg.high))
if sig.isErr:
return err((errSkSigResult,$sig.error))
# Recover the public key from signature and seal hash
# clique/clique.go(159): pubkey, err := crypto.Ecrecover( [..]
let pubKey = recover(sig.value, SKMessage(header.hashSealHeader.data))
if pubKey.isErr:
return err((errSkPubKeyResult,$pubKey.error))
# Convert public key to address.
return ok(pubKey.value.toCanonicalAddress)
cache.initCache(toKey, toValue, INMEMORY_SIGNATURES)
proc initEcRecover*: EcRecover {.gcsafe, raises: [Defect].} =
result.initEcRecover
# clique/clique.go(145): func ecrecover(header [..]
proc getEcRecover*(addrCache: var EcRecover; header: BlockHeader): auto {.
gcsafe, raises: [Defect,CatchableError].} =
## extract Ethereum account address from a signed header block, the relevant
## signature used is appended to the re-purposed extra data field
addrCache.getItem(header)
proc append*(rw: var RlpWriter; ecRec: EcRecover) {.
inline, raises: [Defect,KeyError].} =
## Generic support for `rlp.encode(ecRec)`
rw.append(ecRec.data)
proc read*(rlp: var Rlp; Q: type EcRecover): Q {.
inline, raises: [Defect,KeyError].} =
## Generic support for `rlp.decode(bytes)` for loading the cache from a
## serialised data stream.
result.initEcRecover
result.data = rlp.read(type result.data)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -40,10 +40,6 @@ const
## Number of recent vote snapshots to keep in memory.
128
INMEMORY_SIGNATURES* = ##\
## Number of recent block signatures to keep in memory
4096
WIGGLE_TIME* = ##\
## PoA mining only (currently unsupported).
##
@ -62,11 +58,6 @@ const
## Suggested 32 bytes to retain the current extra-data allowance and/or use.
32
EXTRA_SEAL* = ##\
## Fixed number of extra-data suffix bytes reserved for signer seal.
## 65 bytes fixed as signatures are based on the standard secp256k1 curve.
65
NONCE_AUTH* = ##\
## Magic nonce number 0xffffffffffffffff to vote on adding a new signer.
0xffffffffffffffffu64.toBlockNonce
@ -208,21 +199,11 @@ type
## unknown.
"unknown ancestor"
# errPrunedAncestor = ##\
# ## is returned when validating a block requires an ancestor that is
# ## known, but the state of which is not available.
# "pruned ancestor"
errFutureBlock = ##\
## is returned when a block's timestamp is in the future according to
## the current node.
"block in the future"
# errInvalidNumber = ##\
# ## is returned if a block's number doesn't equal its parent's plus one.
# "invalid block number"
# additional/bespoke errors, manually added
# -----------------------------------------
@ -237,8 +218,9 @@ type
## Attempt to assign a value to a non-existing slot
"Missing LRU slot for snapshot"
errSkSigResult ## eth/keys subsytem error: signature
errSkPubKeyResult ## eth/keys subsytem error: public key
errEcRecover = ##\
## Subsytem error"
"ecRecover failed"
errSnapshotLoad ## DB subsytem error
errSnapshotStore ## ..

View File

@ -133,7 +133,7 @@ proc verifySeal(c: Clique; header: BlockHeader): CliqueOkResult
# Resolve the authorization key and check against signers
let signer = c.cfg.ecRecover(header)
if signer.isErr:
return err(signer.error)
return err((errEcRecover,$signer.error))
if not snapshot.isSigner(signer.value):
return err((errUnauthorizedSigner,""))

View File

@ -141,23 +141,25 @@ proc snapshotApplySeq*(s: Snapshot; headers: var seq[BlockHeader],
s.recents.del(number - limit)
# Resolve the authorization key and check against signers
let signer = ? s.cfg.ecRecover(header)
let signer = s.cfg.ecRecover(header)
if signer.isErr:
return err((errEcRecover,$signer.error))
#s.say "applySnapshot signer=", s.pp(signer)
if not s.ballot.isAuthSigner(signer):
if not s.ballot.isAuthSigner(signer.value):
s.say "applySnapshot signer not authorised => fail ", s.pp(29)
return err((errUnauthorizedSigner,""))
for recent in s.recents.values:
if recent == signer:
s.say "applySnapshot signer recently seen ", s.pp(signer)
if recent == signer.value:
s.say "applySnapshot signer recently seen ", s.pp(signer.value)
echo "+++ applySnapshot #", header.blockNumber, " err=errRecentlySigned"
return err((errRecentlySigned,""))
s.recents[number] = signer
s.recents[number] = signer.value
# Header authorized, discard any previous vote from the signer
# clique/snapshot.go(233): for i, vote := range snap.Votes {
s.ballot.delVote(signer = signer, address = header.coinbase)
s.ballot.delVote(signer = signer.value, address = header.coinbase)
# Tally up the new vote from the signer
# clique/snapshot.go(244): var authorize bool
@ -167,7 +169,7 @@ proc snapshotApplySeq*(s: Snapshot; headers: var seq[BlockHeader],
elif header.nonce != NONCE_DROP:
return err((errInvalidVote,""))
let vote = Vote(address: header.coinbase,
signer: signer,
signer: signer.value,
blockNumber: number,
authorize: authOk)
#s.say "applySnapshot calling addVote ", s.pp(vote)

137
nimbus/utils/ec_recover.nim Normal file
View File

@ -0,0 +1,137 @@
# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
##
## Recover Address From Signature
## ==============================
##
## This module provides caching and direct versions for recovering the
## `EthAddress` from an extended signature. The caching version reduces
## calculation time for the price of maintaing it in a LRU cache.
import
./utils_defs,
./lru_cache,
../constants,
eth/[common, keys, rlp],
nimcrypto,
stew/results,
stint
const
INMEMORY_SIGNATURES* = ##\
## Number of recent block signatures to keep in memory
4096
type
# simplify Hash256 for rlp serialisation
EcKey32 = array[32, byte]
EcRecover* = LruCache[BlockHeader,EcKey32,EthAddress,UtilsError]
{.push raises: [Defect].}
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
proc encodePreSealed(header: BlockHeader): seq[byte] {.inline.} =
## Cut sigature off `extraData` header field and consider new `baseFee`
## field for Eip1559.
doAssert EXTRA_SEAL < header.extraData.len
var rlpHeader = header
rlpHeader.extraData.setLen(header.extraData.len - EXTRA_SEAL)
rlp.encode(rlpHeader)
proc hashPreSealed(header: BlockHeader): Hash256 {.inline.} =
## Returns the hash of a block prior to it being sealed.
keccak256.digest header.encodePreSealed
proc ecRecover*(extraData: openArray[byte];
hash: Hash256): Result[EthAddress,UtilsError] {.inline.} =
## Extract account address from the last 65 bytes of the `extraData` argument
## (which is typically the bock header field with the same name.) The second
## argument `hash` is used to extract the intermediate public key. Typically,
## this would be the hash of the block header without the last 65 bytes of
## the `extraData` field reserved for the signature.
if extraData.len < EXTRA_SEAL:
return err((errMissingSignature,""))
let sig = Signature.fromRaw(
extraData.toOpenArray(extraData.len - EXTRA_SEAL, extraData.high))
if sig.isErr:
return err((errSkSigResult,$sig.error))
# Recover the public key from signature and seal hash
let pubKey = recover(sig.value, SKMessage(hash.data))
if pubKey.isErr:
return err((errSkPubKeyResult,$pubKey.error))
# Convert public key to address.
return ok(pubKey.value.toCanonicalAddress)
# ------------------------------------------------------------------------------
# Public function: straight ecRecover version
# ------------------------------------------------------------------------------
proc ecRecover*(header: BlockHeader): Result[EthAddress,UtilsError] =
## Extract account address from the `extraData` field (last 65 bytes) of the
## argument header.
header.extraData.ecRecover(header.hashPreSealed)
# ------------------------------------------------------------------------------
# Public constructor for caching ecRecover version
# ------------------------------------------------------------------------------
proc initEcRecover*(cache: var EcRecover; cacheSize = INMEMORY_SIGNATURES) =
var toKey: LruKey[BlockHeader,EcKey32] =
proc(header:BlockHeader): EcKey32 =
header.blockHash.data
cache.initCache(toKey, ecRecover, cacheSize)
proc initEcRecover*: EcRecover {.gcsafe, raises: [Defect].} =
result.initEcRecover
# ------------------------------------------------------------------------------
# Public function: caching ecRecover version
# ------------------------------------------------------------------------------
proc ecRecover*(addrCache: var EcRecover;
header: BlockHeader): Result[EthAddress,UtilsError]
{.gcsafe, raises: [Defect,CatchableError].} =
## Extract account address from `extraData` field (last 65 bytes) of the
## argument header. The result is kept in a LRU cache to re-purposed for
## improved result delivery avoiding calculations.
addrCache.getItem(header)
# ------------------------------------------------------------------------------
# Public PLP mixin functions for caching version
# ------------------------------------------------------------------------------
proc append*(rw: var RlpWriter; ecRec: EcRecover) {.
inline, raises: [Defect,KeyError].} =
## Generic support for `rlp.encode(ecRec)`
rw.append(ecRec.data)
proc read*(rlp: var Rlp; Q: type EcRecover): Q {.
inline, raises: [Defect,KeyError].} =
## Generic support for `rlp.decode(bytes)` for loading the cache from a
## serialised data stream.
result.initEcRecover
result.data = rlp.read(type result.data)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,52 @@
# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
##
## Definitions, Error Constants, etc.
## ===================================
##
type
UtilsErrorType* = enum
resetUtilsError = ##\
## Default/reset value (use `utilsNoError` below rather than this valie)
(0, "no error")
errMissingSignature = ##\
## is returned if the `extraData` header field does not seem to contain
## a 65 byte secp256k1 signature.
"extraData 65 byte signature suffix missing"
errSkSigResult = ##\
## eth/keys subsytem error: signature
"signature error"
errSkPubKeyResult = ##\
## eth/keys subsytem error: public key
"public key error"
UtilsError* = ##\
## Error message, tinned component + explanatory text (if any)
(UtilsErrorType,string)
const
utilsNoError* = ##\
## No-error constant
(resetUtilsError, "")
proc `$`*(e: UtilsError): string {.inline.} =
## Join text fragments
result = $e[0]
if e[1] != "":
result &= ": " & e[1]
# End

View File

@ -9,12 +9,32 @@
# according to those terms.
import
macros, strformat, tables, sets, options,
eth/[common, keys, rlp], nimcrypto/keccak,
./interpreter/gas_costs, ../errors, ../forks,
../constants, ../db/[db_chain, accounts_cache],
../utils, json, ./transaction_tracer, ./types,
../config, ../../stateless/[witness_from_tree, witness_types]
std/[json, macros, options, sets, strformat, tables],
../../stateless/[witness_from_tree, witness_types],
../config,
../constants,
../db/[db_chain, accounts_cache],
../errors,
../forks,
../utils,
../utils/ec_recover,
./interpreter/gas_costs,
./transaction_tracer,
./types,
eth/[common, keys]
proc getMinerAddress(vmState: BaseVMState): EthAddress =
if not vmState.consensusEnginePoA:
return vmState.blockHeader.coinbase
let account = vmState.blockHeader.ecRecover
if account.isErr:
let msg = "Could not recover account address: " & $account.error
raise newException(ValidationError, msg)
account.value
proc newAccessLogs*: AccessLogs =
AccessLogs(reads: initTable[string, string](), writes: initTable[string, string]())
@ -29,8 +49,6 @@ proc `$`*(vmState: BaseVMState): string =
else:
result = &"VMState {vmState.name}:\n header: {vmState.blockHeader}\n chaindb: {vmState.chaindb}"
proc getMinerAddress(vmState: BaseVMState): EthAddress
proc init*(self: BaseVMState, prevStateRoot: Hash256, header: BlockHeader,
chainDB: BaseChainDB, tracerFlags: set[TracerFlags] = {}) =
self.prevHeaders = @[]
@ -74,43 +92,6 @@ proc consensusEnginePoA*(vmState: BaseVMState): bool =
# using `real` engine configuration
vmState.chainDB.config.poaEngine
proc getSignature(bytes: openArray[byte], output: var Signature): bool =
let sig = Signature.fromRaw(bytes)
if sig.isOk:
output = sig[]
return true
return false
proc headerHashOriExtraData(vmState: BaseVMState): Hash256 =
var tmp = vmState.blockHeader
tmp.extraData.setLen(tmp.extraData.len-65)
result = keccak256.digest(rlp.encode(tmp))
proc calcMinerAddress(sigRaw: openArray[byte], vmState: BaseVMState, output: var EthAddress): bool =
var sig: Signature
if sigRaw.getSignature(sig):
let headerHash = headerHashOriExtraData(vmState)
let pubkey = recover(sig, SKMessage(headerHash.data))
if pubkey.isOk:
output = pubkey[].toCanonicalAddress()
result = true
proc getMinerAddress(vmState: BaseVMState): EthAddress =
if not vmState.consensusEnginePoA:
return vmState.blockHeader.coinbase
template data: untyped =
vmState.blockHeader.extraData
let len = data.len
doAssert(len >= 65)
var miner: EthAddress
if calcMinerAddress(data.toOpenArray(len - 65, len-1), vmState, miner):
result = miner
else:
raise newException(ValidationError, "Could not derive miner address from header extradata")
proc updateBlockHeader*(vmState: BaseVMState, header: BlockHeader) =
vmState.blockHeader = header
vmState.touchedAccounts.clear()
@ -141,7 +122,7 @@ method difficulty*(vmState: BaseVMState): UInt256 {.base, gcsafe.} =
method gasLimit*(vmState: BaseVMState): GasInt {.base, gcsafe.} =
vmState.blockHeader.gasLimit
method baseFee*(vmState: BaseVMState): Uint256 {.base, gcsafe.} =
method baseFee*(vmState: BaseVMState): UInt256 {.base, gcsafe.} =
vmState.blockHeader.baseFee
when defined(geth):

View File

@ -9,12 +9,31 @@
# according to those terms.
import
macros, strformat, tables, sets, options,
eth/[common, keys, rlp], nimcrypto/keccak,
../errors, ../forks,
../constants, ../db/[db_chain, accounts_cache],
../utils, json, ./transaction_tracer, ./types,
../config, ../../stateless/[witness_from_tree, witness_types]
std/[json, macros, options, sets, strformat, tables],
../../stateless/[witness_from_tree, witness_types],
../config,
../constants,
../db/[db_chain, accounts_cache],
../errors,
../forks,
../utils,
../utils/ec_recover,
./transaction_tracer,
./types,
eth/[common, keys]
proc getMinerAddress(vmState: BaseVMState): EthAddress =
if not vmState.consensusEnginePoA:
return vmState.blockHeader.coinbase
let account = vmState.blockHeader.ecRecover
if account.isErr:
let msg = "Could not recover account address: " & $account.error
raise newException(ValidationError, msg)
account.value
proc newAccessLogs*: AccessLogs =
AccessLogs(reads: initTable[string, string](), writes: initTable[string, string]())
@ -29,8 +48,6 @@ proc `$`*(vmState: BaseVMState): string =
else:
result = &"VMState {vmState.name}:\n header: {vmState.blockHeader}\n chaindb: {vmState.chaindb}"
proc getMinerAddress(vmState: BaseVMState): EthAddress
proc init*(self: BaseVMState, prevStateRoot: Hash256, header: BlockHeader,
chainDB: BaseChainDB, tracerFlags: set[TracerFlags] = {}) =
self.prevHeaders = @[]
@ -62,43 +79,6 @@ proc consensusEnginePoA*(vmState: BaseVMState): bool =
# using `real` engine configuration
vmState.chainDB.config.poaEngine
proc getSignature(bytes: openArray[byte], output: var Signature): bool =
let sig = Signature.fromRaw(bytes)
if sig.isOk:
output = sig[]
return true
return false
proc headerHashOriExtraData(vmState: BaseVMState): Hash256 =
var tmp = vmState.blockHeader
tmp.extraData.setLen(tmp.extraData.len-65)
result = keccak256.digest(rlp.encode(tmp))
proc calcMinerAddress(sigRaw: openArray[byte], vmState: BaseVMState, output: var EthAddress): bool =
var sig: Signature
if sigRaw.getSignature(sig):
let headerHash = headerHashOriExtraData(vmState)
let pubkey = recover(sig, SKMessage(headerHash.data))
if pubkey.isOk:
output = pubkey[].toCanonicalAddress()
result = true
proc getMinerAddress(vmState: BaseVMState): EthAddress =
if not vmState.consensusEnginePoA:
return vmState.blockHeader.coinbase
template data: untyped =
vmState.blockHeader.extraData
let len = data.len
doAssert(len >= 65)
var miner: EthAddress
if calcMinerAddress(data.toOpenArray(len - 65, len-1), vmState, miner):
result = miner
else:
raise newException(ValidationError, "Could not derive miner address from header extradata")
proc updateBlockHeader*(vmState: BaseVMState, header: BlockHeader) =
vmState.blockHeader = header
vmState.touchedAccounts.clear()

View File

@ -246,11 +246,11 @@ when isMainModule:
# `test_clique/indiump.dumpGroupNl()`
# placed at the end of
# `p2p/chain/persist_blocks.persistBlocks()`.
captureFile = "test_clique" / "goerli504192.txt.gz"
#captureFile = "test_clique" / "dump-stream.out.gz"
captureFile = "goerli504192.txt.gz"
#captureFile = "dump-stream.out.gz"
proc goerliReplay(noisy = true; showElapsed = true;
dir = "."; captureFile = captureFile;
dir = "/status"; captureFile = captureFile;
startAtBlock = 0u64; stopAfterBlock = 0u64) =
runGoerliReplay(
noisy = noisy, showElapsed = showElapsed,

View File

@ -386,8 +386,8 @@ proc commitVoterChain*(ap: TesterPool; postProcessOk = false;
## If `postProcessOk` is set, an additional verification step is added at
## the end of each transaction.
##
## if `stopFaultyHeader` is set, the function stopps immediately on error.
## Otherwise the offending bloch is removed, the rest of the batch is
## if `stopFaultyHeader` is set, the function stops immediately on error.
## Otherwise the offending block is removed, the rest of the batch is
## adjusted and applied again repeatedly.
result = ap