diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index fecb56233a..5434979ab0 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -184,7 +184,7 @@ func (r *GigaRouter) BlockByNumber(ctx context.Context, n atypes.GlobalBlockNumb // TODO(autobahn): replace this with a direct read from // sei-db/ledger_db/block.BlockDB.GetBlockByHash once a writer is wired into // block execution. The data.State-side index can also go away at that point. -func (r *GigaRouter) BlockByHash(_ context.Context, hash atypes.BlockHeaderHash) (*coretypes.ResultBlock, error) { +func (r *GigaRouter) BlockByHash(ctx context.Context, hash atypes.BlockHeaderHash) (*coretypes.ResultBlock, error) { opt, err := r.data.GlobalBlockByHash(hash) if err != nil { return nil, fmt.Errorf("data.GlobalBlockByHash: %w", err) @@ -203,12 +203,20 @@ func (r *GigaRouter) BlockByHash(_ context.Context, hash atypes.BlockHeaderHash) // translateGlobalBlock converts an Autobahn GlobalBlock to the CometBFT // coretypes.ResultBlock shape used by env.Block / env.BlockByHash and -// downstream evmrpc consumers. Caller must pass a non-nil *GlobalBlock -// with non-nil Header and Payload — that's the contract data.State -// guarantees on a successful lookup, and matches how executeBlock -// dereferences b.Header without a nil-check on the same type. The -// "no such block" case is rejected at the BlockByHash call site -// before delegating here. +// downstream evmrpc consumers. Caller must pass a non-nil *GlobalBlock with +// non-nil Header and Payload — that's the contract data.State guarantees on +// a successful lookup, and matches how executeBlock dereferences b.Header +// without a nil-check on the same type. The "no such block" case is +// rejected at the BlockByHash call site before delegating here. +// +// LastCommit is non-nil with empty Signatures, mirroring executeBlock's +// FinalizeBlock call which passes an empty abci.CommitInfo. Under Autobahn +// the committee is fixed by genesis (no validator-set updates), so the +// application is not in control of jailing — surfacing N "absent sig" +// entries here would make trace replay's BeginBlock bump missed-block +// counters and diverge from production. ToReqBeginBlock skips the per- +// validator loop when Signatures is empty, so empty Votes flow into +// distribution/slashing on both paths. func (r *GigaRouter) translateGlobalBlock(gb *atypes.GlobalBlock) *coretypes.ResultBlock { srcTxs := gb.Payload.Txs() tmTxs := make(types.Txs, len(srcTxs)) @@ -227,7 +235,8 @@ func (r *GigaRouter) translateGlobalBlock(gb *atypes.GlobalBlock) *coretypes.Res Height: utils.Clamp[int64](gb.GlobalNumber), Time: gb.Timestamp, }, - Data: types.Data{Txs: tmTxs}, + Data: types.Data{Txs: tmTxs}, + LastCommit: &types.Commit{}, }, } } diff --git a/sei-tendermint/internal/p2p/giga_router_test.go b/sei-tendermint/internal/p2p/giga_router_test.go index 075a976f61..eb15d2593d 100644 --- a/sei-tendermint/internal/p2p/giga_router_test.go +++ b/sei-tendermint/internal/p2p/giga_router_test.go @@ -372,6 +372,14 @@ func TestGigaRouter_FinalizeBlocks(t *testing.T) { require.Equal(t, committed, rb.Block.Height, "router[%v].BlockByNumber(%v) height", i, committed) require.NotEmpty(t, rb.BlockID.Hash, "router[%v].BlockByNumber(%v) block hash", i, committed) require.Equal(t, genDoc.ChainID, rb.Block.Header.ChainID, "router[%v].BlockByNumber(%v) chain id", i, committed) + // LastCommit is non-nil with empty Signatures — mirrors + // executeBlock's FinalizeBlock(DecidedLastCommit: empty) + // so trace replay and production both see "no votes" on + // the prior block. ToReqBeginBlock skips the per-val loop + // when Signatures is empty, so this is also enough to + // avoid the OOB deref the original PR was guarding against. + require.NotNil(t, rb.Block.LastCommit, "router[%v].BlockByNumber(%v) LastCommit", i, committed) + require.Empty(t, rb.Block.LastCommit.Signatures, "router[%v].BlockByNumber(%v) Signatures", i, committed) // Round-trip the just-fetched block hash back through // BlockByHash and assert we get the same ResultBlock back. var hashKey atypes.BlockHeaderHash diff --git a/sei-tendermint/types/block.go b/sei-tendermint/types/block.go index 07d85242d0..8382fcb430 100644 --- a/sei-tendermint/types/block.go +++ b/sei-tendermint/types/block.go @@ -251,13 +251,17 @@ func (b *Block) ToProto() (*tmproto.Block, error) { func (b *Block) ToReqBeginBlock(vals []*Validator) abci.RequestBeginBlock { tmHeader := b.Header.ToProto() - votes := make([]abci.VoteInfo, 0, b.LastCommit.Size()) - for i, val := range vals { - commitSig := b.LastCommit.Signatures[i] - votes = append(votes, abci.VoteInfo{ - Validator: TM2PB.Validator(val), - SignedLastBlock: commitSig.BlockIDFlag != BlockIDFlagAbsent, - }) + // b.LastCommit.Signatures is only empty on the trace path. + var votes []abci.VoteInfo + if len(b.LastCommit.Signatures) > 0 { + votes = make([]abci.VoteInfo, 0, b.LastCommit.Size()) + for i, val := range vals { + commitSig := b.LastCommit.Signatures[i] + votes = append(votes, abci.VoteInfo{ + Validator: TM2PB.Validator(val), + SignedLastBlock: commitSig.BlockIDFlag != BlockIDFlagAbsent, + }) + } } abciEvidence := b.Evidence.ToABCI() byzantineValidators := make([]abci.Evidence, 0, len(abciEvidence))