508 lines
17 KiB
Go
508 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.EthBlockNumberOrHash) (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.EthBlockNumberOrHash) (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.EthBlockNumberOrHash) (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.EthBlockNumberOrHash) (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.EthBlockNumberOrHash) (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", ðapi, 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.NewEthBlockNumberOrHashFromPredefined("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.NewEthBlockNumberOrHashFromHexString("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.NewEthBlockNumberOrHashFromNumber(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.NewEthBlockNumberOrHashFromHexString("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.NewEthBlockNumberOrHashFromNumber(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(ðapi)
|
|
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
|
|
}
|