mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-26 20:19:31 +00:00
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:
parent
aef7a25174
commit
491149c6d5
@ -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
591
nimbus/p2p/clique.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
56
nimbus/p2p/clique/clique_cfg.nim
Normal file
56
nimbus/p2p/clique/clique_cfg.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
281
nimbus/p2p/clique/clique_defs.nim
Normal file
281
nimbus/p2p/clique/clique_defs.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
173
nimbus/p2p/clique/clique_poll.nim
Normal file
173
nimbus/p2p/clique/clique_poll.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
216
nimbus/p2p/clique/clique_utils.nim
Normal file
216
nimbus/p2p/clique/clique_utils.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
100
nimbus/p2p/clique/ec_recover.nim
Normal file
100
nimbus/p2p/clique/ec_recover.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
206
nimbus/p2p/clique/recent_snaps.nim
Normal file
206
nimbus/p2p/clique/recent_snaps.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
285
nimbus/p2p/clique/snapshot.nim
Normal file
285
nimbus/p2p/clique/snapshot.nim
Normal 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
|
||||
# ------------------------------------------------------------------------------
|
Loading…
x
Reference in New Issue
Block a user