Skip to content
Open
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
45 changes: 41 additions & 4 deletions evmrpc/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"math/big"
"strings"
"sync"
"time"

Expand All @@ -20,6 +19,7 @@ import (
"github.com/sei-protocol/sei-chain/sei-cosmos/client"
sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types"
banktypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/bank/types"
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/receipt"
rpcclient "github.com/sei-protocol/sei-chain/sei-tendermint/rpc/client"
"github.com/sei-protocol/sei-chain/sei-tendermint/rpc/coretypes"
wasmtypes "github.com/sei-protocol/sei-chain/sei-wasmd/x/wasm/types"
Expand All @@ -40,9 +40,31 @@ const genesisBlockHashHex = "0xF9D3845DF25B43B1C6926F3CEDA6845C17F5624E12212FD88

var genesisBlockHash = common.HexToHash(genesisBlockHashHex)

// ErrReceiptsPruned signals that a block's receipts have been pruned from this
// node, so receipt-derived fields cannot be served reliably.
var ErrReceiptsPruned = errors.New("block receipts have been pruned from this node")

// genesisBlockTxCount is the transaction count for the synthetic genesis block (eth_getBlockTransactionCountByHash/ByNumber for genesis).
var genesisBlockTxCount = func() *hexutil.Uint { u := hexutil.Uint(0); return &u }()

func checkReceiptsAvailable(store receipt.ReceiptStore, block *coretypes.ResultBlock) error {
if store == nil {
return nil
}
earliest := store.EarliestVersion()
if earliest == 0 || block.Block.Height >= earliest {
return nil
}
return fmt.Errorf("%w: requested height %d, earliest retained %d", ErrReceiptsPruned, block.Block.Height, earliest)
}

func (a *BlockAPI) receiptStore() receipt.ReceiptStore {
if a.keeper == nil {
return nil
}
return a.keeper.ReceiptStore()
}

func encodeGenesisBlock() map[string]any {
return map[string]any{
"number": (*hexutil.Big)(big.NewInt(0)),
Expand Down Expand Up @@ -180,6 +202,9 @@ func (a *BlockAPI) GetBlockTransactionCountByNumber(ctx context.Context, number
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(a.receiptStore(), block); err != nil {
return nil, err
}
return a.getEvmTxCount(block), nil
}

Expand All @@ -195,6 +220,9 @@ func (a *BlockAPI) GetBlockTransactionCountByHash(ctx context.Context, blockHash
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(a.receiptStore(), block); err != nil {
return nil, err
}
return a.getEvmTxCount(block), nil
}

Expand Down Expand Up @@ -230,6 +258,9 @@ func (a *BlockAPI) getBlockByHash(ctx context.Context, blockHash common.Hash, fu
return nil, err
}

if err := checkReceiptsAvailable(a.receiptStore(), block); err != nil {
return nil, err
}
blockRes, err := blockResultsWithRetry(ctx, a.tmClient, &block.Block.Height)
if err != nil {
return nil, err
Expand Down Expand Up @@ -277,6 +308,9 @@ func (a *BlockAPI) getBlockByNumber(
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(a.receiptStore(), block); err != nil {
return nil, err
}
blockRes, err := blockResultsWithRetry(ctx, a.tmClient, &block.Block.Height)
if err != nil {
return nil, err
Expand Down Expand Up @@ -314,6 +348,9 @@ func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.Block
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(a.receiptStore(), block); err != nil {
return nil, err
}

// Get all tx hashes for the block
height := block.Block.Height
Expand All @@ -329,16 +366,16 @@ func (a *BlockAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.Block
go func(i int, hash typedTxHash) {
defer wg.Done()
defer recoverAndLog()
receipt, err := getOrSetCachedReceiptErr(a.cacheCreationMutex, a.globalBlockCache, a.ctxProvider(height), a.keeper, block, hash.hash)
rcpt, err := getOrSetCachedReceiptErr(a.cacheCreationMutex, a.globalBlockCache, a.ctxProvider(height), a.keeper, block, hash.hash)
if err != nil {
if !strings.Contains(err.Error(), "not found") {
if !errors.Is(err, receipt.ErrNotFound) {
mtx.Lock()
returnErr = err
mtx.Unlock()
}
return
}
encodedReceipt, err := encodeReceipt(a.ctxProvider, a.txConfigProvider, receipt, a.keeper, block, a.includeShellReceipts, a.globalBlockCache, a.cacheCreationMutex)
encodedReceipt, err := encodeReceipt(a.ctxProvider, a.txConfigProvider, rcpt, a.keeper, block, a.includeShellReceipts, a.globalBlockCache, a.cacheCreationMutex)
if err != nil {
mtx.Lock()
returnErr = err
Expand Down
56 changes: 56 additions & 0 deletions evmrpc/block_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package evmrpc_test

import (
"context"
"crypto/sha256"
"math/big"
"sync"
Expand All @@ -26,6 +27,61 @@ import (
"github.com/stretchr/testify/require"
)

func TestGetBlockByNumberReturnsReceiptsPrunedBelowReceiptWatermark(t *testing.T) {
k := &testkeeper.EVMTestApp.EvmKeeper
store := k.ReceiptStore()
require.NotNil(t, store)

oldEarliest := store.EarliestVersion()
require.NoError(t, store.SetEarliestVersion(MockHeight8+1))
t.Cleanup(func() {
require.NoError(t, store.SetEarliestVersion(oldEarliest))
})

tmClient := NewMockClientWithLatest(MockHeight103)
ctxProvider := func(height int64) sdk.Context {
if height == evmrpc.LatestCtxHeight {
return Ctx
}
return Ctx.WithBlockHeight(height)
}
txConfigProvider := func(int64) client.TxConfig { return TxConfig }
watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, store)
api := evmrpc.NewBlockAPI(tmClient, k, ctxProvider, txConfigProvider, evmrpc.ConnectionTypeHTTP, watermarks, evmrpc.NewBlockCache(3000), &sync.Mutex{})

_, err := api.GetBlockByNumber(context.Background(), MockHeight8, false)
require.ErrorIs(t, err, evmrpc.ErrReceiptsPruned)
}

func TestGetBlockTransactionCountReturnsReceiptsPrunedBelowReceiptWatermark(t *testing.T) {
k := &testkeeper.EVMTestApp.EvmKeeper
store := k.ReceiptStore()
require.NotNil(t, store)

oldEarliest := store.EarliestVersion()
require.NoError(t, store.SetEarliestVersion(MockHeight8+1))
t.Cleanup(func() {
require.NoError(t, store.SetEarliestVersion(oldEarliest))
})

tmClient := NewMockClientWithLatest(MockHeight103)
ctxProvider := func(height int64) sdk.Context {
if height == evmrpc.LatestCtxHeight {
return Ctx
}
return Ctx.WithBlockHeight(height)
}
txConfigProvider := func(int64) client.TxConfig { return TxConfig }
watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, store)
api := evmrpc.NewBlockAPI(tmClient, k, ctxProvider, txConfigProvider, evmrpc.ConnectionTypeHTTP, watermarks, evmrpc.NewBlockCache(3000), &sync.Mutex{})

_, err := api.GetBlockTransactionCountByNumber(context.Background(), MockHeight8)
require.ErrorIs(t, err, evmrpc.ErrReceiptsPruned)

_, err = api.GetBlockTransactionCountByHash(context.Background(), common.HexToHash(TestBlockHash))
require.ErrorIs(t, err, evmrpc.ErrReceiptsPruned)
}

func TestEncodeTmBlock_EmptyTransactions(t *testing.T) {
k := &testkeeper.EVMTestApp.EvmKeeper
ctx := testkeeper.EVMTestApp.GetContextForDeliverTx([]byte{}).WithBlockTime(time.Now())
Expand Down
17 changes: 13 additions & 4 deletions evmrpc/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"math"
"math/big"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -94,6 +93,10 @@ func NewSeiTransactionAPI(
return &SeiTransactionAPI{TransactionAPI: baseAPI, isPanicTx: isPanicTx}
}

func (t *TransactionAPI) receiptStore() receiptpkg.ReceiptStore {
return t.keeper.ReceiptStore()
}

func (t *SeiTransactionAPI) GetTransactionReceiptExcludeTraceFail(ctx context.Context, hash common.Hash) (result map[string]interface{}, returnErr error) {
return getTransactionReceipt(ctx, t.TransactionAPI, hash, true, t.isPanicTx, true)
}
Expand Down Expand Up @@ -128,7 +131,7 @@ func getTransactionReceipt(

receipt, err := t.keeper.GetReceipt(sdkctx, hash)
if err != nil {
if strings.Contains(err.Error(), "not found") {
if errors.Is(err, receiptpkg.ErrNotFound) {
// When the transaction doesn't exist, the RPC method should return JSON null
// as per specification.
return nil, nil
Expand Down Expand Up @@ -214,6 +217,9 @@ func (t *TransactionAPI) getTransactionByBlockNumberAndIndex(ctx context.Context
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(t.receiptStore(), block); err != nil {
return nil, err
}
return t.getTransactionWithBlock(block, txIndex, t.includeSynthetic)
}

Expand All @@ -230,6 +236,9 @@ func (t *TransactionAPI) GetTransactionByBlockHashAndIndex(ctx context.Context,
if err != nil {
return nil, err
}
if err := checkReceiptsAvailable(t.receiptStore(), block); err != nil {
return nil, err
}
var idx uint32
idx, err = txIndexToUint32(txIndex)
if err != nil {
Expand Down Expand Up @@ -281,7 +290,7 @@ func (t *TransactionAPI) GetTransactionByHash(ctx context.Context, hash common.H
// then try get from committed
receipt, err := t.keeper.GetReceipt(t.ctxProvider(LatestCtxHeight), hash)
if err != nil {
if strings.Contains(err.Error(), "not found") {
if errors.Is(err, receiptpkg.ErrNotFound) {
return nil, nil
}
return nil, err
Expand Down Expand Up @@ -309,7 +318,7 @@ func (t *TransactionAPI) GetTransactionErrorByHash(ctx context.Context, hash com
}()
receipt, err := t.keeper.GetReceipt(t.ctxProvider(LatestCtxHeight), hash)
if err != nil {
if strings.Contains(err.Error(), "not found") {
if errors.Is(err, receiptpkg.ErrNotFound) {
return "", nil
}
return "", err
Expand Down
30 changes: 30 additions & 0 deletions evmrpc/tx_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package evmrpc_test

import (
"context"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -311,6 +312,35 @@ func TestGetTransactionByBlockHashAndIndexErrors(t *testing.T) {
require.Nil(t, resObj["result"])
}

func TestGetTransactionByBlockAndIndexReturnsReceiptsPrunedBelowReceiptWatermark(t *testing.T) {
k := &testkeeper.EVMTestApp.EvmKeeper
store := k.ReceiptStore()
require.NotNil(t, store)

oldEarliest := store.EarliestVersion()
require.NoError(t, store.SetEarliestVersion(MockHeight8+1))
t.Cleanup(func() {
require.NoError(t, store.SetEarliestVersion(oldEarliest))
})

tmClient := NewMockClientWithLatest(MockHeight103)
ctxProvider := func(height int64) sdk.Context {
if height == evmrpc.LatestCtxHeight {
return Ctx
}
return Ctx.WithBlockHeight(height)
}
txConfigProvider := func(int64) client.TxConfig { return TxConfig }
watermarks := evmrpc.NewWatermarkManager(tmClient, ctxProvider, nil, store)
api := evmrpc.NewTransactionAPI(tmClient, k, ctxProvider, txConfigProvider, "", evmrpc.ConnectionTypeHTTP, watermarks, evmrpc.NewBlockCache(3000), &sync.Mutex{})

_, err := api.GetTransactionByBlockNumberAndIndex(context.Background(), MockHeight8, 0)
require.ErrorIs(t, err, evmrpc.ErrReceiptsPruned)

_, err = api.GetTransactionByBlockHashAndIndex(context.Background(), common.HexToHash(TestBlockHash), 0)
require.ErrorIs(t, err, evmrpc.ErrReceiptsPruned)
}

func TestGetTransactionByHashNotFound(t *testing.T) {
nonExistentHash := "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
body := fmt.Sprintf(`{"jsonrpc": "2.0","method": "eth_getTransactionByHash","params":["%s"],"id":"test"}`, nonExistentHash)
Expand Down
6 changes: 5 additions & 1 deletion evmrpc/watermark_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ var errNoHeightSource = errors.New("unable to determine height information")
// node's safe latest watermark. eth_getBlockByNumber maps this to result null (Ethereum spec).
var ErrBlockHeightNotYetAvailable = errors.New("block height not yet available")

// ErrBlockHeightPruned is returned when a concrete block height is below the
// node's earliest available block or state watermark.
var ErrBlockHeightPruned = errors.New("block height pruned")

// WatermarkManager coordinates access to block, state, and receipt stores to
// determine queryable block heights for RPC consumers. It ensures read-side
// requests only target heights where all backing data sources are fully
Expand Down Expand Up @@ -224,7 +228,7 @@ func (m *WatermarkManager) ensureWithinWatermarks(height, earliest, latest int64
return fmt.Errorf("requested height %d is not yet available; safe latest is %d: %w", height, latest, ErrBlockHeightNotYetAvailable)
}
if height < earliest {
return fmt.Errorf("requested height %d has been pruned; earliest available is %d", height, earliest)
return fmt.Errorf("requested height %d has been pruned; earliest available is %d: %w", height, earliest, ErrBlockHeightPruned)
}
return nil
}
Expand Down
35 changes: 27 additions & 8 deletions evmrpc/watermark_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ func TestResolveHeightGating(t *testing.T) {

tooHigh := rpc.BlockNumber(6)
_, err := wm.ResolveHeight(context.Background(), rpc.BlockNumberOrHash{BlockNumber: &tooHigh})
require.Error(t, err)
require.Contains(t, err.Error(), "not yet available")
require.ErrorIs(t, err, ErrBlockHeightNotYetAvailable)

within := rpc.BlockNumber(4)
height, err := wm.ResolveHeight(context.Background(), rpc.BlockNumberOrHash{BlockNumber: &within})
Expand Down Expand Up @@ -100,8 +99,8 @@ func TestEnsureBlockHeightAvailableBounds(t *testing.T) {

require.NoError(t, wm.EnsureBlockHeightAvailable(context.Background(), 5))

require.ErrorContains(t, wm.EnsureBlockHeightAvailable(context.Background(), 7), "not yet available")
require.ErrorContains(t, wm.EnsureBlockHeightAvailable(context.Background(), 2), "has been pruned")
require.ErrorIs(t, wm.EnsureBlockHeightAvailable(context.Background(), 7), ErrBlockHeightNotYetAvailable)
require.ErrorIs(t, wm.EnsureBlockHeightAvailable(context.Background(), 2), ErrBlockHeightPruned)
}

func TestLatestAndEarliestHeightHelpers(t *testing.T) {
Expand Down Expand Up @@ -129,15 +128,27 @@ func TestResolveHeightUsesStateEarliest(t *testing.T) {

belowState := rpc.BlockNumber(9)
_, err := wm.ResolveHeight(context.Background(), rpc.BlockNumberOrHash{BlockNumber: &belowState})
require.Error(t, err)
require.Contains(t, err.Error(), "has been pruned")
require.ErrorIs(t, err, ErrBlockHeightPruned)

within := rpc.BlockNumber(12)
resolved, err := wm.ResolveHeight(context.Background(), rpc.BlockNumberOrHash{BlockNumber: &within})
require.NoError(t, err)
require.Equal(t, int64(12), resolved)
}

func TestResolveHeightByHashUsesStateEarliest(t *testing.T) {
tmClient := &fakeTMClient{
status: &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 20, EarliestBlockHeight: 5}},
blockByHash: makeBlockResult(9),
}
stateStore := &fakeStateStore{latest: 18, earliest: 10}
wm := NewWatermarkManager(tmClient, nil, stateStore, nil)

h := common.HexToHash("0x9")
_, err := wm.ResolveHeight(context.Background(), rpc.BlockNumberOrHash{BlockHash: &h})
require.ErrorIs(t, err, ErrBlockHeightPruned)
}

func TestStateWatermarksCanLagBlocks(t *testing.T) {
tmClient := &fakeTMClient{
status: &coretypes.ResultStatus{SyncInfo: coretypes.SyncInfo{LatestBlockHeight: 30, EarliestBlockHeight: 12}},
Expand Down Expand Up @@ -233,7 +244,8 @@ func (f *fakeStateStore) Prune(_ int64) error
func (f *fakeStateStore) Close() error { return nil }

type fakeReceiptStore struct {
latest int64
latest int64
earliest int64
}

func (f *fakeReceiptStore) LatestVersion() int64 {
Expand All @@ -245,7 +257,14 @@ func (f *fakeReceiptStore) SetLatestVersion(version int64) error {
return nil
}

func (f *fakeReceiptStore) SetEarliestVersion(_ int64) error { return nil }
func (f *fakeReceiptStore) EarliestVersion() int64 {
return f.earliest
}

func (f *fakeReceiptStore) SetEarliestVersion(version int64) error {
f.earliest = version
return nil
}

func (f *fakeReceiptStore) GetReceipt(sdk.Context, common.Hash) (*evmtypes.Receipt, error) {
return nil, errors.New("not found")
Expand Down
Loading
Loading