diff --git a/mysticeti/validator.nim b/mysticeti/validator.nim index e39ba41..af44deb 100644 --- a/mysticeti/validator.nim +++ b/mysticeti/validator.nim @@ -65,22 +65,31 @@ func updateCertified(validator: Validator, certificate: Block) = proposal.certifyBy(certificate.id, stake) proc propose*(validator: Validator, transactions: seq[Transaction]): auto = - assert validator.rounds.latest[validator.membership].proposals.len == 0 + type SignedBlock = blocks.SignedBlock[Validator.Signing, Validator.Hashing] + let round = validator.rounds.latest + if round[validator.membership].proposals.len > 0: + return SignedBlock.failure "already proposed this round" var parents: seq[BlockId[Validator.Hashing]] - if previous =? validator.rounds.latest.previous: + var parentStake: Stake + if previous =? round.previous: for slot in previous.slots: if slot.proposals.len == 1: - parents.add(slot.proposals[0].blck.id) + let parent = slot.proposals[0].blck + parents.add(parent.id) + parentStake += validator.committee.stake(parent.author) + if round.number > 0: + if parentStake <= 2/3: + return SignedBlock.failure "not enough parents to represent > 2/3 stake" let blck = Block.new( author = validator.membership, - round = validator.round, + round = round.number, parents = parents, transactions = transactions ) let signedBlock = validator.identity.sign(blck) - validator.rounds.latest.addProposal(signedBlock) + round.addProposal(signedBlock) validator.updateCertified(blck) - signedBlock + success signedBlock func check*(validator: Validator, signed: SignedBlock): auto = type BlockCheck = checks.BlockCheck[SignedBlock.Signing, SignedBlock.Hashing] diff --git a/tests/mysticeti/validator/testMultiple.nim b/tests/mysticeti/validator/testMultiple.nim index 7795877..2f55305 100644 --- a/tests/mysticeti/validator/testMultiple.nim +++ b/tests/mysticeti/validator/testMultiple.nim @@ -37,7 +37,7 @@ suite "Multiple Validators": proc exchangeProposals(exchanges: openArray[(int, seq[int])]): seq[SignedBlock] = for (proposer, receivers) in exchanges: let proposer = validators[proposer] - let proposal = proposer.propose(seq[Transaction].example) + let proposal = !proposer.propose(seq[Transaction].example) for receiver in receivers: let receiver = validators[receiver] exchangeProposal(proposer, receiver, proposal) @@ -53,12 +53,26 @@ suite "Multiple Validators": test "validators include blocks from previous round as parents": let previous = exchangeProposals() nextRound() - let proposal = validators[0].propose(seq[Transaction].example) + let proposal = !validators[0].propose(seq[Transaction].example) for parent in previous: check parent.blck.id in proposal.blck.parents + test "validator can't propose a block with too few parents": + # first round: validator 0 does not receive enough proposals for >2/3 stake + discard exchangeProposals { + 0: @[0, 1, 2, 3], + 1: @[1, 2, 3], + 2: @[1, 2, 3], + 3: @[0, 1, 2, 3] + } + # second round: validator 0 cannot propose a block + nextRound() + let outcome = validators[0].propose(seq[Transaction].example) + check outcome.isFailure + check outcome.error.msg == "not enough parents to represent > 2/3 stake" + test "by default received proposals are undecided": - let proposal = validators[1].propose(seq[Transaction].example) + let proposal = !validators[1].propose(seq[Transaction].example) let round = proposal.blck.round let author = proposal.blck.author let checked = validators[0].check(proposal) @@ -66,7 +80,7 @@ suite "Multiple Validators": check validators[0].status(round, author) == some SlotStatus.undecided test "refuses proposals that are not signed by the author": - let proposal = validators[1].propose(seq[Transaction].example) + let proposal = !validators[1].propose(seq[Transaction].example) let signedByOther = identities[2].sign(proposal.blck) let checked = validators[0].check(signedByOther) check checked.verdict == BlockVerdict.invalid @@ -76,7 +90,7 @@ suite "Multiple Validators": let otherIdentity = Identity.example let otherCommittee = Committee.new({otherIdentity.identifier: 1/1}) let otherValidator = !Validator.new(otherIdentity, otherCommittee) - let proposal = otherValidator.propose(seq[Transaction].example) + let proposal = !otherValidator.propose(seq[Transaction].example) let checked = validators[0].check(proposal) check checked.verdict == BlockVerdict.invalid check checked.reason == "block is not signed by a committee member" @@ -137,7 +151,7 @@ suite "Multiple Validators": } # second round: validator 0 creates block with parent that others didn't see nextRound() - let proposal = validators[0].propose(seq[Transaction].example) + let proposal = !validators[0].propose(seq[Transaction].example) # other validator will not accept block before it receives the parent let checked = validators[1].check(proposal) check checked.verdict == BlockVerdict.incomplete @@ -164,14 +178,14 @@ suite "Multiple Validators": discard toSeq(validators[1].committed()) # validator 0 comes back online and creates block for second round validators[0].nextRound() - let proposal = validators[0].propose(seq[Transaction].example) + let proposal = !validators[0].propose(seq[Transaction].example) # validator 1 accepts block even though parent has already been cleaned up check validators[1].check(proposal).verdict == BlockVerdict.correct test "refuses proposals with a round number that is too high": discard exchangeProposals() validators[0].nextRound() - let proposal = validators[0].propose(seq[Transaction].example) + let proposal = !validators[0].propose(seq[Transaction].example) let checked = validators[1].check(proposal) check checked.verdict == BlockVerdict.invalid check checked.reason == "block has a round number that is too high" @@ -194,7 +208,7 @@ suite "Multiple Validators": let author = proposals[0].blck.author # second round: voting nextRound() - let votes = validators.mapIt(it.propose(seq[Transaction].example)) + let votes = validators.mapIt(!it.propose(seq[Transaction].example)) validators[0].receive(validators[0].check(votes[1]).blck) validators[0].receive(validators[0].check(votes[2]).blck) check validators[0].status(round, author) == some SlotStatus.undecided @@ -211,7 +225,7 @@ suite "Multiple Validators": discard exchangeProposals() # third round: certifying nextRound() - let certificates = validators.mapIt(it.propose(seq[Transaction].example)) + let certificates = validators.mapIt(!it.propose(seq[Transaction].example)) validators[0].receive(validators[0].check(certificates[1]).blck) check validators[0].status(round, author) == some SlotStatus.undecided validators[0].receive(validators[0].check(certificates[2]).blck) diff --git a/tests/mysticeti/validator/testSingle.nim b/tests/mysticeti/validator/testSingle.nim index 963c12d..0788903 100644 --- a/tests/mysticeti/validator/testSingle.nim +++ b/tests/mysticeti/validator/testSingle.nim @@ -24,18 +24,19 @@ suite "Single Validator": check validator.round == 3 test "validators sign their proposals": - let proposal = validator.propose(seq[Transaction].example) + let proposal = !validator.propose(seq[Transaction].example) check proposal.blck.round == validator.round check proposal.blck.author == validator.membership check proposal.signer == validator.identifier test "validator cannot propose more than once in a round": - discard validator.propose(seq[Transaction].example) - expect AssertionDefect: - discard validator.propose(seq[Transaction].example) + discard !validator.propose(seq[Transaction].example) + let outcome = validator.propose(seq[Transaction].example) + check outcome.isFailure + check outcome.error.msg == "already proposed this round" test "by default our own proposals are undecided": - let proposal = validator.propose(seq[Transaction].example) + let proposal = !validator.propose(seq[Transaction].example) let round = proposal.blck.round let author = proposal.blck.author check validator.status(round, author) == some SlotStatus.undecided