Skip to content

Commit

Permalink
Test sway at commit and converge for unseen candidates
Browse files Browse the repository at this point in the history
Test that a participant sways for unseen candidates at `COMMIT` and
`CONVERGE`.

Part of #237
  • Loading branch information
masih committed Sep 6, 2024
1 parent a4deda5 commit d84abb1
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 7 deletions.
6 changes: 6 additions & 0 deletions emulator/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func (d *Driver) AddInstance(instance *Instance) {
d.require.NoError(d.host.addInstance(instance))
}

// PeekLastBroadcastRequest gets the last broadcast requested by the subject
// participant without removing it from the pending broadcasts.
func (d *Driver) PeekLastBroadcastRequest() *gpbft.GMessage {
return d.host.peekLastBroadcast()
}

func (d *Driver) DeliverAlarm() (bool, error) {
if d.host.maybeReceiveAlarm() {
return true, d.subject.ReceiveAlarm()
Expand Down
21 changes: 15 additions & 6 deletions emulator/driver_assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ func (d *Driver) RequireDeliverAlarm() {
}

func (d *Driver) RequireNoBroadcast() {
d.require.Nil(d.host.popReceivedBroadcast())
d.require.Nil(d.host.popNextBroadcast())
}

func (d *Driver) RequireQuality() {
msg := d.host.popReceivedBroadcast()
msg := d.host.popNextBroadcast()
d.require.NotNil(msg)
instance := d.host.getInstance(msg.Vote.Instance)
d.require.NotNil(instance)
Expand All @@ -56,7 +56,7 @@ func (d *Driver) RequirePrepare(value gpbft.ECChain) {
}

func (d *Driver) RequirePrepareAtRound(round uint64, value gpbft.ECChain, justification *gpbft.Justification) {
msg := d.host.popReceivedBroadcast()
msg := d.host.popNextBroadcast()
d.require.NotNil(msg)
instance := d.host.getInstance(msg.Vote.Instance)
d.require.NotNil(instance)
Expand All @@ -76,7 +76,7 @@ func (d *Driver) RequireCommitForBottom(round uint64) {
}

func (d *Driver) RequireCommit(round uint64, vote gpbft.ECChain, justification *gpbft.Justification) {
msg := d.host.popReceivedBroadcast()
msg := d.host.popNextBroadcast()
d.require.NotNil(msg)
instance := d.host.getInstance(msg.Vote.Instance)
d.require.NotNil(instance)
Expand All @@ -92,7 +92,7 @@ func (d *Driver) RequireCommit(round uint64, vote gpbft.ECChain, justification *
}

func (d *Driver) RequireConverge(round uint64, vote gpbft.ECChain, justification *gpbft.Justification) {
msg := d.host.popReceivedBroadcast()
msg := d.host.popNextBroadcast()
d.require.NotNil(msg)
instance := d.host.getInstance(msg.Vote.Instance)
d.require.NotNil(instance)
Expand All @@ -108,7 +108,7 @@ func (d *Driver) RequireConverge(round uint64, vote gpbft.ECChain, justification
}

func (d *Driver) RequireDecide(vote gpbft.ECChain, justification *gpbft.Justification) {
msg := d.host.popReceivedBroadcast()
msg := d.host.popNextBroadcast()
d.require.NotNil(msg)
instance := d.host.getInstance(msg.Vote.Instance)
d.require.NotNil(instance)
Expand All @@ -130,3 +130,12 @@ func (d *Driver) RequireDecision(instanceID uint64, expect gpbft.ECChain) {
d.require.NotNil(decision)
d.require.Equal(expect, decision.Vote.Value)
}

// RequirePeekAtLastVote asserts that the last message broadcasted by the subject
// participant was for the given step, round and vote.
func (d *Driver) RequirePeekAtLastVote(step gpbft.Phase, round uint64, vote gpbft.ECChain) {
last := d.host.peekLastBroadcast()
d.require.Equal(step, last.Vote.Step, "Expected last vote step %s, but got %s", step, last.Vote.Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-fuzz

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-fuzz

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-check / All

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-check / All

last.Vote.Step undefined (type gpbft.Payload has no field or method Step) (compile)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-check / All

last.Vote.Step undefined (type gpbft.Payload has no field or method Step) (compile)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-test / ubuntu (go this)

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-test / ubuntu (go this)

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-test / ubuntu (go next)

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)

Check failure on line 138 in emulator/driver_assertions.go

View workflow job for this annotation

GitHub Actions / go-test / ubuntu (go next)

last.Vote.Step undefined (type gpbft.Payload has no field or method Step)
d.require.Equal(round, last.Vote.Round, "Expected last vote round %d, but got %d", round, last.Vote.Round)
d.require.Equal(vote, last.Vote.Value, "Expected last vote value %s, but got %s", vote, last.Vote.Value)
}
11 changes: 10 additions & 1 deletion emulator/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (h *driverHost) getInstance(id uint64) *Instance {
return h.chain[id]
}

func (h *driverHost) popReceivedBroadcast() *gpbft.GMessage {
func (h *driverHost) popNextBroadcast() *gpbft.GMessage {
switch len(h.receivedBroadcasts) {
case 0:
return nil
Expand All @@ -107,6 +107,15 @@ func (h *driverHost) popReceivedBroadcast() *gpbft.GMessage {
}
}

func (h *driverHost) peekLastBroadcast() *gpbft.GMessage {
switch l := len(h.receivedBroadcasts); l {
case 0:
return nil
default:
return h.receivedBroadcasts[l-1]
}
}

func (h *driverHost) NetworkName() gpbft.NetworkName { return networkName }
func (h *driverHost) Time() time.Time { return h.now }
func (h *driverHost) SetAlarm(at time.Time) { h.pendingAlarm = &at }
122 changes: 122 additions & 0 deletions gpbft/gpbft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1622,3 +1622,125 @@ func TestGPBFT_DropOld(t *testing.T) {
// But we should still accept decides from the latest instance.
driver.RequireDeliverMessage(newDecide0)
}

func TestGPBFT_Sway(t *testing.T) {
t.Parallel()
newInstanceAndDriver := func(t *testing.T) (*emulator.Instance, *emulator.Driver) {
driver := emulator.NewDriver(t)
instance := emulator.NewInstance(t,
0,
gpbft.PowerEntries{
gpbft.PowerEntry{
ID: 0,
Power: gpbft.NewStoragePower(2),
},
gpbft.PowerEntry{
ID: 1,
Power: gpbft.NewStoragePower(2),
},
gpbft.PowerEntry{
ID: 2,
Power: gpbft.NewStoragePower(1),
},
gpbft.PowerEntry{
ID: 3,
Power: gpbft.NewStoragePower(2),
},
},
tipset0, tipSet1, tipSet2, tipSet3, tipSet4,
)
driver.AddInstance(instance)
driver.RequireNoBroadcast()
return instance, driver
}

swayed := func(t *testing.T) bool {
instance, driver := newInstanceAndDriver(t)
driver.StartInstance(instance.ID())
baseProposal := instance.Proposal().BaseChain()
proposal2 := baseProposal.Extend(tipSet1.Key)
proposal1 := proposal2.Extend(tipSet2.Key).Extend(tipSet3.Key)

// Trigger alarm to immediately complete QUALITY.
driver.RequireDeliverAlarm()
driver.RequirePeekAtLastVote(gpbft.PREPARE_PHASE, 0, baseProposal)

// Deliver PREPARE messages such that reaching quorum is impossible which should
// complete the phase.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 0,
Vote: instance.NewPrepare(0, instance.Proposal()),
})
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewPrepare(0, proposal1),
})
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 2,
Vote: instance.NewPrepare(0, proposal2),
})
driver.RequirePeekAtLastVote(gpbft.COMMIT_PHASE, 0, gpbft.ECChain{})

// Deliver COMMIT messages and trigger timeout to complete the phase but with no
// strong quorum. This should progress the instance to CONVERGE at round 1.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewCommit(0, proposal2),
Justification: instance.NewJustification(0, gpbft.PREPARE_PHASE, proposal2, 1, 3, 2),
})
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 0,
Vote: instance.NewCommit(0, gpbft.ECChain{}),
})
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 3,
Vote: instance.NewCommit(0, proposal1),
Justification: instance.NewJustification(0, gpbft.PREPARE_PHASE, proposal1, 1, 3, 2),
})
driver.RequireDeliverAlarm()

