package backend import ( "bytes" "fmt" "math/big" "strconv" rpctypes "github.com/cerc-io/laconicd/rpc/types" evmtypes "github.com/cerc-io/laconicd/x/evm/types" sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie" "github.com/pkg/errors" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) // BlockNumber returns the current block number in abci app state. Because abci // app state could lag behind from tendermint latest block, it's more stable for // the client to use the latest block number in abci app state than tendermint // rpc. func (b *Backend) BlockNumber() (hexutil.Uint64, error) { // do any grpc query, ignore the response and use the returned block height var header metadata.MD _, err := b.queryClient.Params(b.ctx, &evmtypes.QueryParamsRequest{}, grpc.Header(&header)) if err != nil { return hexutil.Uint64(0), err } blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) if headerLen := len(blockHeightHeader); headerLen != 1 { return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1) } height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64) if err != nil { return 0, fmt.Errorf("failed to parse block height: %w", err) } return hexutil.Uint64(height), nil } // GetBlockByNumber returns the JSON-RPC compatible Ethereum block identified by // block number. Depending on fullTx it either returns the full transaction // objects or if false only the hashes of the transactions. func (b *Backend) GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { resBlock, err := b.TendermintBlockByNumber(blockNum) if err != nil { return nil, nil } // return if requested block height is greater than the current one if resBlock == nil || resBlock.Block == nil { return nil, nil } blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) if err != nil { b.logger.Debug("failed to fetch block result from Tendermint", "height", blockNum, "error", err.Error()) return nil, nil } res, err := b.RPCBlockFromTendermintBlock(resBlock, blockRes, fullTx) if err != nil { b.logger.Debug("GetEthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) return nil, err } return res, nil } // GetBlockByHash returns the JSON-RPC compatible Ethereum block identified by // hash. func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { resBlock, err := b.TendermintBlockByHash(hash) if err != nil { return nil, err } if resBlock == nil { // block not found return nil, nil } blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) if err != nil { b.logger.Debug("failed to fetch block result from Tendermint", "block-hash", hash.String(), "error", err.Error()) return nil, nil } res, err := b.RPCBlockFromTendermintBlock(resBlock, blockRes, fullTx) if err != nil { b.logger.Debug("GetEthBlockFromTendermint failed", "hash", hash, "error", err.Error()) return nil, err } return res, nil } // GetBlockTransactionCountByHash returns the number of Ethereum transactions in // the block identified by hash. func (b *Backend) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { block, err := b.clientCtx.Client.BlockByHash(b.ctx, hash.Bytes()) if err != nil { b.logger.Debug("block not found", "hash", hash.Hex(), "error", err.Error()) return nil } if block.Block == nil { b.logger.Debug("block not found", "hash", hash.Hex()) return nil } return b.GetBlockTransactionCount(block) } // GetBlockTransactionCountByNumber returns the number of Ethereum transactions // in the block identified by number. func (b *Backend) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { block, err := b.TendermintBlockByNumber(blockNum) if err != nil { b.logger.Debug("block not found", "height", blockNum.Int64(), "error", err.Error()) return nil } if block.Block == nil { b.logger.Debug("block not found", "height", blockNum.Int64()) return nil } return b.GetBlockTransactionCount(block) } // GetBlockTransactionCount returns the number of Ethereum transactions in a // given block. func (b *Backend) GetBlockTransactionCount(block *tmrpctypes.ResultBlock) *hexutil.Uint { blockRes, err := b.TendermintBlockResultByNumber(&block.Block.Height) if err != nil { return nil } ethMsgs := b.EthMsgsFromTendermintBlock(block, blockRes) n := hexutil.Uint(len(ethMsgs)) return &n } // TendermintBlockByNumber returns a Tendermint-formatted block for a given // block number func (b *Backend) TendermintBlockByNumber(blockNum rpctypes.BlockNumber) (*tmrpctypes.ResultBlock, error) { height := blockNum.Int64() if height <= 0 { // fetch the latest block number from the app state, more accurate than the tendermint block store state. n, err := b.BlockNumber() if err != nil { return nil, err } height = int64(n) } resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) if err != nil { b.logger.Debug("tendermint client failed to get block", "height", height, "error", err.Error()) return nil, err } if resBlock.Block == nil { b.logger.Debug("TendermintBlockByNumber block not found", "height", height) return nil, nil } return resBlock, nil } // TendermintBlockResultByNumber returns a Tendermint-formatted block result // by block number func (b *Backend) TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) { return b.clientCtx.Client.BlockResults(b.ctx, height) } // TendermintBlockByHash returns a Tendermint-formatted block by block number func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { resBlock, err := b.clientCtx.Client.BlockByHash(b.ctx, blockHash.Bytes()) if err != nil { b.logger.Debug("tendermint client failed to get block", "blockHash", blockHash.Hex(), "error", err.Error()) return nil, err } if resBlock == nil || resBlock.Block == nil { b.logger.Debug("TendermintBlockByHash block not found", "blockHash", blockHash.Hex()) return nil, nil } return resBlock, nil } // BlockNumberFromTendermint returns the BlockNumber from BlockNumberOrHash func (b *Backend) BlockNumberFromTendermint(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { switch { case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") case blockNrOrHash.BlockHash != nil: blockNumber, err := b.BlockNumberFromTendermintByHash(*blockNrOrHash.BlockHash) if err != nil { return rpctypes.EthEarliestBlockNumber, err } return rpctypes.NewBlockNumber(blockNumber), nil case blockNrOrHash.BlockNumber != nil: return *blockNrOrHash.BlockNumber, nil default: return rpctypes.EthEarliestBlockNumber, nil } } // BlockNumberFromTendermintByHash returns the block height of given block hash func (b *Backend) BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) { resBlock, err := b.TendermintBlockByHash(blockHash) if err != nil { return nil, err } if resBlock == nil { return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) } return big.NewInt(resBlock.Block.Height), nil } // EthMsgsFromTendermintBlock returns all real MsgEthereumTxs from a // Tendermint block. It also ensures consistency over the correct txs indexes // across RPC endpoints func (b *Backend) EthMsgsFromTendermintBlock( resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, ) []*evmtypes.MsgEthereumTx { var result []*evmtypes.MsgEthereumTx block := resBlock.Block txResults := blockRes.TxsResults for i, tx := range block.Txs { // Check if tx exists on EVM by cross checking with blockResults: // - Include unsuccessful tx that exceeds block gas limit // - Exclude unsuccessful tx with any other error but ExceedBlockGasLimit if !rpctypes.TxSuccessOrExceedsBlockGasLimit(txResults[i]) { b.logger.Debug("invalid tx result code", "cosmos-hash", hexutil.Encode(tx.Hash())) continue } tx, err := b.clientCtx.TxConfig.TxDecoder()(tx) if err != nil { b.logger.Debug("failed to decode transaction in block", "height", block.Height, "error", err.Error()) continue } for _, msg := range tx.GetMsgs() { ethMsg, ok := msg.(*evmtypes.MsgEthereumTx) if !ok { continue } ethMsg.Hash = ethMsg.AsTransaction().Hash().Hex() result = append(result, ethMsg) } } return result } // HeaderByNumber returns the block header identified by height. func (b *Backend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) { resBlock, err := b.TendermintBlockByNumber(blockNum) if err != nil { return nil, err } if resBlock == nil { return nil, errors.Errorf("block not found for height %d", blockNum) } blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) if err != nil { return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) } bloom, err := b.BlockBloom(blockRes) if err != nil { b.logger.Debug("HeaderByNumber BlockBloom failed", "height", resBlock.Block.Height) } baseFee, err := b.BaseFee(blockRes) if err != nil { // handle the error for pruned node. b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) } ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) return ethHeader, nil } // HeaderByHash returns the block header identified by hash. func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { resBlock, err := b.TendermintBlockByHash(blockHash) if err != nil { return nil, err } if resBlock == nil { return nil, errors.Errorf("block not found for hash %s", blockHash.Hex()) } blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) if err != nil { return nil, errors.Errorf("block result not found for height %d", resBlock.Block.Height) } bloom, err := b.BlockBloom(blockRes) if err != nil { b.logger.Debug("HeaderByHash BlockBloom failed", "height", resBlock.Block.Height) } baseFee, err := b.BaseFee(blockRes) if err != nil { // handle the error for pruned node. b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err) } ethHeader := rpctypes.EthHeaderFromTendermint(resBlock.Block.Header, bloom, baseFee) return ethHeader, nil } // BlockBloom query block bloom filter from block results func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) { for _, event := range blockRes.EndBlockEvents { if event.Type != evmtypes.EventTypeBlockBloom { continue } for _, attr := range event.Attributes { if bytes.Equal(attr.Key, bAttributeKeyEthereumBloom) { return ethtypes.BytesToBloom(attr.Value), nil } } } return ethtypes.Bloom{}, errors.New("block bloom event is not found") } // RPCBlockFromTendermintBlock returns a JSON-RPC compatible Ethereum block from a // given Tendermint block and its block result. func (b *Backend) RPCBlockFromTendermintBlock( resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool, ) (map[string]interface{}, error) { ethRPCTxs := []interface{}{} block := resBlock.Block baseFee, err := b.BaseFee(blockRes) if err != nil { // handle the error for pruned node. b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", block.Height, "error", err) } msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) for txIndex, ethMsg := range msgs { if !fullTx { hash := common.HexToHash(ethMsg.Hash) ethRPCTxs = append(ethRPCTxs, hash) continue } tx := ethMsg.AsTransaction() rpcTx, err := rpctypes.NewRPCTransaction( tx, common.BytesToHash(block.Hash()), uint64(block.Height), uint64(txIndex), baseFee, ) if err != nil { b.logger.Debug("NewTransactionFromData for receipt failed", "hash", tx.Hash().Hex(), "error", err.Error()) continue } ethRPCTxs = append(ethRPCTxs, rpcTx) } bloom, err := b.BlockBloom(blockRes) if err != nil { b.logger.Debug("failed to query BlockBloom", "height", block.Height, "error", err.Error()) } req := &evmtypes.QueryValidatorAccountRequest{ ConsAddress: sdk.ConsAddress(block.Header.ProposerAddress).String(), } var validatorAccAddr sdk.AccAddress ctx := rpctypes.ContextWithHeight(block.Height) res, err := b.queryClient.ValidatorAccount(ctx, req) if err != nil { b.logger.Debug( "failed to query validator operator address", "height", block.Height, "cons-address", req.ConsAddress, "error", err.Error(), ) // use zero address as the validator operator address validatorAccAddr = sdk.AccAddress(common.Address{}.Bytes()) } else { validatorAccAddr, err = sdk.AccAddressFromBech32(res.AccountAddress) if err != nil { return nil, err } } validatorAddr := common.BytesToAddress(validatorAccAddr) gasLimit, err := rpctypes.BlockMaxGasFromConsensusParams(ctx, b.clientCtx, block.Height) if err != nil { b.logger.Error("failed to query consensus params", "error", err.Error()) } gasUsed := uint64(0) for _, txsResult := range blockRes.TxsResults { // workaround for cosmos-sdk bug. https://github.com/cosmos/cosmos-sdk/issues/10832 if ShouldIgnoreGasUsed(txsResult) { // block gas limit has exceeded, other txs must have failed with same reason. break } gasUsed += uint64(txsResult.GetGasUsed()) } formattedBlock := rpctypes.FormatBlock( block.Header, block.Size(), gasLimit, new(big.Int).SetUint64(gasUsed), ethRPCTxs, bloom, validatorAddr, baseFee, ) return formattedBlock, nil } // EthBlockByNumber returns the Ethereum Block identified by number. func (b *Backend) EthBlockByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Block, error) { resBlock, err := b.TendermintBlockByNumber(blockNum) if err != nil { return nil, err } if resBlock == nil { // block not found return nil, fmt.Errorf("block not found for height %d", blockNum) } blockRes, err := b.TendermintBlockResultByNumber(&resBlock.Block.Height) if err != nil { return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height) } return b.EthBlockFromTendermintBlock(resBlock, blockRes) } // EthBlockFromTendermintBlock returns an Ethereum Block type from Tendermint block // EthBlockFromTendermintBlock func (b *Backend) EthBlockFromTendermintBlock( resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, ) (*ethtypes.Block, error) { block := resBlock.Block height := block.Height bloom, err := b.BlockBloom(blockRes) if err != nil { b.logger.Debug("HeaderByNumber BlockBloom failed", "height", height) } baseFee, err := b.BaseFee(blockRes) if err != nil { // handle error for pruned node and log b.logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", height, "error", err) } ethHeader := rpctypes.EthHeaderFromTendermint(block.Header, bloom, baseFee) msgs := b.EthMsgsFromTendermintBlock(resBlock, blockRes) txs := make([]*ethtypes.Transaction, len(msgs)) for i, ethMsg := range msgs { txs[i] = ethMsg.AsTransaction() } // TODO: add tx receipts ethBlock := ethtypes.NewBlock(ethHeader, txs, nil, nil, trie.NewStackTrie(nil)) return ethBlock, nil }