# 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 `_ ## and ## `go-ethereum `_ ## import std/[sequtils, strutils], chronicles, eth/[keys], stew/[keyed_queue, results], "../.."/[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 # 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 ", 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 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 # ------------------------------------------------------------------------------