Skip to content
Draft
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
4 changes: 4 additions & 0 deletions sei-tendermint/internal/p2p/giga_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ type GigaRouterCommonConfig struct {

// Whether validator should proxy txs which do not belong to the local node.
EnableEvmProxy bool

// EventBus receives NewBlock, NewBlockHeader, and per-tx events after
// each Autobahn commit.
EventBus types.BlockEventPublisher
}

// GigaValidatorConfig configures a committee-member GigaRouter.
Expand Down
49 changes: 49 additions & 0 deletions sei-tendermint/internal/p2p/giga_router_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,62 @@
if err != nil {
return nil, fmt.Errorf("app.Commit(): %w", err)
}
// Indexer-backed RPCs (broadcast_tx_commit, /tx) wait on these events.
r.publishExecutedBlockEvents(b, proposerAddress, resp)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Autobahn event reindex path

Medium Severity

Autobahn now publishes indexer events only inside executeBlock after app.Commit, but restart recovery advances from the app tip without re-publishing the last committed height. A crash after commit and before those events are indexed permanently skips that block for /tx and broadcast_tx_commit. CometBFT covers this with handshake replayEvents; Autobahn skips the handshaker.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1c31e68. Configure here.

if err := r.data.PushAppHash(ctx, b.GlobalNumber, resp.AppHash); err != nil {
return nil, fmt.Errorf("r.data.PushAppHash(%v): %w", b.GlobalNumber, err)
}
r.data.PushGasUsed(finalizeBlockGasUsed(resp))
return commitResp, nil
}

// publishExecutedBlockEvents publishes NewBlock, NewBlockHeader, and per-tx
// events for a committed Autobahn block.
func (r *gigaRouterCommon) publishExecutedBlockEvents(
b *atypes.GlobalBlock,
proposer types.Address,
resp *abci.ResponseFinalizeBlock,
) {
translated := r.translateGlobalBlock(b)
translated.Block.Header.ProposerAddress = proposer

Check failure on line 264 in sei-tendermint/internal/p2p/giga_router_common.go

View workflow job for this annotation

GitHub Actions / lint

QF1008: could remove embedded field "Header" from selector (staticcheck)
block := translated.Block
blockID := translated.BlockID
if len(resp.TxResults) != len(block.Txs) {
panic(fmt.Sprintf("number of TXs (%d) and ABCI TX responses (%d) do not match",
len(block.Txs), len(resp.TxResults)))
}
eventBus := r.cfg.EventBus

if err := eventBus.PublishEventNewBlock(types.EventDataNewBlock{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The translated block's header has no ValidatorsHash, so Block.Hash() returns nil for these events (types/block.go:511 short-circuits on len(h.ValidatorsHash) == 0). Only BlockID carries the real Autobahn hash. That was harmless while nothing consumed these events, but publishing them makes consumers that key off Block.Hash() start emitting zero hashes rather than nothing: the RPC event log subscribes to all bus events and is on by default (event-log-window-size = 30s), and evmrpc's block filters read exactly that field — getBlockHeadersAfter does common.BytesToHash(block.Block.Hash()) (evmrpc/filter.go:695), so eth_newBlockFilter + eth_getFilterChanges will now return 0x000…0 for every Autobahn block, which clients can mistake for a real hash. Same applies to websocket tm.event='NewBlock' subscribers. Either populate the header fields the hash derives from, or switch the affected consumers to BlockID (and note the limitation on translateGlobalBlock, whose doc currently lists only the evmrpc receipt path as a caller).

Block: block,
BlockID: blockID,
ResultFinalizeBlock: *resp,
}); err != nil {
logger.Error("failed publishing new block", "err", err)
}

if err := eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
Header: block.Header,
NumTxs: int64(len(block.Txs)),
ResultFinalizeBlock: *resp,
}); err != nil {
logger.Error("failed publishing new block header", "err", err)
}

for i, tx := range block.Txs {
if err := eventBus.PublishEventTx(types.EventDataTx{
TxResultV2: abci.TxResultV2{
Height: block.Height,
Index: uint32(i), //nolint:gosec // i is bounded by block.Txs length which fits in uint32
Tx: tx,
Result: *(resp.TxResults[i]),
},
}); err != nil {
logger.Error("failed publishing event TX", "err", err)
}
}
}

