diff --git a/.github/workflows/ci-test.yaml b/.github/workflows/ci-test.yaml index eda6bec4..6890ce16 100644 --- a/.github/workflows/ci-test.yaml +++ b/.github/workflows/ci-test.yaml @@ -106,10 +106,10 @@ jobs: ports: - 5432:5432 env: - OASIS_CORE_VERSION: "23.0.9" + OASIS_CORE_VERSION: "23.0.10" OASIS_NODE: ${{ github.workspace }}/oasis_core/oasis-node OASIS_NET_RUNNER: ${{ github.workspace }}/oasis_core/oasis-net-runner - SAPPHIRE_PARATIME_VERSION: 0.7.1-testnet + SAPPHIRE_PARATIME_VERSION: 0.7.2-testnet GATEWAY__CHAIN_ID: 23293 GATEWAY__OASIS_RPCS: true SAPPHIRE_PARATIME: ${{ github.workspace }}/oasis_core/sapphire-paratime diff --git a/conf/tests.yml b/conf/tests.yml index 0b5fbb45..6ff0d4bf 100644 --- a/conf/tests.yml +++ b/conf/tests.yml @@ -21,13 +21,13 @@ database: max_open_conns: 0 gateway: - chain_id: 42262 + chain_id: 23293 http: host: "localhost" - port: 8545 + port: 8945 ws: host: "localhost" - port: 8546 + port: 8946 monitoring: host: "localhost" port: 9999 diff --git a/docker/README.md b/docker/README.md index 54c0a78b..1238be14 100644 --- a/docker/README.md +++ b/docker/README.md @@ -68,3 +68,18 @@ By default, the Oasis Web3 gateway and the Oasis node are configured with the *warn* verbosity level. To increase verbosity to *debug*, you can run the Docker container with `-e LOG__LEVEL=debug` for the Web3 gateway and `-e OASIS_NODE_LOG_LEVEL=debug` for the Oasis node. + +## Running Tests + +As an alternatively to running Postgres & using `spinup-oasis-stack.sh` the +Docker containers can be used to test changes to the gateway or run tests by +bind-mounting the runtime state directory (`/serverdir/node`) into your local +filesystem. + +```bash +docker run --rm -ti -p5432:5432 -p8545:8545 -p8546:8546 -v /tmp/eth-runtime-test:/serverdir/node ghcr.io/oasisprotocol/sapphire-localnet:local -test-mnemonic -n 4 +``` + +The `oasis-web3-gateway` or `sapphire-paratime` executables could also be +bind-mounted into the container, allowing for quick tunraround time when testing +the full gateway & paratime stack together. diff --git a/docker/common/start.sh b/docker/common/start.sh index 2b5a7dda..b68f80e2 100755 --- a/docker/common/start.sh +++ b/docker/common/start.sh @@ -87,6 +87,13 @@ notice "Waiting for Oasis node to start..." while [[ ! -S ${OASIS_NODE_SOCKET} ]]; do echo -n .; sleep 1; done echo +# Fixes permissions so oasis-web3-gateway tests can be run +# While /serverdir/node is bind-mounted to /tmp/eth-runtime-test +chmod 755 /serverdir/node/net-runner/ +chmod 755 /serverdir/node/net-runner/network/ +chmod 755 /serverdir/node/net-runner/network/client-0/ +chmod a+rw /serverdir/node/net-runner/network/client-0/internal.sock + notice "Starting oasis-web3-gateway...\n" ${OASIS_WEB3_GATEWAY} --config ${OASIS_WEB3_GATEWAY_CONFIG_FILE} 2>1 &>/var/log/oasis-web3-gateway.log & OASIS_WEB3_GATEWAY_PID=$! diff --git a/rpc/eth/api.go b/rpc/eth/api.go index 604c7408..43e9f28f 100644 --- a/rpc/eth/api.go +++ b/rpc/eth/api.go @@ -367,11 +367,13 @@ func (api *publicAPI) Call(ctx context.Context, args utils.TransactionArgs, bloc sender = common.Address{1} gasPrice = []byte{1} // This gas cap should be enough for SimulateCall an ethereum transaction - gas uint64 = 30_000_000 + gas uint64 = 30_000_000 + toBytes = []byte{} ) - if args.To == nil { - return []byte{}, errors.New("to address not specified") + // When simulating a contract deploy the To address may not be specified + if args.To != nil { + toBytes = args.To.Bytes() } if args.GasPrice != nil { gasPrice = args.GasPrice.ToInt().Bytes() @@ -396,14 +398,14 @@ func (api *publicAPI) Call(ctx context.Context, args utils.TransactionArgs, bloc } res, err := evm.NewV1(api.client).SimulateCall( - ctx, - round, - gasPrice, - gas, - sender.Bytes(), - args.To.Bytes(), - amount, - input, + ctx, // context + round, // round + gasPrice, // gasPrice + gas, // gasLimit + sender.Bytes(), // caller + toBytes, // address + amount, // value + input, // data ) if err != nil { return nil, api.handleCallFailure(ctx, logger, err) diff --git a/tests/rpc/contracts/evm_constructor_require.hex b/tests/rpc/contracts/evm_constructor_require.hex new file mode 100644 index 00000000..94486537 --- /dev/null +++ b/tests/rpc/contracts/evm_constructor_require.hex @@ -0,0 +1 @@ +6080604052348015600e575f80fd5b5060405162461bcd60e51b815260206004820152601360248201527f4578616d706c654572726f725265717569726500000000000000000000000000604482015260640160405180910390fdfe \ No newline at end of file diff --git a/tests/rpc/contracts/evm_constructor_revert.hex b/tests/rpc/contracts/evm_constructor_revert.hex new file mode 100644 index 00000000..b5af4c8d --- /dev/null +++ b/tests/rpc/contracts/evm_constructor_revert.hex @@ -0,0 +1 @@ +6080604052348015600e575f80fd5b5060405163547796ff60e01b815260206004820152601260248201527122bc30b6b83632a1bab9ba37b6a2b93937b960711b604482015260640160405180910390fdfe \ No newline at end of file diff --git a/tests/rpc/tx_test.go b/tests/rpc/tx_test.go index 5dfc40bf..dce00a8f 100644 --- a/tests/rpc/tx_test.go +++ b/tests/rpc/tx_test.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "math/big" + "reflect" "strings" "testing" "time" @@ -71,6 +72,130 @@ func waitTransaction(ctx context.Context, ec *ethclient.Client, txhash common.Ha } } +/* +contract ExampleRequire { + constructor () { + require(false, "ExampleErrorRequire"); + } +} +*/ +//go:embed contracts/evm_constructor_require.hex +var evmConstructorRequireHex string + +/* +contract ExampleCustomError { + error Example(string x); + constructor () { + revert Example("ExampleCustomError"); + } +} +*/ +//go:embed contracts/evm_constructor_revert.hex +var evmConstructorRevertHex string + +// extractDataFromUnexportedError extracts the "Data" field from *rpc.jsonError +// that is not exported using reflection. +func extractDataFromUnexportedError(err error) string { + if err == nil { + return "" + } + + val := reflect.ValueOf(err) + if val.Kind() == reflect.Ptr && !val.IsNil() { + // Assuming jsonError is a struct + errVal := val.Elem() + + // Check if the struct has a field named "Data". + dataField := errVal.FieldByName("Data") + if dataField.IsValid() && dataField.CanInterface() { + // Assuming the data field is a string + return dataField.Interface().(string) + } + } + + return "" +} + +func skipIfNotSapphire(t *testing.T, ec *ethclient.Client) { + chainID, err := ec.ChainID(context.Background()) + require.Nil(t, err, "get chainid") + + switch chainID.Uint64() { + case 0x5afd, 0x5afe, 0x5aff: // For sapphire chain IDs, do nothing + default: + t.Skip("Skipping contract constructor tests on non-Sapphire chain") + } +} + +// Verifies that require(false,"...") in constructor works as expected. +func testContractConstructorError(t *testing.T, contractCode string, expectedError string) { + ec := localClient(t, false) + skipIfNotSapphire(t, ec) + + t.Logf("compiled contract: %s", contractCode) + code := common.FromHex(strings.TrimSpace(contractCode)) + + callMsg := ethereum.CallMsg{ + From: tests.TestKey1.EthAddress, + Gas: 0, + Data: code, + } + + // Estimate contract creation gas requirements. + gasCost, err := ec.EstimateGas(context.Background(), callMsg) + require.Nil(t, err, "estimate gas failed") + require.Greater(t, gasCost, uint64(0), "gas estimate must be positive") + t.Logf("gas estimate for contract creation: %d", gasCost) + + // Evaluate the contract creation via `eth_call`. + callMsg.Gas = 250000 // gasCost + result, err := ec.CallContract(context.Background(), callMsg, nil) + require.NotNil(t, err, "constructor expected to throw error") + require.Equal(t, expectedError, extractDataFromUnexportedError(err)) + require.Nil(t, result) +} + +func TestContractConstructorRequire(t *testing.T) { + testContractConstructorError(t, evmConstructorRequireHex, "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000134578616d706c654572726f725265717569726500000000000000000000000000") +} + +func TestContractConstructorRevert(t *testing.T) { + testContractConstructorError(t, evmConstructorRevertHex, "0x547796ff000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000124578616d706c65437573746f6d4572726f720000000000000000000000000000") +} + +// Verify contract creation can be simulated and returns an address. +func TestContractCreateInCall(t *testing.T) { + ec := localClient(t, false) + skipIfNotSapphire(t, ec) + + t.Logf("compiled contract: %s", evmSolTestCompiledHex) + code := common.FromHex(strings.TrimSpace(evmSolTestCompiledHex)) + + gasPrice, err := ec.SuggestGasPrice(context.Background()) + require.Nil(t, err, "get gasPrice failed") + + callMsg := ethereum.CallMsg{ + From: tests.TestKey1.EthAddress, + Gas: 0, + GasPrice: gasPrice, + Data: code, + } + + // Estimate contract creation gas requirements. + gasCost, err := ec.EstimateGas(context.Background(), callMsg) + require.NoError(t, err, "estimate gas failed") + require.Greater(t, gasCost, uint64(0), "gas estimate must be positive") + t.Logf("gas estimate for contract creation: %d", gasCost) + + // Evaluate the contract creation via `eth_call`. + callMsg.Gas = 250000 // gasCost + result, err := ec.CallContract(context.Background(), callMsg, nil) + require.Nil(t, err, "call contract") + + require.NotNil(t, result) + require.Equal(t, len(result), 20) +} + func testContractCreation(t *testing.T, value *big.Int) uint64 { ec := localClient(t, false) diff --git a/tests/tools/spinup-oasis-stack.sh b/tests/tools/spinup-oasis-stack.sh index e6cc5f7f..db6a6b16 100755 --- a/tests/tools/spinup-oasis-stack.sh +++ b/tests/tools/spinup-oasis-stack.sh @@ -19,8 +19,12 @@ function paratime_ver { export FIXTURE_FILE="${OASIS_NODE_DATADIR}/fixture.json" export STAKING_GENESIS_FILE="$(dirname "$0")/staking_genesis.json" -rm -rf "$OASIS_NODE_DATADIR" -mkdir -p "$OASIS_NODE_DATADIR" +rm -rf "$OASIS_NODE_DATADIR/net-runner" +rm -rf "$OASIS_NODE_DATADIR/net-runner.log" +rm -rf "$OASIS_NODE_DATADIR/fixture.json" +# When $OASIS_NODE_DATADIR is bind-mounted, below fails, but the above succeed +rm -rf "$OASIS_NODE_DATADIR" || true +mkdir -p "$OASIS_NODE_DATADIR" || true # Prepare configuration for oasis-node (fixture). ${OASIS_NET_RUNNER} dump-fixture \