Skip to content

Commit

Permalink
Include QUALITY messages in rebroadcast (#597)
Browse files Browse the repository at this point in the history
To improve censorship resistance and avoid lack of progress due to
partially disseminated QUALITY messages include them in rebroadcast.

Part of #591
  • Loading branch information
masih authored Aug 27, 2024
1 parent 0d6c270 commit 3cd2e35
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 20 deletions.
39 changes: 32 additions & 7 deletions gpbft/gpbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -1374,29 +1374,54 @@ func newBroadcastState() *broadcastState {
// record stores messages that are required should rebroadcast becomes necessary.
// The messages stored depend on the current progress of instance. If the
// instance progresses to DECIDE then only the decide message will be recorded.
// Otherwise, all messages except QUALITY from the current and previous rounds
// Otherwise, all messages including QUALITY from the current and previous rounds
// (i.e. the latest two rounds) are recorded.
//
// Note, the messages recorded are more than what FIL-00896 strictly requires at
// the benefit of less reliance on rebroadcast and reduction in the possibility
// of participants getting stuck.
//
// The rationale for including QUALITY messages as part of rebroadcast is to
// improve censorship resistance at the face of unreliable message delivery,
// which can ultimately result in lack of instance progress for extended periods
// of time. See:
// - https://github.com/filecoin-project/FIPs/discussions/809#discussioncomment-10409902
// - https://github.com/filecoin-project/FIPs/discussions/809#discussioncomment-10424988
func (bs *broadcastState) record(mb *MessageBuilder) {
switch mb.Payload.Step {
case DECIDE_PHASE:
// Clear all previous messages, as only DECIDE message need to be rebroadcasted.
// Note that DECIDE message is not associated to any round, and is always
// broadcasted using round zero.
bs.messagesByRound = make(map[uint64][]*MessageBuilder)
clear(bs.messagesByRound)
bs.messagesByRound[0] = []*MessageBuilder{mb}
case COMMIT_PHASE, PREPARE_PHASE, CONVERGE_PHASE:
case QUALITY_PHASE, CONVERGE_PHASE, PREPARE_PHASE, COMMIT_PHASE:
bs.messagesByRound[mb.Payload.Round] = append(bs.messagesByRound[mb.Payload.Round], mb)
// Remove all messages that are older than the latest two rounds.
// Remove all messages that are older than the latest two rounds, except QUALITY
// which only appears in round 0.
for round := int(mb.Payload.Round) - 2; round >= 0; round-- {
redundantRound := uint64(round)
delete(bs.messagesByRound, redundantRound)
switch redundantRound := uint64(round); redundantRound {
case 0:
var found bool
for i, mb := range bs.messagesByRound[0] {
if mb.Payload.Step == QUALITY_PHASE {
bs.messagesByRound[0] = bs.messagesByRound[0][i : i+1 : 1]
found = true
break
}
}
if !found {
log.Warn("No QUALITY message found for round 0 while trimming rebroadcast messages")
delete(bs.messagesByRound, redundantRound)
}
default:
delete(bs.messagesByRound, redundantRound)
}
}
default:
// No need to rebroadcast QUALITY messages.
// There should not be any message recorded with any other payload step. Warn if
// we see any.
log.Warnw("Unexpected payload step while recording broadcasted messages", "step", mb.Payload.Step)
}
}

Expand Down
70 changes: 57 additions & 13 deletions gpbft/gpbft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,14 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
driver.RequireNoBroadcast()
// Trigger rebroadcast alarm.
driver.RequireDeliverAlarm()
// Expect rebroadcast of PREPARE only; no QUALITY message should be rebroadcasted.
// Expect rebroadcast of QUALITY and PREPARE messages.
driver.RequireQuality()
driver.RequirePrepare(baseChain)
driver.RequireNoBroadcast()
// Trigger alarm and expect immediate rebroadcast of PREMARE.
driver.RequireDeliverAlarm()
// Expect rebroadcast of PREPARE only; no QUALITY message should be rebroadcasted.
// Expect rebroadcast of QUALITY and PREPARE messages.
driver.RequireQuality()
driver.RequirePrepare(baseChain)
driver.RequireNoBroadcast()

Expand All @@ -302,7 +304,8 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
// Trigger rebroadcast alarm.
driver.RequireDeliverAlarm()

// Expect rebroadcast of PREPARE and COMMIT.
// Expect rebroadcast of QUALITY, PREPARE and COMMIT.
driver.RequireQuality()
driver.RequirePrepare(baseChain)
driver.RequireCommit(0, baseChain, evidenceOfPrepare)
driver.RequireNoBroadcast()
Expand Down Expand Up @@ -331,7 +334,8 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
// Trigger rebroadcast alarm.
driver.RequireDeliverAlarm()

// Expect rebroadcast of all messages from previous and current round (except QUALITY)
// Expect rebroadcast of all messages from previous and current round, including QUALITY.
driver.RequireQuality()
driver.RequirePrepare(baseChain)
driver.RequireCommit(0, baseChain, evidenceOfPrepare)
driver.RequireConverge(1, baseChain, evidenceOfPrepareForBase)
Expand All @@ -357,39 +361,79 @@ func TestGPBFT_WithEvenPowerDistribution(t *testing.T) {
// Trigger rebroadcast alarm.
driver.RequireDeliverAlarm()

// Expect rebroadcast of all messages from previous and current round, except
// Expect rebroadcast of all messages from previous and current round, including
// QUALITY.
driver.RequireQuality()
driver.RequirePrepare(baseChain)
driver.RequireCommit(0, baseChain, evidenceOfPrepare)
driver.RequireConverge(1, baseChain, evidenceOfPrepareForBase)
driver.RequirePrepareAtRound(1, baseChain, evidenceOfPrepareForBase)
driver.RequireCommit(1, baseChain, evidenceOfPrepareAtRound1)

// Deliver COMMIT at round 1 to facilitate progress to DECIDE.
// Facilitate skip to future round to assert rebroadcast only contains messages
// from the latest 2 rounds, plus QUALITY from round 0.

futureRoundProposal := instance.Proposal().Extend(tipSet4.Key)
evidenceOfPrepareAtRound76 := instance.NewJustification(76, gpbft.PREPARE_PHASE, futureRoundProposal, 0, 1)

// Send Prepare messages to facilitate weak quorum of prepare at future round.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewCommit(1, baseChain),
Justification: evidenceOfPrepareAtRound1,
Vote: instance.NewPrepare(77, futureRoundProposal),
Justification: evidenceOfPrepareAtRound76,
})
// Send Converge at future round to facilitate proposal with highest ticket.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewConverge(77, futureRoundProposal),
Justification: evidenceOfPrepareAtRound76,
Ticket: emulator.ValidTicket,
})
// Expect skip to round.
driver.RequireConverge(77, futureRoundProposal, evidenceOfPrepareAtRound76)
driver.RequirePrepareAtRound(77, futureRoundProposal, evidenceOfPrepareAtRound76)
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound76)
// Expect no messages until the rebroadcast timeout has expired.
driver.RequireNoBroadcast()
// Trigger rebroadcast alarm.
driver.RequireDeliverAlarm()

