# 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 `_ ## and ## `go-ethereum `_ ## import std/[random, sequtils, strformat, tables, times], ../constants, ../db/[db_chain, state_db], ../utils, ./clique/[clique_cfg, clique_defs, clique_utils, ec_recover, recent_snaps], chronicles, chronos, eth/[common, keys, rlp], nimcrypto export clique_cfg, clique_defs type CliqueSyncDefect* = object of Defect ## Defect raised with lock/unlock problem # 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 stopSealReq: bool ## Stop running `seal()` function stopVHeaderReq: bool ## Stop running `verifyHeader()` function fakeDiff: bool ## Testing only: skip difficulty verifications debug: bool ## debug mode {.push raises: [Defect].} logScope: topics = "clique PoA" # ------------------------------------------------------------------------------ # Private Helpers # ------------------------------------------------------------------------------ template doExclusively(c: var Clique; action: untyped) = waitFor c.lock.acquire action c.lock.release template syncExceptionWrap(action: untyped) = try: action except: raise (ref CliqueSyncDefect)(msg: getCurrentException().msg) # ------------------------------------------------------------------------------ # Private functions # ------------------------------------------------------------------------------ # clique/clique.go(145): func ecrecover(header [..] proc ecrecover(c: var Clique; header: BlockHeader): Result[EthAddress,CliqueError] {. gcsafe, raises: [Defect,CatchableError].} = ## ecrecover extracts the Ethereum account address from a signed header. c.cfg.signatures.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] {. gcsafe, raises: [Defect,CatchableError].} = ## 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 {. gcsafe, raises: [Defect,CatchableError].} = ## 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 = c.ecrecover(header) 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 {. gcsafe, raises: [Defect,CatchableError].} = ## 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(246): func (c *Clique) verifyHeader(chain [..] proc verifyHeader(c: var Clique; header: BlockHeader; parents: openArray[BlockHeader]): CliqueResult {. gcsafe, raises: [Defect,CatchableError].} = ## 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 != EMPTY_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 # ------------------------------------------------------------------------------ # 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) proc setDebug*(c: var Clique; debug: bool) = ## Set debugging mode on/off and set the `fakeDiff` flag `true` c.fakeDiff = true c.debug = debug c.recents.setDebug(debug) # clique/clique.go(212): func (c *Clique) Author(header [..] proc author*(c: var Clique; header: BlockHeader): Result[EthAddress,CliqueError] {. gcsafe, raises: [Defect,CatchableError].} = ## For the Consensus Engine, `author()` retrieves the Ethereum address of the ## account that minted the given block, which may be different from the ## header's coinbase if a consensus engine is based on signatures. ## ## This implementation returns the Ethereum address recovered from the ## signature in the header's extra-data section. c.ecrecover(header) # clique/clique.go(217): func (c *Clique) VerifyHeader(chain [..] proc verifyHeader*(c: var Clique; header: BlockHeader): CliqueResult {. gcsafe, raises: [Defect,CatchableError].} = ## For the Consensus Engine, `verifyHeader()` checks whether a header ## conforms to the consensus rules of a given engine. Verifying the seal ## may be done optionally here, or explicitly via the `verifySeal()` method. ## ## This implementation checks whether a header conforms to the consensus ## rules. c.verifyHeader(header, @[]) # clique/clique.go(224): func (c *Clique) VerifyHeader(chain [..] proc verifyHeaders*(c: var Clique; headers: openArray[BlockHeader]): Future[seq[CliqueResult]] {.async,gcsafe.} = ## For the Consensus Engine, `verifyHeader()` s similar to VerifyHeader, but ## verifies a batch of headers concurrently. This method is accompanied ## by a `stopVerifyHeader()` method that can abort the operations. ## ## This implementation checks whether a header conforms to the consensus ## rules. It verifies a batch of headers. If running in the background, ## the process can be stopped by calling the `stopVerifyHeader()` function. syncExceptionWrap: c.doExclusively: c.stopVHeaderReq = false for n in 0 ..< headers.len: c.doExclusively: let isStopRequest = c.stopVHeaderReq if isStopRequest: result.add cliqueResultErr((errCliqueStopped,"")) break result.add c.verifyHeader(headers[n], headers[0 ..< n]) c.doExclusively: c.stopVHeaderReq = false proc stopVerifyHeader*(c: var Clique): bool {.discardable.} = ## Activate the stop flag for running `verifyHeader()` function. ## Returns `true` if the stop flag could be activated. syncExceptionWrap: c.doExclusively: if not c.stopVHeaderReq: c.stopVHeaderReq = true result = true # clique/clique.go(450): func (c *Clique) VerifyUncles(chain [..] proc verifyUncles*(c: var Clique; ethBlock: EthBlock): CliqueResult = ## For the Consensus Engine, `verifyUncles()` verifies that the given ## block's uncles conform to the consensus rules of a given engine. ## ## This implementation always returns an error for existing uncles as this ## consensus mechanism doesn't permit uncles. if 0 < ethBlock.uncles.len: return err((errCliqueUnclesNotAllowed,"")) result = ok() # clique/clique.go(506): func (c *Clique) Prepare(chain [..] proc prepare*(c: var Clique; header: var BlockHeader): CliqueResult {. gcsafe, raises: [Defect,CatchableError].} = ## For the Consensus Engine, `prepare()` initializes the consensus fields ## of a block header according to the rules of a particular engine. The ## changes are executed inline. ## ## This implementation prepares all the consensus fields of the header for ## running the transactions on top. # If the block isn't a checkpoint, cast a random vote (good enough for now) header.coinbase.reset header.nonce.reset # Assemble the voting snapshot to check which votes make sense var snap = c.snapshot(header.blockNumber-1, header.parentHash, @[]) if snap.isErr: return err(snap.error) if (header.blockNumber mod c.cfg.epoch) != 0: c.doExclusively: # Gather all the proposals that make sense voting on var addresses: seq[EthAddress] for (address,authorize) in c.proposals.pairs: if 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) == 0: header.extraData.add snap.value.signers.mapIt(toSeq(it)).concat header.extraData.add 0.byte.repeat(EXTRA_SEAL) # Mix digest is reserved for now, set to empty header.mixDigest.reset # Ensure the timestamp has the correct delay 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; db: AccountStateDB) = ## For the Consensus Engine, `finalize()` runs any post-transaction state ## modifications (e.g. block rewards) but does not assemble the block. ## ## Note: The block header and state database might be updated to reflect any ## consensus rules that happen at finalization (e.g. block rewards). ## ## Not implemented here, raises `AssertionDefect` raiseAssert "Not implemented" # # ## This implementation ensures no uncles are set, nor block rewards given. # # No block rewards in PoA, so the state remains as is and uncles are dropped # let deleteEmptyObjectsOk = c.cfg.config.eip158block <= header.blockNumber # header.stateRoot = db.intermediateRoot(deleteEmptyObjectsOk) # header.ommersHash = EMPTY_UNCLE_HASH # clique/clique.go(579): func (c *Clique) FinalizeAndAssemble(chain [..] proc finalizeAndAssemble*(c: var Clique; header: BlockHeader; db: AccountStateDB; txs: openArray[Transaction]; receipts: openArray[Receipt]): Result[EthBlock,CliqueError] = ## For the Consensus Engine, `finalizeAndAssemble()` runs any ## post-transaction state modifications (e.g. block rewards) and assembles ## the final block. ## ## Note: The block header and state database might be updated to reflect any ## consensus rules that happen at finalization (e.g. block rewards). ## ## Not implemented here, raises `AssertionDefect` raiseAssert "Not implemented" # ## Ensuring no uncles are set, nor block rewards given, and returns the # ## final block. # # # Finalize block # c.finalize(header, state, txs, uncles) # # # Assemble and return the final block for sealing # return types.NewBlock(header, txs, nil, receipts, # trie.NewStackTrie(nil)), nil # clique/clique.go(589): func (c *Clique) Authorize(signer [..] proc authorize*(c: var Clique; signer: EthAddress; signFn: CliqueSignerFn) = ## Injects private key into the consensus engine to mint new blocks with. syncExceptionWrap: c.doExclusively: c.signer = signer c.signFn = signFn # clique/clique.go(724): func CliqueRLP(header [..] proc cliqueRlp*(header: BlockHeader): seq[byte] = ## Returns the rlp bytes which needs to be signed for the proof-of-authority ## sealing. The RLP to sign consists of the entire header apart from the 65 ## byte signature contained at the end of the extra data. ## ## Note, the method requires the extra data to be at least 65 bytes, ## otherwise it panics. This is done to avoid accidentally using both forms ## (signature present or not), which could be abused to produce different ##hashes for the same header. header.encodeSealHeader # clique/clique.go(688): func SealHash(header *types.Header) common.Hash { proc sealHash*(header: BlockHeader): Hash256 = ## For the Consensus Engine, `sealHash()` returns the hash of a block prior ## to it being sealed. ## ## This implementation returns the hash of a block prior to it being sealed. header.hashSealHeader # clique/clique.go(599): func (c *Clique) Seal(chain [..] proc seal*(c: var Clique; ethBlock: EthBlock): Future[Result[EthBlock,CliqueError]] {.async,gcsafe.} = ## For the Consensus Engine, `seal()` generates a new sealing request for ## the given input block and pushes the result into the given channel. ## ## Note, the method returns immediately and will send the result async. More ## than one result may also be returned depending on the consensus algorithm. ## ## This implementation attempts to create a sealed block using the local ## signing credentials. If running in the background, the process can be ## stopped by calling the `stopSeal()` function. c.doExclusively: c.stopSealReq = false var header = ethBlock.header # Sealing the genesis block is not supported if header.blockNumber.isZero: return err((errUnknownBlock,"")) # For 0-period chains, refuse to seal empty blocks (no reward but would spin # sealing) if c.cfg.period.isZero and ethBlock.txs.len == 0: info $nilCliqueSealNoBlockYet return err((nilCliqueSealNoBlockYet,"")) # Don't hold the signer fields for the entire sealing procedure c.doExclusively: let signer = c.signer signFn = c.signFn # Bail out if we're unauthorized to sign a block 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. syncExceptionWrap: 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] {. gcsafe, raises: [Defect,CatchableError].} = ## For the Consensus Engine, `calcDifficulty()` is the difficulty adjustment ## algorithm. It returns the difficulty that a new block should have. ## ## This implementation returns the difficulty that a new block should have: ## * DIFF_NOTURN(2) if BLOCK_NUMBER % SIGNER_COUNT != SIGNER_INDEX ## * DIFF_INTURN(1) if BLOCK_NUMBER % SIGNER_COUNT == SIGNER_INDEX 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 # ------------------------------------------------------------------------------ # Test interface # ------------------------------------------------------------------------------ proc snapshotInternal*(c: var Clique; number: BlockNumber; hash: Hash256; parent: openArray[Blockheader]): auto {. gcsafe, raises: [Defect,CatchableError].} = c.snapshot(number, hash, parent) proc cfgInternal*(c: var Clique): CliqueCfg = c.cfg proc pp*(rc: var Result[Snapshot,CliqueError]; indent = 0): string = if rc.isOk: rc.value.pp(indent) else: "(error: " & rc.error.pp & ")" # ------------------------------------------------------------------------------ # End # ------------------------------------------------------------------------------