mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-15 06:44:12 +00:00
ca07c40a48
* Provide API details: API is bundled via clique.nim. * Set extraValidation as default for PoA chains why: This triggers consensus verification and an update of the list of authorised signers. These signers are integral part of the PoA block chain. todo: Option argument to control validation for the nimbus binary. * Fix snapshot state block number why: Using sub-sequence here, so the len() function was wrong. * Optional start where block verification begins why: Can speed up time building loading initial parts of block chain. For PoA, this allows to prove & test that authorised signers can be (correctly) calculated starting at any point on the block chain. todo: On Goerli around blocks #193537..#197568, processing time increases disproportionally -- needs to be understand * For Clique test, get old grouping back (7 transactions per log entry) why: Forgot to change back after troubleshooting * Fix field/function/module-name misunderstanding why: Make compilation work * Use eth_types.blockHash() rather than utils.hash() in Clique modules why: Prefer lib module * Dissolve snapshot_misc.nim details: .. into clique_verify.nim (the other source file clique_unused.nim is inactive) * Hide unused AsyncLock in Clique descriptor details: Unused here but was part of the Go reference implementation * Remove fakeDiff flag from Clique descriptor details: This flag was a kludge in the Go reference implementation used for the canonical tests. The tests have been adapted so there is no need for the fakeDiff flag and its implementation. * Not observing minimum distance from epoch sync point why: For compiling PoA state, the go implementation will walk back to the epoch header with at least 90000 blocks apart from the current header in the absence of other synchronisation points. Here just the nearest epoch header is used. The assumption is that all the checkpoints before have been vetted already regardless of the current branch. details: The behaviour of using the nearest vs the minimum distance epoch is controlled by a flag and can be changed at run time. * Analysing processing time (patch adds some debugging/visualisation support) why: At the first half million blocks of the Goerli replay, blocks on the interval #194854..#196224 take exceptionally long to process, but not due to PoA processing. details: It turns out that much time is spent in p2p/excecutor.processBlock() where the elapsed transaction execution time is significantly greater for many of these blocks. Between the 1371 blocks #194854..#196224 there are 223 blocks with more than 1/2 seconds execution time whereas there are only 4 such blocks before and 13 such after this range up to #504192. * fix debugging symbol in clique_desc (causes CI failing) * Fixing canonical reference tests why: Two errors were introduced earlier but ovelooked: 1. "Remove fakeDiff flag .." patch was incomplete 2. "Not observing minimum distance .." introduced problem w/tests 23/24 details: Fixing 2. needed to revert the behaviour by setting the applySnapsMinBacklog flag for the Clique descriptor. Also a new test was added to lock the new behaviour. * Remove cruft why: Clique/PoA processing was intended to take place somewhere in executor/process_block.processBlock() but was decided later to run from chain/persist_block.persistBlock() instead. * Update API comment * ditto
429 lines
16 KiB
Nim
429 lines
16 KiB
Nim
# 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.
|
|
|
|
##
|
|
## Mining Support for Clique PoA Consensus Protocol
|
|
## ================================================
|
|
##
|
|
## Note that mining in currently unsupported by `NIMBUS`
|
|
##
|
|
## 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
|
|
std/[sequtils, tables, times],
|
|
../../constants,
|
|
../../db/[db_chain, state_db],
|
|
../../utils,
|
|
./clique_cfg,
|
|
./clique_defs,
|
|
./clique_desc,
|
|
./clique_helpers,
|
|
./clique_snapshot,
|
|
./clique_verify,
|
|
./snapshot/[ballot, snapshot_desc],
|
|
chronicles,
|
|
chronos,
|
|
eth/[common, keys, rlp],
|
|
nimcrypto
|
|
|
|
when not enableCliqueAsyncLock:
|
|
{.fatal: "Async locks must be enabled in clique_desc, try: -d:clique_async_lock"}
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
logScope:
|
|
topics = "clique PoA Mining"
|
|
|
|
type
|
|
CliqueSyncDefect* = object of Defect
|
|
## Defect raised with lock/unlock problem
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private Helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
template syncExceptionWrap(action: untyped) =
|
|
try:
|
|
action
|
|
except:
|
|
raise (ref CliqueSyncDefect)(msg: getCurrentException().msg)
|
|
|
|
|
|
# clique/clique.go(217): func (c *Clique) VerifyHeader(chain [..]
|
|
proc verifyHeader(c: Clique; header: BlockHeader): CliqueOkResult
|
|
{.gcsafe, raises: [Defect,CatchableError].} =
|
|
## See `clique.cliqueVerify()`
|
|
var blind: seq[BlockHeader]
|
|
c.cliqueVerifySeq(header, blind)
|
|
|
|
proc verifyHeader(c: Clique; header: BlockHeader;
|
|
parents: openArray[BlockHeader]): CliqueOkResult
|
|
{.gcsafe, raises: [Defect,CatchableError].} =
|
|
## See `clique.cliqueVerify()`
|
|
var list = toSeq(parents)
|
|
c.cliqueVerifySeq(header, list)
|
|
|
|
|
|
proc isValidVote(s: Snapshot; a: EthAddress; authorize: bool): bool {.inline.}=
|
|
s.ballot.isValidVote(a, authorize)
|
|
|
|
proc isSigner*(s: Snapshot; address: EthAddress): bool =
|
|
## See `clique_verify.isSigner()`
|
|
s.ballot.isAuthSigner(address)
|
|
|
|
# clique/snapshot.go(319): func (s *Snapshot) inturn(number [..]
|
|
proc inTurn*(s: Snapshot; number: BlockNumber, signer: EthAddress): bool =
|
|
## See `clique_verify.inTurn()`
|
|
let ascSignersList = s.ballot.authSigners
|
|
for offset in 0 ..< ascSignersList.len:
|
|
if ascSignersList[offset] == signer:
|
|
return (number mod ascSignersList.len.u256) == offset.u256
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# clique/clique.go(681): func calcDifficulty(snap [..]
|
|
proc calcDifficulty(s: Snapshot; signer: EthAddress): DifficultyInt =
|
|
if s.inTurn(s.blockNumber + 1, signer):
|
|
DIFF_INTURN
|
|
else:
|
|
DIFF_NOTURN
|
|
|
|
proc recentBlockNumber*(s: Snapshot;
|
|
a: EthAddress): Result[BlockNumber,void] {.inline.} =
|
|
## Return `BlockNumber` for `address` argument (if any)
|
|
for (number,recent) in s.recents.pairs:
|
|
if recent == a:
|
|
return ok(number)
|
|
return err()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# clique/clique.go(212): func (c *Clique) Author(header [..]
|
|
proc author*(c: Clique; header: BlockHeader): Result[EthAddress,CliqueError]
|
|
{.gcsafe, raises: [Defect,CatchableError].} =
|
|
## For the Consensus Engine, `author()` retrieves the Ethereum address of the
|
|
## account that minted the given block, which may be different from the
|
|
## header's coinbase if a consensus engine is based on signatures.
|
|
##
|
|
## This implementation returns the Ethereum address recovered from the
|
|
## signature in the header's extra-data section.
|
|
c.cfg.ecRecover(header)
|
|
|
|
|
|
# clique/clique.go(224): func (c *Clique) VerifyHeader(chain [..]
|
|
proc verifyHeaders*(c: Clique; headers: openArray[BlockHeader]):
|
|
Future[seq[CliqueOkResult]] {.async,gcsafe.} =
|
|
## For the Consensus Engine, `verifyHeader()` s similar to VerifyHeader, but
|
|
## verifies a batch of headers concurrently. This method is accompanied
|
|
## by a `stopVerifyHeader()` method that can abort the operations.
|
|
##
|
|
## This implementation checks whether a header conforms to the consensus
|
|
## rules. It verifies a batch of headers. If running in the background,
|
|
## the process can be stopped by calling the `stopVerifyHeader()` function.
|
|
syncExceptionWrap:
|
|
c.doExclusively:
|
|
c.stopVHeaderReq = false
|
|
for n in 0 ..< headers.len:
|
|
c.doExclusively:
|
|
let isStopRequest = c.stopVHeaderReq
|
|
if isStopRequest:
|
|
result.add cliqueResultErr((errCliqueStopped,""))
|
|
break
|
|
result.add c.verifyHeader(headers[n], headers[0 ..< n])
|
|
c.doExclusively:
|
|
c.stopVHeaderReq = false
|
|
|
|
proc stopVerifyHeader*(c: Clique): bool {.discardable.} =
|
|
## Activate the stop flag for running `verifyHeader()` function.
|
|
## Returns `true` if the stop flag could be activated.
|
|
syncExceptionWrap:
|
|
c.doExclusively:
|
|
if not c.stopVHeaderReq:
|
|
c.stopVHeaderReq = true
|
|
result = true
|
|
|
|
|
|
# clique/clique.go(450): func (c *Clique) VerifyUncles(chain [..]
|
|
proc verifyUncles*(c: Clique; ethBlock: EthBlock): CliqueOkResult =
|
|
## For the Consensus Engine, `verifyUncles()` verifies that the given
|
|
## block's uncles conform to the consensus rules of a given engine.
|
|
##
|
|
## This implementation always returns an error for existing uncles as this
|
|
## consensus mechanism doesn't permit uncles.
|
|
if 0 < ethBlock.uncles.len:
|
|
return err((errCliqueUnclesNotAllowed,""))
|
|
result = ok()
|
|
|
|
|
|
# clique/clique.go(506): func (c *Clique) Prepare(chain [..]
|
|
proc prepare*(c: Clique; header: var BlockHeader): CliqueOkResult
|
|
{.gcsafe, raises: [Defect,CatchableError].} =
|
|
## For the Consensus Engine, `prepare()` initializes the consensus fields
|
|
## of a block header according to the rules of a particular engine. The
|
|
## changes are executed inline.
|
|
##
|
|
## This implementation prepares all the consensus fields of the header for
|
|
## running the transactions on top.
|
|
|
|
# If the block isn't a checkpoint, cast a random vote (good enough for now)
|
|
header.coinbase.reset
|
|
header.nonce.reset
|
|
|
|
# Assemble the voting snapshot to check which votes make sense
|
|
let rc = c.cliqueSnapshot(header.parentHash, @[])
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
|
|
if (header.blockNumber mod c.cfg.epoch) != 0:
|
|
c.doExclusively:
|
|
# Gather all the proposals that make sense voting on
|
|
var addresses: seq[EthAddress]
|
|
for (address,authorize) in c.proposals.pairs:
|
|
if c.snapshot.isValidVote(address, authorize):
|
|
addresses.add address
|
|
|
|
# If there's pending proposals, cast a vote on them
|
|
if 0 < addresses.len:
|
|
header.coinbase = addresses[c.cfg.rand(addresses.len-1)]
|
|
header.nonce = if header.coinbase in c.proposals: NONCE_AUTH
|
|
else: NONCE_DROP
|
|
|
|
# Set the correct difficulty
|
|
header.difficulty = c.snapshot.calcDifficulty(c.signer)
|
|
|
|
# Ensure the extra data has all its components
|
|
header.extraData.setLen(EXTRA_VANITY)
|
|
if (header.blockNumber mod c.cfg.epoch) == 0:
|
|
header.extraData.add c.snapshot.ballot.authSigners.mapIt(toSeq(it)).concat
|
|
header.extraData.add 0.byte.repeat(EXTRA_SEAL)
|
|
|
|
# Mix digest is reserved for now, set to empty
|
|
header.mixDigest.reset
|
|
|
|
# Ensure the timestamp has the correct delay
|
|
var parent: BlockHeader
|
|
if not c.db.getBlockHeader(header.blockNumber-1, parent):
|
|
return err((errUnknownAncestor,""))
|
|
|
|
header.timestamp = parent.timestamp + c.cfg.period
|
|
if header.timestamp < getTime():
|
|
header.timestamp = getTime()
|
|
|
|
return ok()
|
|
|
|
|
|
# clique/clique.go(571): func (c *Clique) Finalize(chain [..]
|
|
proc finalize*(c: Clique; header: BlockHeader; db: AccountStateDB) =
|
|
## For the Consensus Engine, `finalize()` runs any post-transaction state
|
|
## modifications (e.g. block rewards) but does not assemble the block.
|
|
##
|
|
## Note: The block header and state database might be updated to reflect any
|
|
## consensus rules that happen at finalization (e.g. block rewards).
|
|
##
|
|
## Not implemented here, raises `AssertionDefect`
|
|
raiseAssert "Not implemented"
|
|
#
|
|
# ## This implementation ensures no uncles are set, nor block rewards given.
|
|
# # No block rewards in PoA, so the state remains as is and uncles are dropped
|
|
# let deleteEmptyObjectsOk = c.cfg.config.eip158block <= header.blockNumber
|
|
# header.stateRoot = db.intermediateRoot(deleteEmptyObjectsOk)
|
|
# header.ommersHash = EMPTY_UNCLE_HASH
|
|
|
|
# clique/clique.go(579): func (c *Clique) FinalizeAndAssemble(chain [..]
|
|
proc finalizeAndAssemble*(c: Clique; header: BlockHeader;
|
|
db: AccountStateDB; txs: openArray[Transaction];
|
|
receipts: openArray[Receipt]):
|
|
Result[EthBlock,CliqueError] =
|
|
## For the Consensus Engine, `finalizeAndAssemble()` runs any
|
|
## post-transaction state modifications (e.g. block rewards) and assembles
|
|
## the final block.
|
|
##
|
|
## Note: The block header and state database might be updated to reflect any
|
|
## consensus rules that happen at finalization (e.g. block rewards).
|
|
##
|
|
## Not implemented here, raises `AssertionDefect`
|
|
raiseAssert "Not implemented"
|
|
# ## Ensuring no uncles are set, nor block rewards given, and returns the
|
|
# ## final block.
|
|
#
|
|
# # Finalize block
|
|
# c.finalize(header, state, txs, uncles)
|
|
#
|
|
# # Assemble and return the final block for sealing
|
|
# return types.NewBlock(header, txs, nil, receipts,
|
|
# trie.NewStackTrie(nil)), nil
|
|
|
|
|
|
# clique/clique.go(589): func (c *Clique) Authorize(signer [..]
|
|
proc authorize*(c: Clique; signer: EthAddress; signFn: CliqueSignerFn) =
|
|
## Injects private key into the consensus engine to mint new blocks with.
|
|
syncExceptionWrap:
|
|
c.doExclusively:
|
|
c.signer = signer
|
|
c.signFn = signFn
|
|
|
|
|
|
# clique/clique.go(724): func CliqueRLP(header [..]
|
|
proc cliqueRlp*(header: BlockHeader): seq[byte] =
|
|
## Returns the rlp bytes which needs to be signed for the proof-of-authority
|
|
## sealing. The RLP to sign consists of the entire header apart from the 65
|
|
## byte signature contained at the end of the extra data.
|
|
##
|
|
## Note, the method requires the extra data to be at least 65 bytes,
|
|
## otherwise it panics. This is done to avoid accidentally using both forms
|
|
## (signature present or not), which could be abused to produce different
|
|
##hashes for the same header.
|
|
header.encodeSealHeader
|
|
|
|
|
|
# clique/clique.go(688): func SealHash(header *types.Header) common.Hash {
|
|
proc sealHash*(header: BlockHeader): Hash256 =
|
|
## For the Consensus Engine, `sealHash()` returns the hash of a block prior
|
|
## to it being sealed.
|
|
##
|
|
## This implementation returns the hash of a block prior to it being sealed.
|
|
header.hashSealHeader
|
|
|
|
|
|
# clique/clique.go(599): func (c *Clique) Seal(chain [..]
|
|
proc seal*(c: Clique; ethBlock: EthBlock):
|
|
Future[Result[EthBlock,CliqueError]] {.async,gcsafe.} =
|
|
## For the Consensus Engine, `seal()` generates a new sealing request for
|
|
## the given input block and pushes the result into the given channel.
|
|
##
|
|
## Note, the method returns immediately and will send the result async. More
|
|
## than one result may also be returned depending on the consensus algorithm.
|
|
##
|
|
## This implementation attempts to create a sealed block using the local
|
|
## signing credentials. If running in the background, the process can be
|
|
## stopped by calling the `stopSeal()` function.
|
|
c.doExclusively:
|
|
c.stopSealReq = false
|
|
var header = ethBlock.header
|
|
|
|
# Sealing the genesis block is not supported
|
|
if header.blockNumber.isZero:
|
|
return err((errUnknownBlock,""))
|
|
|
|
# For 0-period chains, refuse to seal empty blocks (no reward but would spin
|
|
# sealing)
|
|
if c.cfg.period.isZero and ethBlock.txs.len == 0:
|
|
info $nilCliqueSealNoBlockYet
|
|
return err((nilCliqueSealNoBlockYet,""))
|
|
|
|
# Don't hold the signer fields for the entire sealing procedure
|
|
c.doExclusively:
|
|
let
|
|
signer = c.signer
|
|
signFn = c.signFn
|
|
|
|
# Bail out if we're unauthorized to sign a block
|
|
let rc = c.cliqueSnapshot(header.parentHash)
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
if not c.snapshot.isSigner(signer):
|
|
return err((errUnauthorizedSigner,""))
|
|
|
|
# If we're amongst the recent signers, wait for the next block
|
|
let seen = c.snapshot.recentBlockNumber(signer)
|
|
if seen.isOk:
|
|
# Signer is among recents, only wait if the current block does not
|
|
# shift it out
|
|
if header.blockNumber < seen.value + c.snapshot.signersThreshold.u256:
|
|
info $nilCliqueSealSignedRecently
|
|
return err((nilCliqueSealSignedRecently,""))
|
|
|
|
# Sweet, the protocol permits us to sign the block, wait for our time
|
|
var delay = header.timestamp - getTime()
|
|
if header.difficulty == DIFF_NOTURN:
|
|
# It's not our turn explicitly to sign, delay it a bit
|
|
let wiggle = c.snapshot.signersThreshold.int64 * WIGGLE_TIME
|
|
# Kludge for limited rand() argument range
|
|
if wiggle.inSeconds < (int.high div 1000).int64:
|
|
let rndWiggleMs = c.cfg.rand(wiggle.inMilliSeconds.int)
|
|
delay += initDuration(milliseconds = rndWiggleMs)
|
|
else:
|
|
let rndWiggleSec = c.cfg.rand((wiggle.inSeconds and int.high).int)
|
|
delay += initDuration(seconds = rndWiggleSec)
|
|
|
|
trace "Out-of-turn signing requested",
|
|
wiggle = $wiggle
|
|
|
|
# Sign all the things!
|
|
let sigHash = signFn(signer,header.cliqueRlp)
|
|
if sigHash.isErr:
|
|
return err((errCliqueSealSigFn,$sigHash.error))
|
|
let extraLen = header.extraData.len
|
|
if EXTRA_SEAL < extraLen:
|
|
header.extraData.setLen(extraLen - EXTRA_SEAL)
|
|
header.extraData.add sigHash.value.data
|
|
|
|
# Wait until sealing is terminated or delay timeout.
|
|
trace "Waiting for slot to sign and propagate",
|
|
delay = $delay
|
|
|
|
# FIXME: double check
|
|
let timeOutTime = getTime() + delay
|
|
while getTime() < timeOutTime:
|
|
c.doExclusively:
|
|
let isStopRequest = c.stopVHeaderReq
|
|
if isStopRequest:
|
|
warn "Sealing result is not read by miner",
|
|
sealhash = sealHash(header)
|
|
return err((errCliqueStopped,""))
|
|
poll()
|
|
|
|
c.doExclusively:
|
|
c.stopSealReq = false
|
|
return ok(ethBlock.withHeader(header))
|
|
|
|
proc stopSeal*(c: Clique): bool {.discardable.} =
|
|
## Activate the stop flag for running `seal()` function.
|
|
## Returns `true` if the stop flag could be activated.
|
|
syncExceptionWrap:
|
|
c.doExclusively:
|
|
if not c.stopSealReq:
|
|
c.stopSealReq = true
|
|
result =true
|
|
|
|
|
|
# clique/clique.go(673): func (c *Clique) CalcDifficulty(chain [..]
|
|
proc calcDifficulty(c: Clique;
|
|
parent: BlockHeader): Result[DifficultyInt,CliqueError]
|
|
{.gcsafe, raises: [Defect,CatchableError].} =
|
|
## For the Consensus Engine, `calcDifficulty()` is the difficulty adjustment
|
|
## algorithm. It returns the difficulty that a new block should have.
|
|
##
|
|
## This implementation returns the difficulty that a new block should have:
|
|
## * DIFF_NOTURN(2) if BLOCK_NUMBER % SIGNER_COUNT != SIGNER_INDEX
|
|
## * DIFF_INTURN(1) if BLOCK_NUMBER % SIGNER_COUNT == SIGNER_INDEX
|
|
let rc = c.cliqueSnapshot(parent)
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
return ok(c.snapshot.calcDifficulty(c.signer))
|
|
|
|
|
|
# # clique/clique.go(710): func (c *Clique) SealHash(header [..]
|
|
# proc sealHash(c: Clique; header: BlockHeader): Hash256 =
|
|
# ## SealHash returns the hash of a block prior to it being sealed.
|
|
# header.encodeSigHeader.keccakHash
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|