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

Test sway at commit and converge for unseen candidates #617

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 phase, round and vote.
func (d *Driver) RequirePeekAtLastVote(phase gpbft.Phase, round uint64, vote gpbft.ECChain) {
last := d.host.peekLastBroadcast()
d.require.Equal(phase, last.Vote.Phase, "Expected last vote phase %s, but got %s", phase, last.Vote.Phase)
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 @@
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) peekLastBroadcast() *gpbft.GMessage {
switch l := len(h.receivedBroadcasts); l {
case 0:
return nil

Check warning on line 113 in emulator/host.go

View check run for this annotation

Codecov / codecov/patch

emulator/host.go#L112-L113

Added lines #L112 - L113 were not covered by tests
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.RequireStartInstance(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.Phase)
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.Phase)
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.")
})
}
2 changes: 1 addition & 1 deletion gpbft/ticket_rank.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

// ComputeTicketRank computes a rank for the given ticket, weighted by a
// participant's power. A lower rank value indicates a better ranking. The
// process involves the following steps:
// process involves the following phases:
//
// 1. Hash the ticket using the Blake2b256 hash function.
// 2. Extract the low 128 bits from the hash and interpret them as a Q.128
Expand Down
Loading