From 22b473763c9e53756c6089bd93360694ecd7ff2d Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Wed, 11 Dec 2024 14:22:39 +0100 Subject: [PATCH] use threshold logical clock to move to next round --- Readme.md | 14 ++--- mysticeti.nim | 1 - mysticeti/validator.nim | 23 +++++--- tests/mysticeti/scenarios.nim | 6 -- tests/mysticeti/simulator.nim | 4 -- tests/mysticeti/validator/testPerformance.nim | 2 - .../validator/testValidatorNetwork.nim | 56 ++++++------------- 7 files changed, 35 insertions(+), 71 deletions(-) diff --git a/Readme.md b/Readme.md index 62c639d..4873d84 100644 --- a/Readme.md +++ b/Readme.md @@ -183,18 +183,12 @@ if checked.verdict == BlockVerdict.incomplete: ### Moving to the next round -The Mysticeti protocol uses a threshold logical clock to move from one round to -the next. This means that each validator moves to the next round when it's seen -enough blocks in the current round to represent >2/3 of the stake. +The validator uses a threshold logical clock to move from one round to the next. +This means it moves to the next round when it's seen enough blocks in the +current round to represent >2/3 of the stake. Additionaly, the protocol mandates that all validators wait for the primary -proposer of the round (with a timeout), before moving to the next round. - -To move to the next round, invoke the `nextRound` function: - -```nim -validator.nextRound() -``` +proposer of the round (with a timeout), before creating their own blocks. The primary proposer for the current round can be retrieved from the validator: diff --git a/mysticeti.nim b/mysticeti.nim index 9321e50..d5e209b 100644 --- a/mysticeti.nim +++ b/mysticeti.nim @@ -11,7 +11,6 @@ export validator.identifier export validator.membership export validator.round export validator.primaryProposer -export validator.nextRound export validator.parentBlocks export validator.check export validator.add diff --git a/mysticeti/validator.nim b/mysticeti/validator.nim index 48dd7af..a8aff10 100644 --- a/mysticeti/validator.nim +++ b/mysticeti/validator.nim @@ -13,6 +13,7 @@ type Validator*[Dependencies] = ref object committee: Committee[Dependencies.Identifier] membership: CommitteeMember rounds: Rounds[Dependencies] + clockThreshold: Voting func new*[Dependencies]( _: type Validator[Dependencies], @@ -40,9 +41,6 @@ func round*(validator: Validator): uint64 = func primaryProposer*(validator: Validator): CommitteeMember = validator.rounds.latest.primaryProposer -func nextRound*(validator: Validator) = - validator.rounds.addNewRound() - func updateSkipped(validator: Validator, supporter: Validator.Dependencies.Block) = func skips(blck: Validator.Dependencies.Block, round: uint64, author: CommitteeMember): bool = for parent in blck.parents: @@ -76,11 +74,22 @@ func updateCertified(validator: Validator, certificate: Validator.Dependencies.B let stake = validator.committee.stake(certificate.author) proposal.certifyBy(certificate.id, stake) +func updateRound(validator: Validator, blck: Validator.Dependencies.Block) = + if blck.round == validator.round: + let author = blck.author + let stake = validator.committee.stake(author) + validator.clockThreshold.add(author, stake) + if validator.clockThreshold.stake > 2/3: + validator.rounds.addNewRound() + validator.clockThreshold.reset() + func addBlock(validator: Validator, signedBlock: SignedBlock) = - if round =? validator.rounds.latest.find(signedBlock.blck.round): + let blck = signedBlock.blck + if round =? validator.rounds.latest.find(blck.round): round.addProposal(signedBlock) - validator.updateSkipped(signedBlock.blck) - validator.updateCertified(signedBlock.blck) + validator.updateSkipped(blck) + validator.updateCertified(blck) + validator.updateRound(blck) func parentBlocks*(validator: Validator): auto = mixin id @@ -104,8 +113,6 @@ func check*(validator: Validator, signed: SignedBlock): auto = return BlockCheck.invalid("block is not signed by a committee member") if member != signed.blck.author: return BlockCheck.invalid("block is not signed by its author") - if signed.blck.round > validator.round: - return BlockCheck.invalid("block has a round number that is too high") for parent in signed.blck.parents: if parent.round >= signed.blck.round: return BlockCheck.invalid("block has a parent from an invalid round") diff --git a/tests/mysticeti/scenarios.nim b/tests/mysticeti/scenarios.nim index 4a701e6..262f26e 100644 --- a/tests/mysticeti/scenarios.nim +++ b/tests/mysticeti/scenarios.nim @@ -18,35 +18,30 @@ proc scenarioFigure4*(simulator: NetworkSimulator): ?!seq[seq[SignedBlock]] = 2: @[0, 2, 3], 3: @[1, 2, 3] }) - simulator.nextRound() proposals.add(? simulator.exchangeProposals { 0: @[0, 1, 3], 1: @[0, 1, 3], 2: @[0, 3], 3: @[1, 3] }) - simulator.nextRound() proposals.add(? simulator.exchangeProposals { 0: @[2, 3, 0, 1], 1: @[2, 3, 0, 1], 3: @[2, 3, 0, 1] }) - simulator.nextRound() proposals.add(? simulator.exchangeProposals { 2: @[2, 3, 0, 1], 3: @[3], 0: @[2, 3, 0, 1], 1: @[2, 3, 0, 1] }) - simulator.nextRound() proposals.add(? simulator.exchangeProposals { 2: @[], 3: @[2, 3, 0], 0: @[2, 3, 0], 1: @[2, 3, 0] }) - simulator.nextRound() proposals.add(? simulator.exchangeProposals { 2: @[2, 3, 0, 1], 3: @[2, 3, 0, 1], @@ -74,5 +69,4 @@ proc randomScenario*(simulator: NetworkSimulator): ?!seq[seq[SignedBlock]] = receivers.add(receiver) exchanges.add( (proposer, receivers) ) proposals.add(? simulator.exchangeProposals(exchanges)) - simulator.nextRound() success proposals diff --git a/tests/mysticeti/simulator.nim b/tests/mysticeti/simulator.nim index f9d850a..1b5a426 100644 --- a/tests/mysticeti/simulator.nim +++ b/tests/mysticeti/simulator.nim @@ -26,10 +26,6 @@ func identities*(simulator: NetworkSimulator): seq[Identity] = func validators*(simulator: NetworkSimulator): seq[Validator] = simulator.validators -func nextRound*(simulator: NetworkSimulator) = - for validator in simulator.validators: - validator.nextRound() - proc propose*(simulator: NetworkSimulator, validatorIndex: int): SignedBlock = let validator = simulator.validators[validatorIndex] let identity = simulator.identities[validatorIndex] diff --git a/tests/mysticeti/validator/testPerformance.nim b/tests/mysticeti/validator/testPerformance.nim index 610ee90..1aa6d62 100644 --- a/tests/mysticeti/validator/testPerformance.nim +++ b/tests/mysticeti/validator/testPerformance.nim @@ -8,9 +8,7 @@ suite "Validator Network Performance": # TODO: 100 validators let simulator = NetworkSimulator.init(20) discard !simulator.exchangeProposals() - simulator.nextRound() discard !simulator.exchangeProposals() - simulator.nextRound() let start = now() discard !simulator.exchangeProposals() let finish = now() diff --git a/tests/mysticeti/validator/testValidatorNetwork.nim b/tests/mysticeti/validator/testValidatorNetwork.nim index ccdfe51..07d86d8 100644 --- a/tests/mysticeti/validator/testValidatorNetwork.nim +++ b/tests/mysticeti/validator/testValidatorNetwork.nim @@ -21,28 +21,30 @@ suite "Validator Network": for validator in simulator.validators: check validator.round == 0 - test "validators can move to next round": - for validator in simulator.validators: - validator.nextRound() - check validator.round == 1 - validator.nextRound() - validator.nextRound() - check validator.round == 3 + test "validators move to the next round after receiving >2/3 stake proposals": + let validator = simulator.validators[0] + for round in 0'u64..10: + check validator.round == round # 0/4 stake + discard !simulator.exchangeProposals({0: @[0, 1, 2, 3]}) + check validator.round == round # 1/4 stake + discard !simulator.exchangeProposals({1: @[0, 1, 2, 3]}) + check validator.round == round # 2/4 stake + discard !simulator.exchangeProposals({2: @[0, 1, 2, 3]}) + check validator.round == round + 1 # 3/4 stake test "primary proposer rotates on a round-robin schedule": check simulator.validators.allIt(it.primaryProposer == CommitteeMember(0)) - simulator.nextRound() + discard !simulator.exchangeProposals() check simulator.validators.allIt(it.primaryProposer == CommitteeMember(1)) - simulator.nextRound() + discard !simulator.exchangeProposals() check simulator.validators.allIt(it.primaryProposer == CommitteeMember(2)) - simulator.nextRound() + discard !simulator.exchangeProposals() check simulator.validators.allIt(it.primaryProposer == CommitteeMember(3)) - simulator.nextRound() + discard !simulator.exchangeProposals() check simulator.validators.allIt(it.primaryProposer == CommitteeMember(0)) test "validators expose blocks from previous round as parents": let previous = !simulator.exchangeProposals() - simulator.nextRound() let parents = simulator.validators[0].parentBlocks for proposal in previous: check proposal.blck.id in parents @@ -86,7 +88,6 @@ suite "Validator Network": let parents = (!simulator.exchangeProposals()).mapIt(it.blck.id) let badParentRound = 1'u64 let badParent = BlockId.init(CommitteeMember(0), badParentRound, Hash.example) - simulator.nextRound() let blck = Block.new( CommitteeMember(0), round = 1, @@ -103,7 +104,6 @@ suite "Validator Network": test "refuses proposals that include a parent more than once": let parents = (!simulator.exchangeProposals()).mapIt(it.blck.id) let badParent = parents.sample - simulator.nextRound() let blck = Block.new( CommitteeMember(0), round = 1, @@ -119,7 +119,6 @@ suite "Validator Network": test "refuses proposals without >2/3 parents from the previous round": let parents = (!simulator.exchangeProposals()).mapIt(it.blck.id) - simulator.nextRound() let blck = Block.new( CommitteeMember(0), round = 1, @@ -143,7 +142,6 @@ suite "Validator Network": 3: @[0, 1, 2, 3], } # second round: validator 0 creates block with parent that others didn't see - simulator.nextRound() let proposal = simulator.propose(0) # other validator will not accept block before it receives the parent let checked = simulator.validators[1].check(proposal) @@ -160,8 +158,6 @@ suite "Validator Network": } # for the second to the sixth round, validator 0 is down for _ in 2..6: - for validator in simulator.validators[1..3]: - validator.nextRound() discard !simulator.exchangeProposals { 1: @[1, 2, 3], 2: @[1, 2, 3], @@ -170,18 +166,11 @@ suite "Validator Network": # validator 1 cleans up old blocks discard toSeq(simulator.validators[1].committed()) # validator 0 comes back online and creates block for second round - simulator.validators[0].nextRound() let proposal = simulator.propose(0) # validator 1 accepts block even though parent has already been cleaned up - check simulator.validators[1].check(proposal).verdict == BlockVerdict.correct - - test "refuses proposals with a round number that is too high": - discard !simulator.exchangeProposals() - simulator.validators[0].nextRound() - let proposal = simulator.propose(0) let checked = simulator.validators[1].check(proposal) - check checked.verdict == BlockVerdict.invalid - check checked.reason == "block has a round number that is too high" + check checked.verdict == BlockVerdict.correct + simulator.validators[1].add(checked.blck) test "refuses a proposal that was already received": let proposals = !simulator.exchangeProposals() @@ -200,7 +189,6 @@ suite "Validator Network": let round = proposals[0].blck.round let author = proposals[0].blck.author # second round: voting - simulator.nextRound() let votes = simulator.propose() simulator.validators[0].add(simulator.validators[0].check(votes[1]).blck) simulator.validators[0].add(simulator.validators[0].check(votes[2]).blck) @@ -217,7 +205,6 @@ suite "Validator Network": 3: @[0, 1, 2, 3] } # second round: first validator does not receive votes - simulator.nextRound() discard !simulator.exchangeProposals { 1: @[1, 2, 3], 2: @[1, 2, 3], @@ -225,7 +212,6 @@ suite "Validator Network": } # third round: first validator receives certificates, and also the votes # from the previous round because they are the parents of the certificates - simulator.nextRound() discard !simulator.exchangeProposals { 1: @[0, 1, 2, 3], 2: @[0, 1, 2, 3], @@ -241,10 +227,8 @@ suite "Validator Network": let round = proposal.blck.round let author = proposal.blck.author # second round: voting - simulator.nextRound() discard !simulator.exchangeProposals() # third round: certifying - simulator.nextRound() let certificates = simulator.propose() simulator.validators[0].add(simulator.validators[0].check(certificates[1]).blck) check simulator.validators[0].status(round, author) == some SlotStatus.undecided @@ -255,14 +239,12 @@ suite "Validator Network": # first round: proposing let proposals = !simulator.exchangeProposals() # second round: first validator does not receive votes - simulator.nextRound() discard !simulator.exchangeProposals { 1: @[1, 2, 3], 2: @[1, 2, 3], 3: @[1, 2, 3] } # third round: first validator does not receive certificates - simulator.nextRound() discard !simulator.exchangeProposals { 1: @[1, 2, 3], 2: @[1, 2, 3], @@ -270,7 +252,6 @@ suite "Validator Network": } # fourth round: first validator receives votes and certificates, because # they are the parents of the blocks from this round - simulator.nextRound() discard !simulator.exchangeProposals { 1: @[0, 1, 2, 3], 2: @[0, 1, 2, 3], @@ -283,15 +264,12 @@ suite "Validator Network": test "can iterate over the list of committed blocks": # blocks proposed in first round, in order of committee members let first = (!simulator.exchangeProposals()).mapIt(it.blck) - simulator.nextRound() # blocks proposed in second round, round-robin order let second = (!simulator.exchangeProposals()).mapIt(it.blck).rotatedLeft(1) - simulator.nextRound() # certify blocks from the first round discard !simulator.exchangeProposals() check toSeq(simulator.validators[0].committed()) == first # certify blocks from the second round - simulator.nextRound() discard !simulator.exchangeProposals() check toSeq(simulator.validators[0].committed()) == second @@ -342,9 +320,7 @@ suite "Validator Network": !exchangeBlock(simulator.validators[0], simulator.validators[2], proposalA) !exchangeBlock(simulator.validators[0], simulator.validators[3], proposalB) # next rounds happen normally - simulator.nextRound() discard !simulator.exchangeProposals() - simulator.nextRound() discard !simulator.exchangeProposals() # check that only the proposal that was sent to the majority is committed for validator in simulator.validators: