Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test gas usage of hostios that don't have good EVM equivalents #2767

Merged
merged 10 commits into from
Nov 19, 2024
33 changes: 33 additions & 0 deletions arbitrator/stylus/tests/hostio-test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,37 @@ impl HostioTest {
fn tx_origin() -> Result<Address> {
Ok(tx::origin())
}

fn storage_cache_bytes32() {
let key = B256::ZERO;
let val = B256::ZERO;
unsafe {
hostio::storage_cache_bytes32(key.as_ptr(), val.as_ptr());
}
}

fn pay_for_memory_grow(pages: U256) {
let pages: u16 = pages.try_into().unwrap();
unsafe {
hostio::pay_for_memory_grow(pages);
}
}

fn write_result_empty() {
}

fn write_result(size: U256) -> Result<Vec<u32>> {
let size: usize = size.try_into().unwrap();
let data = vec![0; size];
Ok(data)
}

fn read_args_no_args() {
}

fn read_args_one_arg(_arg1: U256) {
}

fn read_args_three_args(_arg1: U256, _arg2: U256, _arg3: U256) {
}
}
2 changes: 1 addition & 1 deletion arbitrator/wasm-libraries/user-host-trait/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ pub trait UserHost<DR: DataReader>: GasMeteredMachine {
fn pay_for_memory_grow(&mut self, pages: u16) -> Result<(), Self::Err> {
if pages == 0 {
self.buy_ink(HOSTIO_INK)?;
return Ok(());
return trace!("pay_for_memory_grow", self, be!(pages), &[]);
}
let gas_cost = self.evm_api().add_pages(pages); // no sentry needed since the work happens after the hostio
self.buy_gas(gas_cost)?;
Expand Down
158 changes: 158 additions & 0 deletions system_tests/program_gas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package arbtest

import (
"context"
"encoding/binary"
"fmt"
"math"
"math/big"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
Expand All @@ -24,6 +26,162 @@ import (
"github.com/offchainlabs/nitro/util/testhelpers"
)

const HOSTIO_INK = 8400

func checkInkUsage(
t *testing.T,
builder *NodeBuilder,
stylusProgram common.Address,
hostio string,
signature string,
params []uint32,
expectedInk uint64,
) {
toU256ByteSlice := func(i uint32) []byte {
arr := make([]byte, 32)
binary.BigEndian.PutUint32(arr[28:32], i)
return arr
}

testName := fmt.Sprintf("%v_%v", signature, params)

data := crypto.Keccak256([]byte(signature))[:4]
for _, p := range params {
data = append(data, toU256ByteSlice(p)...)
}

const txGas uint64 = 32_000_000
tx := builder.L2Info.PrepareTxTo("Owner", &stylusProgram, txGas, nil, data)

err := builder.L2.Client.SendTransaction(builder.ctx, tx)
Require(t, err, "testName", testName)
_, err = builder.L2.EnsureTxSucceeded(tx)
Require(t, err, "testName", testName)

stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), tx)
Require(t, err, "testName", testName)

_, ok := stylusGasUsage[hostio]
if !ok {
Fatal(t, "hostio not found in gas usage", "hostio", hostio, "stylusGasUsage", stylusGasUsage, "testName", testName)
}

if len(stylusGasUsage[hostio]) != 1 {
Fatal(t, "unexpected number of gas usage", "hostio", hostio, "stylusGasUsage", stylusGasUsage, "testName", testName)
}

expectedGas := float64(expectedInk) / 10000
returnedGas := stylusGasUsage[hostio][0]
if math.Abs(expectedGas-returnedGas) > 1e-9 {
Fatal(t, "unexpected gas usage", "hostio", hostio, "expected", expectedGas, "returned", returnedGas, "testName", testName)
}
}

func TestWriteResultGasUsage(t *testing.T) {
t.Parallel()

builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test"))

hostio := "write_result"

// writeResultEmpty doesn't return any value
signature := "writeResultEmpty()"
expectedInk := HOSTIO_INK + 16381*2
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk))

// writeResult(uint256) returns an array of uint256
signature = "writeResult(uint256)"
numberOfElementsInReturnedArray := 10000
arrayOverhead := 32 + 32 // 32 bytes for the array length and 32 bytes for the array offset
expectedInk = HOSTIO_INK + (16381+55*(32*numberOfElementsInReturnedArray+arrayOverhead-32))*2
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{uint32(numberOfElementsInReturnedArray)}, uint64(expectedInk))

signature = "writeResult(uint256)"
numberOfElementsInReturnedArray = 0
expectedInk = HOSTIO_INK + (16381+55*(arrayOverhead-32))*2
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{uint32(numberOfElementsInReturnedArray)}, uint64(expectedInk))
}

func TestReadArgsGasUsage(t *testing.T) {
t.Parallel()

builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test"))

hostio := "read_args"

signature := "readArgsNoArgs()"
expectedInk := HOSTIO_INK + 5040
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk))

signature = "readArgsOneArg(uint256)"
signatureOverhead := 4
expectedInk = HOSTIO_INK + 5040 + 30*(32+signatureOverhead-32)
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{1}, uint64(expectedInk))

signature = "readArgsThreeArgs(uint256,uint256,uint256)"
expectedInk = HOSTIO_INK + 5040 + 30*(3*32+signatureOverhead-32)
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{1, 1, 1}, uint64(expectedInk))
}

func TestMsgReentrantGasUsage(t *testing.T) {
t.Parallel()

builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test"))

hostio := "msg_reentrant"

signature := "writeResultEmpty()"
expectedInk := HOSTIO_INK
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk))
}

func TestStorageCacheBytes32GasUsage(t *testing.T) {
t.Parallel()

builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test"))

hostio := "storage_cache_bytes32"

signature := "storageCacheBytes32()"
expectedInk := HOSTIO_INK + (13440-HOSTIO_INK)*2
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, nil, uint64(expectedInk))
}

func TestPayForMemoryGrowGasUsage(t *testing.T) {
t.Parallel()

builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test"))

hostio := "pay_for_memory_grow"
signature := "payForMemoryGrow(uint256)"

expectedInk := 9320660000
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{100}, uint64(expectedInk))

expectedInk = HOSTIO_INK
// #nosec G115
checkInkUsage(t, builder, stylusProgram, hostio, signature, []uint32{0}, uint64(expectedInk))
}

func TestProgramSimpleCost(t *testing.T) {
builder := setupGasCostTest(t)
auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx)
Expand Down
Loading