Skip to content

Commit 301bac1

Browse files
committed
Merge branch 'andrius/fix-relayed-key-lookup' into 'main'
[MINOR] fix(finalizer): use dedicated key type for already-relayed lookup See merge request flarenetwork/FSP/flare-system-client!82
2 parents 1bf9fe6 + a585fb4 commit 301bac1

3 files changed

Lines changed: 91 additions & 5 deletions

File tree

client/finalizer/finalizer_queue.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func (p *finalizerQueueProcessor) processDelayedQueue(ctx context.Context, items
212212
}
213213

214214
for _, item := range items {
215-
if relayedItems[*item] {
215+
if relayedItems[relayedKey{protocolID: item.protocolID, votingRoundID: item.votingRoundID}] {
216216
continue
217217
}
218218
logger.Infof("Finalizer processes delayed queue item for round %v for protocol %v", item.votingRoundID, item.protocolID)

client/finalizer/relay_client.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,34 @@ func (r *relayContractClient) SubmitPayloads(ctx context.Context, input []byte,
198198
}
199199
}
200200

201-
// ProtocolMessageRelayed returns a set of pairs of protocol and round that have been finalized.
202-
func (r *relayContractClient) ProtocolMessageRelayed(ctx context.Context, db finalizerDB, from time.Time, to time.Time) (map[queueItem]bool, error) {
201+
// relayedKey is the lookup key for ProtocolMessageRelayed events.
202+
//
203+
// It deliberately excludes the seed and msgHash fields of queueItem:
204+
// ProtocolMessageRelayed events identify a finalization uniquely by
205+
// (protocolID, votingRoundID), and using queueItem directly as a map
206+
// key would compare *big.Int by pointer identity — guaranteeing the
207+
// lookup in processDelayedQueue never matches.
208+
type relayedKey struct {
209+
protocolID uint8
210+
votingRoundID uint32
211+
}
212+
213+
// ProtocolMessageRelayed returns a set of (protocolID, votingRoundID)
214+
// pairs that have already been finalized on chain in the given time
215+
// range.
216+
func (r *relayContractClient) ProtocolMessageRelayed(ctx context.Context, db finalizerDB, from time.Time, to time.Time) (map[relayedKey]bool, error) {
203217
logs, err := db.FetchLogsByAddressAndTopic0(ctx, r.address, r.topic0PMR, from.Unix(), to.Unix())
204218
if err != nil {
205219
return nil, err
206220
}
207221

208-
result := make(map[queueItem]bool)
222+
result := make(map[relayedKey]bool)
209223
for _, log := range logs {
210224
data, err := shared.ParseProtocolMessageRelayedEvent(r.relay, log)
211225
if err != nil {
212226
return nil, err
213227
}
214-
result[queueItem{
228+
result[relayedKey{
215229
protocolID: data.ProtocolId,
216230
votingRoundID: data.VotingRoundId,
217231
}] = true
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package finalizer
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestRelayedKey_LookupMatchesWithUnrelatedQueueItemFields pins the
12+
// fix for the duplicate-suppression bug in processDelayedQueue: the
13+
// lookup key built from a ProtocolMessageRelayed event must match the
14+
// lookup performed against a queueItem regardless of that item's seed
15+
// (*big.Int, pointer-compared by maps) and msgHash.
16+
//
17+
// Pre-fix, ProtocolMessageRelayed used queueItem as the map key with
18+
// only protocolID + votingRoundID populated, so the lookup with a
19+
// fully-populated queueItem never matched and every delayed item was
20+
// re-sent (the relay contract returns the non-fatal "Already relayed"
21+
// error, so the bug only burned gas).
22+
func TestRelayedKey_LookupMatchesWithUnrelatedQueueItemFields(t *testing.T) {
23+
relayed := map[relayedKey]bool{
24+
{protocolID: 100, votingRoundID: 42}: true,
25+
{protocolID: 200, votingRoundID: 99}: true,
26+
}
27+
28+
item := &queueItem{
29+
seed: big.NewInt(0xdeadbeef), // non-nil pointer — would have broken the old lookup
30+
votingRoundID: 42,
31+
protocolID: 100,
32+
msgHash: common.HexToHash("0xabc"), // non-zero — would have broken the old lookup
33+
}
34+
35+
require.True(
36+
t,
37+
relayed[relayedKey{protocolID: item.protocolID, votingRoundID: item.votingRoundID}],
38+
"already-relayed lookup must match by (protocolID, votingRoundID) regardless of seed/msgHash",
39+
)
40+
41+
miss := &queueItem{
42+
seed: big.NewInt(1),
43+
votingRoundID: 43, // different round
44+
protocolID: 100,
45+
msgHash: common.HexToHash("0xabc"),
46+
}
47+
require.False(
48+
t,
49+
relayed[relayedKey{protocolID: miss.protocolID, votingRoundID: miss.votingRoundID}],
50+
"different (protocolID, votingRoundID) must not match",
51+
)
52+
}
53+
54+
// TestRelayedKey_TwoNonNilSeedsDoNotCollide makes the failure mode of
55+
// the pre-fix code explicit: under the old shape, two queueItems
56+
// holding distinct *big.Int seeds compared as DIFFERENT map keys even
57+
// when their (protocolID, votingRoundID) were equal — that's exactly
58+
// why the producer-built map (seed=nil) never matched the
59+
// consumer-side lookup (seed != nil).
60+
func TestRelayedKey_TwoNonNilSeedsDoNotCollide(t *testing.T) {
61+
a := queueItem{seed: big.NewInt(1), votingRoundID: 1, protocolID: 1}
62+
b := queueItem{seed: big.NewInt(1), votingRoundID: 1, protocolID: 1}
63+
64+
m := map[queueItem]bool{a: true}
65+
require.False(t, m[b], "*big.Int is pointer-compared in map keys — distinct allocations of equal values DO NOT match")
66+
67+
// And the fix: relayedKey excludes the pointer field entirely.
68+
ra := relayedKey{protocolID: 1, votingRoundID: 1}
69+
rb := relayedKey{protocolID: 1, votingRoundID: 1}
70+
rm := map[relayedKey]bool{ra: true}
71+
require.True(t, rm[rb], "relayedKey is pure value type — equal values match")
72+
}

0 commit comments

Comments
 (0)