# 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. ## Block PoW Support (Verifying & Mining) ## ====================================== ## import std/[options, strutils], ../utils/utils, ./pow/[pow_cache, pow_dataset], eth/[common, keys, p2p, rlp], ethash, stint {.push raises: [Defect].} type PowDigest = tuple ##\ ## Return value from the `hashimotoLight()` function mixDigest: Hash256 value: Hash256 PowSpecs* = object ##\ ## Relevant block header parts for PoW mining & verifying. This object ## might be more useful for testing and debugging than for production. blockNumber*: BlockNumber miningHash*: Hash256 nonce*: BlockNonce mixDigest*: Hash256 difficulty*: DifficultyInt PowHeader = object ##\ ## Stolen from `p2p/validate.MiningHeader` parentHash : Hash256 ommersHash : Hash256 coinbase : EthAddress stateRoot : Hash256 txRoot : Hash256 receiptRoot : Hash256 bloom : common.BloomFilter difficulty : DifficultyInt blockNumber : BlockNumber gasLimit : GasInt gasUsed : GasInt timestamp : EthTime extraData : Blob PowRef* = ref object of RootObj ##\ ## PoW context descriptor lightByEpoch: PowCacheRef ## PoW cache indexed by epoch fullByEpoch: PowDatasetRef ## Ditto for dataset nonceAttempts: uint64 ## Unsuccessful tests in last mining process # You should only create one instance of the RNG per application / library # Ref is used so that it can be shared between components rng: ref HmacDrbgContext # ------------------------------------------------------------------------------ # Private functions: RLP support # ------------------------------------------------------------------------------ proc append(w: var RlpWriter; specs: PowSpecs) = ## RLP support w.startList(5) w.append(HashOrNum(isHash: false, number: specs.blockNumber)) w.append(HashOrNum(isHash: true, hash: specs.miningHash)) w.append(specs.nonce.toUint) w.append(HashOrNum(isHash: true, hash: specs.mixDigest)) w.append(specs.difficulty) proc read(rlp: var Rlp; Q: type PowSpecs): Q {.raises: [Defect,RlpError].} = ## RLP support rlp.tryEnterList() result.blockNumber = rlp.read(HashOrNum).number result.miningHash = rlp.read(HashOrNum).hash result.nonce = rlp.read(uint64).toBlockNonce result.mixDigest = rlp.read(HashOrNum).hash result.difficulty = rlp.read(DifficultyInt) proc rlpTextEncode(specs: PowSpecs): string = "specs #" & $specs.blockNumber & " " & rlp.encode(specs).toHex proc decodeRlpText(data: string): PowSpecs {.raises: [Defect,CatchableError].} = if 180 < data.len and data[0 .. 6] == "specs #": let hexData = data.split if hexData.len == 3: var rlpData = hexData[2].rlpFromHex result = rlpData.read(PowSpecs) # ------------------------------------------------------------------------------ # Private functions # ------------------------------------------------------------------------------ proc miningHash(header: BlockHeader): Hash256 = ## Calculate hash from mining relevant fields of the argument `header` let miningHeader = PowHeader( parentHash: header.parentHash, ommersHash: header.ommersHash, coinbase: header.coinbase, stateRoot: header.stateRoot, txRoot: header.txRoot, receiptRoot: header.receiptRoot, bloom: header.bloom, difficulty: header.difficulty, blockNumber: header.blockNumber, gasLimit: header.gasLimit, gasUsed: header.gasUsed, timestamp: header.timestamp, extraData: header.extraData) rlp.encode(miningHeader).keccakHash # --------------- proc tryNonceFull(nonce: uint64; ds: PowDatasetItemRef; hash: Hash256): UInt256 = let rc = hashimotoFull(ds.size, ds.data, hash, nonce) value = readUintBE[256](rc.value.data) # echo ">>> nonce=", nonce.toHex, " value=", value.toHex return value proc mineFull(tm: PowRef; blockNumber: BlockNumber; powHeaderDigest: Hash256, difficulty: DifficultyInt; startNonce: BlockNonce): uint64 {.gcsafe,raises: [Defect,CatchableError].} = ## Returns a valid nonce. This function was inspired by the function ## python function `mine()` from ## `ethash `_. result = startNonce.toUint if difficulty.isZero: # Ooops??? return let ds = tm.fullByEpoch.get(blockNumber) valueMax = UInt256.high div difficulty while valueMax < result.tryNonceFull(ds, powHeaderDigest): result.inc # rely on uint overflow mod 2^64 # Book keeping, debugging support tm.nonceAttempts = if result <= startNonce.toUint: startNonce.toUint - result else: (uint64.high - startNonce.toUint) + result # --------------- proc init(tm: PowRef; rng: Option[ref HmacDrbgContext]; light: Option[PowCacheRef]; full: Option[PowDatasetRef]) = ## Constructor if rng.isSome: tm.rng = rng.get else: tm.rng = newRng() if light.isSome: tm.lightByEpoch = light.get else: tm.lightByEpoch = PowCacheRef.new if full.isSome: tm.fullByEpoch = full.get else: tm.fullByEpoch = PowDatasetRef.new(cache = tm.lightByEpoch) # ------------------------------------------------------------------------------ # Public functions, Constructor # ------------------------------------------------------------------------------ proc new*(T: type PowRef; rng: ref HmacDrbgContext; cache: PowCacheRef; dataset: PowDatasetRef): T = ## Constructor new result result.init( some(rng), some(cache), some(dataset)) proc new*(T: type PowRef; cache: PowCacheRef; dataset: PowDatasetRef): T = ## Constructor new result result.init( none(ref HmacDrbgContext), some(cache), some(dataset)) proc new*(T: type PowRef; rng: ref HmacDrbgContext): T = ## Constructor new result result.init( some(rng), none(PowCacheRef), none(PowDatasetRef)) proc new*(T: type PowRef): T = ## Constructor new result result.init( none(ref HmacDrbgContext), none(PowCacheRef), none(PowDatasetRef)) # ------------------------------------------------------------------------------ # Public functions # ------------------------------------------------------------------------------ proc getPowSpecs*(header: BlockHeader): PowSpecs = ## Extracts relevant parts from the `header` argument that are needed ## for mining or pow verification. This function might be more useful for ## testing and debugging than for production. PowSpecs( blockNumber: header.blockNumber, miningHash: header.miningHash, nonce: header.nonce, mixDigest: header.mixDigest, difficulty: header.difficulty) proc getPowCacheLookup*(tm: PowRef; blockNumber: BlockNumber): (uint64, Hash256) {.gcsafe, raises: [KeyError, Defect, CatchableError].} = ## Returns the pair `(size,digest)` derived from the lookup cache for the ## `hashimotoLight()` function for the given block number. The `size` is the ## full size of the dataset (the cache represents) as passed on to the ## `hashimotoLight()` function. The `digest` is a hash derived from the ## cache that would be passed on to `hashimotoLight()`. ## ## This function is intended for error reporting and might also be useful ## for testing and debugging. let ds = tm.lightByEpoch.get(blockNumber) if ds == nil: raise newException(KeyError, "block not found") result[0] = ds.size result[1] = withKeccakHash: for a in ds.data: h.update(a.data) # ------------------------ proc getPowDigest*(tm: PowRef; blockNumber: BlockNumber; powHeaderDigest: Hash256; nonce: BlockNonce): PowDigest {.gcsafe,raises: [Defect,CatchableError].} = ## Calculate the expected value of `header.mixDigest` using the ## `hashimotoLight()` library method. let ds = tm.lightByEpoch.get(blockNumber) u64Nonce = uint64.fromBytesBE(nonce) hashimotoLight(ds.size, ds.data, powHeaderDigest, u64Nonce) proc getPowDigest*(tm: PowRef; header: BlockHeader): PowDigest {.gcsafe,raises: [Defect,CatchableError].} = ## Variant of `getPowDigest()` tm.getPowDigest(header.blockNumber, header.miningHash, header.nonce) proc getPowDigest*(tm: PowRef; specs: PowSpecs): PowDigest {.gcsafe,raises: [Defect,CatchableError].} = ## Variant of `getPowDigest()` tm.getPowDigest(specs.blockNumber, specs.miningHash, specs.nonce) # ------------------ proc getNonce*(tm: PowRef; number: BlockNumber; powHeaderDigest: Hash256; difficulty: DifficultyInt; startNonce: BlockNonce): BlockNonce {.gcsafe,raises: [Defect,CatchableError].} = ## Mining function that calculates the value of a `nonce` satisfying the ## difficulty challenge. This is the most basic function of the ## `getNonce()` series with explicit argument `startNonce`. If this is ## a valid `nonce` already, the function stops and returns that value. ## Otherwise it derives other nonces from the `startNonce` start and ## continues trying. ## ## The function depends on a mining dataset which can be generated with ## `generatePowDataset()` before that function is invoked. ## ## This mining logic was inspired by the Python function `mine()` from ## `ethash `_. tm.mineFull(number, powHeaderDigest, difficulty, startNonce).toBytesBE proc getNonce*(tm: PowRef; number: BlockNumber; powHeaderDigest: Hash256; difficulty: DifficultyInt): BlockNonce {.gcsafe,raises: [Defect,CatchableError].} = ## Variant of `getNonce()` var startNonce: array[8,byte] tm.rng[].generate(startNonce) tm.getNonce(number, powHeaderDigest, difficulty, startNonce) proc getNonce*(tm: PowRef; header: BlockHeader): BlockNonce {.gcsafe,raises: [Defect,CatchableError].} = ## Variant of `getNonce()` tm.getNonce(header.blockNumber, header.miningHash, header.difficulty) proc getNonce*(tm: PowRef; specs: PowSpecs): BlockNonce {.gcsafe,raises: [Defect,CatchableError].} = ## Variant of `getNonce()` tm.getNonce(specs.blockNumber, specs.miningHash, specs.difficulty) proc nGetNonce*(tm: PowRef): uint64 = ## Number of unsucchessful internal tests in the last invocation ## of `getNonce()`. tm.nonceAttempts # ------------------ proc generatePowDataset*(tm: PowRef; number: BlockNumber) {.gcsafe,raises: [Defect,CatchableError].} = ## Prepare dataset for the `getNonce()` mining function. This dataset ## changes with the epoch of the argument `number` so it is applicable for ## the full epoch. If not generated explicitely, it will be done so by the ## next invocation of `getNonce()`. ## ## This is a slow process which produces a huge data table. So expect this ## function to hang on for a while and do not mind if the OS starts swapping. ## A list of the data sizes indexed by epoch is available at the end of ## the `ethash `_ Python ## reference implementation. discard tm.fullByEpoch.get(number) # ------------------------------------------------------------------------------ # Public functions, debugging & testing # ------------------------------------------------------------------------------ proc dumpPowSpecs*(specs: PowSpecs): string = ## Text representation of `PowSpecs` argument object specs.rlpTextEncode proc dumpPowSpecs*(header: BlockHeader): string = ## Variant of `dumpPowSpecs()` header.getPowSpecs.dumpPowSpecs proc undumpPowSpecs*(data: string): PowSpecs {.raises: [Defect,CatchableError].} = ## Recover `PowSpecs` object from text representation data.decodeRlpText # ------------------------------------------------------------------------------ # End # ------------------------------------------------------------------------------