diff --git a/arbitrator/stylus/tests/hostio-test/src/main.rs b/arbitrator/stylus/tests/hostio-test/src/main.rs
index 17a5d10266..47b46daad2 100644
--- a/arbitrator/stylus/tests/hostio-test/src/main.rs
+++ b/arbitrator/stylus/tests/hostio-test/src/main.rs
@@ -204,4 +204,37 @@ impl HostioTest {
fn tx_origin() -> Result
{
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> {
+ 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) {
+ }
}
diff --git a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs
index 35a4a31347..2f410849fc 100644
--- a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs
+++ b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs
@@ -936,7 +936,7 @@ pub trait UserHost: 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)?;
diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go
index e924b224b2..3450214a14 100644
--- a/system_tests/program_gas_test.go
+++ b/system_tests/program_gas_test.go
@@ -2,6 +2,7 @@ package arbtest
import (
"context"
+ "encoding/binary"
"fmt"
"math"
"math/big"
@@ -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"
@@ -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)