Jordan Hrycaj ca07c40a48
Fearture/poa clique tuning (#765)
* 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
2021-07-30 15:06:51 +01:00

244 lines
8.6 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.
##
## Votes Management 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
std/[sequtils, tables],
# std/[strutils],
../clique_cfg,
../clique_helpers,
eth/common
type
Vote* = object ## Vote represent single votes that an authorized
## signer made to modify the list of authorizations.
signer*: EthAddress ## authorized signer that cast this vote
address*: EthAddress ## account being voted on to change its
## authorization type (`true` or `false`)
blockNumber*: BlockNumber ## block number the vote was cast in
## (expire old votes)
authorize*: bool ## authorization type, whether to authorize or
## deauthorize the voted account
Tally = object
authorize: bool
signers: Table[EthAddress,Vote]
Ballot* = object
votes: Table[EthAddress,Tally] ## votes by account -> signer
authSig: Table[EthAddress,bool] ## currently authorised signers
authRemoved: bool ## last `addVote()` action was removing an
## authorised signer from the `authSig` list
debug: bool ## debug mode
{.push raises: [Defect].}
# ------------------------------------------------------------------------------
# Private
# ------------------------------------------------------------------------------
proc say(t: var Ballot; v: varargs[string,`$`]) {.inline.} =
## Debugging output
ppExceptionWrap:
# if t.debug: stderr.write "*** " & v.join & "\n"
discard
# ------------------------------------------------------------------------------
# Public debugging/pretty-printer support
# ------------------------------------------------------------------------------
proc votesInternal*(t: var Ballot): seq[(EthAddress,EthAddress,Vote)] =
for account,tally in t.votes.pairs:
for signer,vote in tally.signers.pairs:
result.add (account, signer, vote)
# ------------------------------------------------------------------------------
# Public constructor
# ------------------------------------------------------------------------------
proc initBallot*(t: var Ballot) =
## Ininialise an empty `Ballot` descriptor.
t.votes = initTable[EthAddress,Tally]()
t.authSig = initTable[EthAddress,bool]()
proc initBallot*(t: var Ballot; signers: openArray[EthAddress]) =
## Ininialise `Ballot` with a given authorised signers list
t.initBallot
for a in signers:
t.authSig[a] = true
# ------------------------------------------------------------------------------
# Public setters
# ------------------------------------------------------------------------------
proc `debug=`*(t: var Ballot; debug: bool) =
## Set debugging mode on/off
t.debug = debug
# ------------------------------------------------------------------------------
# Public getters
# ------------------------------------------------------------------------------
proc authSigners*(t: var Ballot): seq[EthAddress] =
## Sorted ascending list of authorised signer addresses
toSeq(t.authSig.keys).sorted(EthAscending)
proc authSignersLen*(t: var Ballot): int =
## Returns the number of currently known authorised signers.
t.authSig.len
proc isAuthSignersListShrunk*(t: var Ballot): bool =
## Check whether the authorised signers list was shrunk recently after
## appying `addVote()`
t.authRemoved
proc authSignersThreshold*(t: var Ballot): int =
## Returns the minimum number of authorised signers needed for authorising
## a addres for voting. This is currently
## ::
## 1 + half of the number of authorised signers
##
1 + (t.authSig.len div 2)
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc isAuthSigner*(t: var Ballot; addresses: var seq[EthAddress]): bool =
## Check whether all `addresses` entries are authorised signers.
##
## Using this function should be preferable over `authSigners()` which has
## complexity `O(log n)` while this function runs with `O(n)`.
for a in addresses:
if a notin t.authSig:
return false
true
proc isAuthSigner*(t: var Ballot; address: EthAddress): bool =
## Check whether `address` is an authorised signer
address in t.authSig
proc delVote*(t: var Ballot; signer, address: EthAddress) {.
gcsafe, raises: [Defect,KeyError].} =
## Remove a particular previously added vote.
if address in t.votes:
if signer in t.votes[address].signers:
if t.votes[address].signers.len <= 1:
t.votes.del(address)
else:
t.votes[address].signers.del(signer)
proc flushVotes*(t: var Ballot) =
## Reset/flush pending votes, authorised signers remain the same.
t.votes.clear
# clique/snapshot.go(141): func (s *Snapshot) validVote(address [..]
proc isValidVote*(t: var Ballot; address: EthAddress; authorize: bool): bool =
## Check whether voting would have an effect in `addVote()`
if address in t.authSig: not authorize else: authorize
proc addVote*(t: var Ballot; vote: Vote) {.
gcsafe, raises: [Defect,KeyError].} =
## Add a new vote collecting the signers for the particular voting address.
##
## Unless it is the first vote for this address, the authorisation type
## `true` or `false` of the vote must match the previous one. For the first
## vote, the authorisation type `true` is accepted if the address is not an
## authorised signer, and `false` if it is an authorised signer. Otherwise
## the vote is ignored.
##
## If the number of signers for the particular address are at least
## `authSignersThreshold()`, the status of this address will change as
## follows.
## * If the authorisation type is `true`, the address is added
## to the list of authorised signers.
## * If the authorisation type is `false`, the address is removed
## from the list of authorised signers.
t.authRemoved = false
var
numVotes = 0
authOk = vote.authorize
# clique/snapshot.go(147): if !s.validVote(address, [..]
if not t.isValidVote(vote.address, vote.authorize):
# Corner case: touch votes for this account
if t.votes.hasKey(vote.address):
let refVote = t.votes[vote.address]
numVotes = refVote.signers.len
authOk = refVote.authorize
t.say "addVote touch votes (corner case)"
elif not t.votes.hasKey(vote.address):
# Collect inital vote
t.votes[vote.address] = Tally(
authorize: vote.authorize,
signers: {vote.signer: vote}.toTable)
numVotes = 1
t.say "addVote accepted, first vote, authorize=", vote.authorize
elif t.votes[vote.address].authorize == vote.authorize:
# Collect additional vote
t.votes[vote.address].signers[vote.signer] = vote
numVotes = t.votes[vote.address].signers.len
t.say "addVote accepted, ", numVotes, " votes, authorize=", vote.authorize
else:
t.say "addVote not applicable!"
return
# clique/snapshot.go(262): if tally := snap.Tally[header.Coinbase]; [..]
# Vote passed, update the list of authorised signers if enough votes
if numVotes < t.authSignersThreshold:
t.say "addVote not enough votes for address yet, have ", numVotes,
" need ", t.authSignersThreshold
return
var obsolete = @[vote.address]
if authOk:
# Has minimum votes, so add it
t.authSig[vote.address] = true
t.say "addVote authorise address .."
else:
t.say "addVote de-authorise address .."
# clique/snapshot.go(266): delete(snap.Signers, [..]
t.authSig.del(vote.address)
t.authRemoved = true
# Not a signer anymore => remove it everywhere
for key,value in t.votes.mpairs:
if vote.address in value.signers:
if 1 < value.signers.len:
value.signers.del(vote.address)
else:
obsolete.add key
for key in obsolete:
t.votes.del(key)
t.say "addVote done"
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------