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

core/qbft: include round when deduping #1505

Merged
merged 1 commit into from
Dec 6, 2022
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
23 changes: 12 additions & 11 deletions core/qbft/qbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ var ruleLabels = map[UponRule]string{
UponRoundTimeout: "round_timeout",
}

// dedupKey defines the key used to deduplicate upon rules.
type dedupKey struct {
UponRule UponRule
Round int64
}

// Run executes the consensus algorithm until the context closed.
// The generic type I is the instance of consensus and can be anything.
// The generic type V is the arbitrary data value being proposed; it only requires an Equal method.
Expand Down Expand Up @@ -188,7 +194,7 @@ func Run[I any, V comparable](ctx context.Context, d Definition[I, V], t Transpo
preparedJustification []Msg[I, V]
qCommit []Msg[I, V]
buffer = make(map[int64][]Msg[I, V])
dedupRules = make(map[UponRule]int64) // map[UponRule]msg.Round()
dedupRules = make(map[dedupKey]bool)
timerChan <-chan time.Time
stopTimer func()
)
Expand Down Expand Up @@ -219,17 +225,12 @@ func Run[I any, V comparable](ctx context.Context, d Definition[I, V], t Transpo

// isDuplicatedRule returns true if the rule has been already executed since last round change.
isDuplicatedRule := func(rule UponRule, msgRound int64) bool {
prevRound, ok := dedupRules[rule]
if !ok {
dedupRules[rule] = msgRound
key := dedupKey{UponRule: rule, Round: msgRound}

return false
}
if !dedupRules[key] {
dedupRules[key] = true

if prevRound != msgRound {
// Upon rules are either for the current round,
// or for a future round followed by a round change (which clears this map).
panic("bug: duplicate rule, but different round")
return false
}

return true
Expand All @@ -242,7 +243,7 @@ func Run[I any, V comparable](ctx context.Context, d Definition[I, V], t Transpo
}
d.LogRoundChange(ctx, instance, process, round, newRound, rule, extractRoundMsgs(buffer, round))
round = newRound
dedupRules = make(map[UponRule]int64)
dedupRules = make(map[dedupKey]bool)
}

// === Algorithm ===
Expand Down
64 changes: 64 additions & 0 deletions core/qbft/qbft_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
)

func TestQBFT(t *testing.T) {
Expand Down Expand Up @@ -535,3 +537,65 @@ func makeIsLeader(n int64) func(int64, int64, int64) bool {
return (instance+round)%n == process
}
}

// TestDuplicatePrePreparesRules tests that two pre-prepares for different rounds are not detected as duplicates.
func TestDuplicatePrePreparesRules(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

const (
noLeader = 1
leader = 2
)

newPreprepare := func(round int64) Msg[int64, int64] {
return msg{
msgType: MsgPrePrepare,
peerIdx: leader,
round: round,
// Justification not required since nodes and quorum both 0.
}
}

def := noopDef
def.IsLeader = func(_ int64, _ int64, process int64) bool {
return process == leader
}
def.LogUponRule = func(ctx context.Context, instance int64, process, round int64, msg Msg[int64, int64], uponRule UponRule) {
log.Info(ctx, "UponRule", z.Str("rule", uponRule.String()), z.I64("round", msg.Round()))
require.Equal(t, uponRule, UponJustifiedPrePrepare)
if msg.Round() == 1 {
return
}
if msg.Round() == 2 {
cancel()
return
}
require.Fail(t, "unexpected round", "round=%d", round)
}

ch := make(chan Msg[int64, int64], 2)
ch <- newPreprepare(1)
ch <- newPreprepare(2)

transport := noopTransport
transport.Receive = ch

_ = Run(ctx, def, transport, 0, noLeader, 1)
}

// noopTransport is a transport that does nothing.
var noopTransport = Transport[int64, int64]{
Broadcast: func(context.Context, MsgType, int64, int64, int64, int64, int64, int64, []Msg[int64, int64]) error {
return nil
},
}

// noopDef is a definition that does nothing.
var noopDef = Definition[int64, int64]{
IsLeader: func(int64, int64, int64) bool { return false },
NewTimer: func(int64) (<-chan time.Time, func()) { return nil, func() {} },
LogUponRule: func(context.Context, int64, int64, int64, Msg[int64, int64], UponRule) {},
LogRoundChange: func(context.Context, int64, int64, int64, int64, UponRule, []Msg[int64, int64]) {},
LogUnjust: func(context.Context, int64, int64, Msg[int64, int64]) {},
}