diff --git a/cmd/rpcdaemon/README.md b/cmd/rpcdaemon/README.md index 6b724ec49e8..83ef81c8d77 100644 --- a/cmd/rpcdaemon/README.md +++ b/cmd/rpcdaemon/README.md @@ -262,17 +262,18 @@ The following table shows the current implementation status of Erigon's RPC daem | erigon_getLogsByHash | Yes | Erigon only | | erigon_forks | Yes | Erigon only | | erigon_issuance | Yes | Erigon only | +| erigon_getBlockByTimeStamp | Yes | Erigon only | | | | | | starknet_call | Yes | Starknet only | -| | | | -| bor_getSnapshot | Yes | Bor only | -| bor_getAuthor | Yes | Bor only | -| bor_getSnapshotAtHash | Yes | Bor only | -| bor_getSigners | Yes | Bor only | -| bor_getSignersAtHash | Yes | Bor only | -| bor_getCurrentProposer | Yes | Bor only | -| bor_getCurrentValidators | Yes | Bor only | -| bor_getRootHash | Yes | Bor only | +| | | | +| bor_getSnapshot | Yes | Bor only | +| bor_getAuthor | Yes | Bor only | +| bor_getSnapshotAtHash | Yes | Bor only | +| bor_getSigners | Yes | Bor only | +| bor_getSignersAtHash | Yes | Bor only | +| bor_getCurrentProposer | Yes | Bor only | +| bor_getCurrentValidators | Yes | Bor only | +| bor_getRootHash | Yes | Bor only | This table is constantly updated. Please visit again. diff --git a/cmd/rpcdaemon/commands/erigon_api.go b/cmd/rpcdaemon/commands/erigon_api.go index 02624c58564..5a5254d1d20 100644 --- a/cmd/rpcdaemon/commands/erigon_api.go +++ b/cmd/rpcdaemon/commands/erigon_api.go @@ -19,6 +19,7 @@ type ErigonAPI interface { // Blocks related (see ./erigon_blocks.go) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) GetHeaderByHash(_ context.Context, hash common.Hash) (*types.Header, error) + GetBlockByTimeStamp(ctx context.Context, timeStamp uint64, fullTx bool) (map[string]interface{}, error) // Receipt related (see ./erigon_receipts.go) GetLogsByHash(ctx context.Context, hash common.Hash) ([][]*types.Log, error) diff --git a/cmd/rpcdaemon/commands/erigon_block.go b/cmd/rpcdaemon/commands/erigon_block.go index fe9f90e0c39..eaac6eec235 100644 --- a/cmd/rpcdaemon/commands/erigon_block.go +++ b/cmd/rpcdaemon/commands/erigon_block.go @@ -3,10 +3,13 @@ package commands import ( "context" "fmt" + "sort" + "github.com/ledgerwatch/erigon-lib/kv" "github.com/ledgerwatch/erigon/common" "github.com/ledgerwatch/erigon/core/rawdb" "github.com/ledgerwatch/erigon/core/types" + "github.com/ledgerwatch/erigon/internal/ethapi" "github.com/ledgerwatch/erigon/rpc" ) @@ -58,3 +61,80 @@ func (api *ErigonImpl) GetHeaderByHash(ctx context.Context, hash common.Hash) (* return header, nil } + +func (api *ErigonImpl) GetBlockByTimeStamp(ctx context.Context, timeStamp uint64, fullTx bool) (map[string]interface{}, error) { + tx, err := api.db.BeginRo(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback() + + currentHeader := rawdb.ReadCurrentHeader(tx) + currenttHeaderTime := currentHeader.Time + highestNumber := currentHeader.Number.Uint64() + + firstHeader := rawdb.ReadHeaderByNumber(tx, 0) + firstHeaderTime := firstHeader.Time + + if currenttHeaderTime <= timeStamp { + blockResponse, err := buildBlockResponse(tx, highestNumber, fullTx) + if err != nil { + return nil, err + } + + return blockResponse, nil + } + + if firstHeaderTime >= timeStamp { + blockResponse, err := buildBlockResponse(tx, 0, fullTx) + if err != nil { + return nil, err + } + + return blockResponse, nil + } + + blockNum := sort.Search(int(currentHeader.Number.Uint64()), func(blockNum int) bool { + currentHeader := rawdb.ReadHeaderByNumber(tx, uint64(blockNum)) + + return currentHeader.Time >= timeStamp + }) + + resultingHeader := rawdb.ReadHeaderByNumber(tx, uint64(blockNum)) + + if resultingHeader.Time > timeStamp { + response, err := buildBlockResponse(tx, uint64(blockNum)-1, fullTx) + if err != nil { + return nil, err + } + return response, nil + } + + response, err := buildBlockResponse(tx, uint64(blockNum), fullTx) + if err != nil { + return nil, err + } + + return response, nil +} + +func buildBlockResponse(db kv.Tx, blockNum uint64, fullTx bool) (map[string]interface{}, error) { + block, err := rawdb.ReadBlockByNumber(db, blockNum) + if err != nil { + return nil, err + } + + if block == nil { + return nil, nil + } + + response, err := ethapi.RPCMarshalBlock(block, true, fullTx) + + if err == nil && rpc.BlockNumber(block.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + return response, err +} diff --git a/cmd/rpcdaemon/commands/eth_call_test.go b/cmd/rpcdaemon/commands/eth_call_test.go index b0c7ec16023..74d0f78bb4d 100644 --- a/cmd/rpcdaemon/commands/eth_call_test.go +++ b/cmd/rpcdaemon/commands/eth_call_test.go @@ -8,6 +8,7 @@ import ( "github.com/ledgerwatch/erigon-lib/kv/kvcache" "github.com/ledgerwatch/erigon/cmd/rpcdaemon/rpcdaemontest" "github.com/ledgerwatch/erigon/common" + "github.com/ledgerwatch/erigon/core/rawdb" "github.com/ledgerwatch/erigon/internal/ethapi" "github.com/ledgerwatch/erigon/rpc" "github.com/ledgerwatch/erigon/turbo/snapshotsync" @@ -42,3 +43,209 @@ func TestEthCallNonCanonical(t *testing.T) { } } } + +func TestGetBlockByTimeStampLatestTime(t *testing.T) { + ctx := context.Background() + db := rpcdaemontest.CreateTestKV(t) + + tx, err := db.BeginRo(ctx) + if err != nil { + t.Errorf("fail at beginning tx") + } + defer tx.Rollback() + + stateCache := kvcache.New(kvcache.DefaultCoherentConfig) + api := NewErigonAPI(NewBaseApi(nil, stateCache, snapshotsync.NewBlockReader(), false), db, nil) + + latestBlock := rawdb.ReadCurrentBlock(tx) + response, err := ethapi.RPCMarshalBlock(latestBlock, true, false) + + if err != nil { + t.Error("couldn't get the rpc marshal block") + } + + if err == nil && rpc.BlockNumber(latestBlock.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + + block, err := api.GetBlockByTimeStamp(ctx, latestBlock.Header().Time, false) + if err != nil { + t.Errorf("couldn't retrieve block %v", err) + } + + if block["timestamp"] != response["timestamp"] || block["hash"] != response["hash"] { + t.Errorf("Retrieved the wrong block.\nexpected block hash: %s expected timestamp: %d\nblock hash retrieved: %s timestamp retrieved: %d", response["hash"], response["timestamp"], block["hash"], block["timestamp"]) + } +} + +func TestGetBlockByTimeStampOldestTime(t *testing.T) { + ctx := context.Background() + db := rpcdaemontest.CreateTestKV(t) + + tx, err := db.BeginRo(ctx) + if err != nil { + t.Errorf("failed at beginning tx") + } + defer tx.Rollback() + + stateCache := kvcache.New(kvcache.DefaultCoherentConfig) + api := NewErigonAPI(NewBaseApi(nil, stateCache, snapshotsync.NewBlockReader(), false), db, nil) + + oldestBlock, err := rawdb.ReadBlockByNumber(tx, 0) + if err != nil { + t.Error("couldn't retrieve oldest block") + } + + response, err := ethapi.RPCMarshalBlock(oldestBlock, true, false) + + if err != nil { + t.Error("couldn't get the rpc marshal block") + } + + if err == nil && rpc.BlockNumber(oldestBlock.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + + block, err := api.GetBlockByTimeStamp(ctx, oldestBlock.Header().Time, false) + if err != nil { + t.Errorf("couldn't retrieve block %v", err) + } + + if block["timestamp"] != response["timestamp"] || block["hash"] != response["hash"] { + t.Errorf("Retrieved the wrong block.\nexpected block hash: %s expected timestamp: %d\nblock hash retrieved: %s timestamp retrieved: %d", response["hash"], response["timestamp"], block["hash"], block["timestamp"]) + } +} + +func TestGetBlockByTimeHigherThanLatestBlock(t *testing.T) { + ctx := context.Background() + db := rpcdaemontest.CreateTestKV(t) + + tx, err := db.BeginRo(ctx) + if err != nil { + t.Errorf("fail at beginning tx") + } + defer tx.Rollback() + + stateCache := kvcache.New(kvcache.DefaultCoherentConfig) + api := NewErigonAPI(NewBaseApi(nil, stateCache, snapshotsync.NewBlockReader(), false), db, nil) + + latestBlock := rawdb.ReadCurrentBlock(tx) + + response, err := ethapi.RPCMarshalBlock(latestBlock, true, false) + + if err != nil { + t.Error("couldn't get the rpc marshal block") + } + + if err == nil && rpc.BlockNumber(latestBlock.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + + block, err := api.GetBlockByTimeStamp(ctx, latestBlock.Header().Time+999999999999, false) + if err != nil { + t.Errorf("couldn't retrieve block %v", err) + } + + if block["timestamp"] != response["timestamp"] || block["hash"] != response["hash"] { + t.Errorf("Retrieved the wrong block.\nexpected block hash: %s expected timestamp: %d\nblock hash retrieved: %s timestamp retrieved: %d", response["hash"], response["timestamp"], block["hash"], block["timestamp"]) + } +} + +func TestGetBlockByTimeMiddle(t *testing.T) { + ctx := context.Background() + db := rpcdaemontest.CreateTestKV(t) + + tx, err := db.BeginRo(ctx) + if err != nil { + t.Errorf("fail at beginning tx") + } + defer tx.Rollback() + + stateCache := kvcache.New(kvcache.DefaultCoherentConfig) + api := NewErigonAPI(NewBaseApi(nil, stateCache, snapshotsync.NewBlockReader(), false), db, nil) + + currentHeader := rawdb.ReadCurrentHeader(tx) + oldestHeader := rawdb.ReadHeaderByNumber(tx, 0) + + middleNumber := (currentHeader.Number.Uint64() + oldestHeader.Number.Uint64()) / 2 + middleBlock, err := rawdb.ReadBlockByNumber(tx, middleNumber) + if err != nil { + t.Error("couldn't retrieve middle block") + } + + response, err := ethapi.RPCMarshalBlock(middleBlock, true, false) + + if err != nil { + t.Error("couldn't get the rpc marshal block") + } + + if err == nil && rpc.BlockNumber(middleBlock.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + + block, err := api.GetBlockByTimeStamp(ctx, middleBlock.Header().Time, false) + if err != nil { + t.Errorf("couldn't retrieve block %v", err) + } + + if block["timestamp"] != response["timestamp"] || block["hash"] != response["hash"] { + t.Errorf("Retrieved the wrong block.\nexpected block hash: %s expected timestamp: %d\nblock hash retrieved: %s timestamp retrieved: %d", response["hash"], response["timestamp"], block["hash"], block["timestamp"]) + } +} + +func TestGetBlockByTimeStamp(t *testing.T) { + ctx := context.Background() + db := rpcdaemontest.CreateTestKV(t) + + tx, err := db.BeginRo(ctx) + if err != nil { + t.Errorf("fail at beginning tx") + } + defer tx.Rollback() + + stateCache := kvcache.New(kvcache.DefaultCoherentConfig) + api := NewErigonAPI(NewBaseApi(nil, stateCache, snapshotsync.NewBlockReader(), false), db, nil) + + highestBlockNumber := rawdb.ReadCurrentHeader(tx).Number + pickedBlock, err := rawdb.ReadBlockByNumber(tx, highestBlockNumber.Uint64()/3) + if err != nil { + t.Errorf("couldn't get block %v", pickedBlock.Number()) + } + + if pickedBlock == nil { + t.Error("couldn't retrieve picked block") + } + response, err := ethapi.RPCMarshalBlock(pickedBlock, true, false) + + if err != nil { + t.Error("couldn't get the rpc marshal block") + } + + if err == nil && rpc.BlockNumber(pickedBlock.NumberU64()) == rpc.PendingBlockNumber { + // Pending blocks need to nil out a few fields + for _, field := range []string{"hash", "nonce", "miner"} { + response[field] = nil + } + } + + block, err := api.GetBlockByTimeStamp(ctx, pickedBlock.Header().Time, false) + if err != nil { + t.Errorf("couldn't retrieve block %v", err) + } + + if block["timestamp"] != response["timestamp"] || block["hash"] != response["hash"] { + t.Errorf("Retrieved the wrong block.\nexpected block hash: %s expected timestamp: %d\nblock hash retrieved: %s timestamp retrieved: %d", response["hash"], response["timestamp"], block["hash"], block["timestamp"]) + } +}