Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spec: Add skip round logic #75

Merged
merged 19 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 101 additions & 11 deletions Specs/Quint/asyncModelsTest.qnt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import statemachineAsync(
Heights = Set(0) // , 1, 2, 3)
) as N4F0 from "./statemachineAsync"

run NewRoundTest = {

run ThreeDecideInRound1V4stillinZeroTest = {
N4F0::init
.then(N4F0::setNextValueToPropose("v2", "block"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v4"))
Expand Down Expand Up @@ -64,21 +65,110 @@ run NewRoundTest = {
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v3"))
.then(N4F0::setNextValueToPropose("v3", "another block"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
// .then(N4F0::deliverProposal("v1", { height: 0, proposal: "block", round: 1, src: "v2", validRound: -1 }))
/* .then(N4F0::deliverProposal("v1", { height: 0, proposal: "block", round: 0, src: "v2", validRound: -1 }))
.then(N4F0::deliverProposal("v2", { height: 0, proposal: "block", round: 0, src: "v2", validRound: -1 }))
.then(N4F0::deliverProposal("v3", { height: 0, proposal: "block", round: 0, src: "v2", validRound: -1 }))
.then(N4F0::deliverProposal("v1", { height: 0, proposal: "another block", round: 1, src: "v3", validRound: -1 }))
.then(N4F0::deliverProposal("v2", { height: 0, proposal: "another block", round: 1, src: "v3", validRound: -1 }))
.then(N4F0::deliverProposal("v3", { height: 0, proposal: "another block", round: 1, src: "v3", validRound: -1 }))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v1", step: "Prevote" }))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v2", step: "Prevote" }))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v3", step: "Prevote" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v1", step: "Prevote" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v2", step: "Prevote" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v3", step: "Prevote" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v1", step: "Prevote" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v2", step: "Prevote" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v3", step: "Prevote" }))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v2", step: "Precommit" }))
.then(N4F0::deliverVote("v1", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v2", step: "Precommit" }))
.then(N4F0::deliverVote("v2", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v2", step: "Precommit" }))
.then(N4F0::deliverVote("v3", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))

*/
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v1"))
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
}

run DecideForFutureRoundTest = {
ThreeDecideInRound1V4stillinZeroTest
.then(N4F0::deliverProposal("v4", { height: 0, proposal: "another block", round: 1, src: "v3", validRound: -1 }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v2", step: "Precommit" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
// .then(4.reps(i => N4F0::valStep("v4")))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(all {
assert(N4F0::system.get("v4").es.chain.head() == "another block"),
N4F0::unchangedAll
})
// .then(N4F0::valStep("v4"))

}

run DecideOnProposalTest = {
ThreeDecideInRound1V4stillinZeroTest
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v2", step: "Precommit" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(N4F0::deliverProposal("v4", { height: 0, proposal: "another block", round: 1, src: "v3", validRound: -1 }))
.then(all {
assert(N4F0::system.get("v4").es.chain == List()),
N4F0::valStep("v4")
})
.then(all {
assert(N4F0::system.get("v4").es.chain.head() == "another block"),
N4F0::unchangedAll
})
}

run RoundswitchTest = {
ThreeDecideInRound1V4stillinZeroTest
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v1", step: "Precommit" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v2", step: "Prevote" }))
.then(N4F0::deliverVote("v4", { height: 0, id: "another block", round: 1, src: "v3", step: "Precommit" }))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(N4F0::valStep("v4"))
.then(all {
// this should work, once the bookkeeper skip event is fixed
assert(N4F0::system.get("v4").es.cs.round == 1),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romac could you please have a look here. This test fails, although as far as I understand if the bookkeeper would generate the skip event properly, it should go through. Here v4

  • lags behind in round 0
  • then receives 3 round 1 messages (two precommits and one prevote). The messages are stored in the bookkeeper
  • at some point a skip event should bring v4 to round 1 but it never happens

Copy link
Member

@romac romac Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that sounds right. But the test appears to be passing, or am I misunderstanding what the test is checking?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, locally it doesn't. Perhaps I have messed something up with branches locally. If the test passes, let's not block on this, and we can have a look (and add more tests later)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does pass locally for me either. Not sure what happened with the CI but let's trust our eyes for now. I'll look into it!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be due to non-determinism? We might have gotten lucky on the CI this one time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure. This is the check <...> that you already added, right?

Yeah I added the check while I was writing the comment.

Copy link
Member

@romac romac Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the point is that even if the votekeeper generates events for round less than the current round, the executor has to figure out whether it still wants to do in some rounds (and sometimes does).

Can you please expand on that? I am not sure I am following what you are getting at.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I am following what you are getting at.

It was just an observation that in the interface between the bookkeeper and the executor, we most of time don't need to pass the current round. That is, only for generating the skip rule we actually need the current round, while for the rest of the bookkeeper logic, the round that is passed as part of the vote is sufficient. Perhaps it is not important, it was just for me to understand ;-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha! Yeah it's a shame this is the only case where we need the current round, maybe this hints at a better way to model the code and decouple things further but I am not seeing it right now. If the current state of things is fine with you then let's leave it at that for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can leave it for now.

If I think longer about it, the current solution also becomes more natural: the consensus algorithm forces us to ignore the current round in several places. In the arXiv paper

  • for line 28 we may need prevotes from the previous round,
  • for line 49 we may need precommits from previous (in fact any) rounds.

So actually, perhaps it is natural that most of the time we don't care about the current round when doing bookkeeping. So perhaps it is just intuition that is failing me here: while we think about messages from the current round a lot for lucky path, etc., the mentioned lines require us to look at messages from all rounds.

So, I guess it is good. Again, thanks for fixing the issue!

N4F0::unchangedAll
})
}



import statemachineAsync(
validators = Set("v1", "v2", "v3", "v4"),
validatorSet = Set("v1", "v2", "v3", "v4").mapBy(x => 1),
Expand All @@ -100,7 +190,7 @@ import statemachineAsync(
Heights = Set(0) // , 1, 2, 3)
) as N4F2 from "./statemachineAsync"

// v3 and v4 are correct. v2 is a N4N4F22aulty proposal and proposes diN4N4F22N4N4F22erently to v3 and v4
// v3 and v4 are correct. v2 is a faulty proposal and proposes differently to v3 and v4
// this run leads to disagreement
run DisagreementTest = {
N4F2::init
Expand Down Expand Up @@ -155,4 +245,4 @@ run DisagreementTest = {



}
}
2 changes: 2 additions & 0 deletions Specs/Quint/consensus.qnt
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ pure def consensus (state: ConsensusState, ev: Event) : ConsResult = {
PrecommitAny(state, ev)
else if (ev.name == "ProposalAndCommitAndValid")
ProposalAndCommitAndValid(state, ev)
else if (ev.name == "RoundSkip")
RoundSkip (state, ev)
else if (ev.name == "TimeoutPropose")
TimeoutPropose (state, ev)
else if (ev.name == "TimeoutPrevote")
Expand Down
57 changes: 39 additions & 18 deletions Specs/Quint/executor.qnt
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,20 @@ pure def Precommit (es: ExecutorState, input: ExecutorInput, eev: ExecutorEvent)
}
}
else if (eev.name == "PrecommitAny" and eev.round == es.cs.round) {
callConsensus(es, es.bk, { name: "PrecommitAny",
height: input.vote.height,
round: input.vote.round,
value: input.vote.id,
vr: -1})
callConsensus(es, es.bk, { name: "PrecommitAny",
height: input.vote.height,
round: input.vote.round,
value: input.vote.id,
vr: -1})
}
else if (eev.name == "Skip" and eev.round > es.cs.round)
callConsensus(es, es.bk, { name: "RoundSkip",
height: input.vote.height,
round: input.vote.round,
value: input.vote.id,
vr: -1})
else
// none of the supported Precommit events. Do nothing
// TODO: catch skip round event
(es, defaultResult)
}

Expand Down Expand Up @@ -249,8 +254,13 @@ pure def Prevote (es: ExecutorState, input: ExecutorInput, eev: ExecutorEvent)
round: es.cs.round,
value: eev.value,
vr: eev.round})
else if (eev.name == "Skip" and eev.round > es.cs.round)
callConsensus(es, es.bk, { name: "RoundSkip",
height: input.vote.height,
round: input.vote.round,
value: input.vote.id,
vr: -1})
else
// TODO: catch skip round event
(es, defaultResult)
}

