diff --git a/sei-db/ledger_db/block/block_db.go b/sei-db/ledger_db/block/block_db.go index 564e97b7e8..1dc0eb81aa 100644 --- a/sei-db/ledger_db/block/block_db.go +++ b/sei-db/ledger_db/block/block_db.go @@ -3,76 +3,93 @@ package block import ( "context" "errors" + "time" ) // ErrNoBlocks is returned by GetLowestBlockHeight and GetHighestBlockHeight // when the database contains no blocks. var ErrNoBlocks = errors.New("block db: no blocks") -// A binary transaction with its hash. -type BinaryTransaction struct { - // The hash of the transaction. - Hash []byte - // The binary transaction data. - Transaction []byte +// Transaction is the BlockDB's view of a transaction inside a block: its +// hash plus its raw bytes. BlockDB itself is block-storage-only — it does +// not index transactions by hash. Per the canonical-receipt-lookup design, +// tx-by-hash routing belongs in a separate Receipt Store; BlockDB exposes +// per-tx Hash() so a Receipt Store (or any other caller) can iterate +// `Block.Transactions()` and register its own (txHash → block, index) +// mapping at WriteBlock time. +type Transaction interface { + // Hash returns the canonical transaction hash. + Hash() []byte + // Bytes returns the raw, on-the-wire transaction bytes. + Bytes() []byte } -// A binary block with its transactions and hash. -type BinaryBlock struct { - // The height of the block. Must be unique. - Height uint64 - // The hash of the block. Must be unique. - Hash []byte - // The binary block data, not including transaction data (unless you are ok with wasting space) - BlockData []byte - // The transactions in the block and their hashes. - Transactions []*BinaryTransaction +// Block is the BlockDB's view of a finalized block. The interface intentionally +// exposes only what BlockDB itself needs to index and serve reads — backends +// must not assume any particular concrete implementation. +// +// Backends are permitted to call Transactions() multiple times across the +// block's lifetime in storage. Implementations that pay a non-trivial cost +// per call (allocation, hashing) should memoize the result at construction. +type Block interface { + // Hash returns the canonical block hash used for indexing. + Hash() []byte + // Height returns the block height (used as the key for the height index). + Height() uint64 + // Time returns the block timestamp. + Time() time.Time + // Transactions returns the block's transactions in order. Must be cheap + // to call repeatedly — backends may call it more than once per block. + Transactions() []Transaction } -// A database for storing binary block and transaction data. +// A database for storing finalized blocks. Block-only — the canonical +// "transaction by hash → execution result" lookup belongs in a separate +// Receipt Store (see the Giga Transaction Query proposal); a future +// Receipt Store reads tx bodies out of BlockDB by (blockHash, index) +// once it has resolved a hash. // -// This store is fully threadsafe. All writes are atomic (that is, after a crash you will either see the write or -// you will not see it at all, i.e. partial writes are not possible). Multiple writes are not atomic with respect -// to each other, meaning if you write A then B and crash, you may observe B but not A (only possible when sharding -// is enabled). Within a single session, read-your-writes consistency is provided. +// This store is fully threadsafe. All writes are atomic (after a crash +// you will either see the write or you will not see it at all, i.e. +// partial writes are not possible). Multiple writes are not atomic with +// respect to each other, meaning if you write A then B and crash, you +// may observe B but not A. Within a single session, read-your-writes +// consistency is provided. type BlockDB interface { - // Write a block to the database. + // WriteBlock writes a block to the database. Idempotent on duplicate + // block hash: a second WriteBlock for the same blockHash is a no-op, + // not an error. // - // This method may return immediately and does not necessarily wait for the block to be written to disk. - // Call Flush() if you need to wait until the block is written to disk. - WriteBlock(ctx context.Context, block *BinaryBlock) error + // This method may return immediately and does not necessarily wait for + // the block to be written to disk. Call Flush() if you need to wait. + WriteBlock(ctx context.Context, block Block) error - // Blocks until all pending writes are flushed to disk. Any call to WriteBlock issued before calling Flush() - // will be crash-durable after Flush() returns. Calls to WriteBlock() made concurrently with Flush() may or - // may not be crash-durable after Flush() returns (but are otherwise eventually durable). - // - // It is not required to call Flush() in order to ensure data is written to disk. The database asyncronously - // pushes data down to disk even if Flush() is never called. Flush() just allows you to syncronize an external - // goroutine with the database's internal write loop. + // Flush blocks until all pending writes are durable. WriteBlocks issued + // before calling Flush() will be crash-durable after Flush() returns. + // Concurrent WriteBlocks may or may not be durable after Flush() + // returns (but are otherwise eventually durable). Flush(ctx context.Context) error - // Retrieves a block by its hash. - GetBlockByHash(ctx context.Context, hash []byte) (block *BinaryBlock, ok bool, err error) - - // Retrieves a block by its height. - GetBlockByHeight(ctx context.Context, height uint64) (block *BinaryBlock, ok bool, err error) + // GetBlockByHash retrieves a block by its hash. + GetBlockByHash(ctx context.Context, hash []byte) (block Block, ok bool, err error) - // Retrieves a transaction by its hash. - GetTransactionByHash(ctx context.Context, hash []byte) (transaction *BinaryTransaction, ok bool, err error) + // GetBlockByHeight retrieves a block by its height. + GetBlockByHeight(ctx context.Context, height uint64) (block Block, ok bool, err error) - // Schedules pruning for all blocks with a height less than the given height. Pruning is asynchronous, - // and so this method does not provide any guarantees about when the pruning will complete. It is possible - // that some data will not be pruned if the database is closed before the pruning is scheduled. + // Prune schedules pruning of all blocks with height < lowestHeightToKeep. + // Pruning is asynchronous; this method does not guarantee when it will + // complete. Some data may not be pruned if the database is closed before + // pruning is scheduled. Prune(ctx context.Context, lowestHeightToKeep uint64) error - // Retrieves the lowest block height in the database. + // GetLowestBlockHeight returns the lowest block height in the database. GetLowestBlockHeight(ctx context.Context) (uint64, error) - // Retrieves the highest block height in the database. + // GetHighestBlockHeight returns the highest block height in the database. GetHighestBlockHeight(ctx context.Context) (uint64, error) - // Closes the database and releases any resources. Any in-flight writes are fully flushed to disk before this - // method returns. + // Close shuts the database down and releases any resources. Any in-flight + // writes are fully flushed to disk before this method returns. Close(ctx context.Context) error } diff --git a/sei-db/ledger_db/block/block_db_test/block_db_test.go b/sei-db/ledger_db/block/block_db_test/block_db_test.go index ed0ae33f9d..626ba20771 100644 --- a/sei-db/ledger_db/block/block_db_test/block_db_test.go +++ b/sei-db/ledger_db/block/block_db_test/block_db_test.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "testing" + "time" crand "github.com/sei-protocol/sei-chain/sei-db/common/rand" "github.com/sei-protocol/sei-chain/sei-db/common/unit" @@ -35,19 +36,38 @@ func newMemBlockDBBuilder() blockDBBuilder { } } -func makeBlock(height uint64, numTxs int) *block.BinaryBlock { - txs := make([]*block.BinaryTransaction, numTxs) +type testTx struct { + hash []byte + bytes []byte +} + +func (t *testTx) Hash() []byte { return t.hash } +func (t *testTx) Bytes() []byte { return t.bytes } + +type testBlock struct { + hash []byte + height uint64 + time time.Time + txs []block.Transaction +} + +func (b *testBlock) Hash() []byte { return b.hash } +func (b *testBlock) Height() uint64 { return b.height } +func (b *testBlock) Time() time.Time { return b.time } +func (b *testBlock) Transactions() []block.Transaction { return b.txs } + +func makeBlock(height uint64, numTxs int) *testBlock { + txs := make([]block.Transaction, numTxs) for i := 0; i < numTxs; i++ { - txs[i] = &block.BinaryTransaction{ - Hash: []byte(fmt.Sprintf("tx-%d-%d", height, i)), - Transaction: []byte(fmt.Sprintf("tx-data-%d-%d", height, i)), + txs[i] = &testTx{ + hash: []byte(fmt.Sprintf("tx-%d-%d", height, i)), + bytes: []byte(fmt.Sprintf("tx-data-%d-%d", height, i)), } } - return &block.BinaryBlock{ - Height: height, - Hash: []byte(fmt.Sprintf("block-%d", height)), - BlockData: []byte(fmt.Sprintf("block-data-%d", height)), - Transactions: txs, + return &testBlock{ + hash: []byte(fmt.Sprintf("block-%d", height)), + height: height, + txs: txs, } } @@ -86,33 +106,13 @@ func TestWriteAndGetBlockByHash(t *testing.T) { blk := makeBlock(5, 3) requireNoError(t, db.WriteBlock(ctx, blk)) - got, ok, err := db.GetBlockByHash(ctx, blk.Hash) + got, ok, err := db.GetBlockByHash(ctx, blk.Hash()) requireNoError(t, err) requireTrue(t, ok, "expected block with matching hash") requireBlockEqual(t, blk, got) }) } -func TestGetTransactionByHash(t *testing.T) { - forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { - ctx := context.Background() - db, err := builder(t.TempDir()) - requireNoError(t, err) - defer db.Close(ctx) - - blk := makeBlock(1, 4) - requireNoError(t, db.WriteBlock(ctx, blk)) - - for _, tx := range blk.Transactions { - got, ok, err := db.GetTransactionByHash(ctx, tx.Hash) - requireNoError(t, err) - requireTrue(t, ok, "expected transaction with hash %s", tx.Hash) - requireBytesEqual(t, tx.Hash, got.Hash, "transaction hash") - requireBytesEqual(t, tx.Transaction, got.Transaction, "transaction data") - } - }) -} - func TestGetBlockNotFound(t *testing.T) { forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { ctx := context.Background() @@ -130,19 +130,6 @@ func TestGetBlockNotFound(t *testing.T) { }) } -func TestGetTransactionNotFound(t *testing.T) { - forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { - ctx := context.Background() - db, err := builder(t.TempDir()) - requireNoError(t, err) - defer db.Close(ctx) - - _, ok, err := db.GetTransactionByHash(ctx, []byte("nonexistent")) - requireNoError(t, err) - requireTrue(t, !ok, "expected no transaction with nonexistent hash") - }) -} - func TestMultipleBlocks(t *testing.T) { forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { ctx := context.Background() @@ -150,61 +137,60 @@ func TestMultipleBlocks(t *testing.T) { requireNoError(t, err) defer db.Close(ctx) - blocks := make([]*block.BinaryBlock, 10) + blocks := make([]*testBlock, 10) for i := range blocks { blocks[i] = makeBlock(uint64(i+1), 2) requireNoError(t, db.WriteBlock(ctx, blocks[i])) } for _, blk := range blocks { - got, ok, err := db.GetBlockByHeight(ctx, blk.Height) + got, ok, err := db.GetBlockByHeight(ctx, blk.Height()) requireNoError(t, err) - requireTrue(t, ok, "expected block at height %d", blk.Height) + requireTrue(t, ok, "expected block at height %d", blk.Height()) requireBlockEqual(t, blk, got) } }) } -func TestPrunePreservesUnprunedBlocks(t *testing.T) { +// TestWriteBlockIdempotent pins the contract that re-writing the same +// block hash is a silent no-op rather than an error or an overwrite. +func TestWriteBlockIdempotent(t *testing.T) { forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { ctx := context.Background() db, err := builder(t.TempDir()) requireNoError(t, err) defer db.Close(ctx) - for i := uint64(1); i <= 10; i++ { - requireNoError(t, db.WriteBlock(ctx, makeBlock(i, 1))) - } - - requireNoError(t, db.Flush(ctx)) - requireNoError(t, db.Prune(ctx, 6)) + blk := makeBlock(4, 2) + requireNoError(t, db.WriteBlock(ctx, blk)) + // Second WriteBlock for the same block hash — must not error. + requireNoError(t, db.WriteBlock(ctx, blk)) - for i := uint64(6); i <= 10; i++ { - _, ok, err := db.GetBlockByHeight(ctx, i) - requireNoError(t, err) - requireTrue(t, ok, "expected block at height %d to survive pruning", i) - } + got, ok, err := db.GetBlockByHash(ctx, blk.Hash()) + requireNoError(t, err) + requireTrue(t, ok, "expected block still present after re-write") + requireBlockEqual(t, blk, got) }) } -func TestPrunePreservesUnprunedTransactions(t *testing.T) { +func TestPrunePreservesUnprunedBlocks(t *testing.T) { forEachBuilder(t, func(t *testing.T, builder func(string) (block.BlockDB, error)) { ctx := context.Background() db, err := builder(t.TempDir()) requireNoError(t, err) defer db.Close(ctx) - survivingBlock := makeBlock(2, 3) - requireNoError(t, db.WriteBlock(ctx, makeBlock(1, 1))) - requireNoError(t, db.WriteBlock(ctx, survivingBlock)) + for i := uint64(1); i <= 10; i++ { + requireNoError(t, db.WriteBlock(ctx, makeBlock(i, 1))) + } requireNoError(t, db.Flush(ctx)) - requireNoError(t, db.Prune(ctx, 2)) + requireNoError(t, db.Prune(ctx, 6)) - for _, tx := range survivingBlock.Transactions { - _, ok, err := db.GetTransactionByHash(ctx, tx.Hash) + for i := uint64(6); i <= 10; i++ { + _, ok, err := db.GetBlockByHeight(ctx, i) requireNoError(t, err) - requireTrue(t, ok, "expected transaction %s to survive pruning", tx.Hash) + requireTrue(t, ok, "expected block at height %d to survive pruning", i) } }) } @@ -248,13 +234,6 @@ func TestCloseAndReopen(t *testing.T) { requireNoError(t, err) requireTrue(t, ok, "expected block to survive close/reopen") requireBlockEqual(t, blk, got) - - for _, tx := range blk.Transactions { - gotTx, ok, err := db2.GetTransactionByHash(ctx, tx.Hash) - requireNoError(t, err) - requireTrue(t, ok, "expected tx to survive close/reopen") - requireBytesEqual(t, tx.Transaction, gotTx.Transaction, "transaction data") - } }) } @@ -307,7 +286,7 @@ func TestBulkWriteAndQuery(t *testing.T) { requireNoError(t, err) defer db.Close(ctx) - blocks := make([]*block.BinaryBlock, numBlocks) + blocks := make([]*testBlock, numBlocks) for i := range blocks { blocks[i] = makeRandomBlock(testRng, uint64(i+1), txsPerBlock) requireNoError(t, db.WriteBlock(ctx, blocks[i])) @@ -316,47 +295,35 @@ func TestBulkWriteAndQuery(t *testing.T) { requireNoError(t, db.Flush(ctx)) for _, expected := range blocks { - byHeight, ok, err := db.GetBlockByHeight(ctx, expected.Height) + byHeight, ok, err := db.GetBlockByHeight(ctx, expected.Height()) requireNoError(t, err) - requireTrue(t, ok, "block not found by height %d", expected.Height) - requireBlockBytesEqual(t, expected, byHeight) + requireTrue(t, ok, "block not found by height %d", expected.Height()) + requireBlockEqual(t, expected, byHeight) - byHash, ok, err := db.GetBlockByHash(ctx, expected.Hash) + byHash, ok, err := db.GetBlockByHash(ctx, expected.Hash()) requireNoError(t, err) - requireTrue(t, ok, "block not found by hash at height %d", expected.Height) - requireBlockBytesEqual(t, expected, byHash) - - for _, expectedTx := range expected.Transactions { - gotTx, ok, err := db.GetTransactionByHash(ctx, expectedTx.Hash) - requireNoError(t, err) - requireTrue(t, ok, "tx not found by hash %x (block height %d)", expectedTx.Hash, expected.Height) - requireBytesEqual(t, expectedTx.Hash, gotTx.Hash, "tx hash") - requireBytesEqual(t, expectedTx.Transaction, gotTx.Transaction, "tx data") - } + requireTrue(t, ok, "block not found by hash at height %d", expected.Height()) + requireBlockEqual(t, expected, byHash) } }) } // makeRandomBlock builds a block with deterministic random binary payloads. // Returned slices are owned copies safe for storage and later comparison. -func makeRandomBlock(rng *crand.CannedRandom, height uint64, numTxs int) *block.BinaryBlock { - txs := make([]*block.BinaryTransaction, numTxs) +func makeRandomBlock(rng *crand.CannedRandom, height uint64, numTxs int) *testBlock { + txs := make([]block.Transaction, numTxs) for i := range txs { txHash := rng.Address('t', int64(height)*1000+int64(i), 32) txDataLen := 64 + int(rng.Int64Range(0, 512)) txData := copyBytes(rng.Bytes(txDataLen)) - txs[i] = &block.BinaryTransaction{Hash: txHash, Transaction: txData} + txs[i] = &testTx{hash: txHash, bytes: txData} } blockHash := rng.Address('b', int64(height), 32) - blockDataLen := 128 + int(rng.Int64Range(0, 1024)) - blockData := copyBytes(rng.Bytes(blockDataLen)) - - return &block.BinaryBlock{ - Height: height, - Hash: blockHash, - BlockData: blockData, - Transactions: txs, + return &testBlock{ + hash: blockHash, + height: height, + txs: txs, } } @@ -366,26 +333,6 @@ func copyBytes(src []byte) []byte { return dst } -// requireBlockBytesEqual does a deep byte-level comparison, suitable for verifying -// round-trip fidelity through serialization. -func requireBlockBytesEqual(t *testing.T, expected, actual *block.BinaryBlock) { - t.Helper() - if expected.Height != actual.Height { - t.Fatalf("height mismatch: expected %d, got %d", expected.Height, actual.Height) - } - requireBytesEqual(t, expected.Hash, actual.Hash, "block hash") - requireBytesEqual(t, expected.BlockData, actual.BlockData, "block data") - if len(expected.Transactions) != len(actual.Transactions) { - t.Fatalf("transaction count mismatch at height %d: expected %d, got %d", - expected.Height, len(expected.Transactions), len(actual.Transactions)) - } - for i, tx := range expected.Transactions { - label := fmt.Sprintf("height %d tx[%d]", expected.Height, i) - requireBytesEqual(t, tx.Hash, actual.Transactions[i].Hash, label+" hash") - requireBytesEqual(t, tx.Transaction, actual.Transactions[i].Transaction, label+" data") - } -} - // --- test helpers --- func requireNoError(t *testing.T, err error) { @@ -409,19 +356,19 @@ func requireBytesEqual(t *testing.T, expected, actual []byte, label string) { } } -func requireBlockEqual(t *testing.T, expected, actual *block.BinaryBlock) { +func requireBlockEqual(t *testing.T, expected, actual block.Block) { t.Helper() - if expected.Height != actual.Height { - t.Fatalf("height mismatch: expected %d, got %d", expected.Height, actual.Height) + if expected.Height() != actual.Height() { + t.Fatalf("height mismatch: expected %d, got %d", expected.Height(), actual.Height()) } - requireBytesEqual(t, expected.Hash, actual.Hash, "block hash") - requireBytesEqual(t, expected.BlockData, actual.BlockData, "block data") - if len(expected.Transactions) != len(actual.Transactions) { - t.Fatalf("transaction count mismatch: expected %d, got %d", - len(expected.Transactions), len(actual.Transactions)) + requireBytesEqual(t, expected.Hash(), actual.Hash(), "block hash") + expTxs := expected.Transactions() + actTxs := actual.Transactions() + if len(expTxs) != len(actTxs) { + t.Fatalf("transaction count mismatch: expected %d, got %d", len(expTxs), len(actTxs)) } - for i, tx := range expected.Transactions { - requireBytesEqual(t, tx.Hash, actual.Transactions[i].Hash, fmt.Sprintf("tx[%d] hash", i)) - requireBytesEqual(t, tx.Transaction, actual.Transactions[i].Transaction, fmt.Sprintf("tx[%d] data", i)) + for i, tx := range expTxs { + requireBytesEqual(t, tx.Hash(), actTxs[i].Hash(), fmt.Sprintf("tx[%d] hash", i)) + requireBytesEqual(t, tx.Bytes(), actTxs[i].Bytes(), fmt.Sprintf("tx[%d] data", i)) } } diff --git a/sei-db/ledger_db/block/blocksim/block_generator.go b/sei-db/ledger_db/block/blocksim/block_generator.go index cac1373233..b08b6de190 100644 --- a/sei-db/ledger_db/block/blocksim/block_generator.go +++ b/sei-db/ledger_db/block/blocksim/block_generator.go @@ -2,6 +2,7 @@ package blocksim import ( "context" + "time" "github.com/sei-protocol/sei-chain/sei-db/common/rand" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block" @@ -12,6 +13,32 @@ const ( txHashType = 't' ) +// genTx is a synthetic transaction that satisfies block.Transaction. +type genTx struct { + hash []byte + bytes []byte +} + +func (t *genTx) Hash() []byte { return t.hash } +func (t *genTx) Bytes() []byte { return t.bytes } + +// genBlock is a synthetic block that satisfies block.Block. extra is held to +// simulate block-level metadata bytes — the BlockDB contract has no field for +// it, but the bytes still occupy memory (and serialized space, for backends +// that materialize the whole Block). +type genBlock struct { + hash []byte + height uint64 + time time.Time + txs []block.Transaction + extra []byte +} + +func (b *genBlock) Hash() []byte { return b.hash } +func (b *genBlock) Height() uint64 { return b.height } +func (b *genBlock) Time() time.Time { return b.time } +func (b *genBlock) Transactions() []block.Transaction { return b.txs } + // Asynchronously generates random blocks and feeds them into a channel. type BlockGenerator struct { ctx context.Context @@ -22,7 +49,7 @@ type BlockGenerator struct { nextHeight uint64 // Generated blocks are sent to this channel. - blocksChan chan *block.BinaryBlock + blocksChan chan *genBlock } // Creates a new BlockGenerator and immediately starts its background goroutine. @@ -38,7 +65,7 @@ func NewBlockGenerator( config: config, rand: rng, nextHeight: startHeight, - blocksChan: make(chan *block.BinaryBlock, config.StagedBlockQueueSize), + blocksChan: make(chan *genBlock, config.StagedBlockQueueSize), } go g.mainLoop() return g @@ -46,7 +73,7 @@ func NewBlockGenerator( // NextBlock blocks until the next generated block is available and returns it. // Returns nil if the context has been cancelled and no more blocks will be produced. -func (g *BlockGenerator) NextBlock() *block.BinaryBlock { +func (g *BlockGenerator) NextBlock() *genBlock { select { case <-g.ctx.Done(): return nil @@ -66,26 +93,26 @@ func (g *BlockGenerator) mainLoop() { } } -func (g *BlockGenerator) buildBlock() *block.BinaryBlock { +func (g *BlockGenerator) buildBlock() *genBlock { height := g.nextHeight g.nextHeight++ - txs := make([]*block.BinaryTransaction, g.config.TransactionsPerBlock) + txs := make([]block.Transaction, g.config.TransactionsPerBlock) for i := uint64(0); i < g.config.TransactionsPerBlock; i++ { txID := int64(height)*int64(g.config.TransactionsPerBlock) + int64(i) //nolint:gosec - txs[i] = &block.BinaryTransaction{ - Hash: g.rand.Address(txHashType, txID, int(g.config.TransactionHashSize)), //nolint:gosec - Transaction: g.rand.Bytes(int(g.config.BytesPerTransaction)), //nolint:gosec + txs[i] = &genTx{ + hash: g.rand.Address(txHashType, txID, int(g.config.TransactionHashSize)), //nolint:gosec + bytes: g.rand.Bytes(int(g.config.BytesPerTransaction)), //nolint:gosec } } blockHash := g.rand.Address(blockHashType, int64(height), int(g.config.BlockHashSize)) //nolint:gosec - blockData := g.rand.Bytes(int(g.config.ExtraBytesPerBlock)) //nolint:gosec + extra := g.rand.Bytes(int(g.config.ExtraBytesPerBlock)) //nolint:gosec - return &block.BinaryBlock{ - Height: height, - Hash: blockHash, - BlockData: blockData, - Transactions: txs, + return &genBlock{ + hash: blockHash, + height: height, + txs: txs, + extra: extra, } } diff --git a/sei-db/ledger_db/block/blocksim/blocksim.go b/sei-db/ledger_db/block/blocksim/blocksim.go index 42bd3f9ff9..750b31a26f 100644 --- a/sei-db/ledger_db/block/blocksim/blocksim.go +++ b/sei-db/ledger_db/block/blocksim/blocksim.go @@ -164,23 +164,24 @@ func (b *BlockSim) maybeThrottle() { } } -func (b *BlockSim) handleNextBlock(blk *block.BinaryBlock) { +func (b *BlockSim) handleNextBlock(blk *genBlock) { b.metrics.SetMainThreadPhase("write_block") if err := b.db.WriteBlock(b.ctx, blk); err != nil { - fmt.Printf("failed to write block %d: %v\n", blk.Height, err) + fmt.Printf("failed to write block %d: %v\n", blk.Height(), err) b.cancel() return } - txCount := int64(len(blk.Transactions)) - blockBytes := int64(len(blk.Hash) + len(blk.BlockData)) - for _, tx := range blk.Transactions { - blockBytes += int64(len(tx.Hash) + len(tx.Transaction)) + txs := blk.Transactions() + txCount := int64(len(txs)) + blockBytes := int64(len(blk.Hash()) + len(blk.extra)) + for _, tx := range txs { + blockBytes += int64(len(tx.Hash()) + len(tx.Bytes())) } b.totalBlocksWritten++ b.totalTransactionsWritten += txCount b.totalBytesWritten += blockBytes - b.highestBlockHeight = blk.Height + b.highestBlockHeight = blk.Height() b.metrics.ReportBlockWritten(txCount, blockBytes) // Periodic flush. @@ -195,9 +196,9 @@ func (b *BlockSim) handleNextBlock(blk *block.BinaryBlock) { } // Periodic prune. - if blk.Height > 0 && blk.Height%b.config.PruneIntervalBlocks == 0 { + if blk.Height() > 0 && blk.Height()%b.config.PruneIntervalBlocks == 0 { b.metrics.SetMainThreadPhase("prune") - lowestToKeep := blk.Height - b.config.UnprunedBlocks + lowestToKeep := blk.Height() - b.config.UnprunedBlocks if err := b.db.Prune(b.ctx, lowestToKeep); err != nil { fmt.Printf("failed to prune: %v\n", err) b.cancel() diff --git a/sei-db/ledger_db/block/mem_block_db/mem_block_db.go b/sei-db/ledger_db/block/mem_block_db/mem_block_db.go index 2e32d6fcd4..b3f1422fde 100644 --- a/sei-db/ledger_db/block/mem_block_db/mem_block_db.go +++ b/sei-db/ledger_db/block/mem_block_db/mem_block_db.go @@ -10,16 +10,19 @@ import ( // Shared backing store, keyed by path in test builders to simulate restarts. type memBlockDBData struct { mu sync.RWMutex - blocksByHash map[string]*block.BinaryBlock - blocksByHeight map[uint64]*block.BinaryBlock - txByHash map[string]*block.BinaryTransaction + blocksByHash map[string]block.Block + blocksByHeight map[uint64]block.Block lowestHeight uint64 highestHeight uint64 hasBlocks bool } -// An in-memory implementation of the BlockDB interface. Useful as a test fixture to sanity check -// test flows. +// An in-memory implementation of the BlockDB interface. Useful as a test +// fixture and as a development-time backend until a persistent BlockDB lands. +// +// TODO(blockdb): add a -race concurrency test — every public method's lock +// shape (WriteBlock under write lock; Get* under read lock) is currently +// verified only by inspection. type memBlockDB struct { data *memBlockDBData } @@ -28,34 +31,37 @@ type memBlockDB struct { func NewMemBlockDB() block.BlockDB { return &memBlockDB{ data: &memBlockDBData{ - blocksByHash: make(map[string]*block.BinaryBlock), - blocksByHeight: make(map[uint64]*block.BinaryBlock), - txByHash: make(map[string]*block.BinaryTransaction), + blocksByHash: make(map[string]block.Block), + blocksByHeight: make(map[uint64]block.Block), }, } } -func (m *memBlockDB) WriteBlock(_ context.Context, blk *block.BinaryBlock) error { +func (m *memBlockDB) WriteBlock(_ context.Context, blk block.Block) error { d := m.data d.mu.Lock() defer d.mu.Unlock() - d.blocksByHash[string(blk.Hash)] = blk - d.blocksByHeight[blk.Height] = blk - for _, tx := range blk.Transactions { - d.txByHash[string(tx.Hash)] = tx + blockHashKey := string(blk.Hash()) + // Idempotent on duplicate: treat a second WriteBlock for the same block + // hash as a no-op rather than overwriting indexes. + if _, exists := d.blocksByHash[blockHashKey]; exists { + return nil } + height := blk.Height() + d.blocksByHash[blockHashKey] = blk + d.blocksByHeight[height] = blk if !d.hasBlocks { - d.lowestHeight = blk.Height - d.highestHeight = blk.Height + d.lowestHeight = height + d.highestHeight = height d.hasBlocks = true } else { - if blk.Height < d.lowestHeight { - d.lowestHeight = blk.Height + if height < d.lowestHeight { + d.lowestHeight = height } - if blk.Height > d.highestHeight { - d.highestHeight = blk.Height + if height > d.highestHeight { + d.highestHeight = height } } return nil @@ -65,7 +71,7 @@ func (m *memBlockDB) Flush(_ context.Context) error { return nil } -func (m *memBlockDB) GetBlockByHash(_ context.Context, hash []byte) (*block.BinaryBlock, bool, error) { +func (m *memBlockDB) GetBlockByHash(_ context.Context, hash []byte) (block.Block, bool, error) { d := m.data d.mu.RLock() defer d.mu.RUnlock() @@ -74,7 +80,7 @@ func (m *memBlockDB) GetBlockByHash(_ context.Context, hash []byte) (*block.Bina return blk, ok, nil } -func (m *memBlockDB) GetBlockByHeight(_ context.Context, height uint64) (*block.BinaryBlock, bool, error) { +func (m *memBlockDB) GetBlockByHeight(_ context.Context, height uint64) (block.Block, bool, error) { d := m.data d.mu.RLock() defer d.mu.RUnlock() @@ -83,15 +89,6 @@ func (m *memBlockDB) GetBlockByHeight(_ context.Context, height uint64) (*block. return blk, ok, nil } -func (m *memBlockDB) GetTransactionByHash(_ context.Context, hash []byte) (*block.BinaryTransaction, bool, error) { - d := m.data - d.mu.RLock() - defer d.mu.RUnlock() - - tx, ok := d.txByHash[string(hash)] - return tx, ok, nil -} - func (m *memBlockDB) Prune(_ context.Context, lowestHeightToKeep uint64) error { d := m.data d.mu.Lock() @@ -107,10 +104,7 @@ func (m *memBlockDB) Prune(_ context.Context, lowestHeightToKeep uint64) error { continue } delete(d.blocksByHeight, h) - delete(d.blocksByHash, string(blk.Hash)) - for _, tx := range blk.Transactions { - delete(d.txByHash, string(tx.Hash)) - } + delete(d.blocksByHash, string(blk.Hash())) } if lowestHeightToKeep > d.highestHeight { diff --git a/sei-db/ledger_db/block/mem_block_db/mem_block_db_test.go b/sei-db/ledger_db/block/mem_block_db/mem_block_db_test.go index 2cf400a923..edec11cede 100644 --- a/sei-db/ledger_db/block/mem_block_db/mem_block_db_test.go +++ b/sei-db/ledger_db/block/mem_block_db/mem_block_db_test.go @@ -4,23 +4,43 @@ import ( "context" "fmt" "testing" + "time" "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block" ) -func makeBlock(height uint64, numTxs int) *block.BinaryBlock { - txs := make([]*block.BinaryTransaction, numTxs) +type testTx struct { + hash []byte + bytes []byte +} + +func (t *testTx) Hash() []byte { return t.hash } +func (t *testTx) Bytes() []byte { return t.bytes } + +type testBlock struct { + hash []byte + height uint64 + time time.Time + txs []block.Transaction +} + +func (b *testBlock) Hash() []byte { return b.hash } +func (b *testBlock) Height() uint64 { return b.height } +func (b *testBlock) Time() time.Time { return b.time } +func (b *testBlock) Transactions() []block.Transaction { return b.txs } + +func makeBlock(height uint64, numTxs int) block.Block { + txs := make([]block.Transaction, numTxs) for i := 0; i < numTxs; i++ { - txs[i] = &block.BinaryTransaction{ - Hash: []byte(fmt.Sprintf("tx-%d-%d", height, i)), - Transaction: []byte(fmt.Sprintf("tx-data-%d-%d", height, i)), + txs[i] = &testTx{ + hash: []byte(fmt.Sprintf("tx-%d-%d", height, i)), + bytes: []byte(fmt.Sprintf("tx-data-%d-%d", height, i)), } } - return &block.BinaryBlock{ - Height: height, - Hash: []byte(fmt.Sprintf("block-%d", height)), - BlockData: []byte(fmt.Sprintf("block-data-%d", height)), - Transactions: txs, + return &testBlock{ + hash: []byte(fmt.Sprintf("block-%d", height)), + height: height, + txs: txs, } } diff --git a/sei-tendermint/internal/autobahn/data/state.go b/sei-tendermint/internal/autobahn/data/state.go index 89b3b537bb..0bf846d332 100644 --- a/sei-tendermint/internal/autobahn/data/state.go +++ b/sei-tendermint/internal/autobahn/data/state.go @@ -149,17 +149,6 @@ type inner struct { blocks map[types.GlobalBlockNumber]*types.Block // [first,nextBlock) + subset of [nextBlock,nextQC) appProposals map[types.GlobalBlockNumber]appProposalWithTimestamp // [first,nextAppProposal) - // blockHashes is a hash → height index mirroring blocks. Maintained - // in lockstep with blocks via insertBlock / pruneFirst, so it covers - // exactly the same retain window without a separate prune cursor or - // startup warmup. Powers BlockByHash. - // - // TODO(autobahn): remove once a writer is wired into block execution - // that populates sei-db/ledger_db/block.BlockDB. BlockDB has a built-in - // hash index that survives process restart and lives outside Autobahn's - // RetainHeight pruning, making this in-memory index obsolete. - blockHashes map[types.BlockHeaderHash]types.GlobalBlockNumber - // first <= nextAppProposal <= nextBlockToPersist <= nextBlock <= nextQC // // This invariant guarantees no race between pruning and persisting: @@ -179,7 +168,6 @@ func newInner(committee *types.Committee) *inner { qcs: map[types.GlobalBlockNumber]*types.FullCommitQC{}, blocks: map[types.GlobalBlockNumber]*types.Block{}, appProposals: map[types.GlobalBlockNumber]appProposalWithTimestamp{}, - blockHashes: map[types.BlockHeaderHash]types.GlobalBlockNumber{}, first: first, nextAppProposal: first, nextBlockToPersist: first, @@ -243,7 +231,6 @@ func (i *inner) insertBlock(committee *types.Committee, n types.GlobalBlockNumbe return fmt.Errorf("block %d header hash mismatch: want %v, got %v", n, want, got) } i.blocks[n] = block - i.blockHashes[got] = n return nil } @@ -269,7 +256,6 @@ func (i *inner) pruneFirst(now time.Time, m *dataMetrics) { delete(i.appProposals, i.first) delete(i.blocks, i.first) delete(i.qcs, i.first) - delete(i.blockHashes, b.Header().Hash()) i.first += 1 } @@ -449,33 +435,6 @@ func (s *State) NextBlock() types.GlobalBlockNumber { panic("unreachable") } -// GlobalBlockByHash returns the finalized GlobalBlock whose stored header -// hashes to the given value, or None if no such block is currently in the -// retained range. The lookup-and-construct happens under a single lock so -// the returned block matches the looked-up hash atomically — pruning can't -// change which height a hash maps to between the index check and the block -// construction. Tracks the same retain window as Block / GlobalBlock since -// the hash index is maintained in lockstep by insertBlock / pruneFirst. -// -// Returns an error in the signature for forward-compat with the eventual -// switch to sei-db/ledger_db/block.BlockDB.GetBlockByHash. Today's -// in-memory implementation never errors. -// -// TODO(autobahn): when BlockDB is wired, take a ctx parameter and narrow -// the error contract — db-internal errors should surface by shutting down -// the persistence background task (matching how persistence handles errors -// today), so the query path's error stays bounded to context.Canceled. -func (s *State) GlobalBlockByHash(hash types.BlockHeaderHash) (utils.Option[*types.GlobalBlock], error) { - for inner := range s.inner.Lock() { - n, ok := inner.blockHashes[hash] - if !ok { - return utils.None[*types.GlobalBlock](), nil - } - return utils.Some(inner.globalBlockAt(s.Committee(), n)), nil - } - panic("unreachable") -} - // Block returns the block with the given global number. // This function is used for syncing - GlobalBlock can be derived from Block and FullCommitQC, // which have to be fetched upfront anyway. diff --git a/sei-tendermint/internal/autobahn/data/state_test.go b/sei-tendermint/internal/autobahn/data/state_test.go index 6c99c1f3be..d85f9d777b 100644 --- a/sei-tendermint/internal/autobahn/data/state_test.go +++ b/sei-tendermint/internal/autobahn/data/state_test.go @@ -331,58 +331,6 @@ func TestPushBlockAcceptsBlockWithQC(t *testing.T) { require.Equal(t, blocks[0], got) } -// TestGlobalBlockByHash isolates the hash-keyed lookup from the -// consensus-driven harness. We push a single QC + block via the same code -// path the network would (insertBlock writes to inner.blockHashes), then: -// -// - the block's own header hash resolves to Some(*GlobalBlock) with the -// expected height/header/payload — the index points at the right -// block, atomically with the block construction -// - a zero hash and a random hash both resolve to None — distinct -// unknown-hash inputs all read as "not found", no panics -// - err is nil throughout — today's in-memory implementation has no -// failure mode; the error return on GlobalBlockByHash is reserved -// for the future BlockDB-backed path -func TestGlobalBlockByHash(t *testing.T) { - ctx := t.Context() - rng := utils.TestRng() - committee, keys := types.GenCommittee(rng, 3) - - state := utils.OrPanic1(NewState(&Config{ - Committee: committee, - }, utils.OrPanic1(NewDataWAL(utils.None[string](), committee)))) - - qc, blocks := TestCommitQC(rng, committee, keys, utils.None[*types.CommitQC]()) - require.NoError(t, state.PushQC(ctx, qc, blocks)) - gr := qc.QC().GlobalRange(committee) - n := gr.First - wantBlock := blocks[0] - wantHash := wantBlock.Header().Hash() - - // Known hash → Some with correct fields. - gotOpt, err := state.GlobalBlockByHash(wantHash) - require.NoError(t, err) - gotGB, ok := gotOpt.Get() - require.True(t, ok, "GlobalBlockByHash(known) returned None") - require.Equal(t, n, gotGB.GlobalNumber) - require.Equal(t, wantBlock.Header(), gotGB.Header) - require.Equal(t, wantBlock.Payload(), gotGB.Payload) - - // Zero hash → None. - zeroOpt, err := state.GlobalBlockByHash(types.BlockHeaderHash{}) - require.NoError(t, err) - _, ok = zeroOpt.Get() - require.False(t, ok, "GlobalBlockByHash(zero) returned Some") - - // Random unknown hash → None. - var randHash types.BlockHeaderHash - rng.Read(randHash[:]) - randOpt, err := state.GlobalBlockByHash(randHash) - require.NoError(t, err) - _, ok = randOpt.Get() - require.False(t, ok, "GlobalBlockByHash(random) returned Some") -} - // ── Reconcile tests (grouped by case number) ────────────────────────── // TestStateRecoveryBlocksOnly simulates a crash after blocks are written diff --git a/sei-tendermint/internal/p2p/giga_blockdb.go b/sei-tendermint/internal/p2p/giga_blockdb.go new file mode 100644 index 0000000000..19e197e808 --- /dev/null +++ b/sei-tendermint/internal/p2p/giga_blockdb.go @@ -0,0 +1,61 @@ +package p2p + +import ( + "time" + + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block" + "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/tmhash" + atypes "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/types" +) + +// globalBlockAdapter wraps *atypes.GlobalBlock so it satisfies block.Block +// without leaking sei-db into autobahn/types. Per-tx hashes use +// tmhash.Sum (sha256), matching CometBFT's tx-hash convention. +// +// txs is computed eagerly in newGlobalBlockAdapter and cached for the +// lifetime of the adapter. mem_block_db calls Transactions() multiple +// times (WriteBlock, Prune); without the cache each call would +// re-allocate the slice and re-sha256 every payload tx — under the +// write lock, on the Prune path. +type globalBlockAdapter struct { + gb *atypes.GlobalBlock + txs []block.Transaction +} + +func newGlobalBlockAdapter(gb *atypes.GlobalBlock) globalBlockAdapter { + src := gb.Payload.Txs() + txs := make([]block.Transaction, len(src)) + for i, tx := range src { + txs[i] = txAdapter{ + hash: tmhash.Sum(tx), + bytes: tx, + } + } + return globalBlockAdapter{gb: gb, txs: txs} +} + +func (a globalBlockAdapter) Hash() []byte { + // TODO(autobahn): memoize parallel to txs — Hash() is called multiple + // times per block. Each call re-runs the proto marshal + sha256 over + // the header. Not hot today but trivial to cache when we revisit. + h := a.gb.Header.Hash() + return h.Bytes() +} + +func (a globalBlockAdapter) Height() uint64 { return uint64(a.gb.GlobalNumber) } + +func (a globalBlockAdapter) Time() time.Time { return a.gb.Timestamp } + +func (a globalBlockAdapter) Transactions() []block.Transaction { return a.txs } + +// txAdapter wraps a single Autobahn tx + its CometBFT-style hash so it +// satisfies block.Transaction. The interface carries only the invariant +// tx body — BlockDB doesn't index by tx hash; per-tx execution results +// belong on a future Receipt Store, not here. +type txAdapter struct { + hash []byte + bytes []byte +} + +func (t txAdapter) Hash() []byte { return t.hash } +func (t txAdapter) Bytes() []byte { return t.bytes } diff --git a/sei-tendermint/internal/p2p/giga_router.go b/sei-tendermint/internal/p2p/giga_router.go index 5434979ab0..c58805b2e5 100644 --- a/sei-tendermint/internal/p2p/giga_router.go +++ b/sei-tendermint/internal/p2p/giga_router.go @@ -8,6 +8,8 @@ import ( "slices" "time" + "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block" + memblockdb "github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/mem_block_db" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" "github.com/sei-protocol/sei-chain/sei-tendermint/crypto" "github.com/sei-protocol/sei-chain/sei-tendermint/internal/autobahn/consensus" @@ -53,6 +55,26 @@ type GigaRouter struct { service *giga.Service poolIn *giga.Pool[NodePublicKey, rpc.Server[giga.API]] poolOut *giga.Pool[NodePublicKey, rpc.Client[giga.API]] + // blockDB indexes finalized blocks by hash and height. Populated by + // runExecute via WriteBlock just before each block is handed to + // executeBlock; read by BlockByHash. BlockDB is block-storage-only — + // per-tx execution results (and the txHash → result lookup) belong + // on a future Receipt Store, not here, per the Giga Transaction + // Query proposal. + // + // Today's instance is mem_block_db (in-memory), so it does not survive + // process restarts — RPC semantics treat that as "unknown hash" + // (BlockByHash returns &ResultBlock{Block: nil}). + // + // TODO(autobahn): make BlockDB injectable via GigaRouterConfig (today + // it's hard-coded to mem_block_db.NewMemBlockDB() in NewGigaRouter, + // and unit tests reach into this unexported field). Will land + // alongside the persistent backend follow-up. + // + // TODO(autobahn): wire blockDB.Prune from runExecute. Today only + // data.PruneBefore runs; mem_block_db grows without bound across the + // chain's lifetime and a long-running process will OOM. + blockDB block.BlockDB // lastCommitQCRecv is subscribed once at construction and reused for the // lifetime of the GigaRouter. Load() is lock-free (a single @@ -103,6 +125,7 @@ func NewGigaRouter(cfg *GigaRouterConfig, key NodeSecretKey) (*GigaRouter, error service: giga.NewService(consensusState), poolIn: giga.NewPool[NodePublicKey, rpc.Server[giga.API]](), poolOut: giga.NewPool[NodePublicKey, rpc.Client[giga.API]](), + blockDB: memblockdb.NewMemBlockDB(), // Subscribe once here (takes avail's internal lock once); subsequent // Load() calls from RPC handlers are lock-free atomic pointer reads. @@ -146,14 +169,6 @@ func (r *GigaRouter) MaxGasPerBlock() int64 { // evmrpc does not read them on the receipt path. If gb.Header is nil // BlockID.Hash also stays empty; if gb.Payload is nil Block.Data.Txs // stays empty (see the malformed-block handling below). -// -// TODO(autobahn): switch this to read from sei-db/ledger_db/block.BlockDB -// once a writer is wired (e.g. from app.FinalizeBlocker or executeBlock). -// Today no production code calls BlockDB.WriteBlock, so Autobahn's in-memory -// data.State is the only place a full block lives — but it's pruned per -// Sei's RetainHeight and exposes only a height index (no GetBlockByHash). -// BlockDB has the right shape (height + hash indexes, async pruning) and -// is the long-term home for this read path. func (r *GigaRouter) BlockByNumber(ctx context.Context, n atypes.GlobalBlockNumber) (*coretypes.ResultBlock, error) { gb, err := r.data.GlobalBlock(ctx, n) if err != nil { @@ -167,7 +182,7 @@ func (r *GigaRouter) BlockByNumber(ctx context.Context, n atypes.GlobalBlockNumb } return nil, fmt.Errorf("data.GlobalBlock(%v): %w", n, err) } - return r.translateGlobalBlock(gb), nil + return r.translateBlock(newGlobalBlockAdapter(gb)), nil } // BlockByHash returns the finalized global block keyed by Autobahn block- @@ -175,39 +190,28 @@ func (r *GigaRouter) BlockByNumber(ctx context.Context, n atypes.GlobalBlockNumb // (same translation as BlockByNumber). Matches CometBFT semantics for // unknown hashes: returns &ResultBlock{Block: nil} with no error. // -// Lookup-and-construct happens under a single data.State lock acquire, so -// the returned block matches the requested hash atomically. Hashes below -// the pruning watermark are not indexed and read as "unknown". Wrong-size -// inputs are rejected at the call site (env.BlockByHash) so this method -// can stay strongly typed on atypes.BlockHeaderHash. -// -// 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. +// Reads from sei-db/ledger_db/block.BlockDB, which runExecute populates +// just before each block is handed to the app. Blocks finalized but not +// yet started executing are not yet indexed and read as "unknown" — same +// shape CometBFT returns for an unknown hash. Wrong-size inputs are +// rejected at the call site (env.BlockByHash) so this method can stay +// strongly typed on atypes.BlockHeaderHash. func (r *GigaRouter) BlockByHash(ctx context.Context, hash atypes.BlockHeaderHash) (*coretypes.ResultBlock, error) { - opt, err := r.data.GlobalBlockByHash(hash) + b, ok, err := r.blockDB.GetBlockByHash(ctx, hash.Bytes()) if err != nil { - return nil, fmt.Errorf("data.GlobalBlockByHash: %w", err) + return nil, fmt.Errorf("blockDB.GetBlockByHash: %w", err) } - // Reject the unknown-hash case here so translateGlobalBlock can rely - // on the *GlobalBlock type contract (non-nil, with non-nil Header - // and Payload) — same way executeBlock dereferences b.Header - // without checking. Mirrors CometBFT's BlockStore.LoadBlockByHash - // returning &ResultBlock{Block: nil} for an unknown hash. - gb, ok := opt.Get() if !ok { return &coretypes.ResultBlock{}, nil } - return r.translateGlobalBlock(gb), nil + return r.translateBlock(b), nil } -// 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. +// translateBlock converts a block.Block into the CometBFT coretypes.ResultBlock +// shape used by env.Block / env.BlockByHash and downstream evmrpc consumers. +// Both BlockByNumber (data.State path, wrapped via globalBlockAdapter) and +// BlockByHash (BlockDB path) feed through here so the read path always emits +// the same shape regardless of source. // // LastCommit is non-nil with empty Signatures, mirroring executeBlock's // FinalizeBlock call which passes an empty abci.CommitInfo. Under Autobahn @@ -217,23 +221,21 @@ func (r *GigaRouter) BlockByHash(ctx context.Context, hash atypes.BlockHeaderHas // 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() +func (r *GigaRouter) translateBlock(b block.Block) *coretypes.ResultBlock { + srcTxs := b.Transactions() tmTxs := make(types.Txs, len(srcTxs)) for i, tx := range srcTxs { - tmTxs[i] = tx + tmTxs[i] = tx.Bytes() } - h := gb.Header.Hash() return &coretypes.ResultBlock{ - BlockID: types.BlockID{Hash: tmbytes.HexBytes(h.Bytes())}, + BlockID: types.BlockID{Hash: tmbytes.HexBytes(b.Hash())}, Block: &types.Block{ Header: types.Header{ ChainID: r.cfg.GenDoc.ChainID, - // Clamp accepts any constraints.Integer for From, so - // gb.GlobalNumber (a typed uint64) goes in directly — no - // intermediate uint64() conversion needed. - Height: utils.Clamp[int64](gb.GlobalNumber), - Time: gb.Timestamp, + // Clamp accepts any constraints.Integer for From, so the + // uint64 height goes in directly — no intermediate cast. + Height: utils.Clamp[int64](b.Height()), + Time: b.Time(), }, Data: types.Data{Txs: tmTxs}, LastCommit: &types.Commit{}, @@ -359,6 +361,17 @@ func (r *GigaRouter) runExecute(ctx context.Context) error { if err != nil { return fmt.Errorf("r.data.GlobalBlock(%v): %w", n, err) } + // Persist to BlockDB before execution. WriteBlock provides + // read-your-writes within this process, so any concurrent RPC + // BlockByHash sees the block from this point forward. The data + // layer's WAL remains the primary durability story; BlockDB is the + // block-by-hash index, not the source of truth on restart. Per-tx + // execution results are NOT recorded here — those belong on a + // future Receipt Store (see the Giga Transaction Query proposal), + // not on BlockDB. + if err := r.blockDB.WriteBlock(ctx, newGlobalBlockAdapter(b)); err != nil { + return fmt.Errorf("r.blockDB.WriteBlock(%v): %w", n, err) + } commitResp, err := r.executeBlock(ctx, b) if err != nil { return fmt.Errorf("r.executeBlock(%v): %w", n, err) diff --git a/sei-tendermint/internal/rpc/core/tx.go b/sei-tendermint/internal/rpc/core/tx.go index 33384d42c4..41e6490c95 100644 --- a/sei-tendermint/internal/rpc/core/tx.go +++ b/sei-tendermint/internal/rpc/core/tx.go @@ -17,6 +17,12 @@ import ( // transaction is in the mempool, invalidated, or was not sent in the first // place. // More: https://docs.tendermint.com/master/rpc/#/Info/tx +// +// TODO(autobahn): once the Receipt Store is wired (canonical +// txHash → execution result lookup unified for EVM + Cosmos txs, per +// the Giga Transaction Query proposal), route this handler through +// it instead of the legacy EventSinks. The current behavior under +// Autobahn is "querying disabled" because EventSinks aren't populated. func (env *Environment) Tx(ctx context.Context, req *coretypes.RequestTx) (*coretypes.ResultTx, error) { // if index is disabled, return error if !indexer.KVSinkEnabled(env.EventSinks) {