From ca033a7f6855ef179f0d4424ee88355af665e08b Mon Sep 17 00:00:00 2001 From: Jakub Pajek Date: Tue, 23 Jan 2024 17:10:30 +0900 Subject: [PATCH] clique: implemented voting ring voting for faster voting times (private hard fork #2) --- consensus/clique/clique.go | 266 +++++++++++---- consensus/clique/clique_test.go | 6 +- consensus/clique/snapshot.go | 63 +++- consensus/clique/snapshot_test.go | 538 +++++++++++++++++++++++------- 4 files changed, 659 insertions(+), 214 deletions(-) diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 3d439b1d380fd..253f05cc2e910 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -93,6 +93,10 @@ var ( // of votes (i.e. vote on dropping self, as wel as other votes) errForbiddenVotes = errors.New("forbidden votes combination in extra-data") + // errSealerRingVotes is returned if a post-PrivateHardFork2 non-checkpoint block in the sealer + // ring contains votes (post PrivateHardFork2, voting is allowed only in the voter ring). + errSealerRingVotes = errors.New("sealer ring block contains votes") + // errInvalidVote is returned if a vote marker value in extra is something else than the three // allowed constants of 0x00, 0x01, 0x02. errInvalidVote = errors.New("invalid vote marker in extra-data") @@ -206,8 +210,9 @@ type Clique struct { lock sync.RWMutex // Protects the signer and proposals fields // The fields below are for testing only - fakeDiff bool // Skip difficulty verifications - fakeRewards bool // Skip accumulating the block rewards + fakeDiff bool // Skip difficulty verifications + fakeRewards bool // Skip accumulating the block rewards + fakeVoterRing bool // Set the ring for the genesis block (the sealer ring or the voter ring) } // loadProposals loads existing proposals from the database. @@ -432,7 +437,7 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainHeaderReader, header if number == 0 { return nil } - // Retrieve the snapshot needed to verify this header and cache it + // Assemble the snapshot needed to access the current config snap, err := c.snapshot(chain, number-1, header.ParentHash, parents) if err != nil { return err @@ -492,7 +497,7 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainHeaderReader, header } } // All basic checks passed, verify the seal and return - return c.verifySeal(snap, header, parent) + return c.verifySeal(chain, snap, header, parent) } // snapshot retrieves the authorization snapshot at a given point in time. @@ -563,7 +568,7 @@ func (c *Clique) snapshot(chain consensus.ChainHeaderReader, number uint64, hash voters = append(voters, signers[i]) } } - snap = newGenesisSnapshot(c.config, c.signatures, number, hash, voters, signers) + snap = newGenesisSnapshot(c.config, c.signatures, number, hash, voters, signers, c.fakeVoterRing) if err := snap.store(c.db); err != nil { return nil, err } @@ -620,17 +625,19 @@ func (c *Clique) VerifyUncles(chain consensus.ChainReader, block *types.Block) e } // verifySeal checks 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. -func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parent *types.Header) error { +// consensus protocol requirements. +func (c *Clique) verifySeal(chain consensus.ChainHeaderReader, snap *Snapshot, header *types.Header, parent *types.Header) error { // Verifying the genesis block is not supported number := header.Number.Uint64() if number == 0 { return errUnknownBlock } - - currConfig := snap.CurrentConfig() + // Get the current config + var ( + currConfig = snap.CurrentConfig() + headerHasVotes bool + okVoter bool + ) // Resolve the authorization key and check against signers signer, err := ecrecover(header, c.signatures) @@ -644,6 +651,13 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parent *types. nextNumber uint64 nextDiff *big.Int ) + + // If any votes are cast, check the signer against voters (only voters can vote) + headerHasVotes = number%currConfig.Epoch != 0 && len(header.Extra)-params.CliqueExtraVanity-params.CliqueExtraSeal > 0 + _, okVoter = snap.Voters[signer] + if headerHasVotes && !okVoter { + return errUnauthorizedVoter + } // Check in which ring we are currently signing blocks in if !snap.VoterRing { // We are currently signing blocks in the sealer ring. @@ -651,30 +665,49 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parent *types. // in the sealer ring or switch to the voter ring. if header.Difficulty.Cmp(snap.maxSealerRingDifficulty()) <= 0 { // We want to stay in the sealer ring + // For post-PrivateHardFork2 blocks, check if no votes are cast + // (voting is no longer allowed in the sealer ring) + if chain.Config().IsPrivateHardFork2(header.Number) && headerHasVotes { + return errSealerRingVotes + } + // Calculate the next signable block number and difficulty for the signer nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) nextDiff = snap.calcSealerRingDifficulty(signer) } else if header.Difficulty.Cmp(snap.maxVoterRingDifficulty()) <= 0 { // We want to switch to the voter ring // Check the signer against voters (only voters can switch to the voter ring) - if _, ok := snap.Voters[signer]; !ok { + if !okVoter { return errUnauthorizedVoter } - // Check if a sufficiently long stall in block creation occurred - if currConfig.Period == 0 || header.Time < parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { - return errWrongDifficultySealerRing + if chain.Config().IsPrivateHardFork2(header.Number) { + // For post-PrivateHardFork2 blocks, the switch is allowed if: + // - votes are cast, or + // - a sufficiently long stall in block creation occurred + if !headerHasVotes && + (currConfig.Period == 0 || header.Time < parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { + return errWrongDifficultySealerRing + } + } else { + // For pre-PrivateHardFork2 blocks, the switch is allowed if: + // - a sufficiently long stall in block creation occurred + if currConfig.Period == 0 || header.Time < parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { + return errWrongDifficultySealerRing + } } + // Calculate the next signable block number and difficulty for the signer nextNumber = snap.nextVoterRingSignableBlockNumber(signed.LastSignedBlock) nextDiff = snap.calcVoterRingDifficulty(signer) } else if header.Difficulty.Cmp(snap.maxRingBreakerDifficulty()) <= 0 { // We want to preemptively prevent switching to the voter ring // Check the signer against voters (only non-voters can prevent the voter ring) - if _, ok := snap.Voters[signer]; ok { + if okVoter { return errUnauthorizedVoter } // Check if a sufficiently long stall in block creation occurred if currConfig.Period == 0 || header.Time < parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { return errWrongDifficultySealerRing } + // Calculate the next signable block number and difficulty for the signer nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) nextDiff = snap.calcRingBreakerDifficulty(signer) } else { @@ -691,17 +724,31 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parent *types. } else if header.Difficulty.Cmp(snap.maxVoterRingDifficulty()) <= 0 { // We want to stay in the voter ring // Check the signer against voters (only voters can sign blocks in the voter ring) - if _, ok := snap.Voters[signer]; !ok { + if !okVoter { return errUnauthorizedVoter } + // Calculate the next signable block number and difficulty for the signer nextNumber = snap.nextVoterRingSignableBlockNumber(signed.LastSignedBlock) nextDiff = snap.calcVoterRingDifficulty(signer) } else if header.Difficulty.Cmp(snap.maxRingBreakerDifficulty()) <= 0 { // We want to return to the sealer ring // Check the signer against voters (only non-voters can disband the voter ring) - if _, ok := snap.Voters[signer]; ok { + if okVoter { return errUnauthorizedVoter } + if chain.Config().IsPrivateHardFork2(header.Number) { + // For post-PrivateHardFork2 blocks, the switch is allowed if: + // - voting has not started, or + // - a sufficiently long stall in block creation occurred + if snap.Voting && + (currConfig.Period == 0 || header.Time < parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { + return errWrongDifficultyVoterRing + } + } else { + // For pre-PrivateHardFork2 blocks, the switch is allowed if: + // - always + } + // Calculate the next signable block number and difficulty for the signer nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) nextDiff = snap.calcRingBreakerDifficulty(signer) } else { @@ -723,13 +770,9 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parent *types. } } // If any votes are cast, verify the votes - extraBytes := len(header.Extra) - params.CliqueExtraVanity - params.CliqueExtraSeal - if number%currConfig.Epoch != 0 && extraBytes > 0 { - // Check the signer against voters - if _, ok := snap.Voters[signer]; !ok { - return errUnauthorizedVoter - } + if headerHasVotes { // Verify the votes list + extraBytes := len(header.Extra) - params.CliqueExtraVanity - params.CliqueExtraSeal votesCast := make(map[common.Address]struct{}) voteCount := extraBytes / (common.AddressLength + 1) for voteIdx := 0; voteIdx < voteCount; voteIdx++ { @@ -767,7 +810,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header header.Nonce = types.BlockNonce{} number := header.Number.Uint64() - // Assemble the voting snapshot to check which votes make sense + // Assemble the snapshot needed to access the current config snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) if err != nil { return err @@ -813,7 +856,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header dropSelf = true } addresses = append(addresses, address) - } else if number > proposal.Block && number-proposal.Block > params.FullImmutabilityThreshold { + } else if number > proposal.Block && number-proposal.Block > params.CliqueEpoch { delete(c.proposals, address) purged++ } @@ -822,7 +865,7 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header if purged > 0 { log.Trace("Purged old clique proposals", "total", len(c.proposals), "purged", purged) if err := c.storeProposals(); err != nil { - log.Warn("Failed to store clique proposals to disk", "err", err) + log.Error("Failed to store clique proposals to disk", "err", err) } else { log.Trace("Stored clique proposals disk") } @@ -866,31 +909,59 @@ func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header // Check in which ring we are currently signing blocks in if !snap.VoterRing { // We are currently signing blocks in the sealer ring. - // If there is a significant stall in block creation and we are a voter, switch to the voter ring. - // If there is a significant stall in block creation and we are a signer, preemptively prevent - // switching to the voter ring. Continue in the sealer ring otherwise. - if currConfig.Period > 0 && header.Time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { - if okVoter { + if okVoter { + // If we are a voter, switch to the voter ring if: + // - we are post-PrivateHardFork2 and want to cast votes, or + // - a sufficiently long stall in block creation occurred + // Continue in the sealer ring otherwise. + headerHasVotes := number%currConfig.Epoch != 0 && len(header.Extra)-params.CliqueExtraVanity-params.CliqueExtraSeal > 0 + if (chain.Config().IsPrivateHardFork2(header.Number) && headerHasVotes) || + (currConfig.Period > 0 && header.Time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { // Set the correct difficulty for the voter ring header.Difficulty = snap.calcVoterRingDifficulty(signer) } else { + // Set the correct difficulty for the sealer ring + header.Difficulty = snap.calcSealerRingDifficulty(signer) + } + } else { + // If we are not a voter, preemptively prevent switching to the voter ring if: + // - a sufficiently long stall in block creation occurred + // Continue in the sealer ring otherwise. + if currConfig.Period > 0 && header.Time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { // Set the correct difficulty for preventing switching to the voter ring header.Difficulty = snap.calcRingBreakerDifficulty(signer) + } else { + // Set the correct difficulty for the sealer ring + header.Difficulty = snap.calcSealerRingDifficulty(signer) } - } else { - // Set the correct difficulty for the sealer ring - header.Difficulty = snap.calcSealerRingDifficulty(signer) } } else { // We are currently signing blocks in the voter ring. - // If we are not a voter, try returning to the sealer ring by disbanding - // the voter ring. Continue in the voter ring otherwise. - if !okVoter { - // Set the correct difficulty for disbanding the voter ring - header.Difficulty = snap.calcRingBreakerDifficulty(signer) - } else { + if okVoter { + // If we are a voter, continue in the voter ring. // Set the correct difficulty for the voter ring header.Difficulty = snap.calcVoterRingDifficulty(signer) + } else { + // If we are not a voter, ... + if chain.Config().IsPrivateHardFork2(header.Number) { + // For post-PrivateHardFork2 blocks, try returning to the sealer ring by disbanding the voter ring if: + // - voting has not started, or + // - a sufficiently long stall in block creation occurred + // Withold sealing the block otherwise. + if !snap.Voting || + (currConfig.Period > 0 && header.Time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { + // Set the correct difficulty for disbanding the voter ring + header.Difficulty = snap.calcRingBreakerDifficulty(signer) + } else { + // Set the invalid difficulty to withold sealing the block + header.Difficulty = diffInvalid + } + } else { + // For pre-PrivateHardFork2 blocks, try returning to the sealer ring by disbanding the voter ring if: + // - always + // Set the correct difficulty for disbanding the voter ring + header.Difficulty = snap.calcRingBreakerDifficulty(signer) + } } } return nil @@ -905,7 +976,7 @@ func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Heade // Resolve the authorization key signer, err := ecrecover(header, c.signatures) if err != nil { - log.Warn("Failed to retrieve block author", "number", header.Number.Uint64(), "hash", header.Hash(), "err", err) + return err } // Accumulate any block rewards (excluding uncle rewards). if signer != (common.Address{}) { @@ -955,7 +1026,7 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header * func (c *Clique) accumulateRewards(chain consensus.ChainHeaderReader, state *state.StateDB, header *types.Header, uncles []*types.Header, signer common.Address) error { number := header.Number.Uint64() - // Assemble the snapshot to select the correct block reward + // Assemble the snapshot needed to access the current config var currConfig *params.CliqueConfigEntry snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) if err != nil { @@ -996,7 +1067,14 @@ func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, res if number == 0 { return errUnknownBlock } - // Assemble the snapshot + // For post-PrivateHardFork2 blocks, withold sealing blocks if the difficulty is set to an invalid value. + // This happens when a non-voter can't try to switch the network back to the sealer ring because: + // - voting has started, and + // - not a sufficiently long stall in block creation occurred + if chain.Config().IsPrivateHardFork2(header.Number) && header.Difficulty.Cmp(diffInvalid) == 0 { + return errors.New("sealing paused while waiting for voting to finish") + } + // Assemble the snapshot needed to access the current config snap, err := c.snapshot(chain, number-1, header.ParentHash, nil) if err != nil { return err @@ -1034,14 +1112,18 @@ func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, res if inturnDiff = snap.maxSealerRingDifficulty(); header.Difficulty.Cmp(inturnDiff) <= 0 { // We want to stay in the sealer ring nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) - } else if header.Difficulty.Cmp(snap.maxVoterRingDifficulty()) <= 0 { + } else if inturnDiff = snap.maxVoterRingDifficulty(); header.Difficulty.Cmp(inturnDiff) <= 0 { // We want to switch to the voter ring nextNumber = snap.nextVoterRingSignableBlockNumber(signed.LastSignedBlock) - // Set in-turn difficulty value to 0, in order to treat all voters trying to switch to the voter ring - // as out-of-turn, thus broadcast their blocks with a delay. This will allow some of the in-turnish - // online signers to broadcast their blocks faster, which in consequence will allow to prevent - // switching to the voter ring. - inturnDiff = big.NewInt(0) + // Since, unlike mobile signers, voters are most likely to always be online, + // they will sign in-turn most of the time while trying to switch to the voter ring. + if headerHasVotes := number%currConfig.Epoch != 0 && len(header.Extra)-params.CliqueExtraVanity-params.CliqueExtraSeal > 0; !headerHasVotes { + // Set in-turn difficulty value to 0, in order to treat all non-voting voters trying to switch + // to the voter ring because of a network stall as out-of-turn. This will result in their block + // being broadcast with a delay and will allow some of the in-turnish online signers to broadcast + // their blocks faster, which in consequence will allow to prevent switching to the voter ring. + inturnDiff = big.NewInt(0) + } } else { // We want to preemptively prevent switching to the voter ring nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) @@ -1051,15 +1133,18 @@ func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, res // We are currently signing blocks in the voter ring. // Check the difficulty to determine if we want to stay // in the voter ring or return to the sealer ring. - if header.Difficulty.Cmp(snap.maxVoterRingDifficulty()) <= 0 { + if inturnDiff = snap.maxVoterRingDifficulty(); header.Difficulty.Cmp(inturnDiff) <= 0 { // We want to stay in the voter ring nextNumber = snap.nextVoterRingSignableBlockNumber(signed.LastSignedBlock) - // Since, unlike mobile signers, voters are most likely to be always online, in the voter ring - // they will sign in-turn most of the time. Set in-turn difficulty value to 0, in order to treat - // all voters in the voter ring as out-of-turn, thus broadcast their blocks with a delay. This - // will allow some of the in-turnish online signers to broadcast their blocks faster, which in - // consequence will allow to disband the voter ring. - inturnDiff = big.NewInt(0) + // Since, unlike mobile signers, voters are most likely to always be online, + // they will sign in-turn most of the time while in the voter ring. + if headerHasVotes := number%currConfig.Epoch != 0 && len(header.Extra)-params.CliqueExtraVanity-params.CliqueExtraSeal > 0; !headerHasVotes { + // Set in-turn difficulty value to 0, in order to treat all non-voting voters in the voter ring + // as out-of-turn, thus broadcast their blocks with a delay. This will allow some of the in-turnish + // online signers to broadcast their blocks faster, which in consequence will allow to disband + // the voter ring. + inturnDiff = big.NewInt(0) + } } else { // We want to return to the sealer ring nextNumber = snap.nextSealerRingSignableBlockNumber(signed.LastSignedBlock) @@ -1111,13 +1196,20 @@ func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, res // CalcDifficulty is the difficulty adjustment algorithm. It returns the difficulty // that a new block should have. func (c *Clique) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { - // MEMO by Jakub Pajek (clique config: variable period) - // Are we running tests using a chain header reader stub? Should check? + number := big.NewInt(0).Add(parent.Number, big.NewInt(1)) + + // Assemble the snapshot needed to access the current config snap, err := c.snapshot(chain, parent.Number.Uint64(), parent.Hash(), nil) if err != nil { + // MEMO by Jakub Pajek (clique config: variable period) + // Are we running tests using a chain header reader stub? + if !chain.IsStub() { + log.Error("Failed to assemble the snapshot for difficulty calculation", "err", err) + } return nil } currConfig := snap.CurrentConfig() + // Check if we are an authorized voter, for future use... c.lock.RLock() signer := c.signer @@ -1127,31 +1219,59 @@ func (c *Clique) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, // Check in which ring we are currently signing blocks in if !snap.VoterRing { // We are currently signing blocks in the sealer ring. - // If there is a significant stall in block creation and we are a voter, switch to the voter ring. - // If there is a significant stall in block creation and we are a signer, preemptively prevent - // switching to the voter ring. Continue in the sealer ring otherwise. - if currConfig.Period > 0 && time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { - if okVoter { + if okVoter { + // If we are a voter, switch to the voter ring if: + // - we are post-PrivateHardFork2 and want to cast votes, or + // - a sufficiently long stall in block creation occurred + // Continue in the sealer ring otherwise. + headerHasVotes := false + if (chain.Config().IsPrivateHardFork2(number) && headerHasVotes) || + (currConfig.Period > 0 && time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { // Set the correct difficulty for the voter ring return snap.calcVoterRingDifficulty(signer) } else { + // Set the correct difficulty for the sealer ring + return snap.calcSealerRingDifficulty(signer) + } + } else { + // If we are not a voter, preemptively prevent switching to the voter ring if: + // - a sufficiently long stall in block creation occurred + // Continue in the sealer ring otherwise. + if currConfig.Period > 0 && time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period) { // Set the correct difficulty for preventing switching to the voter ring return snap.calcRingBreakerDifficulty(signer) + } else { + // Set the correct difficulty for the sealer ring + return snap.calcSealerRingDifficulty(signer) } - } else { - // Set the correct difficulty for the sealer ring - return snap.calcSealerRingDifficulty(signer) } } else { // We are currently signing blocks in the voter ring. - // If we are not a voter, try returning to the sealer ring by disbanding - // the voter ring. Continue in the voter ring otherwise. - if !okVoter { - // Set the correct difficulty for disbanding the voter ring - return snap.calcRingBreakerDifficulty(signer) - } else { + if okVoter { + // If we are a voter, continue in the voter ring. // Set the correct difficulty for the voter ring return snap.calcVoterRingDifficulty(signer) + } else { + // If we are not a voter, ... + if chain.Config().IsPrivateHardFork2(number) { + // For post-PrivateHardFork2 blocks, try returning to the sealer ring by disbanding the voter ring if: + // - voting has not started, or + // - a sufficiently long stall in block creation occurred + // Withold sealing the block otherwise. + if !snap.Voting || + (currConfig.Period > 0 && time >= parent.Time+(currConfig.MinStallPeriod*currConfig.Period)) { + // Set the correct difficulty for disbanding the voter ring + return snap.calcRingBreakerDifficulty(signer) + } else { + // Set the invalid difficulty to withold sealing the block + return diffInvalid + } + } else { + // For pre-PrivateHardFork2 blocks, try returning to the sealer ring by disbanding the voter ring if: + // - always + // Set the correct difficulty for disbanding the voter ring + return snap.calcRingBreakerDifficulty(signer) + } } } } diff --git a/consensus/clique/clique_test.go b/consensus/clique/clique_test.go index 9ef3159285634..ce0cdac998fcf 100644 --- a/consensus/clique/clique_test.go +++ b/consensus/clique/clique_test.go @@ -47,6 +47,10 @@ func TestReimportMirroredState(t *testing.T) { engine = New(params.AllCliqueProtocolChanges.Clique, db) signer = new(types.HomesteadSigner) ) + // ADDED by Jakub Pajek (clique static block rewards) + // In the future consider disabling block rewards, because this test tests the case when consecutive + // blocks have the same state root. However rewards are enabled on mainnets, so prioritize this case for now. + //engine.fakeRewards = true genspec := &core.Genesis{ Config: params.AllCliqueProtocolChanges, // MODIFIED by Jakub Pajek (clique permissions) @@ -291,7 +295,7 @@ func (test *testCalcDifficulty) run(t *testing.T) { } return bytes.Compare(iAddr[:], jAddr[:]) < 0 }) - snap := newGenesisSnapshot(nil, nil, 0, common.Hash{}, make([]common.Address, 0), signers) + snap := newGenesisSnapshot(nil, nil, 0, common.Hash{}, make([]common.Address, 0), signers, false) for _, signer := range signers { snap.Signers[signer] = test.lastSigned[signer] } diff --git a/consensus/clique/snapshot.go b/consensus/clique/snapshot.go index 5f20cf067528a..72d62cc948953 100644 --- a/consensus/clique/snapshot.go +++ b/consensus/clique/snapshot.go @@ -69,6 +69,7 @@ type Snapshot struct { Hash common.Hash `json:"hash"` // Block hash where the snapshot was created ConfigIdx int `json:"configIdx"` // Index of the current config entry inside the config array VoterRing bool `json:"voterRing"` // Flag to indicate the ring in which blocks are signed (the sealer ring or the voter ring) + Voting bool `json:"voting"` // Flag to indicate voting activity (heuristic) Voters map[common.Address]uint64 `json:"voters"` // Set of authorized voters at this moment and their most recently signed block Signers map[common.Address]Signer `json:"signers"` // Set of authorized signers at this moment and their state Dropped map[common.Address]uint64 `json:"dropped"` // Set of authorized signers dropped due to inactivity and their drop block number @@ -86,7 +87,7 @@ func (s addressesAscending) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // newGenesisSnapshot creates a new snapshot with the specified startup parameters. This // method does not initialize the signers most recently signed blocks (nor other custom // fields added to Signer and Snapshot structs), so only ever use it for the genesis block. -func newGenesisSnapshot(config params.CliqueConfig, sigcache *sigLRU, number uint64, hash common.Hash, voters []common.Address, signers []common.Address) *Snapshot { +func newGenesisSnapshot(config params.CliqueConfig, sigcache *sigLRU, number uint64, hash common.Hash, voters []common.Address, signers []common.Address, fakeVoterRing bool) *Snapshot { // Set the initial config entry index based on the initial sealer count. // The last entry's MaxSealerCount is always MaxInt, so the for loop will always break. var configIndex int @@ -102,7 +103,8 @@ func newGenesisSnapshot(config params.CliqueConfig, sigcache *sigLRU, number uin Number: number, Hash: hash, ConfigIdx: configIndex, - VoterRing: false, + VoterRing: fakeVoterRing, + Voting: false, Voters: make(map[common.Address]uint64), Signers: make(map[common.Address]Signer), Dropped: make(map[common.Address]uint64), @@ -154,6 +156,7 @@ func (s *Snapshot) copy() *Snapshot { Hash: s.Hash, ConfigIdx: s.ConfigIdx, VoterRing: s.VoterRing, + Voting: s.Voting, Voters: make(map[common.Address]uint64), Signers: make(map[common.Address]Signer), Dropped: make(map[common.Address]uint64), @@ -236,9 +239,15 @@ func (s *Snapshot) uncast(address common.Address, proposal uint64) bool { // apply creates a new authorization snapshot by applying the given headers to the original one. // For each applied header, the processing order is as follows: // - If necessary, switch between the sealer and the voter ring +// - For post-PrivateHardFork2 blocks, update the voting activity heuristic // - If in the sealer ring, apply offline penalties (drop inactive signers) // - If any votes are cast, process the votes // - Update the current config entry index based on the final sealer count +// +// Note: +// We assume that headers passed to apply are already verified by the function caller and valid. +// Thanks to this we do not need to repeat all the verification checks already done by Consensus.VerifyHeader, +// in particular: header hashes, times, difficulties, sealer ring votes post-PrivateHardFork2, etc. func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (*Snapshot, error) { // Allow passing in no headers for cleaner code if len(headers) == 0 { @@ -262,11 +271,15 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* ) for i, header := range headers { var ( - currConfig = snap.CurrentConfig() - number = header.Number.Uint64() + currConfig = snap.CurrentConfig() + number = header.Number.Uint64() + checkpoint = number%currConfig.Epoch == 0 + headerHasVotes bool + okVoter bool ) + // Remove any votes on checkpoint blocks - if number%currConfig.Epoch == 0 { + if checkpoint { snap.Votes = nil snap.Tally = make(map[common.Address]Tally) } @@ -279,10 +292,16 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* return nil, errUnauthorizedSigner } else { var ( - _, okVoter = snap.Voters[signer] nextNumber uint64 voterRing bool ) + + // If any votes are cast, check the signer against voters (only voters can vote) + headerHasVotes = !checkpoint && len(header.Extra)-params.CliqueExtraVanity-params.CliqueExtraSeal > 0 + _, okVoter = snap.Voters[signer] + if headerHasVotes && !okVoter { + return nil, errUnauthorizedVoter + } // Check in which ring we are currently signing blocks in if !snap.VoterRing { // We are currently signing blocks in the sealer ring. @@ -355,6 +374,13 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* snap.Signers[signer] = signed // Update the ring state snap.VoterRing = voterRing + // For post-PrivateHardFork2 blocks, update the voting activity heuristic + if config.IsPrivateHardFork2(header.Number) { + // Assume voting is active if: + // - current header has votes, or + // - previous header had votes and current header could not include votes because it is a checkpoint header. + snap.Voting = headerHasVotes || (snap.Voting && checkpoint) + } } // If in the sealer ring, apply offline penalties (drop inactive signers) @@ -378,6 +404,12 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* snap.Signers[address] = signed // If the strike count exceeded threshold, drop the signer from authorized signers. // This does not apply to voters. Voters can only be removed thru explicit voting. + // Note: + // A constant strike threshold value should be used during a single header processing. + // It is ok to call the calcStrikeThreshold here (which accesses the sealer count) + // without caching the returend value, because we use it only once during the header + // processing. Even if the sealer count changes due to offline penalties or voting, + // a new strike threshold value will not be used. if _, ok := snap.Voters[address]; !ok && signed.StrikeCount > snap.calcStrikeThreshold() { // Delete the signer from authorized signers delete(snap.Signers, address) @@ -398,18 +430,14 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* } // If any votes are cast, process the votes - extraBytes := len(header.Extra) - params.CliqueExtraVanity - params.CliqueExtraSeal - if number%currConfig.Epoch != 0 && extraBytes > 0 { - // Check the signer against voters - if _, ok := snap.Voters[signer]; !ok { - return nil, errUnauthorizedVoter - } + if headerHasVotes { // Calculate the effective vote threshold at the beginning of vote processing // (Voter count might change later due to passed votes) // Effective vote threshold: vote_threshold = voter_count / voting_rule voteThreshold := len(snap.Voters) / currConfig.VotingRule // Process every vote // Note that the protocol forbids casting other votes when voting on dropping self. + extraBytes := len(header.Extra) - params.CliqueExtraVanity - params.CliqueExtraSeal voteCount := extraBytes / (common.AddressLength + 1) for voteIdx := 0; voteIdx < voteCount; voteIdx++ { index := params.CliqueExtraVanity + voteIdx*(common.AddressLength+1) @@ -489,11 +517,6 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* delete(snap.Tally, address) } } - // If we're taking too much time (ecrecover), notify the user once a while - if time.Since(logged) > 8*time.Second { - log.Info("Reconstructing voting history", "processed", i, "total", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) - logged = time.Now() - } } // Update the current config entry index based on the final sealer count. @@ -504,6 +527,12 @@ func (s *Snapshot) apply(config *params.ChainConfig, headers []*types.Header) (* break } } + + // If we're taking too much time (ecrecover), notify the user once a while + if time.Since(logged) > 8*time.Second { + log.Info("Reconstructing voting history", "processed", i, "total", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } } if time.Since(start) > 8*time.Second { log.Info("Reconstructed voting history", "processed", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) diff --git a/consensus/clique/snapshot_test.go b/consensus/clique/snapshot_test.go index 9c0c038366ba9..fbc4402bf88c9 100644 --- a/consensus/clique/snapshot_test.go +++ b/consensus/clique/snapshot_test.go @@ -53,7 +53,7 @@ func (ap *testerAccountPool) checkpoint(header *types.Header, signers []string) for i, signer := range signers { auths[i] = ap.address(signer) } - // MODIFIED by Jakub Pajek BEG (clique permissions) + // MODIFIED by Jakub Pajek (clique permissions) //sort.Sort(signersAscending(auths)) sort.Sort(addressesAscending(auths)) for i, auth := range auths { @@ -103,6 +103,8 @@ type testerVote struct { auth bool checkpoint []string newbatch bool + // ADDED by Jakub Pajek (voter ring voting) + signersCount int64 } type cliqueTest struct { @@ -111,207 +113,213 @@ type cliqueTest struct { votes []testerVote results []string failure error + // ADDED by Jakub Pajek (clique config: voting rule) + votingRule int + // ADDED by Jakub Pajek (voter ring voting) + privateHardFork2Block *big.Int } // Tests that Clique signer voting is evaluated correctly for various simple and // complex scenarios, as well as that a few special corner cases fail correctly. -func TestClique(t *testing.T) { +func TestClique_VotingRuleMajority(t *testing.T) { // Define the various voting scenarios to test tests := []cliqueTest{ { // Single signer, no votes cast signers: []string{"A"}, - votes: []testerVote{{signer: "A"}}, + votes: []testerVote{ + {signer: "A", signersCount: 1}, + }, results: []string{"A"}, }, { // Single signer, voting to add two others (only accept first, second needs 2 votes) signers: []string{"A"}, votes: []testerVote{ - {signer: "A", voted: "B", auth: true}, - {signer: "B"}, - {signer: "A", voted: "C", auth: true}, + {signer: "A", voted: "B", auth: true, signersCount: 1}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Two signers, voting to add three others (only accept first two, third needs 3 votes already) signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: true}, - {signer: "B", voted: "C", auth: true}, - {signer: "A", voted: "D", auth: true}, - {signer: "B", voted: "D", auth: true}, - {signer: "C"}, - {signer: "A", voted: "E", auth: true}, - {signer: "B", voted: "E", auth: true}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", voted: "C", auth: true, signersCount: 2}, + {signer: "A", voted: "D", auth: true, signersCount: 3}, + {signer: "B", voted: "D", auth: true, signersCount: 3}, + {signer: "C", signersCount: 4}, + {signer: "A", voted: "E", auth: true, signersCount: 4}, + {signer: "B", voted: "E", auth: true, signersCount: 4}, }, results: []string{"A", "B", "C", "D"}, }, { // Single signer, dropping itself (weird, but one less cornercase by explicitly allowing this) signers: []string{"A"}, votes: []testerVote{ - {signer: "A", voted: "A", auth: false}, + {signer: "A", voted: "A", auth: false, signersCount: 1}, }, results: []string{}, }, { // Two signers, actually needing mutual consent to drop either of them (not fulfilled) signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "B", auth: false}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Two signers, actually needing mutual consent to drop either of them (fulfilled) signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "B", auth: false}, - {signer: "B", voted: "B", auth: false}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, + {signer: "B", voted: "B", auth: false, signersCount: 2}, }, results: []string{"A"}, }, { // Three signers, two of them deciding to drop the third signers: []string{"A", "B", "C"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B", voted: "C", auth: false}, + {signer: "A", voted: "C", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, }, results: []string{"A", "B"}, }, { // Four signers, consensus of two not being enough to drop anyone signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B", voted: "C", auth: false}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", voted: "C", auth: false, signersCount: 4}, }, results: []string{"A", "B", "C", "D"}, }, { // Four signers, consensus of three already being enough to drop someone signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "D", auth: false}, - {signer: "B", voted: "D", auth: false}, - {signer: "C", voted: "D", auth: false}, + {signer: "A", voted: "D", auth: false, signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 4}, + {signer: "C", voted: "D", auth: false, signersCount: 4}, }, results: []string{"A", "B", "C"}, }, { // Authorizations are counted once per signer per target signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: true}, - {signer: "B"}, - {signer: "A", voted: "C", auth: true}, - {signer: "B"}, - {signer: "A", voted: "C", auth: true}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Authorizing multiple accounts concurrently is permitted signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: true}, - {signer: "B"}, - {signer: "A", voted: "D", auth: true}, - {signer: "B"}, - {signer: "A"}, - {signer: "B", voted: "D", auth: true}, - {signer: "A"}, - {signer: "B", voted: "C", auth: true}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "D", auth: true, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", voted: "D", auth: true, signersCount: 2}, + {signer: "A", signersCount: 3}, + {signer: "B", voted: "C", auth: true, signersCount: 3}, }, results: []string{"A", "B", "C", "D"}, }, { // Deauthorizations are counted once per signer per target signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "B", auth: false}, - {signer: "B"}, - {signer: "A", voted: "B", auth: false}, - {signer: "B"}, - {signer: "A", voted: "B", auth: false}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Deauthorizing multiple accounts concurrently is permitted signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B"}, - {signer: "C"}, - {signer: "A", voted: "D", auth: false}, - {signer: "B"}, - {signer: "C"}, - {signer: "A"}, - {signer: "B", voted: "D", auth: false}, - {signer: "C", voted: "D", auth: false}, - {signer: "A"}, - {signer: "B", voted: "C", auth: false}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", voted: "D", auth: false, signersCount: 4}, + {signer: "B", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 4}, + {signer: "C", voted: "D", auth: false, signersCount: 4}, + {signer: "A", signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, }, results: []string{"A", "B"}, }, { // Votes from deauthorized signers are discarded immediately (deauth votes) signers: []string{"A", "B", "C"}, votes: []testerVote{ - {signer: "C", voted: "B", auth: false}, - {signer: "A", voted: "C", auth: false}, - {signer: "B", voted: "C", auth: false}, - {signer: "A", voted: "B", auth: false}, + {signer: "C", voted: "B", auth: false, signersCount: 3}, + {signer: "A", voted: "C", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Votes from deauthorized signers are discarded immediately (auth votes) signers: []string{"A", "B", "C"}, votes: []testerVote{ - {signer: "C", voted: "D", auth: true}, - {signer: "A", voted: "C", auth: false}, - {signer: "B", voted: "C", auth: false}, - {signer: "A", voted: "D", auth: true}, + {signer: "C", voted: "D", auth: true, signersCount: 3}, + {signer: "A", voted: "C", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, + {signer: "A", voted: "D", auth: true, signersCount: 2}, }, results: []string{"A", "B"}, }, { // Cascading changes are not allowed, only the account being voted on may change signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B"}, - {signer: "C"}, - {signer: "A", voted: "D", auth: false}, - {signer: "B", voted: "C", auth: false}, - {signer: "C"}, - {signer: "A"}, - {signer: "B", voted: "D", auth: false}, - {signer: "C", voted: "D", auth: false}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", voted: "D", auth: false, signersCount: 4}, + {signer: "B", voted: "C", auth: false, signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 4}, + {signer: "C", voted: "D", auth: false, signersCount: 4}, }, results: []string{"A", "B", "C"}, }, { // Changes reaching consensus out of bounds (via a deauth) execute on touch signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B"}, - {signer: "C"}, - {signer: "A", voted: "D", auth: false}, - {signer: "B", voted: "C", auth: false}, - {signer: "C"}, - {signer: "A"}, - {signer: "B", voted: "D", auth: false}, - {signer: "C", voted: "D", auth: false}, - {signer: "A"}, - {signer: "C", voted: "C", auth: true}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", voted: "D", auth: false, signersCount: 4}, + {signer: "B", voted: "C", auth: false, signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 4}, + {signer: "C", voted: "D", auth: false, signersCount: 4}, + {signer: "A", signersCount: 3}, + {signer: "C", voted: "C", auth: true, signersCount: 3}, }, results: []string{"A", "B"}, }, { // Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch signers: []string{"A", "B", "C", "D"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: false}, - {signer: "B"}, - {signer: "C"}, - {signer: "A", voted: "D", auth: false}, - {signer: "B", voted: "C", auth: false}, - {signer: "C"}, - {signer: "A"}, - {signer: "B", voted: "D", auth: false}, - {signer: "C", voted: "D", auth: false}, - {signer: "A"}, - {signer: "B", voted: "C", auth: true}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", voted: "D", auth: false, signersCount: 4}, + {signer: "B", voted: "C", auth: false, signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "A", signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 4}, + {signer: "C", voted: "D", auth: false, signersCount: 4}, + {signer: "A", signersCount: 3}, + {signer: "B", voted: "C", auth: true, signersCount: 3}, }, results: []string{"A", "B", "C"}, }, { @@ -322,19 +330,19 @@ func TestClique(t *testing.T) { // the final signer outcome. signers: []string{"A", "B", "C", "D", "E"}, votes: []testerVote{ - {signer: "A", voted: "F", auth: true}, // Authorize F, 3 votes needed - {signer: "B", voted: "F", auth: true}, - {signer: "C", voted: "F", auth: true}, - {signer: "D", voted: "F", auth: false}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged") - {signer: "E", voted: "F", auth: false}, - {signer: "B", voted: "F", auth: false}, - {signer: "C", voted: "F", auth: false}, - {signer: "D", voted: "F", auth: true}, // Almost authorize F, 2/3 votes needed - {signer: "E", voted: "F", auth: true}, - {signer: "B", voted: "A", auth: false}, // Deauthorize A, 3 votes needed - {signer: "C", voted: "A", auth: false}, - {signer: "D", voted: "A", auth: false}, - {signer: "B", voted: "F", auth: true}, // Finish authorizing F, 3/3 votes needed + {signer: "A", voted: "F", auth: true, signersCount: 5}, // Authorize F, 3 votes needed + {signer: "B", voted: "F", auth: true, signersCount: 5}, + {signer: "C", voted: "F", auth: true, signersCount: 5}, + {signer: "D", voted: "F", auth: false, signersCount: 6}, // Deauthorize F, 4 votes needed (leave A's previous vote "unchanged") + {signer: "E", voted: "F", auth: false, signersCount: 6}, + {signer: "B", voted: "F", auth: false, signersCount: 6}, + {signer: "C", voted: "F", auth: false, signersCount: 6}, + {signer: "D", voted: "F", auth: true, signersCount: 5}, // Almost authorize F, 2/3 votes needed + {signer: "E", voted: "F", auth: true, signersCount: 5}, + {signer: "B", voted: "A", auth: false, signersCount: 5}, // Deauthorize A, 3 votes needed + {signer: "C", voted: "A", auth: false, signersCount: 5}, + {signer: "D", voted: "A", auth: false, signersCount: 5}, + {signer: "B", voted: "F", auth: true, signersCount: 4}, // Finish authorizing F, 3/3 votes needed }, results: []string{"B", "C", "D", "E", "F"}, }, { @@ -342,25 +350,25 @@ func TestClique(t *testing.T) { epoch: 3, signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A", voted: "C", auth: true}, - {signer: "B"}, - {signer: "A", checkpoint: []string{"A", "B"}}, - {signer: "B", voted: "C", auth: true}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", checkpoint: []string{"A", "B"}, signersCount: 2}, + {signer: "B", voted: "C", auth: true, signersCount: 2}, }, results: []string{"A", "B"}, }, { // An unauthorized signer should not be able to sign blocks signers: []string{"A"}, votes: []testerVote{ - {signer: "B"}, + {signer: "B", signersCount: 1}, }, failure: errUnauthorizedSigner, }, { // An authorized signer that signed recently should not be able to sign again signers: []string{"A", "B"}, votes: []testerVote{ - {signer: "A"}, - {signer: "A"}, + {signer: "A", signersCount: 2}, + {signer: "A", signersCount: 2}, }, failure: errRecentlySigned, }, { @@ -368,10 +376,10 @@ func TestClique(t *testing.T) { epoch: 3, signers: []string{"A", "B", "C"}, votes: []testerVote{ - {signer: "A"}, - {signer: "B"}, - {signer: "A", checkpoint: []string{"A", "B", "C"}}, - {signer: "A"}, + {signer: "A", signersCount: 3}, + {signer: "B", signersCount: 3}, + {signer: "A", checkpoint: []string{"A", "B", "C"}, signersCount: 3}, + {signer: "A", signersCount: 3}, }, failure: errRecentlySigned, }, { @@ -383,10 +391,10 @@ func TestClique(t *testing.T) { epoch: 3, signers: []string{"A", "B", "C"}, votes: []testerVote{ - {signer: "A"}, - {signer: "B"}, - {signer: "A", checkpoint: []string{"A", "B", "C"}}, - {signer: "A", newbatch: true}, + {signer: "A", signersCount: 3}, + {signer: "B", signersCount: 3}, + {signer: "A", checkpoint: []string{"A", "B", "C"}, signersCount: 3}, + {signer: "A", newbatch: true, signersCount: 3}, }, failure: errRecentlySigned, }, @@ -394,7 +402,273 @@ func TestClique(t *testing.T) { // Run through the scenarios and test them for i, tt := range tests { + // ADDED by Jakub Pajek (clique config: voting rule) + tt.votingRule = 2 // Majority + t.Run(fmt.Sprint(i), tt.run) + // ADDED by Jakub Pajek BEG (voter ring voting) + // Run the same test post PrivateHardFork2 + tt.privateHardFork2Block = big.NewInt(0) t.Run(fmt.Sprint(i), tt.run) + // ADDED by Jakub Pajek END (voter ring voting) + } +} + +// Tests that Clique signer voting is evaluated correctly for various simple and +// complex scenarios, as well as that a few special corner cases fail correctly. +func TestClique_VotingRuleSingle(t *testing.T) { + // Define the various voting scenarios to test + tests := []cliqueTest{ + { + // Single signer, no votes cast + signers: []string{"A"}, + votes: []testerVote{ + {signer: "A", signersCount: 1}, + }, + results: []string{"A"}, + }, { + // Single signer, voting to add two others (fulfilled) + signers: []string{"A"}, + votes: []testerVote{ + {signer: "A", voted: "B", auth: true, signersCount: 1}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "C", auth: true, signersCount: 2}, + }, + results: []string{"A", "B", "C"}, + }, { + // Two signers, voting to add three others (fulfilled) + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", voted: "C", auth: true, signersCount: 3}, + {signer: "A", voted: "D", auth: true, signersCount: 3}, + {signer: "C", signersCount: 4}, + {signer: "B", voted: "D", auth: true, signersCount: 4}, + {signer: "D", signersCount: 4}, + {signer: "A", voted: "E", auth: true, signersCount: 4}, + {signer: "B", voted: "E", auth: true, signersCount: 5}, + }, + results: []string{"A", "B", "C", "D", "E"}, + }, { + // Single signer, dropping itself (weird, but one less cornercase by explicitly allowing this) + signers: []string{"A"}, + votes: []testerVote{ + {signer: "A", voted: "A", auth: false, signersCount: 1}, + }, + results: []string{}, + }, { + // Two signers, one dropping another (fulfilled) + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "B", auth: false, signersCount: 2}, + }, + results: []string{"A"}, + }, { + // Three signers, two of them deciding to drop the third (fulfilled) + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 2}, + }, + results: []string{"A", "B"}, + }, { + // Four signers, consensus of one already being enough to drop anyone + signers: []string{"A", "B", "C", "D"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", voted: "D", auth: false, signersCount: 3}, + {signer: "A", voted: "B", auth: false, signersCount: 2}, + }, + results: []string{"A"}, + }, { + // Authorizations are counted once per signer per target + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 3}, + {signer: "A", voted: "C", auth: true, signersCount: 3}, + {signer: "B", signersCount: 3}, + {signer: "A", voted: "C", auth: true, signersCount: 3}, + }, + results: []string{"A", "B", "C"}, + }, { + // Authorizing multiple accounts concurrently is permitted + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 3}, + {signer: "A", voted: "D", auth: true, signersCount: 3}, + {signer: "C", signersCount: 4}, + {signer: "D", signersCount: 4}, + {signer: "B", voted: "D", auth: true, signersCount: 4}, + {signer: "A", signersCount: 4}, + {signer: "C", signersCount: 4}, + {signer: "B", voted: "C", auth: true, signersCount: 4}, + }, + results: []string{"A", "B", "C", "D"}, + }, { + // Deauthorizations are counted once per signer per target + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "B", auth: false, signersCount: 2}, + {signer: "A", voted: "B", auth: false, signersCount: 1}, + {signer: "A", voted: "B", auth: false, signersCount: 1}, + }, + results: []string{"A"}, + }, { + // Deauthorizing multiple accounts concurrently is permitted + signers: []string{"A", "B", "C", "D"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 3}, + {signer: "D", signersCount: 3}, + {signer: "A", voted: "D", auth: false, signersCount: 3}, + {signer: "B", signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", voted: "D", auth: false, signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", voted: "C", auth: false, signersCount: 2}, + }, + results: []string{"A", "B"}, + }, { + // Votes from deauthorized signers are discarded immediately (deauth votes) + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "C", voted: "B", auth: false, signersCount: 3}, + {signer: "A", voted: "C", auth: false, signersCount: 2}, + {signer: "A", voted: "B", auth: false, signersCount: 1}, + }, + results: []string{"A"}, + }, { + // Votes from deauthorized signers are discarded immediately (auth votes) + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "C", voted: "D", auth: true, signersCount: 3}, + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, + {signer: "A", voted: "D", auth: true, signersCount: 3}, + }, + results: []string{"A", "B", "D"}, + }, { + // Cascading changes are not allowed, only the account being voted on may change + signers: []string{"A", "B", "C", "D"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 3}, + {signer: "D", signersCount: 3}, + {signer: "A", voted: "D", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "D", auth: false, signersCount: 2}, + {signer: "B", voted: "D", auth: false, signersCount: 2}, + }, + results: []string{"A", "B"}, + }, { + // Changes reaching consensus out of bounds (via a deauth) execute on touch + // Changes reaching consensus out of bounds (via a deauth) may go out of consensus on first touch + signers: []string{"A", "B", "C", "D"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: false, signersCount: 4}, + {signer: "B", signersCount: 3}, + {signer: "D", signersCount: 3}, + {signer: "A", voted: "D", auth: false, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", signersCount: 2}, + {signer: "A", voted: "D", auth: false, signersCount: 2}, + {signer: "B", voted: "D", auth: false, signersCount: 2}, + {signer: "A", signersCount: 2}, + {signer: "B", voted: "C", auth: true, signersCount: 2}, + }, + results: []string{"A", "B", "C"}, + }, { + // Ensure that pending votes don't survive authorization status changes. This + // corner case can only appear if a signer is quickly added, removed and then + // re-added (or the inverse), while one of the original voters dropped. If a + // past vote is left cached in the system somewhere, this will interfere with + // the final signer outcome. + signers: []string{"A", "B", "C", "D", "E"}, + votes: []testerVote{ + {signer: "A", voted: "F", auth: true, signersCount: 5}, + {signer: "B", voted: "F", auth: true, signersCount: 6}, + {signer: "C", voted: "F", auth: true, signersCount: 6}, + {signer: "D", voted: "F", auth: false, signersCount: 6}, + {signer: "E", voted: "F", auth: false, signersCount: 5}, + {signer: "B", voted: "F", auth: false, signersCount: 5}, + {signer: "C", voted: "F", auth: false, signersCount: 5}, + {signer: "D", voted: "F", auth: true, signersCount: 5}, + {signer: "E", voted: "F", auth: true, signersCount: 6}, + {signer: "B", voted: "A", auth: false, signersCount: 6}, + {signer: "C", voted: "A", auth: false, signersCount: 5}, + {signer: "D", voted: "A", auth: false, signersCount: 5}, + {signer: "B", voted: "F", auth: true, signersCount: 5}, + }, + results: []string{"B", "C", "D", "E", "F"}, + }, { + // Epoch transitions reset all votes to allow chain checkpointing + epoch: 3, + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", voted: "C", auth: true, signersCount: 2}, + {signer: "B", signersCount: 3}, + {signer: "A", checkpoint: []string{"A", "B", "C"}, signersCount: 3}, + {signer: "B", voted: "C", auth: false, signersCount: 3}, + }, + results: []string{"A", "B"}, + }, { + // An unauthorized signer should not be able to sign blocks + signers: []string{"A"}, + votes: []testerVote{ + {signer: "B", signersCount: 1}, + }, + failure: errUnauthorizedSigner, + }, { + // An authorized signer that signed recently should not be able to sign again + signers: []string{"A", "B"}, + votes: []testerVote{ + {signer: "A", signersCount: 2}, + {signer: "A", signersCount: 2}, + }, + failure: errRecentlySigned, + }, { + // Recent signatures should not reset on checkpoint blocks imported in a batch + epoch: 3, + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "A", signersCount: 3}, + {signer: "B", signersCount: 3}, + {signer: "A", checkpoint: []string{"A", "B", "C"}, signersCount: 3}, + {signer: "A", signersCount: 3}, + }, + failure: errRecentlySigned, + }, { + // Recent signatures should not reset on checkpoint blocks imported in a new + // batch (https://github.com/ethereum/go-ethereum/issues/17593). Whilst this + // seems overly specific and weird, it was a Rinkeby consensus split. + // ADDED by Jakub Pajek (clique tests) + // https://github.com/ethereum/go-ethereum/pull/17620 + epoch: 3, + signers: []string{"A", "B", "C"}, + votes: []testerVote{ + {signer: "A", signersCount: 3}, + {signer: "B", signersCount: 3}, + {signer: "A", checkpoint: []string{"A", "B", "C"}, signersCount: 3}, + {signer: "A", newbatch: true, signersCount: 3}, + }, + failure: errRecentlySigned, + }, + } + + // Run through the scenarios and test them + for i, tt := range tests { + // ADDED by Jakub Pajek (clique config: voting rule) + tt.votingRule = 1 // Single vote + t.Run(fmt.Sprint(i), tt.run) + // ADDED by Jakub Pajek BEG (voter ring voting) + // Run the same test post PrivateHardFork2 + tt.privateHardFork2Block = big.NewInt(0) + t.Run(fmt.Sprint(i), tt.run) + // ADDED by Jakub Pajek END (voter ring voting) } } @@ -435,7 +709,7 @@ func (tt *cliqueTest) run(t *testing.T) { config.RefundableFees = true // ADDED by Jakub Pajek BEG (hard fork: list) config.PrivateHardFork1Block = big.NewInt(0) - config.PrivateHardFork2Block = big.NewInt(0) + config.PrivateHardFork2Block = tt.privateHardFork2Block // ADDED by Jakub Pajek END (hard fork: list) // MODIFIED by Jakub Pajek (clique config: variable period) //config.Clique = ¶ms.CliqueConfig{ @@ -446,7 +720,7 @@ func (tt *cliqueTest) run(t *testing.T) { // ADDED by Jakub Pajek (clique config: block reward) BlockReward: params.CliqueBlockReward, // ADDED by Jakub Pajek (clique config: voting rule) - VotingRule: params.CliqueVotingRule, + VotingRule: tt.votingRule, // ADDED by Jakub Pajek (clique config: min stall period) MinStallPeriod: params.CliqueMinStallPeriod, // ADDED by Jakub Pajek (clique config: min offline time) @@ -461,6 +735,8 @@ func (tt *cliqueTest) run(t *testing.T) { engine.fakeDiff = true // ADDED by Jakub Pajek (clique static block rewards) engine.fakeRewards = true + // ADDED by Jakub Pajek (voter ring voting) + engine.fakeVoterRing = (tt.privateHardFork2Block != nil) _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, len(tt.votes), func(j int, gen *core.BlockGen) { // COMMENTED by Jakub Pajek (clique multiple votes) @@ -506,7 +782,23 @@ func (tt *cliqueTest) run(t *testing.T) { // MODIFIED by Jakub Pajek (clique 1-n scale difficulties) //header.Difficulty = diffInTurn // Ignored, we just need a valid number - header.Difficulty = big.NewInt(1) // Ignored, we just need a valid number + // MODIFIED by Jakub Pajek (voter ring voting) + //header.Difficulty = big.NewInt(1) // Ignored, we just need a valid number + if tt.votes[j].signersCount > 0 { + // If signers count for a block is set, use it to estimate the difficulty + // (It does not have to be exact, just within the current ring's allowed range) + if engine.fakeVoterRing { + // Set the difficulty to the maximum allowed value in the voter ring range + header.Difficulty = big.NewInt(tt.votes[j].signersCount * 2) + } else { + // Set the difficulty to the maximum allowed value in the sealer ring range + header.Difficulty = big.NewInt(tt.votes[j].signersCount) + } + } else { + // If signers count for a block is not set, use the lowest allowed value in the + // sealer ring range, which will cause all the tests in the voter ring to faild. + header.Difficulty = big.NewInt(1) + } // Generate the signature, embed it into the header and the block accounts.sign(header, tt.votes[j].signer)