Expand Down Expand Up @@ -279,7 +289,7 @@ pure def ProposalMsg (es: ExecutorState, input: ExecutorInput) : (ExecutorState,
val receivedCommit = checkThreshold( newES.bk,
input.proposal.round,
"Precommit",
{name: "PrecommitValue",
{name: "Value",
value: id(input.proposal.proposal)})
if (receivedCommit)
// we have a commit that matches the proposal. We don't need to compare against
Expand All @@ -292,7 +302,7 @@ pure def ProposalMsg (es: ExecutorState, input: ExecutorInput) : (ExecutorState,
round: input.proposal.round,
value: id(input.proposal.proposal),
vr: input.proposal.validRound})
else if (input.proposal.round != es.cs.round or input.proposal.height != es.cs.round)
else if (input.proposal.round != es.cs.round or input.proposal.height != es.cs.height)
// the proposal is from the right proposer and valid, but not for this round
// keep the proposal, do nothing else
(newES, defaultResult)
Expand All @@ -301,17 +311,17 @@ pure def ProposalMsg (es: ExecutorState, input: ExecutorInput) : (ExecutorState,
val receivedPolkaValidRoundVal = checkThreshold(newES.bk,
input.proposal.validRound,
"Prevote",
{name: "PolkaValue",
{name: "Value",
value: id(input.proposal.proposal)})
val receivedPolkaCurrentVal = checkThreshold( newES.bk,
newES.cs.round,
"Prevote",
{name: "PolkaValue",
{name: "Value",
value: id(input.proposal.proposal)})
val receivedCommitCurrentVal = checkThreshold( newES.bk,
newES.cs.round,
"Precommit",
{name: "PrecommitValue",
{name: "Value",
value: id(input.proposal.proposal)})
if (newES.cs.step == "propose")
if (input.proposal.validRound == -1)
Expand Down Expand Up @@ -375,7 +385,7 @@ pure def ProposalMsg (es: ExecutorState, input: ExecutorInput) : (ExecutorState,
// (not(valid(input.proposal)))
// keep ES (don't use newES here), that is, drop proposal
if (es.cs.step == "propose" and es.cs.round == input.proposal.round and es.cs.height == input.proposal.height)
if (checkThreshold(es.bk, es.cs.round, "Prevote", {name: "PolkaValue", value: id(input.proposal.proposal)}))
if (checkThreshold(es.bk, es.cs.round, "Prevote", {name: "Value", value: id(input.proposal.proposal)}))
val event: Event = {name: "ProposalAndPolkaAndInValid",
height: es.cs.height,
round: es.cs.round,
Expand Down Expand Up @@ -460,11 +470,13 @@ pure def setValue(es: ExecutorState, value: Value_t) : (ExecutorState, Consensus
pure def executor (es: ExecutorState, input: ExecutorInput) : (ExecutorState, ConsensusOutput) = {
// TODO: shall we check whether the sender actually is in the validator set
if (input.name == "proposal") {
val cons_res = ProposalMsg(es, input)
if (cons_res._2.name == "decided")
decided (cons_res._1, cons_res._2)
val res = ProposalMsg(es, input)
if (res._2.name == "decided")
decided (res._1, res._2)
else if (res._2.name == "skipRound")
skip (res._1, res._2.skipRound)
else
cons_res
res
}
else if (input.name == "votemessage" and input.vote.step == "Precommit") {
val res = applyVote(es.bk, toVote(input.vote), es.valset.get(input.vote.src))
Expand All @@ -473,14 +485,23 @@ pure def executor (es: ExecutorState, input: ExecutorInput) : (ExecutorState, Co
val cons_res = Precommit(newES, input, res.event)
if (cons_res._2.name == "decided")
decided (cons_res._1, cons_res._2)
else if (cons_res._2.name == "skipRound")
skip (cons_res._1, cons_res._2.skipRound)
else
cons_res
}
else if (input.name == "votemessage" and input.vote.step == "Prevote") {
val res = applyVote(es.bk, toVote(input.vote), es.valset.get(input.vote.src))
val newES = { ...es, bk: res.bookkeeper, applyvotesResult: res.event}
// only a commit event can come here.
Prevote(newES, input, res.event)
val cons_res = Prevote(newES, input, res.event)
if (cons_res._2.name == "decided")
// TODO: dead branch. But we should put this after consensus call logic into a function
decided (cons_res._1, cons_res._2)
else if (cons_res._2.name == "skipRound")
skip (cons_res._1, cons_res._2.skipRound)
else
cons_res
}
else if (input.name == "timeout") {
val res = Timeout(es, input)
Expand Down
95 changes: 95 additions & 0 deletions Specs/Quint/parameterizedTest.qnt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
module parameterizedTest {

import statemachineAsync(
validators = Set("v1", "v2", "v3", "v4"),
validatorSet = Set("v1", "v2", "v3", "v4").mapBy(x => 1),
Faulty = Set(),
Values = Set("a", "b"),
Rounds = Set(0, 1, 2, 3),
Heights = Set(0) // , 1, 2, 3)
) as N4F0 from "./statemachineAsync"


// run everyoneTakesAStep (proposer, nonprop) = {
// N4F0::valStep(nonprop._1)
// .then(N4F0::valStep(nonprop._2))
// .then(N4F0::valStep(nonprop._3))
// .then(N4F0::valStep(proposer))
// }

run everyoneTakesAStep = {
N4F0::valStep("v1")
.then(N4F0::valStep("v2"))
.then(N4F0::valStep("v3"))
.then(N4F0::valStep("v4"))
}

run oneDeliversPrevote (validator, prop, r, proposer, nonprop) = {
N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._1, step: "Prevote" })
.then(N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._2, step: "Prevote" }))
.then(N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._3, step: "Prevote" }))
.then(N4F0::deliverVote(validator, { height: 0, id: prop, round: r, src: proposer, step: "Prevote" }))
}

run oneDeliversPrecommit (validator, prop, r, proposer, nonprop) = {
N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._1, step: "Precommit" })
.then(N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._2, step: "Precommit" }))
.then(N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: nonprop._3, step: "Precommit" }))
.then(N4F0::deliverVote(validator, { height: 0, id: "nil", round: r, src: proposer, step: "Precommit" }))
}

