From ec9354d2d003fa1473ff16b4f1db7ab7891acb42 Mon Sep 17 00:00:00 2001 From: Jordan Hrycaj Date: Tue, 3 Aug 2021 08:15:32 +0100 Subject: [PATCH] Jordan/poa voting header (#782) * Provide PoA voting header generator why: Handy for hive/smoke test details: Header generator is a re-implementation of the generator previously used for the canonical reference tests. * try fixing ci out-of-mem condition why: for some reason, the ci began behaving like a real win7/i386 machine where gcc is limited to 64k optimiser space * fix comments, typos .. --- .github/workflows/ci.yml | 5 + nimbus/p2p/clique/clique_genvote.nim | 209 ++++++++++++++++++ nimbus/p2p/clique/clique_helpers.nim | 6 +- nimbus/p2p/clique/clique_verify.nim | 8 +- nimbus/p2p/clique/snapshot/snapshot_apply.nim | 2 +- tests/test_clique/pool.nim | 73 ++---- tests/test_clique/voter_samples.nim | 76 +++++-- 7 files changed, 289 insertions(+), 90 deletions(-) create mode 100644 nimbus/p2p/clique/clique_genvote.nim diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f9f8c022..32b7cf5d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,11 @@ jobs: elif [[ '${{ matrix.target.evmc }}' == 'nimvm2' ]]; then echo "ENABLE_EVMC=0" >> $GITHUB_ENV echo "ENABLE_VM2=1" >> $GITHUB_ENV + case '${{ matrix.target.cpu }}:${{ runner.os }}' in + 'i386:Windows') + # on a real win7/i386, there is only 64k optimiser space for gcc + echo "ENABLE_VM2LOWMEM=1" >> $GITHUB_ENV + esac else echo "ENABLE_EVMC=0" >> $GITHUB_ENV echo "ENABLE_VM2=0" >> $GITHUB_ENV diff --git a/nimbus/p2p/clique/clique_genvote.nim b/nimbus/p2p/clique/clique_genvote.nim new file mode 100644 index 000000000..f3ccb519d --- /dev/null +++ b/nimbus/p2p/clique/clique_genvote.nim @@ -0,0 +1,209 @@ +# 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. + +## +## Generate PoA Voting Header +## ========================== +## +## For details see +## `EIP-225 `_ +## and +## `go-ethereum `_ +## + +import + std/[sequtils, times], + ../../constants, + ../../db/db_chain, + ./clique_cfg, + ./clique_defs, + ./clique_desc, + ./clique_helpers, + eth/[common, keys] + +{.push raises: [Defect].} + +# ------------------------------------------------------------------------------ +# Private functions +# ------------------------------------------------------------------------------ + +# clique/snapshot_test.go(49): func (ap *testerAccountPool) [..] +proc extraCheckPoint(header: var BlockHeader; signers: openArray[EthAddress]) = + ## creates a Clique checkpoint signer section from the provided list + ## of authorized signers and embeds it into the provided header. + header.extraData.setLen(EXTRA_VANITY) + header.extraData.add signers.mapIt(toSeq(it)).concat + header.extraData.add 0.byte.repeat(EXTRA_SEAL) + +# clique/snapshot_test.go(77): func (ap *testerAccountPool) sign(header n[..] +proc sign(header: var BlockHeader; signer: PrivateKey) {.inline.} = + ## sign calculates a Clique digital signature for the given block and embeds + ## it back into the header. + # + # Sign the header and embed the signature in extra data + let + hashData = header.hashSealHeader.data + signature = signer.sign(SkMessage(hashData)).toRaw + extraLen = header.extraData.len + header.extraData.setLen(extraLen - EXTRA_SEAL) + header.extraData.add signature + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ + +# clique/snapshot_test.go(415): blocks, _ := core.GenerateChain(&config, [..] +proc cliqueGenvote*( + c: Clique; + voter: EthAddress; # new voter account/identity + seal: PrivateKey; # signature key + parent: BlockHeader; + elapsed = initDuration(); + voteInOk = false; # vote in the new voter if `true` + outOfTurn = false; + checkPoint: seq[EthAddress] = @[]): BlockHeader = + ## Generate PoA voting header (as opposed to `epoch` synchronisation header.) + ## The function arguments are as follows: + ## + ## :c: + ## Clique descriptor. see the `newClique()` object constructor. + ## + ## :voter: + ## New voter account address to vote in or out (see `voteInOk`). A trivial + ## example for the first block #1 header would be choosing one of the + ## accounts listed in the `extraData` field fo the genesis header (note + ## that Goerli has exactly one of those accounts.) This trivial example + ## has no effect on the authorised voters' list. + ## + ## :seal: + ## Private key related to an authorised voter account. Again, a trivial + ## example for the block #1 header would be to (know and) use the + ## associated key for one of the accounts listed in the `extraData` field + ## fo the genesis header. + ## + ## :parent: + ## parent header to chain with (not necessarily on block chain yet). For + ## a block #1 header as a trivial example, this would be the genesis + ## header. + ## + ## :elapsed: + ## Optional timestamp distance from parent. This value defaults to valid + ## minimum time interval `c.cfg.period` + ## + ## :voteInOk: + ## Role of voting account. If `true`, the `voter` account address is voted + ## in to be accepted as authorised account. If `false`, the `voter` account + ## is voted to be removed (if it exists as authorised account, at all.) + ## + ## :outOfTurn: + ## Must be `false` if the `voter` is `in-turn` which is defined as the + ## property of a header block number retrieving the `seal` account address + ## when used as list index (modulo list-length) into the (internally + ## calculated and sorted) list of authorised signers. Absence of this + ## property is called `out-of-turn`. + ## + ## The classification `in-turn` and `out-of-turn` is used only with a + ## multi mining strategy where an `in-turn` block is slightly preferred. + ## Nevertheless, this property is to be locked into the block chain. In a + ## trivial example of an authorised signers list with exactly one entry, + ## all block numbers are zero modulo one, so are `in-turn`, and + ## `outOfTurn` would be left `false`. + ## + ## :checkPoint: + ## List of currently authorised signers. According to the Clique protocol + ## EIP-225, this list must be the same as the internally computed list of + ## authorised signers from the block chain. + ## + ## This list must appear on an `epoch` block and nowhere else. An `epoch` + ## block is a block where the block number is a multiple of `c.cfg.epoch`. + ## Typically, `c.cfg.epoch` is initialised as `30'000`. + ## + let timeElapsed = if elapsed == initDuration(): c.cfg.period else: elapsed + + result = BlockHeader( + parentHash: parent.blockHash, + ommersHash: EMPTY_UNCLE_HASH, + stateRoot: parent.stateRoot, + timestamp: parent.timestamp + timeElapsed, + txRoot: BLANK_ROOT_HASH, + receiptRoot: BLANK_ROOT_HASH, + blockNumber: parent.blockNumber + 1, + gasLimit: parent.gasLimit, + # + # clique/snapshot_test.go(417): gen.SetCoinbase(accounts.address( [..] + coinbase: voter, + # + # clique/snapshot_test.go(418): if tt.votes[j].auth { + nonce: if voteInOk: NONCE_AUTH else: NONCE_DROP, + # + # clique/snapshot_test.go(436): header.Difficulty = diffInTurn [..] + difficulty: if outOfTurn: DIFF_NOTURN else: DIFF_INTURN, + # + extraData: 0.byte.repeat(EXTRA_VANITY + EXTRA_SEAL)) + + # clique/snapshot_test.go(432): if auths := tt.votes[j].checkpoint; [..] + if 0 < checkPoint.len: + result.extraCheckPoint(checkPoint) + + # Generate the signature and embed it into the header + result.sign(seal) + + +proc cliqueGenvote*( + c: Clique; voter: EthAddress; seal: PrivateKey; + elapsed = initDuration(); + voteInOk = false; + outOfTurn = false; + checkPoint: seq[EthAddress] = @[]): BlockHeader + {.gcsafe, raises: [Defect,CatchableError].} = + ## Variant of `clique_genvote()` where the `parent` is the canonical head + ## on the the block chain database. + ## + ## Trivial example (aka smoke test): + ## + ## :signature: `S` + ## :account address: `a(S)` + ## :genesis: extraData contains exactly one signer `a(S)` + ## + ## [..] + ## + ## | import pkg/[times], .. + ## | import p2p/[chain,clique], p2p/clique/clique_genvote, .. + ## + ## [..] + ## + ## | var db: BaseChainDB = ... + ## | var c = db.newChain + ## + ## + ## | \# overwrite, typically initialised at 15s + ## | const threeSecs = initDuration(seconds = 3) + ## | c.clique.cfg.period = threeSecs + ## + ## + ## | \# create first block (assuming empty block chain), mind `a(S)`, `S` + ## | let header = c.clique.clique_genvote(`a(S)`, `S`, elapsed = threeSecs) + ## + ## [..] + ## + ## let ok = c.persistBlocks(@[header],@[BlockBody()]) + ## + ## [..] + ## + c.clique_genvote(voter, seal, + parent = c.cfg.db.getCanonicalHead, + elapsed = elapsed, + voteInOk = voteInOk, + outOfTurn = outOfTurn, + checkPoint = checkPoint) + +# ------------------------------------------------------------------------------ +# End +# ------------------------------------------------------------------------------ diff --git a/nimbus/p2p/clique/clique_helpers.nim b/nimbus/p2p/clique/clique_helpers.nim index 046832938..ad4c770b2 100644 --- a/nimbus/p2p/clique/clique_helpers.nim +++ b/nimbus/p2p/clique/clique_helpers.nim @@ -9,16 +9,14 @@ # according to those terms. ## -## Tuoole & Utils for Clique PoA Consensus Protocol -## ================================================ +## Tools & Utils for Clique PoA Consensus Protocol +## =============================================== ## ## For details see ## `EIP-225 `_ ## and ## `go-ethereum `_ ## -## Caveat: Not supporting RLP serialisation encode()/decode() -## import std/[algorithm, times], diff --git a/nimbus/p2p/clique/clique_verify.nim b/nimbus/p2p/clique/clique_verify.nim index 0761438ee..2bb37a2f4 100644 --- a/nimbus/p2p/clique/clique_verify.nim +++ b/nimbus/p2p/clique/clique_verify.nim @@ -102,11 +102,11 @@ proc isSigner*(s: Snapshot; address: EthAddress): bool {.inline.} = # clique/snapshot.go(319): func (s *Snapshot) inturn(number [..] proc inTurn*(s: Snapshot; number: BlockNumber, signer: EthAddress): bool = - ## Returns `true` if a signer at a given block height is in-turn or not. + ## Returns `true` if a signer at a given block height is in-turn. let ascSignersList = s.ballot.authSigners - for offset in 0 ..< ascSignersList.len: - if ascSignersList[offset] == signer: - return (number mod ascSignersList.len.u256) == offset.u256 + if 0 < ascSignersList.len: + let offset = (number mod ascSignersList.len.u256).truncate(int64) + return ascSignersList[offset] == signer # ------------------------------------------------------------------------------ # Private functions diff --git a/nimbus/p2p/clique/snapshot/snapshot_apply.nim b/nimbus/p2p/clique/snapshot/snapshot_apply.nim index 0d3e35958..9cac7621d 100644 --- a/nimbus/p2p/clique/snapshot/snapshot_apply.nim +++ b/nimbus/p2p/clique/snapshot/snapshot_apply.nim @@ -40,7 +40,7 @@ logScope: proc say(s: Snapshot; v: varargs[string,`$`]) {.inline.} = discard # uncomment body to enable - s.cfg.say v + #s.cfg.say v proc pp(a: openArray[BlockHeader]; first, last: int): string {.inline.} = result = "[" diff --git a/tests/test_clique/pool.nim b/tests/test_clique/pool.nim index 56eac1bcd..0afc04587 100644 --- a/tests/test_clique/pool.nim +++ b/tests/test_clique/pool.nim @@ -15,6 +15,7 @@ import ../../nimbus/p2p/[chain, clique, clique/clique_desc, + clique/clique_genvote, clique/clique_helpers, clique/clique_snapshot, clique/snapshot/snapshot_desc], @@ -304,39 +305,6 @@ proc address*(ap: TesterPool; account: string): EthAddress = if account != "": result = ap.privateKey(account).toPublicKey.toCanonicalAddress - -# clique/snapshot_test.go(49): func (ap *testerAccountPool) [..] -proc checkpoint*(ap: TesterPool; - header: var BlockHeader; signers: openArray[string]) = - ## creates a Clique checkpoint signer section from the provided list - ## of authorized signers and embeds it into the provided header. - header.extraData.setLen(EXTRA_VANITY) - header.extraData.add signers - .mapIt(ap.address(it)) - .sorted(EthAscending) - .mapIt(toSeq(it)) - .concat - header.extraData.add 0.byte.repeat(EXTRA_SEAL) - - -# clique/snapshot_test.go(77): func (ap *testerAccountPool) sign(header n[..] -proc sign*(ap: TesterPool; header: var BlockHeader; signer: string) = - ## sign calculates a Clique digital signature for the given block and embeds - ## it back into the header. - # - # Sign the header and embed the signature in extra data - let - hashData = header.hashSealHeader.data - signature = ap.privateKey(signer).sign(SkMessage(hashData)).toRaw - extraLen = header.extraData.len - header.extraData.setLen(extraLen - EXTRA_SEAL) - header.extraData.add signature - # - # Register for debugging - ap.xSeals[signature] = XSealValue( - blockNumber: header.blockNumber.truncate(uint64), - account: signer) - # ------------------------------------------------------------------------------ # Public: set up & manage voter database # ------------------------------------------------------------------------------ @@ -378,34 +346,25 @@ proc appendVoter*(ap: TesterPool; else: ap.batch[^1][^1] - var header = BlockHeader( - parentHash: parent.blockHash, - ommersHash: EMPTY_UNCLE_HASH, - stateRoot: parent.stateRoot, - timestamp: parent.timestamp + initDuration(seconds = 100), - txRoot: BLANK_ROOT_HASH, - receiptRoot: BLANK_ROOT_HASH, - blockNumber: parent.blockNumber + 1, - gasLimit: parent.gasLimit, - # - # clique/snapshot_test.go(417): gen.SetCoinbase(accounts.address( [..] - coinbase: ap.address(voter.voted), - # - # clique/snapshot_test.go(418): if tt.votes[j].auth { - nonce: if voter.auth: NONCE_AUTH else: NONCE_DROP, - # - # clique/snapshot_test.go(436): header.Difficulty = diffInTurn [..] - difficulty: if voter.noTurn: DIFF_NOTURN else: DIFF_INTURN, - # - extraData: 0.byte.repeat(EXTRA_VANITY + EXTRA_SEAL)) + let header = ap.chain.clique.cliqueGenvote( + voter = ap.address(voter.voted), + seal = ap.privateKey(voter.signer), + parent = parent, + elapsed = initDuration(seconds = 100), + voteInOk = voter.auth, + outOfTurn = voter.noTurn, + checkPoint = voter.checkpoint.mapIt(ap.address(it)).sorted(EthAscending)) - # clique/snapshot_test.go(432): if auths := tt.votes[j].checkpoint; [..] if 0 < voter.checkpoint.len: doAssert (header.blockNumber mod ap.clique.cfg.epoch) == 0 - ap.checkpoint(header,voter.checkpoint) - # Generate the signature, embed it into the header and the block - ap.sign(header, voter.signer) + # Register for debugging + let + extraLen = header.extraData.len + extraSeal = header.extraData[extraLen - EXTRA_SEAL ..< extraLen] + ap.xSeals[toArray(XSealKey.len,extraSeal)] = XSealValue( + blockNumber: header.blockNumber.truncate(uint64), + account: voter.signer) if voter.newbatch: ap.batch.add @[] diff --git a/tests/test_clique/voter_samples.nim b/tests/test_clique/voter_samples.nim index 6dd2090e6..64d82e19f 100644 --- a/tests/test_clique/voter_samples.nim +++ b/tests/test_clique/voter_samples.nim @@ -17,32 +17,60 @@ type TesterVote* = object ## VoterBlock represents a single block signed by a ## particular account, where the account may or may not ## have cast a Clique vote. - signer*: string ## Account that signed this particular block - voted*: string ## Optional value if the signer voted on - ## adding/removing ## someone - auth*: bool ## Whether the vote was to authorize (or - ## deauthorize) - checkpoint*: seq[string] ## List of authorized signers if this is an epoch - ## block - noTurn*: bool ## initialise `NOTURN` it `true`, otherwise - ## `INTURN` (not part of Go ref implementation, - ## used here to avoid `fakeDiff` kludge in the - ## Go implementation) + signer*: string ##\ + ## Account that signed this particular block + + voted*: string ##\ + ## Optional value if the signer voted on adding/removing ## someone + + auth*: bool ##\ + ## Whether the vote was to authorize (or deauthorize) + + checkpoint*: seq[string] ##\ + ## List of authorized signers if this is an epoch block + + noTurn*: bool ##\ + ## Initialise `NOTURN` if `true`, otherwise use `INTURN`. This is not + ## part of Go ref test implementation. The flag used here to avoid what + ## is implemented as `fakeDiff` kludge in the Go ref test implementation. + ## + ## Note that the `noTurn` value depends on the sort order of the + ## calculated authorised signers account address list. These account + ## addresses in turn (no pun intended) depend on the private keys of + ## these accounts. Now, the private keys are generated on-the-fly by a + ## PRNG which re-seeded the same for each test. So the sort order is + ## predictable and the correct value of the the `noTurn` flag can be set + ## by sort of experimenting with the tests (and/or refering to earlier + ## woking test specs.) + newbatch*: bool - TestSpecs* = object ## Define the various voting scenarios to test - id*: int ## Test id - info*: string ## Test description - epoch*: int ## Number of blocks in an epoch (unset = 30000) - runBack*: bool ## Set `applySnapsMinBacklog` flag - signers*: seq[string] ## Initial list of authorized signers in the - ## genesis - votes*: seq[TesterVote] ## Chain of signed blocks, potentially influencing - ## auths - results*: seq[string] ## Final list of authorized signers after all - ## blocks - failure*: CliqueErrorType ## Failure if some block is invalid according to - ## the rules + + TestSpecs* = object ## Defining genesis and the various voting scenarios + ## to test (see `votes`.) + id*: int ##\ + ## Test id + + info*: string ##\ + ## Test description + + epoch*: int ##\ + ## Number of blocks in an epoch (unset = 30000) + + runBack*: bool ##\ + ## Set `applySnapsMinBacklog` flag + + signers*: seq[string] ##\ + ## Initial list of authorized signers in the genesis + + votes*: seq[TesterVote] ##\ + ## Chain of signed blocks, potentially influencing auths + + results*: seq[string] ##\ + ## Final list of authorized signers after all blocks + + failure*: CliqueErrorType ##\ + ## Failure if some block is invalid according to the rules const # Define the various voting scenarios to test