Skip to content

Commit

Permalink
Merge pull request #2767 from OffchainLabs/test_gas_usage_hostio
Browse files Browse the repository at this point in the history
Test gas usage of hostios that don't have good EVM equivalents
  • Loading branch information
tsahee authored Nov 19, 2024
2 parents fdadb1b + 3d3e37b commit 387ac79
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 1 deletion.
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

0 comments on commit 387ac79

Please sign in to comment.