run UnSuccessfulRound (prop: str, r: int) : bool = {
val proposer = N4F0::Proposer(N4F0::validatorSet, 0, r)
val nonprop = if (proposer == "v1") ("v2", "v3", "v4")
else if (proposer == "v2") ("v1", "v3", "v4")
else if (proposer == "v3") ("v1", "v2", "v4")
else ("v1", "v2", "v3")
// everyone is in round r and proposer sent a proposal
all {
assert(N4F0::propBuffer.keys().forall(p => N4F0::propBuffer.get(p).contains(
{ height: 0, proposal: prop, round: r, src: proposer, validRound: -1 }) )),
N4F0::unchangedAll
}
.then(N4F0::deliverProposal(proposer, { height: 0, proposal: prop, round: r, src: proposer, validRound: -1 }))
.then(everyoneTakesAStep)
.then(oneDeliversPrevote(proposer, prop, r, proposer, nonprop))
.then(oneDeliversPrevote(nonprop._1, prop, r, proposer, nonprop))
.then(oneDeliversPrevote(nonprop._2, prop, r, proposer, nonprop))
.then(oneDeliversPrevote(nonprop._3, prop, r, proposer, nonprop))
.then(everyoneTakesAStep)
.then(everyoneTakesAStep)
.then(everyoneTakesAStep)
.then(all {
assert(N4F0::system.keys().forall(p => N4F0::system.get(p).timeout.contains(
("TimeoutPrevote", 0, r)) )),
N4F0::unchangedAll
})
.then(everyoneTakesAStep)
.then(oneDeliversPrecommit(proposer, prop, r, proposer, nonprop))
.then(oneDeliversPrecommit(nonprop._1, prop, r, proposer, nonprop))
.then(oneDeliversPrecommit(nonprop._2, prop, r, proposer, nonprop))
.then(oneDeliversPrecommit(nonprop._3, prop, r, proposer, nonprop))
.then(everyoneTakesAStep)
.then(everyoneTakesAStep)
.then(everyoneTakesAStep)
.then(everyoneTakesAStep)
}

run UnSuccessfulRoundWithSetup (prop: str, r: int) : bool = {
N4F0::setNextValueToPropose(N4F0::Proposer(N4F0::validatorSet, 0, r), prop)
.then(everyoneTakesAStep)
.then(all {
assert(N4F0::system.keys().forall(p => N4F0::system.get(p).es.cs.round == r)),
UnSuccessfulRound (prop, r)
})
}

run multiRoundTest = {
val proposals = ["blue", "red", "green", "yellow"]
val repetitions = proposals.length()
N4F0::init
.then(repetitions.reps(i => UnSuccessfulRoundWithSetup(proposals[i], i)))
}


}
Loading