// manages lifecycle of evmrpc connections to validators.
func (r *gigaRouterCommon) runEvmProxies(ctx context.Context) error {
return scope.Run(ctx, func(ctx context.Context, s scope.Scope) error {
Expand Down
25 changes: 25 additions & 0 deletions sei-tendermint/internal/p2p/giga_router_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ import (
"time"

ethrpc "github.com/ethereum/go-ethereum/rpc"
dbm "github.com/tendermint/tm-db"

"github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/memblock"
"github.com/sei-protocol/sei-chain/sei-db/state_db/sc/hashvault"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/blockstore"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/data"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/epoch"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/state/indexer"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/state/indexer/sink/kv"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require"
tmtypes "github.com/sei-protocol/sei-chain/sei-tendermint/types"
Expand All @@ -31,6 +36,26 @@ func registerEvmProxyForTest(t *testing.T, router *gigaRouterCommon, validator a
return client
}

func startedEventBus(t *testing.T) *eventbus.EventBus {
t.Helper()
bus := eventbus.NewDefault()
require.NoError(t, bus.Start(t.Context()))
t.Cleanup(bus.Wait)
return bus
}

func startedTxIndexer(t *testing.T, bus *eventbus.EventBus) indexer.EventSink {
t.Helper()
sink := kv.NewEventSink(dbm.NewMemDB(), nil)
svc := indexer.NewService(indexer.ServiceArgs{
Sinks: []indexer.EventSink{sink},
EventBus: bus,
})
require.NoError(t, svc.Start(t.Context()))
t.Cleanup(svc.Wait)
return sink
}

type fixedHeightApp struct {
abci.BaseApplication
height int64
Expand Down
164 changes: 164 additions & 0 deletions sei-tendermint/internal/p2p/giga_router_events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package p2p

import (
"testing"
"time"

abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus"
tmpubsub "github.com/sei-protocol/sei-chain/sei-tendermint/internal/pubsub"
tmquery "github.com/sei-protocol/sei-chain/sei-tendermint/internal/pubsub/query"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require"
"github.com/sei-protocol/sei-chain/sei-tendermint/types"
)

func testGlobalBlock(t *testing.T, rng utils.Rng, height atypes.GlobalBlockNumber, txs [][]byte) *atypes.GlobalBlock {
t.Helper()
payload, err := atypes.PayloadBuilder{
CreatedAt: time.Now(),
Txs: txs,
}.Build()
require.NoError(t, err)
block := atypes.NewBlock(atypes.GenLaneID(rng), 1, atypes.GenBlockHeaderHash(rng), payload)
return &atypes.GlobalBlock{
Header: block.Header(),
Timestamp: time.Now(),
GlobalNumber: height,
Payload: payload,
}
}

func subscribe(t *testing.T, bus *eventbus.EventBus, clientID string, query *tmquery.Query, limit int) eventbus.Subscription {
t.Helper()
sub, err := bus.SubscribeWithArgs(t.Context(), tmpubsub.SubscribeArgs{
ClientID: clientID,
Query: query,
Limit: limit,
})
require.NoError(t, err)
return sub
}

func waitForN(t *testing.T, sub eventbus.Subscription, n int) {
t.Helper()
for range n {
_, err := sub.Next(t.Context())
require.NoError(t, err)
}
}

func TestPublishExecutedBlockEventsIndexesTxs(t *testing.T) {
rng := utils.TestRng()

// Setup: EventBus, KV indexer, and subscriptions for header + tx events.
genDoc := &types.GenesisDoc{ChainID: "giga-index-test", InitialHeight: 1}
require.NoError(t, genDoc.ValidateAndComplete())

bus := startedEventBus(t)
sink := startedTxIndexer(t, bus)
txSub := subscribe(t, bus, "txs", types.EventQueryTx, 8)
headerSub := subscribe(t, bus, "headers", types.EventQueryNewBlockHeader, 8)

// Setup: committed Autobahn block with two txs and matching FinalizeBlock results.
txs := [][]byte{[]byte("tx-a"), []byte("tx-b")}
height := atypes.GlobalBlockNumber(7)
gb := testGlobalBlock(t, rng, height, txs)
resp := &abci.ResponseFinalizeBlock{
TxResults: []*abci.ExecTxResult{
{Code: abci.CodeTypeOK, GasUsed: 11},
{Code: abci.CodeTypeOK, GasUsed: 22},
},
}

// Test: publish NewBlock / NewBlockHeader / per-tx events as executeBlock does.
r := &gigaRouterCommon{cfg: &GigaRouterCommonConfig{GenDoc: genDoc, EventBus: bus}}
r.publishExecutedBlockEvents(gb, nil, resp)

// Validate: indexer has the block and each tx by hash, with height/index/gas.
waitForN(t, headerSub, 1)
waitForN(t, txSub, len(txs))

ok, err := sink.HasBlock(int64(height))
require.NoError(t, err)
require.True(t, ok)

for i, tx := range txs {
got, err := sink.GetTxByHash(types.Tx(tx).Hash().Bytes())
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, int64(height), got.Height)
require.Equal(t, uint32(i), got.Index)
require.Equal(t, tx, []byte(got.Tx))
require.Equal(t, resp.TxResults[i].GasUsed, got.Result.GasUsed)
}
}

func TestPublishExecutedBlockEventsIndexesEmptyBlock(t *testing.T) {
rng := utils.TestRng()

// Setup: EventBus, KV indexer, and a header subscription.
genDoc := &types.GenesisDoc{ChainID: "giga-index-empty", InitialHeight: 1}
require.NoError(t, genDoc.ValidateAndComplete())

bus := startedEventBus(t)
sink := startedTxIndexer(t, bus)
headerSub := subscribe(t, bus, "headers", types.EventQueryNewBlockHeader, 4)

// Test: publish events for a committed block with no txs.
height := atypes.GlobalBlockNumber(3)
gb := testGlobalBlock(t, rng, height, nil)
r := &gigaRouterCommon{cfg: &GigaRouterCommonConfig{GenDoc: genDoc, EventBus: bus}}
r.publishExecutedBlockEvents(gb, nil, &abci.ResponseFinalizeBlock{})

// Validate: indexer recorded the empty block.
waitForN(t, headerSub, 1)

ok, err := sink.HasBlock(int64(height))
require.NoError(t, err)
require.True(t, ok)
}

func TestPublishExecutedBlockEventsPanicsBeforeDispatchOnTxCountMismatch(t *testing.T) {
rng := utils.TestRng()

// Setup: counting EventBus and a one-tx block whose TxResults length does not match.
genDoc := &types.GenesisDoc{ChainID: "giga-index-mismatch", InitialHeight: 1}
require.NoError(t, genDoc.ValidateAndComplete())

bus := &countingBlockEvents{}
gb := testGlobalBlock(t, rng, 1, [][]byte{[]byte("tx-a")})
r := &gigaRouterCommon{cfg: &GigaRouterCommonConfig{GenDoc: genDoc, EventBus: bus}}

// Test: publish with empty TxResults.
require.Panics(t, func() {
r.publishExecutedBlockEvents(gb, nil, &abci.ResponseFinalizeBlock{})
})

// Validate: no EventBus methods ran before the panic.
require.Equal(t, 0, bus.n)
}

type countingBlockEvents struct{ n int }

func (c *countingBlockEvents) PublishEventNewBlock(types.EventDataNewBlock) error {
c.n++
return nil
}
func (c *countingBlockEvents) PublishEventNewBlockHeader(types.EventDataNewBlockHeader) error {
c.n++
return nil
}
func (c *countingBlockEvents) PublishEventNewEvidence(types.EventDataNewEvidence) error {
c.n++
return nil
}
func (c *countingBlockEvents) PublishEventTx(types.EventDataTx) error {
c.n++
return nil
}
func (c *countingBlockEvents) PublishEventValidatorSetUpdates(types.EventDataValidatorSetUpdates) error {
c.n++
return nil
}
19 changes: 19 additions & 0 deletions sei-tendermint/internal/p2p/giga_router_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import (
atypes "github.com/sei-protocol/sei-chain/sei-tendermint/autobahn/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/blockstore"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/producer"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/eventbus"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/conn"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/giga"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/p2p/rpc"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/proxy"
"github.com/sei-protocol/sei-chain/sei-tendermint/internal/state/indexer"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/require"
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils/scope"
Expand Down Expand Up @@ -60,6 +62,8 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) {
var apps []*testApp
var gigas []*gigaValidatorRouter
var allTxs [][]byte
var indexSink indexer.EventSink
var indexSub eventbus.Subscription
for i, cfg := range cfgs {
nodeInfo := makeInfo(cfg.nodeKey)
nodeInfo.ListenAddr = cfg.addr.String()
Expand All @@ -80,6 +84,12 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) {
blockStore, err := blockstore.New(db)
require.NoError(t, err, "blockstore.New[%v]", i)
t.Cleanup(func() { _ = blockStore.Close() })
bus := startedEventBus(t)
if i == 0 {
// Setup: node 0 KV indexer (the sink BroadcastTxCommit polls).
indexSink = startedTxIndexer(t, bus)
indexSub = subscribe(t, bus, "node0-txs", types.EventQueryTx, 1024)
}
commonCfg := GigaRouterCommonConfig{
// Aggressive dialing rate to speed up startup.
DialInterval: 100 * time.Millisecond,
Expand All @@ -88,6 +98,7 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) {
App: proxyApp,
GenDoc: genDoc,
EnableEvmProxy: true,
EventBus: bus,
}
dataState, err := BuildDataState(&commonCfg, blockStore)
require.NoError(t, err, "BuildDataState[%v]", i)
Expand Down Expand Up @@ -146,6 +157,14 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) {
require.NoError(t, app.WaitForTx(ctx, tx), "WaitForTx")
}
}
// Validate: every submitted tx is in the KV indexer by hash.
waitForN(t, indexSub, len(allTxs))
for _, tx := range allTxs {
got, err := indexSink.GetTxByHash(types.Tx(tx).Hash().Bytes())
require.NoError(t, err, "GetTxByHash")
require.NotNil(t, got)
require.Equal(t, tx, []byte(got.Tx))
}
// Nodes should agree on the final state.
want := apps[0].Snapshot()
for i, app := range apps {
Expand Down
Loading
Loading