// Assert sway from base to either proposal1 or proposal2 at COMMIT.
latestBroadcast := driver.PeekLastBroadcastRequest()
require.Equal(t, gpbft.CONVERGE_PHASE, latestBroadcast.Vote.Step)
require.EqualValues(t, 1, latestBroadcast.Vote.Round)
swayedToProposal2AtCommit := latestBroadcast.Vote.Value.Eq(proposal2)
require.True(t, latestBroadcast.Vote.Value.Eq(proposal1) || swayedToProposal2AtCommit)
if !swayedToProposal2AtCommit {
// Only proceed if swayed to proposal2 at COMMIT, so that we can test sway to
// proposal1 at CONVERGE as an unseen proposal. Otherwise, it is possible that
// CONVERGE may stick with proposal2, i.e. an already seen candidate.
return false
}

// Deliver converge message for alternative proposal with strong quorum of
// PREPARE, which should sway the subject. Then trigger alarm to end CONVERGE.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 3,
Vote: instance.NewConverge(1, proposal1),
Ticket: emulator.ValidTicket,
Justification: instance.NewJustification(0, gpbft.PREPARE_PHASE, proposal1, 0, 1, 3, 2),
})
driver.RequireDeliverAlarm()

// Only pass if CONVERGE swayed to either proposal1 or proposal2 but not the same
// as whichever COMMIT swayed to.
latestBroadcast = driver.PeekLastBroadcastRequest()
require.Equal(t, gpbft.PREPARE_PHASE, latestBroadcast.Vote.Step)
require.EqualValues(t, 1, latestBroadcast.Vote.Round)
swayedToProposal1AtConverge := latestBroadcast.Vote.Value.Eq(proposal1)
require.True(t, swayedToProposal1AtConverge || latestBroadcast.Vote.Value.Eq(proposal2))
return swayedToProposal1AtConverge
}

// Swaying is nondeterministic. Require that eventually we sway to one proposal in
// COMMIT and other in CONVERGE.
t.Run("Sways to alternative unseen proposal at COMMIT and CONVERGE", func(t *testing.T) {
// Try 10 times to hit the exact sway we want.
for i := 0; i < 10; i++ {
if swayed(t) {
return
}
}
require.Fail(t, "after 10 tries did not swayed to proposals 1 and 2 at CONVERGE and COMMIT, respectively.")
})
}

0 comments on commit d84abb1

Please sign in to comment.