Skip to content

Commit 7523c93

Browse files
committed
fix/non-determinism-in-aggregator-order
1 parent b39dab3 commit 7523c93

2 files changed

Lines changed: 48 additions & 7 deletions

File tree

pkg/capabilities/consensus/ocr3/aggregators/reduce_aggregator.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -468,16 +468,17 @@ func mode(items []values.Value) (values.Value, int, error) {
468468
}
469469
}
470470

471-
var modes []values.Value
472-
for _, ctr := range counts {
471+
var tied [][32]byte
472+
for sha, ctr := range counts {
473473
if ctr.count == maxCount {
474-
modes = append(modes, ctr.fullObservation)
474+
tied = append(tied, sha)
475475
}
476476
}
477-
478-
// If more than one mode found, choose first
479-
480-
return modes[0], maxCount, nil
477+
slices.SortFunc(tied, func(a, b [32]byte) int {
478+
return bytes.Compare(a[:], b[:])
479+
})
480+
// If more than one mode ties for max count, pick the one with smallest content hash (stable across nodes).
481+
return counts[tied[0]].fullObservation, maxCount, nil
481482
}
482483

483484
func modeHasQuorum(quorumType string, count int, f int) error {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package aggregators
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"google.golang.org/protobuf/proto"
8+
9+
"github.com/smartcontractkit/chainlink-protos/cre/go/values"
10+
"github.com/smartcontractkit/chainlink-protos/cre/go/values/pb"
11+
)
12+
13+
// TestMode_twoWayTieIsDeterministicAcrossRepeatedCalls targets a bimodal tie: two
14+
// distinct values each appear the same number of times (here, 3× "tie-a" and 3× "tie-b").
15+
// The winner must not depend on map iteration order, so repeated calls with the same
16+
// multiset must return an observation equal under protobuf semantics every time.
17+
func TestMode_twoWayTieIsDeterministicAcrossRepeatedCalls(t *testing.T) {
18+
t.Parallel()
19+
20+
a, err := values.Wrap("tie-a")
21+
require.NoError(t, err)
22+
b, err := values.Wrap("tie-b")
23+
require.NoError(t, err)
24+
items := []values.Value{a, a, a, b, b, b}
25+
26+
var baseline *pb.Value
27+
for i := range 400 {
28+
got, maxCount, err := mode(items)
29+
require.NoError(t, err)
30+
require.Equal(t, 3, maxCount)
31+
32+
gotProto := values.Proto(got)
33+
if i == 0 {
34+
baseline = proto.Clone(gotProto).(*pb.Value)
35+
continue
36+
}
37+
require.True(t, proto.Equal(baseline, gotProto),
38+
"iteration %d: mode() must pick the same tied winner on every call", i)
39+
}
40+
}

0 commit comments

Comments
 (0)