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)