nimbus-eth1/nimbus/p2p/clique/clique_snapshot.nim

429 lines
14 KiB
Nim

# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
##
## Snapshot for Clique PoA Consensus Protocol
## ==========================================
##
## For details see
## `EIP-225 <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-225.md>`_
## and
## `go-ethereum <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-225.md>`_
##
import
std/[sequtils, strutils],
chronicles,
eth/[common, keys],
stew/[keyed_queue, results],
stint,
"../.."/[constants, db/db_chain, utils/prettify],
"."/[clique_cfg, clique_defs, clique_desc],
./snapshot/[snapshot_apply, snapshot_desc]
type
# Internal sub-descriptor for `LocalSnapsDesc`
LocalPivot = object
header: BlockHeader
hash: Hash256
# Internal sub-descriptor for `LocalSnapsDesc`
LocalPath = object
snaps: Snapshot ## snapshot for given hash
chain: seq[BlockHeader] ## header chain towards snapshot
error: CliqueError ## error message
# Internal sub-descriptor for `LocalSnapsDesc`
LocalSubChain = object
first: int ## first chain[] element to be used
top: int ## length of chain starting at position 0
LocalSnaps = object
c: Clique
start: LocalPivot ## start here searching for checkpoints
trail: LocalPath ## snapshot location
subChn: LocalSubChain ## chain[] sub-range
parents: seq[BlockHeader] ## explicit parents
{.push raises: [Defect].}
logScope:
topics = "clique PoA snapshot"
# ------------------------------------------------------------------------------
# Private debugging functions, pretty printing
# ------------------------------------------------------------------------------
template say(d: var LocalSnaps; v: varargs[untyped]): untyped =
discard
# uncomment body to enable, note that say() prints on <stderr>
# d.c.cfg.say v
proc pp(a: Hash256): string =
if a == EMPTY_ROOT_HASH:
"*blank-root*"
elif a == EMPTY_SHA3:
"*empty-sha3*"
else:
a.data.mapIt(it.toHex(2)).join[56 .. 63].toLowerAscii
proc pp(q: openArray[BlockHeader]; n: int): string =
result = "["
if 5 < n:
result &= toSeq(q[0 .. 2]).mapIt("#" & $it.blockNumber).join(", ")
result &= " .." & $n & ".. #" & $q[n-1].blockNumber
else:
result &= toSeq(q[0 ..< n]).mapIt("#" & $it.blockNumber).join(", ")
result &= "]"
proc pp(b: BlockNumber, q: openArray[BlockHeader]; n: int): string =
"#" & $b & " + " & q.pp(n)
proc pp(q: openArray[BlockHeader]): string =
q.pp(q.len)
proc pp(b: BlockNumber, q: openArray[BlockHeader]): string =
b.pp(q, q.len)
proc pp(h: BlockHeader, q: openArray[BlockHeader]; n: int): string =
"headers=(" & h.blockNumber.pp(q,n) & ")"
proc pp(h: BlockHeader, q: openArray[BlockHeader]): string =
h.pp(q,q.len)
proc pp(t: var LocalPath; w: var LocalSubChain): string =
var (a, b) = (w.first, w.top)
if a == 0 and b == 0: b = t.chain.len
"trail=(#" & $t.snaps.blockNumber & " + " & t.chain[a ..< b].pp & ")"
proc pp(t: var LocalPath): string =
var w = LocalSubChain()
t.pp(w)
proc pp(err: CliqueError): string =
"(" & $err[0] & "," & err[1] & ")"
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
proc maxCheckPointLe(d: var LocalSnaps; number: BlockNumber): BlockNumber =
let epc = number mod d.c.cfg.ckpInterval
if epc < number:
number - epc
else:
# epc == number => number < ckpInterval
0.u256
proc isCheckPoint(d: var LocalSnaps; number: BlockNumber): bool =
(number mod d.c.cfg.ckpInterval) == 0
proc isEpoch(d: var LocalSnaps; number: BlockNumber): bool =
(number mod d.c.cfg.epoch) == 0
proc isSnapshotPosition(d: var LocalSnaps; number: BlockNumber): bool =
# clique/clique.go(394): if number == 0 || (number%c.config.Epoch [..]
if d.isEpoch(number):
if number.isZero:
# At the genesis => snapshot the initial state.
return true
if not d.c.applySnapsMinBacklog:
return true
if d.c.cfg.roThreshold < d.trail.chain.len:
# We have piled up more headers than allowed to be re-orged (chain
# reinit from a freezer), regard checkpoint trusted and snapshot it.
return true
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc findSnapshot(d: var LocalSnaps): bool
{.gcsafe, raises: [Defect,CatchableError].} =
## Search for a snapshot starting at current header starting at the pivot
## value `d.start`. The snapshot returned in `trail` is a clone of the
## cached snapshot and can be modified later.
var
(header, hash) = (d.start.header, d.start.hash)
parentsLen = d.parents.len
# For convenience, ignore the current header as top parents list entry
if 0 < parentsLen and d.parents[^1] == header:
parentsLen.dec
while true:
#d.say "findSnapshot ", header.pp(d.parents, parentsLen),
# " trail=", d.trail.chain.pp
let number = header.blockNumber
# Check whether the snapshot was recently visited and cached
block:
let rc = d.c.recents.lruFetch(hash.data)
if rc.isOk:
d.trail.snaps = rc.value.cloneSnapshot
# d.say "findSnapshot cached ", d.trail.pp
trace "Found recently cached voting snapshot",
blockNumber = number,
blockHash = hash
return true
# If an on-disk checkpoint snapshot can be found, use that
if d.isCheckPoint(number):
let rc = d.c.cfg.loadSnapshot(hash)
if rc.isOk:
d.trail.snaps = rc.value.cloneSnapshot
d.say "findSnapshot on disk ", d.trail.pp
trace "Loaded voting snapshot from disk",
blockNumber = number,
blockHash = hash
# clique/clique.go(386): snap = s
return true
# Note that epoch is a restart and sync point. Eip-225 requires that the
# epoch header contains the full list of currently authorised signers.
if d.isSnapshotPosition(number):
# clique/clique.go(395): checkpoint := chain.GetHeaderByNumber [..]
d.trail.snaps = d.c.cfg.newSnapshot(header)
let rc = d.c.cfg.storeSnapshot(d.trail.snaps)
if rc.isOk:
d.say "findSnapshot <epoch> ", d.trail.pp
trace "Stored voting snapshot to disk",
blockNumber = number,
blockHash = hash,
nSnaps = d.c.cfg.nSnaps,
snapsTotal = d.c.cfg.snapsData.toSI
return true
# No snapshot for this header, get the parent header and move backward
hash = header.parentHash
# Add to batch (reversed list order, biggest block number comes first)
d.trail.chain.add header
# Assign parent header
if 0 < parentsLen:
# If we have explicit parents, pop it from the parents list
parentsLen.dec
header = d.parents[parentsLen]
# clique/clique.go(416): if header.Hash() != hash [..]
if header.blockHash != hash:
d.trail.error = (errUnknownAncestor,"")
return false
# No explicit parents (or no more parents left), reach out to the database
elif not d.c.cfg.db.getBlockHeader(hash, header):
d.trail.error = (errUnknownAncestor,"")
return false
# => while loop
# notreached
raiseAssert "findSnapshot(): wrong exit from forever-loop"
proc applyTrail(d: var LocalSnaps): CliqueOkResult
{.gcsafe, raises: [Defect,CatchableError].} =
## Apply any `trail` headers on top of the snapshot `snap`
if d.subChn.first < d.subChn.top:
block:
# clique/clique.go(434): snap, err := snap.apply(headers)
d.say "applyTrail ", d.trail.pp(d.subChn)
let rc = d.trail.snaps.snapshotApplySeq(
d.trail.chain, d.subChn.top-1, d.subChn.first)
if rc.isErr:
d.say "applyTrail snaps=#", d.trail.snaps.blockNumber,
" err=", rc.error.pp
return err(rc.error)
d.say "applyTrail snaps=#", d.trail.snaps.blockNumber
# If we've generated a new checkpoint snapshot, save to disk
if d.isCheckPoint(d.trail.snaps.blockNumber):
var rc = d.c.cfg.storeSnapshot(d.trail.snaps)
if rc.isErr:
return err(rc.error)
d.say "applyTrail <disk> chechkpoint #", d.trail.snaps.blockNumber
trace "Stored voting snapshot to disk",
blockNumber = d.trail.snaps.blockNumber,
blockHash = d.trail.snaps.blockHash,
nSnaps = d.c.cfg.nSnaps,
snapsTotal = d.c.cfg.snapsData.toSI
ok()
proc updateSnapshot(d: var LocalSnaps): SnapshotResult
{.gcsafe, raises: [Defect,CatchableError].} =
## Find snapshot for header `d.start.header` and assign it to the LRU cache.
## This function was expects thet the LRU cache already has a slot allocated
## for the snapshot having run `getLruSnaps()`.
d.say "updateSnapshot begin ", d.start.header.blockNumber.pp(d.parents)
# Search for previous snapshots
if not d.findSnapshot:
return err(d.trail.error)
# Initialise range for header chain[] to be applied to `d.trail.snaps`
d.subChn.top = d.trail.chain.len
# Previous snapshot found, apply any pending trail headers on top of it
if 0 < d.subChn.top:
let
first = d.trail.chain[^1].blockNumber
last = d.trail.chain[0].blockNumber
ckpt = d.maxCheckPointLe(last)
# If there is at least one checkpoint part of the trail sequence, make sure
# that we can store the latest one. This will be done by the `applyTrail()`
# handler for the largest block number in the sequence (note that the trail
# block numbers are in reverse order.)
if first <= ckpt and ckpt < last:
# Split the trail sequence so that the first one has the checkpoint
# entry with largest block number.
let inx = (last - ckpt).truncate(int)
# First part (note reverse block numbers.)
d.subChn.first = inx
let rc = d.applyTrail
if rc.isErr:
return err(rc.error)
# Second part (note reverse block numbers.)
d.subChn.first = 0
d.subChn.top = inx
var rc = d.applyTrail
if rc.isErr:
return err(rc.error)
# clique/clique.go(438): c.recents.Add(snap.Hash, snap)
discard d.c.recents.lruAppend(
d.trail.snaps.blockHash.data, d.trail.snaps, INMEMORY_SNAPSHOTS)
if 1 < d.trail.chain.len:
d.say "updateSnapshot ok #", d.trail.snaps.blockNumber,
" trail.len=", d.trail.chain.len
ok(d.trail.snaps)
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc cliqueSnapshotSeq*(c: Clique; header: BlockHeader;
parents: var seq[BlockHeader]): SnapshotResult
{.gcsafe, raises: [Defect,CatchableError].} =
## Create authorisation state snapshot of a given point in the block chain
## and store it in the `Clique` descriptor to be retrievable as `c.snapshot`
## if successful.
##
## If the `parents[]` argument list top element (if any) is the same as the
## `header` argument, this top element is silently ignored.
##
## If this function is successful, the compiled `Snapshot` will also be
## stored in the `Clique` descriptor which can be retrieved later
## via `c.snapshot`.
block:
let rc = c.recents.lruFetch(header.blockHash.data)
if rc.isOk:
c.snapshot = rc.value
return ok(rc.value)
# Avoid deep copy, sequence will not be changed by `updateSnapshot()`
parents.shallow
var snaps = LocalSnaps(
c: c,
parents: parents,
start: LocalPivot(
header: header,
hash: header.blockHash))
let rc = snaps.updateSnapshot
if rc.isOk:
c.snapshot = rc.value
rc
proc cliqueSnapshotSeq*(c: Clique; hash: Hash256;
parents: var seq[BlockHeader]): SnapshotResult
{.gcsafe,raises: [Defect,CatchableError].} =
## Create authorisation state snapshot of a given point in the block chain
## and store it in the `Clique` descriptor to be retrievable as `c.snapshot`
## if successful.
##
## If the `parents[]` argument list top element (if any) is the same as the
## `header` argument, this top element is silently ignored.
##
## If this function is successful, the compiled `Snapshot` will also be
## stored in the `Clique` descriptor which can be retrieved later
## via `c.snapshot`.
block:
let rc = c.recents.lruFetch(hash.data)
if rc.isOk:
c.snapshot = rc.value
return ok(rc.value)
var header: BlockHeader
if not c.cfg.db.getBlockHeader(hash, header):
return err((errUnknownHash,""))
# Avoid deep copy, sequence will not be changed by `updateSnapshot()`
parents.shallow
var snaps = LocalSnaps(
c: c,
parents: parents,
start: LocalPivot(
header: header,
hash: hash))
let rc = snaps.updateSnapshot
if rc.isOk:
c.snapshot = rc.value
rc
# clique/clique.go(369): func (c *Clique) snapshot(chain [..]
proc cliqueSnapshot*(c: Clique; header: BlockHeader;
parents: var seq[BlockHeader]): SnapshotResult
{.gcsafe, raises: [Defect,CatchableError].} =
var list = toSeq(parents)
c.cliqueSnapshotSeq(header,list)
proc cliqueSnapshot*(c: Clique;hash: Hash256;
parents: openArray[BlockHeader]): SnapshotResult
{.gcsafe, raises: [Defect,CatchableError].} =
var list = toSeq(parents)
c.cliqueSnapshotSeq(hash,list)
proc cliqueSnapshot*(c: Clique; header: BlockHeader): SnapshotResult
{.gcsafe,raises: [Defect,CatchableError].} =
## Short for `cliqueSnapshot(c,header,@[])`
var blind: seq[BlockHeader]
c.cliqueSnapshotSeq(header, blind)
proc cliqueSnapshot*(c: Clique; hash: Hash256): SnapshotResult
{.gcsafe,raises: [Defect,CatchableError].} =
## Short for `cliqueSnapshot(c,hash,@[])`
var blind: seq[BlockHeader]
c.cliqueSnapshot(hash, blind)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------