diff --git a/eth/executionclient/logs.go b/eth/executionclient/logs.go index 37d421d891..cea62baa46 100644 --- a/eth/executionclient/logs.go +++ b/eth/executionclient/logs.go @@ -14,12 +14,20 @@ type BlockLogs struct { // PackLogs packs logs into []BlockLogs by their block number. func PackLogs(logs []ethtypes.Log) []BlockLogs { - // Sort the logs by block number. + // Sort into canonical on-chain order. The Index (logIndex) tiebreaker is what keeps logs + // emitted by the same transaction in order: sort.Slice is not stable, and a single tx can + // emit multiple order-dependent registry events (e.g. bulkRegisterValidator emits one + // ValidatorAdded per validator, each bumping the owner's nonce), which the handler must + // process in order. Without it, same-tx logs could be reordered and valid registrations + // silently rejected on a nonce mismatch. sort.Slice(logs, func(i, j int) bool { - if logs[i].BlockNumber == logs[j].BlockNumber { + if logs[i].BlockNumber != logs[j].BlockNumber { + return logs[i].BlockNumber < logs[j].BlockNumber + } + if logs[i].TxIndex != logs[j].TxIndex { return logs[i].TxIndex < logs[j].TxIndex } - return logs[i].BlockNumber < logs[j].BlockNumber + return logs[i].Index < logs[j].Index }) var all []BlockLogs diff --git a/eth/executionclient/logs_test.go b/eth/executionclient/logs_test.go index 9bbd3a7507..14594fcf24 100644 --- a/eth/executionclient/logs_test.go +++ b/eth/executionclient/logs_test.go @@ -83,3 +83,26 @@ func TestPackLogs(t *testing.T) { assert.Equal(t, uint(0), result[0].Logs[0].TxIndex) // should be sorted assert.Equal(t, uint(1), result[0].Logs[1].TxIndex) } + +// TestPackLogsOrdersByLogIndexWithinTransaction covers logs emitted by the same transaction +// (same TxIndex, distinct logIndex) — e.g. bulkRegisterValidator, whose per-owner nonces require +// in-order processing. They must be packed in logIndex order, not left in the arbitrary order a +// non-stable sort by (block, tx) would leave them. Input is deliberately shuffled. +func TestPackLogsOrdersByLogIndexWithinTransaction(t *testing.T) { + logs := []types.Log{ + {BlockNumber: 5, TxIndex: 2, Index: 11}, + {BlockNumber: 5, TxIndex: 2, Index: 9}, + {BlockNumber: 5, TxIndex: 0, Index: 3}, // earlier tx in the same block + {BlockNumber: 5, TxIndex: 2, Index: 10}, + } + + result := PackLogs(logs) + assert.Len(t, result, 1) + assert.Equal(t, uint64(5), result[0].BlockNumber) + + gotIndexes := make([]uint, 0, len(result[0].Logs)) + for _, l := range result[0].Logs { + gotIndexes = append(gotIndexes, l.Index) + } + assert.Equal(t, []uint{3, 9, 10, 11}, gotIndexes) +}