diff --git a/README.md b/README.md index f402bc7d..cd61d78b 100644 --- a/README.md +++ b/README.md @@ -29,45 +29,27 @@ make build ### Testing on Localnet -Start PostgreSQL (for testing [Postgres Docker](https://hub.docker.com/_/postgres) container can be used): +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 --name postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:13.3-alpine +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 ``` -Next, download and extract the latest [oasis-core] release to get `oasis-node` -and `oasis-net-runner` binaries. +If needed, the `oasis-web3-gateway` or `sapphire-paratime` executables could also be +bind-mounted into the container, allowing for quick turnaround time when testing +the full gateway & paratime stack together. -In a separate terminal, start the Oasis development local network. +### Running tests -For non-confidential ParaTimes (e.g. Emerald), this can be done as follows: +For running tests, start the docker network without the gateway, by setting the `OASIS_DOCKER_NO_GATEWAY=yes` environment variable: ```bash -export OASIS_NODE= -export OASIS_NET_RUNNER= -export PARATIME= -export PARATIME_VERSION= -export OASIS_NODE_DATADIR=/tmp/eth-runtime-test - -./tests/tools/spinup-oasis-stack.sh -``` - -Confidential ParaTimes (e.g. Sapphire) also require a key manager. You can use -`simple-keymanager` which you will need to compile yourself. It is part of the -[oasis-core] repository. Then, run the following: - -```bash -export OASIS_NODE= -export OASIS_NET_RUNNER= -export PARATIME= -export PARATIME_VERSION= -export KEYMANAGER_BINARY= -export OASIS_NODE_DATADIR=/tmp/eth-runtime-test - -./tests/tools/spinup-oasis-stack.sh +docker run --rm -ti -e OASIS_DOCKER_NO_GATEWAY=yes -p5432:5432 -p8545:8545 -p8546:8546 -v /tmp/eth-runtime-test:/serverdir/node ghcr.io/oasisprotocol/sapphire-localnet:local -test-mnemonic -n 4 ``` -Finally, run the tests: +Once bootstrapped, run the tests: ```bash make test diff --git a/db/migrations/20240527164010_receipt_effective_gas_price.up.sql b/db/migrations/20240527164010_receipt_effective_gas_price.up.sql new file mode 100644 index 00000000..ba133eed --- /dev/null +++ b/db/migrations/20240527164010_receipt_effective_gas_price.up.sql @@ -0,0 +1 @@ +ALTER TABLE receipts ADD COLUMN "effective_gas_price" bigint; diff --git a/db/migrations/migrations.go b/db/migrations/migrations.go index 9660ac3b..58bda020 100644 --- a/db/migrations/migrations.go +++ b/db/migrations/migrations.go @@ -108,6 +108,11 @@ func init() { Name: "20220324091030", Up: migrator.NewSQLMigrationFunc(migrations, "20220324091030_receipt_round_index.up.sql"), }, + // Add effective gas price field to receipt. + { + Name: "20240527164010", + Up: migrator.NewSQLMigrationFunc(migrations, "20240527164010_receipt_effective_gas_price.up.sql"), + }, } { Migrations.Add(m) } diff --git a/db/model/model.go b/db/model/model.go index 9f016cbe..7041b3c9 100644 --- a/db/model/model.go +++ b/db/model/model.go @@ -155,6 +155,7 @@ type Receipt struct { FromAddr string ToAddr string ContractAddress string + EffectiveGasPrice string } // Size returns the approximate size of a Receipt in bytes. @@ -174,5 +175,6 @@ func (r *Receipt) Size() int { sz += len(r.FromAddr) sz += len(r.ToAddr) sz += len(r.ContractAddress) + sz += len(r.EffectiveGasPrice) return sz } diff --git a/docker/README.md b/docker/README.md index 13201d8d..408e550e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -25,7 +25,7 @@ docker run -it -p8545:8545 -p8546:8546 ghcr.io/oasisprotocol/sapphire-localnet # ### Mac M Chips -There is currently no arm64 build available for M Macs, so make sure to force the docker image to use _linux/x86_64_, +There is currently no arm64 build available for M Macs, so make sure to force the docker image to use _linux/x86_64_, like this: ```sh @@ -77,18 +77,3 @@ 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 d720245f..b25507c9 100755 --- a/docker/common/start.sh +++ b/docker/common/start.sh @@ -13,6 +13,8 @@ set -euo pipefail +export OASIS_DOCKER_NO_GATEWAY=${OASIS_DOCKER_NO_GATEWAY:-no} + export OASIS_NODE_LOG_LEVEL=${OASIS_NODE_LOG_LEVEL:-warn} export OASIS_DOCKER_USE_TIMESTAMPS_IN_NOTICES=${OASIS_DOCKER_USE_TIMESTAMPS_IN_NOTICES:-no} @@ -118,9 +120,13 @@ 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=$! +if [[ $OASIS_DOCKER_NO_GATEWAY == 'yes' ]]; then + notice "Skipping oasis-web3-gateway start-up...\n" +else + 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=$! +fi # Wait for compute nodes before initiating deposit. notice "Bootstrapping network (this might take a minute)" @@ -151,7 +157,7 @@ else fi echo -if [[ ${PARATIME_NAME} == 'sapphire' ]]; then +if [[ $OASIS_DOCKER_NO_GATEWAY != 'yes' && $PARATIME_NAME == 'sapphire' ]]; then notice "Waiting for key manager..." function is_km_ready() { curl -X POST -s \ diff --git a/indexer/utils.go b/indexer/utils.go index 6ac50c52..48702aea 100644 --- a/indexer/utils.go +++ b/indexer/utils.go @@ -16,6 +16,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/quantity" "github.com/oasisprotocol/oasis-core/go/roothash/api/block" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/core" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/evm" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" @@ -90,7 +91,7 @@ func blockToModels( transactions ethtypes.Transactions, logs []*ethtypes.Log, txsStatus []uint8, - txsGasUsed []uint64, + txsGas []txGas, results []types.CallResult, blockGasLimit uint64, ) (*model.Block, []*model.Transaction, []*model.Receipt, []*model.Log, error) { @@ -180,7 +181,7 @@ func blockToModels( innerTxs = append(innerTxs, tx) // receipts - cumulativeGasUsed += txsGasUsed[idx] + cumulativeGasUsed += txsGas[idx].used receipt := &model.Receipt{ Status: uint(txsStatus[idx]), CumulativeGasUsed: cumulativeGasUsed, @@ -188,7 +189,8 @@ func blockToModels( LogsBloom: bloomHex, TransactionHash: ethTxHex, BlockHash: bhash, - GasUsed: txsGasUsed[idx], + GasUsed: txsGas[idx].used, + EffectiveGasPrice: txsGas[idx].effectivePrice.String(), Type: uint(ethTx.Type()), Round: block.Header.Round, TransactionIndex: uint64(idx), @@ -219,6 +221,11 @@ func blockToModels( return innerBlock, innerTxs, innerReceipts, dbLogs, nil } +type txGas struct { + used uint64 + effectivePrice *big.Int +} + // StoreBlockData parses oasis block and stores in db. func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Block, txResults []*client.TransactionWithResults, blockGasLimit uint64) error { //nolint: gocyclo encoded := oasisBlock.Header.EncodedHash() @@ -228,7 +235,7 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl ethTxs := ethtypes.Transactions{} logs := []*ethtypes.Log{} txsStatus := []uint8{} - txsGasUsed := []uint64{} + txsGas := []txGas{} results := []types.CallResult{} var medianTransactionGasPrice *quantity.Quantity @@ -278,6 +285,7 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl ethTxs = append(ethTxs, ethTx) var txGasUsed uint64 + txGasSpent := big.NewInt(0) var oasisLogs []*Log resEvents := item.Events for eventIndex, event := range resEvents { @@ -322,6 +330,35 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl } txGasUsed = ce.GasUsed.Amount } + case event.Module == accounts.ModuleName && event.Code == accounts.TransferEventCode: + // Parse fee transfer event, to obtain fee payment. + events, err := accounts.DecodeEvent(event) + if err != nil { + ib.logger.Error("failed to decode accounts transfer event", "index", eventIndex, "err", err) + continue + } + for _, ev := range events { + ae, ok := ev.(*accounts.Event) + if !ok { + ib.logger.Error("invalid accounts event", "event", ev, "index", eventIndex) + continue + } + te := ae.Transfer + if te == nil { + ib.logger.Error("invalid accounts transfer event", "event", ev, "index", eventIndex) + continue + } + // Skip transfers that are not to the fee accumulator. + if !te.To.Equal(accounts.FeeAccumulatorAddress) { + continue + } + // Skip non native denom. + if !te.Amount.Denomination.IsNative() { + continue + } + txGasSpent.Add(txGasSpent, te.Amount.Amount.ToBigInt()) + } + default: // Ignore any other events. } @@ -334,11 +371,23 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl ib.logger.Debug("no GasUsedEvent for a transaction, defaulting to gas limit", "transaction_index", txIndex, "round", oasisBlock.Header.Round) txGasUsed = ethTx.Gas() } - txsGasUsed = append(txsGasUsed, txGasUsed) + + var txEffectivePrice *big.Int + switch { + case txGasSpent.Cmp(big.NewInt(0)) > 0: + // If there was a fee payment event observed, calculate effective gas price. + txEffectivePrice = new(big.Int).Div(txGasSpent, new(big.Int).SetUint64(txGasUsed)) + default: + // In old runtimes, there was no transfer events emitted for fee payments, just use gas price in those cases. + // Gas price should likely be correct since those runtimes also didn't support EIP-1559 transactions, so + // gas price should match effective gas price. + txEffectivePrice = ethTx.GasPrice() + } + txsGas = append(txsGas, txGas{used: txGasUsed, effectivePrice: txEffectivePrice}) } // Convert to DB models. - blk, txs, receipts, dbLogs, err := blockToModels(oasisBlock, ethTxs, logs, txsStatus, txsGasUsed, results, blockGasLimit) + blk, txs, receipts, dbLogs, err := blockToModels(oasisBlock, ethTxs, logs, txsStatus, txsGas, results, blockGasLimit) if err != nil { ib.logger.Debug("Failed to ConvertToEthBlock", "height", blockNum, "err", err) return err @@ -466,7 +515,7 @@ func (ib *indexBackend) StoreBlockData(ctx context.Context, oasisBlock *block.Bl return nil } -// db2EthReceipt converts model.Receipt to the GetTransactipReceipt format. +// db2EthReceipt converts model.Receipt to the GetTransactionReceipt format. func db2EthReceipt(dbReceipt *model.Receipt) map[string]interface{} { ethLogs := make([]*ethtypes.Log, 0, len(dbReceipt.Logs)) for _, dbLog := range dbReceipt.Logs { @@ -492,6 +541,7 @@ func db2EthReceipt(dbReceipt *model.Receipt) map[string]interface{} { ethLogs = append(ethLogs, log) } + effectiveGasPrice, _ := new(big.Int).SetString(dbReceipt.EffectiveGasPrice, 10) receipt := map[string]interface{}{ "status": hexutil.Uint(dbReceipt.Status), "cumulativeGasUsed": hexutil.Uint64(dbReceipt.CumulativeGasUsed), @@ -506,6 +556,7 @@ func db2EthReceipt(dbReceipt *model.Receipt) map[string]interface{} { "from": nil, "to": nil, "contractAddress": nil, + "effectiveGasPrice": (*hexutil.Big)(effectiveGasPrice), } if dbReceipt.FromAddr != "" { receipt["from"] = dbReceipt.FromAddr diff --git a/tests/rpc/tx_test.go b/tests/rpc/tx_test.go index 30f6e385..b81d0e26 100644 --- a/tests/rpc/tx_test.go +++ b/tests/rpc/tx_test.go @@ -240,7 +240,8 @@ func testContractCreation(t *testing.T, value *big.Int, txEIP int) uint64 { Nonce: nonce, Value: value, Gas: gasLimit, - GasFeeCap: gasPrice, + GasFeeCap: new(big.Int).Add(gasPrice, big.NewInt(100_000_000_000_000_000)), + GasTipCap: big.NewInt(500_000_000), Data: code, }) case 2930: @@ -276,20 +277,21 @@ func testContractCreation(t *testing.T, value *big.Int, txEIP int) uint64 { t.Logf("Contract address: %s", receipt.ContractAddress) - /* - // Ensure the effective gas price is what was returned from `eth_gasPrice` - t.Logf("effective: %d, %d", receipt.EffectiveGasPrice, gasPrice) - require.Equal(t, receipt.EffectiveGasPrice, gasPrice) - */ + // Ensure the effective gas price is what was returned from `eth_gasPrice`. + t.Logf("Effective gas price: %d, gas used: %d", receipt.EffectiveGasPrice, receipt.GasUsed) balanceAfter, err := ec.BalanceAt(context.Background(), tests.TestKey1.EthAddress, nil) require.NoError(t, err) - t.Logf("Spent for gas: %s", new(big.Int).Sub(balanceBefore, balanceAfter)) + actualSpent := new(big.Int).Sub(balanceBefore, balanceAfter) + t.Logf("Spent for gas: %s", actualSpent) // Always require that some gas was spent. require.Positive(t, balanceBefore.Cmp(balanceAfter)) + // Ensure that the spent was exactly the effective gas price * gas used. + require.EqualValues(t, actualSpent, new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), receipt.EffectiveGasPrice), "`effective gas price * gas used` should match actual spent") + // If the contract deployed successfully, check that unspent gas was returned. if receipt.Status == 1 { minBalanceAfter := new(big.Int).Sub(balanceBefore, new(big.Int).Mul(big.NewInt(int64(GasLimit)), GasPrice))