// Assert that rebroadcast includes QUALITY and messages from round 77, but none
// from round 1 since the node should only retain messages from the latest two
// rounds, plus quality since it skiped round and has never seen round 76.
//
// See: https://github.com/filecoin-project/go-f3/issues/595
driver.RequireQuality()
driver.RequireConverge(77, futureRoundProposal, evidenceOfPrepareAtRound76)
driver.RequirePrepareAtRound(77, futureRoundProposal, evidenceOfPrepareAtRound76)
driver.RequireCommit(77, futureRoundProposal, evidenceOfPrepareAtRound76)
driver.RequireNoBroadcast()

// Deliver COMMIT at round 77 to facilitate progress to DECIDE.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewCommit(77, futureRoundProposal),
Justification: evidenceOfPrepareAtRound76,
})

// Expect DECIDE with strong evidence of COMMIT.
evidenceOfCommit := instance.NewJustification(1, gpbft.COMMIT_PHASE, baseChain, 0, 1)
driver.RequireDecide(baseChain, evidenceOfCommit)
evidenceOfCommit := instance.NewJustification(77, gpbft.COMMIT_PHASE, futureRoundProposal, 0, 1)
driver.RequireDecide(futureRoundProposal, evidenceOfCommit)

// Trigger alarm and expect immediate rebroadcast, because DECIDE phase has no
// phase timeout.
driver.RequireDeliverAlarm()

// Expect rebroadcast of only the DECIDE message.
driver.RequireDecide(baseChain, evidenceOfCommit)
driver.RequireDecide(futureRoundProposal, evidenceOfCommit)

// Deliver DECIDE to facilitate progress to decision.
driver.RequireDeliverMessage(&gpbft.GMessage{
Sender: 1,
Vote: instance.NewDecide(0, baseChain),
Vote: instance.NewDecide(0, futureRoundProposal),
Justification: evidenceOfCommit,
})
driver.RequireDecision(instance.ID(), baseChain)
driver.RequireDecision(instance.ID(), futureRoundProposal)
})
}

Expand Down

0 comments on commit 3cd2e35

Please sign in to comment.