Skip to content

Commit

Permalink
Merge pull request #575 from oasisprotocol/ptrus/feature/effective-ga…
Browse files Browse the repository at this point in the history
…s-price

feat: include `effectiveGasPrice` in `eth_getTransactionReceipt` response
  • Loading branch information
ptrus authored May 28, 2024
2 parents 791c8df + 3e17cd1 commit 37b7166
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 63 deletions.
40 changes: 11 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path-to-oasis-node-binary>
export OASIS_NET_RUNNER=<path-to-oasis-net-runner-binary>
export PARATIME=<path-to-paratime-localnet-binary-elf>
export PARATIME_VERSION=<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=<path-to-oasis-node-binary>
export OASIS_NET_RUNNER=<path-to-oasis-net-runner-binary>
export PARATIME=<path-to-paratime-localnet-binary-elf>
export PARATIME_VERSION=<paratime-version>
export KEYMANAGER_BINARY=<path-to-simple-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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE receipts ADD COLUMN "effective_gas_price" bigint;
5 changes: 5 additions & 0 deletions db/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions db/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
17 changes: 1 addition & 16 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
14 changes: 10 additions & 4 deletions docker/common/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -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 \
Expand Down
65 changes: 58 additions & 7 deletions indexer/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -180,15 +181,16 @@ func blockToModels(
innerTxs = append(innerTxs, tx)

// receipts
cumulativeGasUsed += txsGasUsed[idx]
cumulativeGasUsed += txsGas[idx].used
receipt := &model.Receipt{
Status: uint(txsStatus[idx]),
CumulativeGasUsed: cumulativeGasUsed,
Logs: txLogs[ethTxHex],
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),
Expand Down Expand Up @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand All @@ -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
Expand Down
16 changes: 9 additions & 7 deletions tests/rpc/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 37b7166

Please sign in to comment.