Eip225 clique/PoA consensus protocol

details:
  formal port from go-lang sources, compiles but will not do anything
  useful yet
This commit is contained in:
Jordan Hrycaj 2021-05-28 18:39:55 +01:00 committed by Jordan Hrycaj
parent aef7a25174
commit 491149c6d5
9 changed files with 1914 additions and 0 deletions

View File

@ -10,6 +10,7 @@ type
canonicalHeadHash
slotHashToSlot
contractHash
cliqueSnapshot
DbKey* = object
# The first byte stores the key type. The rest are key-specific values
@ -52,6 +53,11 @@ proc contractHashKey*(h: Hash256): DbKey {.inline.} =
result.data[1 .. 32] = h.data
result.dataEndPos = uint8 32
proc cliqueSnapshotKey*(h: Hash256): DbKey {.inline.} =
result.data[0] = byte ord(cliqueSnapshot)
result.data[1 .. 32] = h.data
result.dataEndPos = uint8 32
template toOpenArray*(k: DbKey): openarray[byte] =
k.data.toOpenArray(0, int(k.dataEndPos))

591
nimbus/p2p/clique.nim Normal file
View File

@ -0,0 +1,591 @@
# 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.
##
## EIP-225 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
../db/db_chain,
../utils,
./clique/[clique_cfg, clique_defs, clique_utils, ec_recover, recent_snaps],
chronicles,
chronos,
eth/[common, keys, rlp],
# ethash,
nimcrypto,
random,
sequtils,
strformat,
tables,
times
type
# clique/clique.go(142): type SignerFn func(signer [..]
CliqueSignerFn* = ## Hashes and signs the data to be signed by
## a backing account
proc(signer: EthAddress;
message: openArray[byte]): Result[Hash256,cstring] {.gcsafe.}
Proposals = Table[EthAddress,bool]
# clique/clique.go(172): type Clique struct { [..]
Clique* = object ## Clique is the proof-of-authority consensus engine
## proposed to support the Ethereum testnet following
## the Ropsten attacks.
cfg: CliqueCfg ## Consensus engine parameters to fine tune behaviour
recents: RecentSnaps ## Snapshots for recent block to speed up reorgs
# signatures => see CliqueCfg
proposals: Proposals ## Current list of proposals we are pushing
signer: EthAddress ## Ethereum address of the signing key
signFn: CliqueSignerFn ## Signer function to authorize hashes with
lock: AsyncLock ## Protects the signer fields
fakeDiff: bool ## Testing only: skip difficulty verifications
stopSealReq: bool ## Stop running `seal()` function
stopVHeaderReq: bool ## Stop running `verifyHeader()` function
{.push raises: [Defect,CatchableError].}
logScope:
topics = "clique PoA"
# ------------------------------------------------------------------------------
# Private Helpers
# ------------------------------------------------------------------------------
template doExclusively(c: var Clique; action: untyped) =
waitFor c.lock.acquire
action
c.lock.release
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
# clique/clique.go(145): func ecrecover(header [..]
proc ecrecover(header: BlockHeader;
sigcache: var EcRecover): Result[EthAddress,CliqueError] =
## ecrecover extracts the Ethereum account address from a signed header.
sigcache.getEcRecover(header)
# clique/clique.go(369): func (c *Clique) snapshot(chain [..]
proc snapshot(c: var Clique; blockNumber: BlockNumber; hash: Hash256;
parents: openArray[Blockheader]): Result[Snapshot,CliqueError] =
## snapshot retrieves the authorization snapshot at a given point in time.
c.recents.getRecentSnaps:
RecentArgs(blockHash: hash,
blockNumber: blockNumber,
parents: toSeq(parents))
# clique/clique.go(463): func (c *Clique) verifySeal(chain [..]
proc verifySeal(c: var Clique; header: BlockHeader;
parents: openArray[BlockHeader]): CliqueResult =
## Check whether the signature contained in the header satisfies the
## consensus protocol requirements. The method accepts an optional list of
## parent headers that aren't yet part of the local blockchain to generate
## the snapshots from.
# Verifying the genesis block is not supported
if header.blockNumber.isZero:
return err((errUnknownBlock,""))
# Retrieve the snapshot needed to verify this header and cache it
var snap = c.snapshot(header.blockNumber-1, header.parentHash, parents)
if snap.isErr:
return err(snap.error)
# Resolve the authorization key and check against signers
let signer = ecrecover(header,c.cfg.signatures)
if signer.isErr:
return err(signer.error)
if not snap.value.isSigner(signer.value):
return err((errUnauthorizedSigner,""))
let seen = snap.value.recent(signer.value)
if seen.isOk:
# Signer is among recents, only fail if the current block does not
# shift it out
if header.blockNumber - snap.value.signersThreshold.u256 < seen.value:
return err((errRecentlySigned,""))
# Ensure that the difficulty corresponds to the turn-ness of the signer
if not c.fakeDiff:
if snap.value.inTurn(header.blockNumber, signer.value):
if header.difficulty != DIFF_INTURN:
return err((errWrongDifficulty,""))
else:
if header.difficulty != DIFF_NOTURN:
return err((errWrongDifficulty,""))
return ok()
# clique/clique.go(314): func (c *Clique) verifyCascadingFields(chain [..]
proc verifyCascadingFields(c: var Clique; header: BlockHeader;
parents: openArray[BlockHeader]): CliqueResult =
## Verify all the header fields that are not standalone, rather depend on a
## batch of previous headers. The caller may optionally pass in a batch of
## parents (ascending order) to avoid looking those up from the database.
## This is useful for concurrently verifying a batch of new headers.
# The genesis block is the always valid dead-end
if header.blockNumber.isZero:
return err((errZeroBlockNumberRejected,""))
# Ensure that the block's timestamp isn't too close to its parent
var parent: BlockHeader
if 0 < parents.len:
parent = parents[^1]
else:
let rc = c.cfg.dbChain.getBlockHeaderResult(header.blockNumber-1)
if rc.isErr:
return err((errUnknownAncestor,""))
parent = rc.value
if parent.blockNumber != header.blockNumber-1 or
parent.hash != header.parentHash:
return err((errUnknownAncestor,""))
if header.timestamp < parent.timestamp + c.cfg.period:
return err((errInvalidTimestamp,""))
# Verify that the gasUsed is <= gasLimit
if header.gasLimit < header.gasUsed:
return err((errCliqueExceedsGasLimit,
&"invalid gasUsed: have {header.gasUsed}, " &
&"gasLimit {header.gasLimit}"))
if not c.cfg.dbChain.config.isLondonOrLater(header.blockNumber):
# Verify BaseFee not present before EIP-1559 fork.
if not header.baseFee.isZero:
return err((errCliqueUnsupportedBaseFee,
"invalid baseFee before London fork: have " &
&"{header.baseFee}, want <0>"))
let rc = c.cfg.dbChain.validateGasLimit(header)
if rc.isErr:
return err(rc.error)
else:
let rc = c.cfg.dbChain.config.verify1559Header(parent = parent,
header = header)
if rc.isErr:
return err(rc.error)
# Retrieve the snapshot needed to verify this header and cache it
var snap = c.snapshot(header.blockNumber-1, header.parentHash, parents)
if snap.isErr:
return err(snap.error)
# If the block is a checkpoint block, verify the signer list
if (header.blockNumber mod c.cfg.epoch.u256) == 0:
let
signersList = snap.value.signers
extraList = header.extraData.extraDataSigners
if signersList != extraList:
return err((errMismatchingCheckpointSigners,""))
# All basic checks passed, verify the seal and return
return c.verifySeal(header, parents)
# clique/clique.go(145): func ecrecover(header [..]
proc verifyHeader(c: var Clique; header: BlockHeader;
parents: openArray[BlockHeader]): CliqueResult =
## Check whether a header conforms to the consensus rules.The caller may
## optionally pass in a batch of parents (ascending order) to avoid looking
## those up from the database. This is useful for concurrently verifying
## a batch of new headers.
if header.blockNumber.isZero:
return err((errUnknownBlock,""))
# Don't waste time checking blocks from the future
if getTime() < header.timestamp:
return err((errFutureBlock,""))
# Checkpoint blocks need to enforce zero beneficiary
let isCheckPoint = (header.blockNumber mod c.cfg.epoch.u256) == 0
if isCheckPoint and not header.coinbase.isZero:
return err((errInvalidCheckpointBeneficiary,""))
# Nonces must be 0x00..0 or 0xff..f, zeroes enforced on checkpoints
if header.nonce != NONCE_AUTH and header.nonce != NONCE_DROP:
return err((errInvalidVote,""))
if isCheckPoint and header.nonce != NONCE_DROP:
return err((errInvalidCheckpointVote,""))
# Check that the extra-data contains both the vanity and signature
if header.extraData.len < EXTRA_VANITY:
return err((errMissingVanity,""))
if header.extraData.len < EXTRA_VANITY + EXTRA_SEAL:
return err((errMissingSignature,""))
# Ensure that the extra-data contains a signer list on checkpoint,
# but none otherwise
let signersBytes = header.extraData.len - EXTRA_VANITY - EXTRA_SEAL
if not isCheckPoint and signersBytes != 0:
return err((errExtraSigners,""))
if isCheckPoint and (signersBytes mod EthAddress.len) != 0:
return err((errInvalidCheckpointSigners,""))
# Ensure that the mix digest is zero as we do not have fork protection
# currently
if not header.mixDigest.isZero:
return err((errInvalidMixDigest,""))
# Ensure that the block does not contain any uncles which are meaningless
# in PoA
if header.ommersHash != UNCLE_HASH:
return err((errInvalidUncleHash,""))
# Ensure that the block's difficulty is meaningful (may not be correct at
# this point)
if not header.blockNumber.isZero:
if header.difficulty.isZero or
(header.difficulty != DIFF_INTURN and
header.difficulty != DIFF_NOTURN):
return err((errInvalidDifficulty,""))
# verify that the gas limit is <= 2^63-1
when header.gasLimit.typeof isnot int64:
if int64.high < header.gasLimit:
return err((errCliqueExceedsGasLimit,
&"invalid gasLimit: have {header.gasLimit}, must be int64"))
# If all checks passed, validate any special fields for hard forks
let rc = c.cfg.dbChain.config.verifyForkHashes(header)
if rc.isErr:
return err(rc.error)
# All basic checks passed, verify cascading fields
return c.verifyCascadingFields(header, parents)
# clique/clique.go(681): func calcDifficulty(snap [..]
proc calcDifficulty(snap: var Snapshot; signer: EthAddress): DifficultyInt =
if snap.inTurn(snap.blockNumber + 1, signer):
DIFF_INTURN
else:
DIFF_NOTURN
# clique/clique.go(730): func encodeSigHeader(w [..]
proc encodeSigHeader(header: BlockHeader): seq[byte] =
## 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)
rlpHeader.encode1559
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
# clique/clique.go(191): func New(config [..]
proc initClique*(c: var Clique; cfg: CliqueCfg) =
## Initialiser for Clique proof-of-authority consensus engine with the
## initial signers set to the ones provided by the user.
c.cfg = cfg
c.recents = initRecentSnaps(cfg)
c.proposals = initTable[EthAddress,bool]()
c.lock = newAsyncLock()
proc initClique*(cfg: CliqueCfg): Clique =
result.initClique(cfg)
# clique/clique.go(212): func (c *Clique) Author(header [..]
proc author*(c: var Clique;
header: BlockHeader): Result[EthAddress,CliqueError] =
## Implements consensus.Engine, returning the Ethereum address recovered
## from the signature in the header's extra-data section.
ecrecover(header, c.cfg.signatures)
# clique/clique.go(217): func (c *Clique) VerifyHeader(chain [..]
proc verifyHeader*(c: var Clique; header: BlockHeader): CliqueResult =
## Checks whether a header conforms to the consensus rules.
c.verifyHeader(header, @[])
# clique/clique.go(224): func (c *Clique) VerifyHeader(chain [..]
proc verifyHeader*(c: var Clique; headers: openArray[BlockHeader]):
Future[seq[CliqueResult]] {.async,gcsafe.} =
## 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.
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: var Clique): bool {.discardable.} =
## Activate the stop flag for running `verifyHeader()` function.
## Returns `true` if the stop flag could be activated.
c.doExclusively:
if not c.stopVHeaderReq:
c.stopVHeaderReq = true
result = true
# clique/clique.go(450): func (c *Clique) VerifyUncles(chain [..]
proc verifyUncles*(c: var Clique; ethBlock: EthBlock): CliqueResult =
## Always returning an error for any 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: var Clique; header: var BlockHeader): CliqueResult =
## Peparing 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
var snap = c.snapshot(header.blockNumber-1, header.parentHash, @[])
if snap.isErr:
return err(snap.error)
if (header.blockNumber mod c.cfg.epoch.u256) != 0:
c.doExclusively:
# Gather all the proposals that make sense voting on
var addresses: seq[EthAddress]
for (address,authorize) in c.proposals.pairs:
if snap.value.validVote(address, authorize):
addresses.add address
# If there's pending proposals, cast a vote on them
if 0 < addresses.len:
header.coinbase = addresses[c.cfg.prng.rand(addresses.len-1)]
header.nonce = if header.coinbase in c.proposals: NONCE_AUTH
else: NONCE_DROP
# Set the correct difficulty
header.difficulty = snap.value.calcDifficulty(c.signer)
# Ensure the extra data has all its components
header.extraData.setLen(EXTRA_VANITY)
if (header.blockNumber mod c.cfg.epoch.u256) == 0:
for a in snap.value.signers:
header.extraData.add a
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
let parent = c.cfg.dbChain.getBlockHeaderResult(header.blockNumber-1)
if parent.isErr:
return err((errUnknownAncestor,""))
header.timestamp = parent.value.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: var Clique; header: BlockHeader; state: StateDB;
# txs: openArray[Transaction]; uncles: openArray[BlockHeader]) =
# ## Ensuring no uncles are set, nor block rewards given.
#
# # No block rewards in PoA, so the state remains as is and uncles are dropped
# header.Root =
# state.intermediateRoot(c.cfg.config.eip158block <= header.BlockNumber)
# header.UncleHash = types.CalcUncleHash(nil)
# clique/clique.go(579): func (c *Clique) FinalizeAndAssemble(chain [..]
#proc finalizeAndAssemble*(c: var Clique; header: BlockHeader; state: StateDB;
# txs: openArray[Transaction];
# uncles: openArray[BlockHeader];
# receipts: openArray[Receipts]):
# Result[EthBlock,CliqueError] =
# ## 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: var Clique; signer: EthAddress; signFn: CliqueSignerFn) =
## Injects private key into the consensus engine to mint new blocks with.
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.encodeSigHeader
# clique/clique.go(688): func SealHash(header *types.Header) common.Hash {
proc sealHash(header: BlockHeader): Hash256 =
## SealHash returns the hash of a block prior to it being sealed.
header.encodeSigHeader.keccakHash
# clique/clique.go(599): func (c *Clique) Seal(chain [..]
proc seal*(c: var Clique; ethBlock: EthBlock):
Future[Result[EthBlock,CliqueError]] {.async,gcsafe.} =
## Attempt 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
var snap = c.snapshot(header.blockNumber-1, header.parentHash, @[])
if snap.isErr:
return err(snap.error)
if not snap.value.isSigner(signer):
return err((errUnauthorizedSigner,""))
# If we're amongst the recent signers, wait for the next block
let seen = snap.value.recent(signer)
if seen.isOk:
# Signer is among recents, only wait if the current block does not
# shift it out
if header.blockNumber < seen.value + snap.value.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 = snap.value.signersThreshold.int64 * WIGGLE_TIME
# Kludge for limited rand() argument range
if wiggle.inSeconds < (int.high div 1000).int64:
let rndWiggleMs = c.cfg.prng.rand(wiggle.inMilliSeconds.int)
delay += initDuration(milliseconds = rndWiggleMs)
else:
let rndWiggleSec = c.cfg.prng.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: var Clique): bool {.discardable.} =
## Activate the stop flag for running `seal()` function.
## Returns `true` if the stop flag could be activated.
c.doExclusively:
if not c.stopSealReq:
c.stopSealReq = true
result =true
# clique/clique.go(673): func (c *Clique) CalcDifficulty(chain [..]
proc calcDifficulty(c: var Clique;
parent: BlockHeader): Result[DifficultyInt,CliqueError] =
## The difficulty adjustment algorithm. It 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
var snap = c.snapshot(parent.blockNumber, parent.blockHash, @[])
if snap.isErr:
return err(snap.error)
return ok(snap.value.calcDifficulty(c.signer))
# # clique/clique.go(710): func (c *Clique) SealHash(header [..]
# proc sealHash(c: var Clique; header: BlockHeader): Hash256 =
# ## SealHash returns the hash of a block prior to it being sealed.
# header.encodeSigHeader.keccakHash
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,56 @@
# 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.
##
## Clique PoA Conmmon Config
## =========================
##
## Constants used by Clique proof-of-authority consensus protocol, 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
../../db/db_chain,
./clique_defs,
./ec_recover,
random,
times
const
prngSeed = 42
type
CliqueCfg* = ref object
dbChain*: BaseChainDB
signatures*: EcRecover ## Recent block signatures to speed up mining
period*: Duration ## time between blocks to enforce
epoch*: uint64 ## Epoch length to reset votes and checkpoint
prng*: Rand ## PRNG state
{.push raises: [Defect,CatchableError].}
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc newCliqueCfg*(dbChain: BaseChainDB;
period = BLOCK_PERIOD; epoch = EPOCH_LENGTH): CliqueCfg =
CliqueCfg(
dbChain: dbChain,
period: period,
epoch: epoch,
signatures: initEcRecover(),
prng: initRand(prngSeed))
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,281 @@
# 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.
##
## Clique PoA Constants & Types
## ============================
##
## Constants used by Clique proof-of-authority consensus protocol, 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>`_
##
const
# debugging, enable with: nim c -r -d:noisy:3 ...
noisy {.intdefine.}: int = 0
isMainOk {.used.} = noisy > 2
import
eth/common,
ethash,
nimcrypto,
stew/results,
times
{.push raises: [].}
# ------------------------------------------------------------------------------
# Constants copied from eip-225 specs & implementation
# ------------------------------------------------------------------------------
# clique/clique.go(48): const ( [..]
const
CHECKPOINT_INTERVAL* = ## Number of blocks after which to save the vote
## snapshot to the database
1024
INMEMORY_SNAPSHOTS* = ## Number of recent vote snapshots to keep in memory
128
INMEMORY_SIGNATURES* = ## Number of recent block signatures to keep in memory
4096
WIGGLE_TIME* = ## Random delay (per signer) to allow concurrent
## signers
initDuration(seconds = 0, milliseconds = 500)
# clique/clique.go(57): var ( [..]
const
EPOCH_LENGTH* = ## Number of blocks after which to checkpoint and reset
## the pending votes.Suggested 30000 for the testnet to
## remain analogous to the mainnet ethash epoch.
ethash.EPOCH_LENGTH.uint64
BLOCK_PERIOD* = ## Minimum difference in seconds between two consecutive
## block's timestamps. Suggested 15s for the testnet to
## remain analogous to the mainnet ethash target.
initDuration(seconds = 15)
EXTRA_VANITY* = ## Fixed number of extra-data prefix bytes reserved for
## signer vanity. 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
NONCE_DROP* = ## Magic nonce number 0x0000000000000000 to vote on removing
## a signer.
0x0000000000000000u64.toBlockNonce
UNCLE_HASH* = ## Always Keccak256(RLP([])) as uncles are meaningless
## outside of PoW.
rlpHash[seq[BlockHeader]](@[])
DIFF_NOTURN* = ## Block score (difficulty) for blocks containing out-of-turn
## signatures. Suggested 1 since it just needs to be an
## arbitrary baseline constant.
1.u256
DIFF_INTURN* = ## Block score (difficulty) for blocks containing in-turn
## signatures. Suggested 2 to show a slight preference over
## out-of-turn signatures.
2.u256
# ------------------------------------------------------------------------------
# Additional constants copied from eip-225 go implementation
# ------------------------------------------------------------------------------
const
# params/network_params.go(60): FullImmutabilityThreshold = 90000
FULL_IMMUTABILITY_THRESHOLD* = ## Number of blocks after which a chain
## segment is considered immutable
## (i.e. soft finality). It is used by
## the downloader as a hard limit
## against deep ancestors, by the
## blockchain against deep reorgs, by
## the freezer as the cutoff threshold
## and by clique as the snapshot trust
## limit.
90000
# params/protocol_params.go(121): BaseFeeChangeDenominator = 8 [..]
EIP1559_BASE_FEE_CHANGE_DENOMINATOR* = ## Bounds the amount the base fee can
## change between blocks.
8
# params/protocol_params.go(122): ElasticityMultiplier = 2 [..]
EIP1559_ELASTICITY_MULTIPLIER* = ## Bounds the maximum gas limit an
## EIP-1559 block may have.
2
# params/protocol_params.go(123): InitialBaseFee = 1000000000 [..]
EIP1559_INITIAL_BASE_FEE* = ## Initial base fee for Eip1559 blocks.
1000000000i64
# ------------------------------------------------------------------------------
# Error tokens
# ------------------------------------------------------------------------------
# clique/clique.go(76): var ( [..]
type
CliqueErrorType* = enum
errUnknownBlock = ## is returned when the list of signers is
## requested for a block that is not part of
## the local blockchain.
"unknown block"
errInvalidCheckpointBeneficiary = ## is returned if a checkpoint/epoch
## transition block has a beneficiary
## set to non-zeroes.
"beneficiary in checkpoint block non-zero"
errInvalidVote = ## is returned if a nonce value is something
## else that the two allowed constants of
## 0x00..0 or 0xff..f.
"vote nonce not 0x00..0 or 0xff..f"
errInvalidCheckpointVote = ## is returned if a checkpoint/epoch
## transition block has a vote nonce set to
## non-zeroes.
"vote nonce in checkpoint block non-zero"
errMissingVanity = ## is returned if a block's extra-data section
## is shorter than 32 bytes, which is required
## to store the signer vanity.
"extra-data 32 byte vanity prefix missing"
errMissingSignature = ## is returned if a block's extra-data section
## doesn't seem to contain a 65 byte secp256k1
## signature.
"extra-data 65 byte signature suffix missing"
errExtraSigners = ## is returned if non-checkpoint block contain
## signer data in their extra-data fields.
"non-checkpoint block contains extra signer list"
errInvalidCheckpointSigners = ## is returned if a checkpoint block contains
## an invalid list of signers (i.e. non
## divisible by 20 bytes).
"invalid signer list on checkpoint block"
errMismatchingCheckpointSigners = ## is returned if a checkpoint block
## contains a list of signers different
## than the one the local node calculated.
"mismatching signer list on checkpoint block"
errInvalidMixDigest = ## is returned if a block's mix digest is
## non-zero.
"non-zero mix digest"
errInvalidUncleHash = ## is returned if a block contains an
## non-empty uncle list.
"non empty uncle hash"
errInvalidDifficulty = ## is returned if the difficulty of a block
## neither 1 or 2.
"invalid difficulty"
errWrongDifficulty = ## is returned if the difficulty of a block
## doesn't match the turn of the signer.
"wrong difficulty"
errInvalidTimestamp = ## is returned if the timestamp of a block is
## lower than the previous block's timestamp
## + the minimum block period.
"invalid timestamp"
errInvalidVotingChain = ## is returned if an authorization list is
## attempted to be modified via out-of-range
## or non-contiguous headers.
"invalid voting chain"
errUnauthorizedSigner = ## is returned if a header is signed by a
## non-authorized entity.
"unauthorized signer"
errRecentlySigned = ## is returned if a header is signed by an
## authorized entity that already signed a
## header recently, thus is temporarily not
## allowed to.
"recently signed"
# additional errors sources elsewhere
# -----------------------------------
errPublicKeyToShort = ## Cannot retrieve public key
"cannot retrieve public key: too short"
# imported from consensus/errors.go
errUnknownAncestor = ## is returned when validating a block
## requires an ancestor that is 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 errors, manually added
# ---------------------------------
errZeroBlockNumberRejected =
"Block number must not be Zero"
errSkSigResult ## eth/keys subsytem error: signature
errSkPubKeyResult ## eth/keys subsytem error: public key
errSnapshotLoad ## DB subsytem error
errSnapshotStore ## ..
errSnapshotClone
errCliqueExceedsGasLimit
errCliqueUnsupportedBaseFee
errCliqueBaseFeeError
errCliqueGasRepriceFork
errCliqueSealSigFn
errCliqueStopped = "Process was interrupted"
errCliqueExpectedBaseFee = "header is missing baseFee"
errCliqueGasLimitTooLow = "gas limit is too low"
errCliqueGasLimitTooHigh = "gas limit is too high"
errCliqueUnclesNotAllowed = "uncles not allowed"
# not really an error
nilCliqueSealNoBlockYet = "Sealing paused, waiting for transactions"
nilCliqueSealSignedRecently = "Signed recently, must wait for others"
# ------------------------------------------------------------------------------
# More types
# ------------------------------------------------------------------------------
type
CliqueError* = (CliqueErrorType,string)
CliqueResult* = Result[void,CliqueError]
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,173 @@
# 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
algorithm,
eth/common,
sequtils,
tables
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 = tuple
authorize: bool
signers: Table[EthAddress,Vote]
CliquePoll* = 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
{.push raises: [Defect,CatchableError].}
# ------------------------------------------------------------------------------
# Public
# ------------------------------------------------------------------------------
proc initCliquePoll*(t: var CliquePoll) =
## Ininialise an empty `CliquePoll` descriptor.
t.votes = initTable[EthAddress,Tally]()
t.authSig = initTable[EthAddress,bool]()
proc initCliquePoll*(t: var CliquePoll; signers: openArray[EthAddress]) =
## Ininialise `CliquePoll` with a given authorised signers list
t.initCliquePoll
for a in signers:
t.authSig[a] = true
proc authSigners*(t: var CliquePoll): seq[EthAddress] =
## Sorted ascending list of authorised signer addresses
result = toSeq(t.authSig.keys)
result.sort do (x, y: EthAddress) -> int:
for n in 0 ..< x.len:
if x[n] < y[n]:
return -1
elif y[n] < x[n]:
return 1
proc isAuthSigner*(t: var CliquePoll; address: EthAddress): bool =
## Check whether `address` is an authorised signer
address in t.authSig
proc authSignersShrunk*(t: var CliquePoll): bool =
## Check whether the authorised signers list was shrunk recently after
## appying `addVote()`
t.authRemoved
proc authSignersThreshold*(t: var CliquePoll): 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)
proc delVote*(t: var CliquePoll; signer, address: EthAddress) =
## 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)
# clique/snapshot.go(141): func (s *Snapshot) validVote(address [..]
proc validVote*(t: var CliquePoll; 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 CliquePoll; vote: Vote) =
## 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
# clique/snapshot.go(147): if !s.validVote(address, [..]
if not t.validVote(vote.address, vote.authorize):
# Voting has no effect
return
# clique/snapshot.go(253): if snap.cast(header.Coinbase, [..]
# Collect vote
var numVotes = 0
if not t.votes.hasKey(vote.address):
t.votes[vote.address] = (vote.authorize, {vote.signer: vote}.toTable)
numVotes = 1
elif t.votes[vote.address].authorize == vote.authorize:
t.votes[vote.address].signers[vote.signer] = vote
numVotes = t.votes[vote.address].signers.len
else:
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:
return
var obsolete = @[vote.address]
if vote.authorize:
# Has minimum votes, so add it
t.authSig[vote.address] = true
else:
# 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)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,216 @@
# 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.
##
## Tuoole & Utils 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>`_
##
## Caveat: Not supporting RLP serialisation encode()/decode()
##
import
../../chain_config,
../../config,
../../constants,
../../db/db_chain,
../../utils,
../../vm_types2,
./clique_defs,
eth/[common, rlp],
stew/results,
stint,
strformat,
times
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
proc toEthAddress(a: openArray[byte]; start: int): EthAddress =
## Concert seq[..] => Array[..]
doAssert start + EthAddress.len <= a.len
for n in 0 ..< EthAddress.len:
result[n] = a[start + n]
proc gasLimitBounds(limit: GasInt): (GasInt, GasInt) =
## See also utils.header.gasLimitBounds()
let
bndRange = limit div GAS_LIMIT_ADJUSTMENT_FACTOR
upperBound = if GAS_LIMIT_MAXIMUM - bndRange < limit: GAS_LIMIT_MAXIMUM
else: limit + bndRange
lowerBound = max(GAS_LIMIT_MINIMUM, limit - bndRange)
return (lowerBound, upperBound)
proc validateGasLimit(header: BlockHeader; limit: GasInt): CliqueResult =
let (lowBound, highBound) = gasLimitBounds(limit)
if header.gasLimit < lowBound:
return err((errCliqueGasLimitTooLow,""))
if highBound < header.gasLimit:
return err((errCliqueGasLimitTooHigh,""))
return ok()
func zeroItem[T](t: typedesc[T]): T {.inline.} =
discard
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc isZero*[T: EthAddress|Hash256|Duration](a: T): bool =
## `true` if `a` is all zero
a == zeroItem(T)
proc cliqueResultErr*(w: CliqueError): CliqueResult =
## Return error result (syntactic sugar)
err(w)
proc extraDataSigners*(extraData: Blob): seq[EthAddress] =
## Extract signer addresses from extraData header field
if EXTRA_VANITY + EXTRA_SEAL < extraData.len:
var addrOffset = EXTRA_VANITY
while addrOffset + EthAddress.len <= EXTRA_SEAL:
result.add extraData.toEthAddress(addrOffset)
addrOffset += EthAddress.len
proc getBlockHeaderResult*(c: BaseChainDB;
number: BlockNumber): Result[BlockHeader,void] =
## Slightly re-phrased dbChain.getBlockHeader(..) command
var header: BlockHeader
if c.getBlockHeader(number, header):
return ok(header)
result = err()
# core/types/block.go(343): func (b *Block) WithSeal(header [..]
proc withHeader*(b: EthBlock; header: BlockHeader): EthBlock =
## New block with the data from `b` but the header replaced with the
## argument one.
EthBlock(
header: header,
txs: b.txs,
uncles: b.uncles)
# consensus/misc/forks.go(30): func VerifyForkHashes(config [..]
proc verifyForkHashes*(c: var ChainConfig; header: BlockHeader): CliqueResult =
## Verify that blocks conforming to network hard-forks do have the correct
## hashes, to avoid clients going off on different chains.
# If the homestead reprice hash is set, validate it
if c.eip150Block.isZero or c.eip150Block != header.blockNumber:
return ok()
let hash = header.hash
if c.eip150Hash.isZero or c.eip150Hash == hash:
return ok()
return err((errCliqueGasRepriceFork,
&"Homestead gas reprice fork: have {c.eip150Hash}, want {hash}"))
proc validateGasLimit*(c: var BaseChainDB; header: BlockHeader): CliqueResult =
## See also private function p2p.validate.validateGasLimit()
let parent = c.getBlockHeader(header.parentHash)
header.validateGasLimit(parent.gasLimit)
# ------------------------------------------------------------------------------
# Eip 1559 support
# ------------------------------------------------------------------------------
# params/config.go(450): func (c *ChainConfig) IsLondon(num [..]
proc isLondonOrLater*(c: var ChainConfig; number: BlockNumber): bool =
## FIXME: London is not defined yet, will come after Berlin
FkBerlin < c.toFork(number)
proc baseFee*(header: BlockHeader): GasInt =
# FIXME: `baseFee` header field not defined before `London` fork
0.GasInt
# clique/clique.go(730): func encodeSigHeader(w [..]
proc encode1559*(header: BlockHeader): seq[byte] =
## Encode header field and considering new `baseFee` field for Eip1559.
var writer = initRlpWriter()
writer.append(header)
if not header.baseFee.isZero:
writer.append(header.baseFee)
result = writer.finish
# consensus/misc/eip1559.go(55): func CalcBaseFee(config [..]
proc calc1599BaseFee*(c: var ChainConfig; parent: BlockHeader): GasInt =
## calculates the basefee of the header.
# If the current block is the first EIP-1559 block, return the
# initial base fee.
if not c.isLondonOrLater(parent.blockNumber):
return EIP1559_INITIAL_BASE_FEE
let parentGasTarget = parent.gasLimit div EIP1559_ELASTICITY_MULTIPLIER
# If the parent gasUsed is the same as the target, the baseFee remains
# unchanged.
if parent.gasUsed == parentGasTarget:
return parent.baseFee
let parentGasDenom = parentGasTarget.i128 *
EIP1559_BASE_FEE_CHANGE_DENOMINATOR.i128
if parentGasTarget < parent.gasUsed:
# If the parent block used more gas than its target, the baseFee should
# increase.
let
gasUsedDelta = (parent.gasUsed - parentGasTarget).i128
baseFeeDelta = (parent.baseFee.i128 * gasUsedDelta) div parentGasDenom
return parent.baseFee + max(baseFeeDelta.truncate(GasInt), 1)
else:
# Otherwise if the parent block used less gas than its target, the
# baseFee should decrease.
let
gasUsedDelta = (parentGasTarget - parent.gasUsed).i128
baseFeeDelta = (parent.baseFee.i128 * gasUsedDelta) div parentGasDenom
return max(parent.baseFee - baseFeeDelta.truncate(GasInt), 0)
# consensus/misc/eip1559.go(32): func VerifyEip1559Header(config [..]
proc verify1559Header*(c: var ChainConfig;
parent, header: BlockHeader): CliqueResult =
## Verify that the gas limit remains within allowed bounds
let limit = if c.isLondonOrLater(parent.blockNumber):
parent.gasLimit
else:
parent.gasLimit * EIP1559_ELASTICITY_MULTIPLIER
let rc = header.validateGasLimit(limit)
if rc.isErr:
return err(rc.error)
# Verify the header is not malformed
if header.baseFee.isZero:
return err((errCliqueExpectedBaseFee,""))
# Verify the baseFee is correct based on the parent header.
var expectedBaseFee = c.calc1599BaseFee(parent)
if header.baseFee != expectedBaseFee:
return err((errCliqueBaseFeeError,
&"invalid baseFee: have {expectedBaseFee}, "&
&"want {header.baseFee}, " &
&"parent.baseFee {parent.baseFee}, "&
&"parent.gasUsed {parent.gasUsed}"))
return ok()
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,100 @@
# 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,
../../utils/lru_cache,
./clique_defs,
eth/[common, keys, rlp],
stint
type
# simplify Hash256 for rlp serialisation
EcKey32 = array[32, byte]
EcRecover* = LruCache[BlockHeader,EcKey32,EthAddress,CliqueError]
{.push raises: [Defect,CatchableError].}
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc initEcRecover*(cache: var EcRecover) {.gcsafe, raises: [Defect].} =
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.hash.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 signature = Signature.fromRaw(
msg.toOpenArray(msg.len - EXTRA_SEAL, msg.high))
if signature.isErr:
return err((errSkSigResult,$signature.error))
# Recover the public key from signature and seal hash
# clique/clique.go(159): pubkey, err := crypto.Ecrecover( [..]
let pubKey = recover(signature.value, SKMessage(header.hash.data))
if pubKey.isErr:
return err((errSkPubKeyResult,$pubKey.error))
# Convert public key to address.
return ok(pubKey.value.toCanonicalAddress)
cache.initLruCache(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 =
## extract Ethereum account address from a signed header block, the relevant
## signature used is appended to the re-purposed extra data field
addrCache.getLruItem(header)
proc append*(rw: var RlpWriter; ecRec: EcRecover) {.inline.} =
## Generic support for `rlp.encode(ecRec)`
rw.append(ecRec.data)
proc read*(rlp: var Rlp; Q: type EcRecover): Q {.inline.} =
## 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,206 @@
# 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.
##
## Snapshot 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>`_
##
## Caveat: Not supporting RLP serialisation encode()/decode()
##
import
../../utils,
../../utils/lru_cache,
./clique_cfg,
./clique_defs,
./clique_utils,
./snapshot,
chronicles,
eth/[common, keys],
nimcrypto,
stint
export
snapshot
type
RecentArgs* = ref object
blockHash*: Hash256
blockNumber*: BlockNumber
parents*: seq[BlockHeader]
# Internal, temporary state variables
LocalArgs = ref object
headers: seq[BlockHeader]
# Internal type, simplify Hash256 for rlp serialisation
RecentKey = array[32, byte]
# Internal descriptor used by toValue()
RecentDesc = object
cfg: CliqueCfg
args: RecentArgs
local: LocalArgs
RecentSnaps* = object
cfg: CliqueCfg
cache: LruCache[RecentDesc,RecentKey,Snapshot,CliqueError]
{.push raises: [Defect,CatchableError].}
logScope:
topics = "clique snap cache"
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
# clique/clique.go(394): if number == 0 || (number%c.config.Epoch [..]
proc canDiskCheckPointOk(d: RecentDesc): bool =
# If we're at the genesis, snapshot the initial state.
if d.args.blockNumber.isZero:
return true
# Alternatively if we're at a checkpoint block without a parent
# (light client CHT), or we have piled up more headers than allowed
# to be re-orged (chain reinit from a freezer), consider the
# checkpoint trusted and snapshot it.
if (d.args.blockNumber mod d.cfg.epoch.u256) == 0:
if (FULL_IMMUTABILITY_THRESHOLD < d.local.headers.len) or
d.cfg.dbChain.getBlockHeaderResult(d.args.blockNumber - 1).isErr:
return true
proc isCheckPointOk(number: BlockNumber): bool =
number mod CHECKPOINT_INTERVAL == 0
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
# clique/clique.go(383): if number%checkpointInterval == 0 [..]
proc tryDiskSnapshot(d: RecentDesc; snap: var Snapshot): bool =
if d.args.blockNumber.isCheckPointOk:
if snap.loadSnapshot(d.cfg, d.args.blockHash).isOk:
trace "Loaded voting snapshot from disk",
blockNumber = d.args.blockNumber,
blockHash = d.args.blockHash
return true
proc tryDiskCheckPoint(d: RecentDesc; snap: var Snapshot): bool =
if d.canDiskCheckPointOk:
# clique/clique.go(395): checkpoint := chain.GetHeaderByNumber [..]
let checkPoint = d.cfg.dbChain.getBlockHeaderResult(d.args.blockNumber)
if checkPoint.isErr:
return false
let
hash = checkPoint.value.hash
signersList = checkPoint.value.extraData.extraDataSigners
snap.initSnapshot(d.cfg, d.args.blockNumber, hash, signersList)
if snap.storeSnapshot.isOk:
info "Stored checkpoint snapshot to disk",
blockNumber = d.args.blockNumber,
blockHash = hash
return true
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc initRecentSnaps*(rs: var RecentSnaps;
cfg: CliqueCfg) {.gcsafe,raises: [Defect].} =
var toKey: LruKey[RecentDesc,RecentKey] =
proc(d: RecentDesc): RecentKey =
d.args.blockHash.data
var toValue: LruValue[RecentDesc,Snapshot,CliqueError] =
proc(d: RecentDesc): Result[Snapshot,CliqueError] =
var snap: Snapshot
while true:
# If an on-disk checkpoint snapshot can be found, use that
if d.tryDiskSnapshot(snap):
# clique/clique.go(386): snap = s
break
# Save checkpoint e.g. when at the genesis ..
if d.tryDiskCheckPoint(snap):
# clique/clique.go(407): log.Info("Stored [..]
break
# No snapshot for this header, gather the header and move backward
var header: BlockHeader
if 0 < d.args.parents.len:
# If we have explicit parents, pick from there (enforced)
header = d.args.parents[^1]
# clique/clique.go(416): if header.Hash() != hash [..]
if header.hash != d.args.blockHash and
header.blockNumber != d.args.blockNumber:
return err((errUnknownAncestor,""))
d.args.parents.setLen(d.args.parents.len-1)
else:
# No explicit parents (or no more left), reach out to the database
let rc = d.cfg.dbChain.getBlockHeaderResult(d.args.blockNumber)
if rc.isErr:
return err((errUnknownAncestor,""))
header = rc.value
d.local.headers.add header
d.args.blockNumber -= 1.u256
d.args.blockHash = header.parentHash
# => while loop
# Previous snapshot found, apply any pending headers on top of it
for i in 0 ..< d.local.headers.len div 2:
swap(d.local.headers[i], d.local.headers[^(1+i)])
block:
# clique/clique.go(434): snap, err := snap.apply(headers)
let rc = snap.applySnapshot(d.local.headers)
if rc.isErr:
return err(rc.error)
# If we've generated a new checkpoint snapshot, save to disk
if snap.blockNumber.isCheckPointOk and 0 < d.local.headers.len:
var rc = snap.storeSnapshot
if rc.isErr:
return err(rc.error)
trace "Stored voting snapshot to disk",
blockNumber = d.blockNumber,
blockHash = hash
# clique/clique.go(438): c.recents.Add(snap.Hash, snap)
return ok(snap)
rs.cfg = cfg
rs.cache.initLruCache(toKey, toValue, INMEMORY_SNAPSHOTS)
proc initRecentSnaps*(cfg: CliqueCfg): RecentSnaps {.gcsafe,raises: [Defect].} =
result.initRecentSnaps(cfg)
proc getRecentSnaps*(rs: var RecentSnaps; args: RecentArgs): auto =
## Get snapshot from cache or disk
rs.cache.getLruItem:
RecentDesc(cfg: rs.cfg,
args: args,
local: LocalArgs())
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1,285 @@
# 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.
##
## Snapshot Structure 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>`_
##
const
# debugging, enable with: nim c -r -d:noisy:3 ...
noisy {.intdefine.}: int = 0
isMainOk {.used.} = noisy > 2
import
../../db/[storage_types, db_chain],
../../utils/lru_cache,
./clique_cfg,
./clique_defs,
./clique_poll,
./ec_recover,
chronicles,
eth/[common, rlp, trie/db],
sequtils,
tables,
times
type
AddressHistory = Table[BlockNumber,EthAddress]
SnapshotData* = object
blockNumber: BlockNumber ## truncated block num where snapshot was created
blockHash: Hash256 ## block hash where snapshot was created
recents: AddressHistory ## recent signers for spam protections
# clique/snapshot.go(58): Recents map[uint64]common.Address [..]
ballot: CliquePoll ## Votes => authorised signers
# clique/snapshot.go(50): type Snapshot struct [..]
Snapshot* = object ## Snapshot is the state of the authorization voting at
## a given point in time.
cfg: CliqueCfg ## parameters to fine tune behavior
data*: SnapshotData ## real snapshot
{.push raises: [Defect,CatchableError].}
logScope:
topics = "clique snapshot"
# ------------------------------------------------------------------------------
# Private functions needed to support RLP conversion
# ------------------------------------------------------------------------------
proc append[K,V](rw: var RlpWriter; tab: Table[K,V]) {.inline.} =
rw.startList(tab.len)
for key,value in tab.pairs:
rw.append((key,value))
proc read[K,V](rlp: var Rlp;
Q: type Table[K,V]): Q {.inline, raises: [Defect,CatchableError].} =
for w in rlp.items:
let (key,value) = w.read((K,V))
result[key] = value
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
# clique/snapshot.go(72): func newSnapshot(config [..]
proc initSnapshot*(s: var Snapshot; cfg: CliqueCfg;
number: BlockNumber; hash: Hash256; signers: openArray[EthAddress]) =
## This creates a new snapshot with the specified startup parameters. The
## method does not initialize the set of recent signers, so only ever use
## if for the genesis block.
s.cfg = cfg
s.data.blockNumber = number
s.data.blockHash = hash
s.data.recents = initTable[BlockNumber,EthAddress]()
s.data.ballot.initCliquePoll(signers)
proc initSnapshot*(cfg: CliqueCfg; number: BlockNumber; hash: Hash256;
signers: openArray[EthAddress]): Snapshot =
result.initSnapshot(cfg, number, hash, signers)
proc blockNumber*(s: var Snapshot): BlockNumber =
## Getter
s.data.blockNumber
# clique/snapshot.go(88): func loadSnapshot(config [..]
proc loadSnapshot*(s: var Snapshot; cfg: CliqueCfg;
hash: Hash256): CliqueResult {.gcsafe, raises: [Defect].} =
## Load an existing snapshot from the database.
try:
let
key = hash.cliqueSnapshotKey
value = cfg.dbChain.db.get(key.toOpenArray)
s.data = value.decode(SnapshotData)
s.cfg = cfg
except CatchableError as e:
return err((errSnapshotLoad,e.msg))
result = ok()
# clique/snapshot.go(104): func (s *Snapshot) store(db [..]
proc storeSnapshot*(s: var Snapshot): CliqueResult {.gcsafe,raises: [Defect].} =
## Insert the snapshot into the database.
try:
let
key = s.data.blockHash.cliqueSnapshotKey
value = rlp.encode(s.data)
s.cfg.dbChain.db.put(key.toOpenArray, value)
except CatchableError as e:
return err((errSnapshotStore,e.msg))
result = ok()
# clique/snapshot.go(185): func (s *Snapshot) apply(headers [..]
proc applySnapshot*(s: var Snapshot;
headers: openArray[BlockHeader]): CliqueResult =
## Initialises an authorization snapshot `snap` by applying the `headers`
## to the argument snapshot `s`.
# Allow passing in no headers for cleaner code
if headers.len == 0:
return ok()
# Sanity check that the headers can be applied
if headers[0].blockNumber != s.data.blockNumber + 1:
return err((errInvalidVotingChain,""))
for i in 0 ..< headers.len:
if headers[i+1].blockNumber != headers[i].blockNumber+1:
return err((errInvalidVotingChain,""))
# Iterate through the headers and create a new snapshot
let
start = getTime()
logInterval = initDuration(seconds = 8)
var
logged = start
# clique/snapshot.go(206): for i, header := range headers [..]
for headersIndex in 0 ..< headers.len:
let
# headersIndex => also used for logging at the end of this loop
header = headers[headersIndex]
number = header.blockNumber
# Remove any votes on checkpoint blocks
if number mod s.cfg.epoch.u256 == 0:
s.data.ballot.initCliquePoll
# Delete the oldest signer from the recent list to allow it signing again
block:
let limit = s.data.ballot.authSignersThreshold.u256
if limit <= number:
s.data.recents.del(number - limit)
# Resolve the authorization key and check against signers
let signer = ? s.cfg.signatures.getEcRecover(header)
if not s.data.ballot.isAuthSigner(signer):
return err((errUnauthorizedSigner,""))
for recent in s.data.recents.values:
if recent == signer:
return err((errRecentlySigned,""))
s.data.recents[number] = signer
# Header authorized, discard any previous vote from the signer
s.data.ballot.delVote(signer = signer, address = header.coinbase)
# Tally up the new vote from the signer
var authOk = false
if header.nonce == NONCE_AUTH:
authOk = true
elif header.nonce != NONCE_DROP:
return err((errInvalidVote,""))
s.data.ballot.addVote:
Vote(address: header.coinbase,
signer: signer,
blockNumber: number,
authorize: authOk)
# clique/snapshot.go(269): if limit := uint64(len(snap.Signers)/2 [..]
if s.data.ballot.authSignersShrunk:
# Signer list shrunk, delete any leftover recent caches
let limit = s.data.ballot.authSignersThreshold.u256
if limit <= number:
s.data.recents.del(number - limit)
# If we're taking too much time (ecrecover), notify the user once a while
if logInterval < logged - getTime():
info "Reconstructing voting history",
processed = headersIndex,
total = headers.len,
elapsed = start - getTime()
logged = getTime()
let sinceStart = start - getTime()
if logInterval < sinceStart:
info "Reconstructed voting history",
processed = headers.len,
elapsed = sinceStart
# clique/snapshot.go(303): snap.Number += uint64(len(headers))
s.data.blockNumber += headers.len.u256
s.data.blockHash = headers[^1].blockHash
result = ok()
proc validVote*(s: var Snapshot; address: EthAddress; authorize: bool): bool =
## Returns `true` if voting makes sense, at all.
s.data.ballot.validVote(address, authorize)
proc recent*(s: var Snapshot; address: EthAddress): Result[BlockNumber,void] =
## Return `BlockNumber` for `address` argument (if any)
for (number,recent) in s.data.recents.pairs:
if recent == address:
return ok(number)
return err()
proc signersThreshold*(s: var Snapshot): int =
## Forward to `CliquePoll`: Minimum number of authorised signers needed.
s.data.ballot.authSignersThreshold
proc isSigner*(s: var Snapshot; address: EthAddress): bool =
## Checks whether argukment ``address` is in signers list
s.data.ballot.isAuthSigner(address)
proc signers*(s: var Snapshot): seq[EthAddress] =
## Retrieves the sorted list of authorized signers
s.data.ballot.authSigners
# clique/snapshot.go(319): func (s *Snapshot) inturn(number [..]
proc inTurn*(s: var Snapshot; number: BlockNumber, signer: EthAddress): bool =
## Returns `true` if a signer at a given block height is in-turn or not.
let ascSignersList = s.data.ballot.authSigners
for offset in 0 ..< ascSignersList.len:
if ascSignersList[offset] == signer:
return (number mod ascSignersList.len.u256) == offset.u256
# ------------------------------------------------------------------------------
# Debugging/testing
# ------------------------------------------------------------------------------
when isMainModule and isMainOK:
var
cfg = newMemoryDB().newBaseChainDB.newCliqueCfg
ssh, ss1, ss2: Snapshot
key: Hash256
hdr: BlockHeader
ssh.init(cfg, 0.u256, key, @[])
ssh.data.blockNumber = 77.u256
key = ssh.data.blockHash
ssh.store.expect("store failed")
echo ">>> ", rlp.encode(ssh.data)
ss2.init(cfg, 0.u256, key, @[])
ss2.load(cfg,key).expect("load failed")
echo ">>> ", rlp.encode(ss2.data)
doAssert rlp.encode(ssh.data) == rlp.encode(ss2.data)
#discard ss1.data.sigcache.getEcRecover(hdr)
ss1 = ss2
echo "ss1.data: ", ss1.data.repr
echo "ss2.data: ", ss2.data.repr
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------