lotus/itests/eth_conformance_test.go
Fridrik Asmundsson f358160cd5 Add EIP-1898 support needed for The Graph compatibility
Fixes: #10814

This PR updates the following RPC methods according to EIP-1898
specs.

The following RPC methods are affected:

- eth_getBalance
- eth_getStorageAt
- eth_getTransactionCount
- eth_getCode
- eth_call

Note that eth_getBlockByNumber was not included in this list in
the spec although it seems it should be affected also?

Currently these methods all accept a blkParam string which can be
one of "latest", "earliest", "pending", or a block number (decimal
or hex). The spec enables caller to additionally specify a json
hash which can include the following fields:

- blockNumber EthUint64: A block number (decimal or hex) which is
  similar to the original use of the blkParam string
- blockHash EthHash: The block hash
- requireCanonical bool) If true we should make sure the block is
  in the canonical chain

Since the blkParam needs to support both being a number/string and
a json hash then this to properly work we need to introduce a new
struct with pointer fields to check if they exist. This is done
in the EthBlockParamByNumberOrHash struct which first tries to
unmarshal as a json hash (according to eip-1898) and then fallback
to unmarshal as string/number.
2023-06-20 09:38:00 +00:00

509 lines
17 KiB
Go

package itests
import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
"os"
"strings"
"testing"
"time"
"github.com/go-openapi/spec"
"github.com/gregdhill/go-openrpc/parse"
orpctypes "github.com/gregdhill/go-openrpc/types"
manet "github.com/multiformats/go-multiaddr/net"
"github.com/stretchr/testify/require"
"github.com/xeipuuv/gojsonschema"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-jsonrpc"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/lotus/build"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
"github.com/filecoin-project/lotus/chain/wallet/key"
"github.com/filecoin-project/lotus/itests/kit"
)
// TODO generate this using reflection. It's the same as the EthAPI except every return value is a json.RawMessage
type ethAPIRaw struct {
EthAccounts func(context.Context) (json.RawMessage, error)
EthBlockNumber func(context.Context) (json.RawMessage, error)
EthCall func(context.Context, ethtypes.EthCall, ethtypes.EthBlockParamByNumberOrHash) (json.RawMessage, error)
EthChainId func(context.Context) (json.RawMessage, error)
EthEstimateGas func(context.Context, ethtypes.EthCall) (json.RawMessage, error)
EthFeeHistory func(context.Context, ethtypes.EthUint64, string, []float64) (json.RawMessage, error)
EthGasPrice func(context.Context) (json.RawMessage, error)
EthGetBalance func(context.Context, ethtypes.EthAddress, ethtypes.EthBlockParamByNumberOrHash) (json.RawMessage, error)
EthGetBlockByHash func(context.Context, ethtypes.EthHash, bool) (json.RawMessage, error)
EthGetBlockByNumber func(context.Context, string, bool) (json.RawMessage, error)
EthGetBlockTransactionCountByHash func(context.Context, ethtypes.EthHash) (json.RawMessage, error)
EthGetBlockTransactionCountByNumber func(context.Context, ethtypes.EthUint64) (json.RawMessage, error)
EthGetCode func(context.Context, ethtypes.EthAddress, ethtypes.EthBlockParamByNumberOrHash) (json.RawMessage, error)
EthGetFilterChanges func(context.Context, ethtypes.EthFilterID) (json.RawMessage, error)
EthGetFilterLogs func(context.Context, ethtypes.EthFilterID) (json.RawMessage, error)
EthGetLogs func(context.Context, *ethtypes.EthFilterSpec) (json.RawMessage, error)
EthGetStorageAt func(context.Context, ethtypes.EthAddress, ethtypes.EthBytes, ethtypes.EthBlockParamByNumberOrHash) (json.RawMessage, error)
EthGetTransactionByBlockHashAndIndex func(context.Context, ethtypes.EthHash, ethtypes.EthUint64) (json.RawMessage, error)
EthGetTransactionByBlockNumberAndIndex func(context.Context, ethtypes.EthUint64, ethtypes.EthUint64) (json.RawMessage, error)
EthGetTransactionByHash func(context.Context, *ethtypes.EthHash) (json.RawMessage, error)
EthGetTransactionCount func(context.Context, ethtypes.EthAddress, ethtypes.EthBlockParamByNumberOrHash) (json.RawMessage, error)
EthGetTransactionReceipt func(context.Context, ethtypes.EthHash) (json.RawMessage, error)
EthMaxPriorityFeePerGas func(context.Context) (json.RawMessage, error)
EthNewBlockFilter func(context.Context) (json.RawMessage, error)
EthNewFilter func(context.Context, *ethtypes.EthFilterSpec) (json.RawMessage, error)
EthNewPendingTransactionFilter func(context.Context) (json.RawMessage, error)
EthSendRawTransaction func(context.Context, ethtypes.EthBytes) (json.RawMessage, error)
EthSubscribe func(context.Context, string, *ethtypes.EthSubscriptionParams) (json.RawMessage, error)
EthUninstallFilter func(context.Context, ethtypes.EthFilterID) (json.RawMessage, error)
EthUnsubscribe func(context.Context, ethtypes.EthSubscriptionID) (json.RawMessage, error)
}
func TestEthOpenRPCConformance(t *testing.T) {
kit.QuietAllLogsExcept("events", "messagepool")
// specs/eth_openrpc.json is built from https://github.com/ethereum/execution-apis
specJSON, err := os.ReadFile("specs/eth_openrpc.json")
require.NoError(t, err)
specParsed := orpctypes.NewOpenRPCSpec1()
err = json.Unmarshal(specJSON, specParsed)
require.NoError(t, err)
parse.GetTypes(specParsed, specParsed.Objects)
schemas := make(map[string]spec.Schema)
for _, method := range specParsed.Methods {
if method.Result != nil {
schemas[method.Name] = method.Result.Schema
}
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC(), kit.WithEthRPC())
ens.InterconnectAll().BeginMining(10 * time.Millisecond)
contractHex, err := os.ReadFile("contracts/EventMatrix.hex")
require.NoError(t, err)
// strip any trailing newlines from the file
contractHex = bytes.TrimRight(contractHex, "\n")
contractBin, err := hex.DecodeString(string(contractHex))
require.NoError(t, err)
senderKey, senderEthAddr, senderFilAddr := client.EVM().NewAccount()
_, receiverEthAddr, _ := client.EVM().NewAccount()
kit.SendFunds(ctx, t, client, senderFilAddr, types.FromFil(1000))
deployerAddr, err := client.EVM().WalletDefaultAddress(ctx)
require.NoError(t, err)
pendingTransactionFilterID, err := client.EthNewPendingTransactionFilter(ctx)
require.NoError(t, err)
blockFilterID, err := client.EthNewBlockFilter(ctx)
require.NoError(t, err)
filterAllLogs := kit.NewEthFilterBuilder().FromBlockEpoch(0).Filter()
logFilterID, err := client.EthNewFilter(ctx, filterAllLogs)
require.NoError(t, err)
uninstallableFilterID, err := client.EthNewFilter(ctx, filterAllLogs)
require.NoError(t, err)
rawSignedEthTx := createRawSignedEthTx(ctx, t, client, senderEthAddr, receiverEthAddr, senderKey, contractBin)
result := client.EVM().DeployContract(ctx, deployerAddr, contractBin)
contractAddr, err := address.NewIDAddress(result.ActorID)
require.NoError(t, err)
contractEthAddr := ethtypes.EthAddress(result.EthAddress)
messageWithEvents, blockHashWithMessage, blockNumberWithMessage := waitForMessageWithEvents(ctx, t, client, deployerAddr, contractAddr)
// create a json-rpc client that returns raw json responses
var ethapi ethAPIRaw
netAddr, err := manet.ToNetAddr(client.ListenAddr)
require.NoError(t, err)
rpcAddr := "ws://" + netAddr.String() + "/rpc/v1"
closer, err := jsonrpc.NewClient(ctx, rpcAddr, "Filecoin", &ethapi, nil)
require.NoError(t, err)
defer closer()
testCases := []struct {
method string
variant string // suffix applied to the test name to distinguish different variants of a method call
call func(*ethAPIRaw) (json.RawMessage, error)
skipReason string
}{
// Alphabetical order
{
method: "eth_accounts",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthAccounts(context.Background())
},
},
{
method: "eth_blockNumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthBlockNumber(context.Background())
},
},
{
method: "eth_call",
variant: "latest",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthCall(context.Background(), ethtypes.EthCall{
From: &senderEthAddr,
Data: contractBin,
}, ethtypes.NewEthBlockParamFromPredefined("latest"))
},
},
{
method: "eth_chainId",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthChainId(context.Background())
},
},
{
method: "eth_estimateGas",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthEstimateGas(context.Background(), ethtypes.EthCall{
From: &senderEthAddr,
Data: contractBin,
})
},
},
{
method: "eth_feeHistory",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthFeeHistory(context.Background(), ethtypes.EthUint64(2), "latest", nil)
},
},
{
method: "eth_gasPrice",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGasPrice(context.Background())
},
},
{
method: "eth_getBalance",
variant: "blocknumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
blockParam, _ := ethtypes.NewEthBlockParamFromHexString("0x0")
return ethapi.EthGetBalance(context.Background(), contractEthAddr, blockParam)
},
},
{
method: "eth_getBlockByHash",
variant: "txhashes",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockByHash(context.Background(), blockHashWithMessage, false)
},
},
{
method: "eth_getBlockByHash",
variant: "txfull",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockByHash(context.Background(), blockHashWithMessage, true)
},
},
{
method: "eth_getBlockByNumber",
variant: "earliest",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockByNumber(context.Background(), "earliest", true)
},
skipReason: "earliest block is not supported",
},
{
method: "eth_getBlockByNumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockByNumber(context.Background(), blockNumberWithMessage.Hex(), true)
},
},
{
method: "eth_getBlockTransactionCountByHash",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockTransactionCountByHash(context.Background(), blockHashWithMessage)
},
},
{
method: "eth_getBlockTransactionCountByNumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetBlockTransactionCountByNumber(context.Background(), blockNumberWithMessage)
},
},
{
method: "eth_getCode",
variant: "blocknumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetCode(context.Background(), contractEthAddr, ethtypes.NewEthBlockParamFromNumber(blockNumberWithMessage))
},
},
{
method: "eth_getFilterChanges",
variant: "pendingtransaction",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return a.EthGetFilterChanges(ctx, pendingTransactionFilterID)
},
},
{
method: "eth_getFilterChanges",
variant: "block",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return a.EthGetFilterChanges(ctx, blockFilterID)
},
},
{
method: "eth_getFilterChanges",
variant: "logs",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return a.EthGetFilterChanges(ctx, logFilterID)
},
},
{
method: "eth_getFilterLogs",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return a.EthGetFilterLogs(ctx, logFilterID)
},
},
{
method: "eth_getLogs",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetLogs(context.Background(), filterAllLogs)
},
},
{
method: "eth_getStorageAt",
variant: "blocknumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
blockParam, _ := ethtypes.NewEthBlockParamFromHexString("0x0")
return ethapi.EthGetStorageAt(context.Background(), contractEthAddr, ethtypes.EthBytes{0}, blockParam)
},
},
{
method: "eth_getTransactionByBlockHashAndIndex",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetTransactionByBlockHashAndIndex(context.Background(), blockHashWithMessage, ethtypes.EthUint64(0))
},
skipReason: "unimplemented",
},
{
method: "eth_getTransactionByBlockNumberAndIndex",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetTransactionByBlockNumberAndIndex(context.Background(), blockNumberWithMessage, ethtypes.EthUint64(0))
},
skipReason: "unimplemented",
},
{
method: "eth_getTransactionByHash",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetTransactionByHash(context.Background(), &messageWithEvents)
},
},
{
method: "eth_getTransactionCount",
variant: "blocknumber",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetTransactionCount(context.Background(), senderEthAddr, ethtypes.NewEthBlockParamFromNumber(blockNumberWithMessage))
},
},
{
method: "eth_getTransactionReceipt",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthGetTransactionReceipt(context.Background(), messageWithEvents)
},
},
{
method: "eth_maxPriorityFeePerGas",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthMaxPriorityFeePerGas(context.Background())
},
},
{
method: "eth_newBlockFilter",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthNewBlockFilter(context.Background())
},
},
{
method: "eth_newFilter",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthNewFilter(context.Background(), filterAllLogs)
},
},
{
method: "eth_newPendingTransactionFilter",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthNewPendingTransactionFilter(context.Background())
},
},
{
method: "eth_sendRawTransaction",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return ethapi.EthSendRawTransaction(context.Background(), rawSignedEthTx)
},
},
{
method: "eth_uninstallFilter",
call: func(a *ethAPIRaw) (json.RawMessage, error) {
return a.EthUninstallFilter(ctx, uninstallableFilterID)
},
},
}
for _, tc := range testCases {
tc := tc
name := tc.method
if tc.variant != "" {
name += "_" + tc.variant
}
t.Run(name, func(t *testing.T) {
if tc.skipReason != "" {
t.Skipf(tc.skipReason)
}
schema, ok := schemas[tc.method]
require.True(t, ok, "method not found in openrpc spec")
resp, err := tc.call(&ethapi)
require.NoError(t, err)
respJson, err := json.Marshal(resp)
require.NoError(t, err)
loader := gojsonschema.NewGoLoader(schema)
resploader := gojsonschema.NewBytesLoader(respJson)
result, err := gojsonschema.Validate(loader, resploader)
require.NoError(t, err)
if !result.Valid() {
if len(result.Errors()) == 1 && strings.Contains(result.Errors()[0].String(), "Must validate one and only one schema (oneOf)") {
// Ignore this error, since it seems the openrpc spec can't handle it
// New transaction and block filters have the same schema: an array of 32 byte hashes
return
}
niceRespJson, err := json.MarshalIndent(resp, "", " ")
if err == nil {
t.Logf("response was %s", niceRespJson)
}
schemaJson, err := json.MarshalIndent(schema, "", " ")
if err == nil {
t.Logf("schema was %s", schemaJson)
}
// check against https://www.jsonschemavalidator.net/
for _, desc := range result.Errors() {
t.Logf("- %s\n", desc)
}
t.Errorf("response did not validate")
}
})
}
}
func createRawSignedEthTx(ctx context.Context, t *testing.T, client *kit.TestFullNode, senderEthAddr ethtypes.EthAddress, receiverEthAddr ethtypes.EthAddress, senderKey *key.Key, contractBin []byte) []byte {
gaslimit, err := client.EthEstimateGas(ctx, ethtypes.EthCall{
From: &senderEthAddr,
Data: contractBin,
})
require.NoError(t, err)
maxPriorityFeePerGas, err := client.EthMaxPriorityFeePerGas(ctx)
require.NoError(t, err)
tx := ethtypes.EthTxArgs{
ChainID: build.Eip155ChainId,
Value: big.NewInt(100),
Nonce: 0,
To: &receiverEthAddr,
MaxFeePerGas: types.NanoFil,
MaxPriorityFeePerGas: big.Int(maxPriorityFeePerGas),
GasLimit: int(gaslimit),
V: big.Zero(),
R: big.Zero(),
S: big.Zero(),
}
client.EVM().SignTransaction(&tx, senderKey.PrivateKey)
signed, err := tx.ToRlpSignedMsg()
require.NoError(t, err)
return signed
}
func waitForMessageWithEvents(ctx context.Context, t *testing.T, client *kit.TestFullNode, sender address.Address, target address.Address) (ethtypes.EthHash, ethtypes.EthHash, ethtypes.EthUint64) {
vals := []uint64{44, 27, 19, 12}
inputData := []byte{}
for _, v := range vals {
buf := make([]byte, 32)
binary.BigEndian.PutUint64(buf[24:], v)
inputData = append(inputData, buf...)
}
// send a message that exercises event logs
ret, err := client.EVM().InvokeSolidity(ctx, sender, target, kit.EventMatrixContract.Fn["logEventThreeIndexedWithData"], inputData)
require.NoError(t, err)
require.True(t, ret.Receipt.ExitCode.IsSuccess(), "contract execution failed")
msgHash, err := client.EthGetTransactionHashByCid(ctx, ret.Message)
require.NoError(t, err)
require.NotNil(t, msgHash)
ts, err := client.ChainGetTipSet(ctx, ret.TipSet)
require.NoError(t, err)
blockNumber := ethtypes.EthUint64(ts.Height())
tsCid, err := ts.Key().Cid()
require.NoError(t, err)
blockHash, err := ethtypes.EthHashFromCid(tsCid)
require.NoError(t, err)
return *msgHash, blockHash, blockNumber
}