Skip to content

Gas Dimension Trace Tests #3229

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

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9a4f103
feat: Live tracing
rollchad Apr 2, 2025
61d011b
Fix OnBlockStart and OnBlockEnd live tracer hooks not firing
relyt29 Apr 10, 2025
db94a54
ignore cursor rules
relyt29 Apr 23, 2025
fce897c
Demonstrate ability to run integration test testing live tracer output
relyt29 Apr 24, 2025
5ccccfc
gas dimension trace test with simple calculation passing
relyt29 Apr 25, 2025
ef2b2ac
rename file
relyt29 Apr 25, 2025
81c6ba8
gas dimension trace tests: SLOAD cold, warm
relyt29 Apr 25, 2025
440f57d
gas dimension trace tests: BALANCE,EXTCODESIZE,EXTCODEHASH warm and cold
relyt29 Apr 28, 2025
48bcdeb
gas dimension trace tests: EXTCODECOPY with memory expansion
relyt29 Apr 28, 2025
ed24023
gas dimension trace tests: use core/params values and one-dimensional…
relyt29 Apr 28, 2025
c7c03cb
gas dimension trace tests: enumerate list of all remaining test cases
relyt29 Apr 28, 2025
0dbe280
gas dimension trace tests: log test cases
relyt29 Apr 30, 2025
4bdb63c
gas dimension trace tests: write header comments for all the sstore
relyt29 Apr 30, 2025
e3449d5
gas dimension trace tests: refactor reduce code duplication on deploy
relyt29 Apr 30, 2025
fc1dcea
gas dimension trace tests: refactor get specific opcode log pattern
relyt29 Apr 30, 2025
5f752bc
gas dimension trace tests: single SSTORE test cases
relyt29 Apr 30, 2025
a19036d
gas dimension trace tests: multiple SSTORE in a row test cases
relyt29 Apr 30, 2025
e573408
gas dimension trace tests: SELFDESTRUCT tests
relyt29 Apr 30, 2025
6147c53
gas dimension trace tests: DELEGATECALL tests
relyt29 May 1, 2025
994ef3b
gas dimension trace tests: STATICCALL tests
relyt29 May 1, 2025
5d0c457
gas dimension trace tests: tweak comments + start on CALL
relyt29 May 1, 2025
a2c8ef9
gas dimension trace tests: split into separate files
relyt29 May 2, 2025
adba03d
gas dimension trace tests: CALL tests
relyt29 May 2, 2025
18dc086
gas dimension trace testing: refactor CALL tests order, comment headers
relyt29 May 2, 2025
24567bc
gas dimension trace tests: CALLCODE tests
relyt29 May 5, 2025
5dda3ec
gas dimension trace tests: CREATE + CREATE2 opcodes
relyt29 May 5, 2025
c42d658
gas dimension trace tests: boilerplate for gas_dim_tx_opcode tests
relyt29 May 6, 2025
1a66177
gas dimension trace tests: computation-only passing, boilerplate BALANCE
relyt29 May 6, 2025
06606c7
gas dimension trace tests: use tracer L1/L2GasUsed + ChildExecutionGas
relyt29 May 6, 2025
1827e8a
gas dimension trace tests: tx opcodes by dimension
relyt29 May 7, 2025
8d7112e
gas dimension trace tests: use capped adjusted gas refund
relyt29 May 7, 2025
a2bb014
gas dimension trace tests: fix linter issues
relyt29 May 8, 2025
4704d17
gas dimension tracing: rebasing
relyt29 May 9, 2025
482aede
Resolve conflicts from #3066 and #3138
relyt29 May 9, 2025
79d975a
gas dimension tracing: tests and solgen added to contracts subrepo
relyt29 May 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ node_modules
.DS_Store
.idea
.vscode
.cursor
cmd/node/data
cmd/node/node
solgen/go/
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ test-go-redis: test-go-deps
gotestsum --format short-verbose --no-color=false -- -p 1 -run TestRedis ./system_tests/... ./arbnode/... -- --test_redis=redis://localhost:6379/0
@printf $(done)

.PHONY: test-go-gas-dimensions
test-go-gas-dimensions: test-go-deps
gotestsum --format short-verbose --no-color=false -- -timeout 120m ./system_tests/... -run "TestDim(Log|TxOp)" -tags gasdimensionstest
@printf $(done)

.PHONY: test-gen-proofs
test-gen-proofs: \
$(arbitrator_test_wasms) \
Expand Down Expand Up @@ -596,6 +601,7 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(prover_bin)
yarn --cwd safe-smart-account build
yarn --cwd contracts build
yarn --cwd contracts build:forge:yul
yarn --cwd contracts build:forge:gas-dimensions
yarn --cwd contracts-legacy build
yarn --cwd contracts-legacy build:forge:yul
@touch $@
Expand Down
2 changes: 1 addition & 1 deletion contracts
Submodule contracts updated 102 files
28 changes: 28 additions & 0 deletions solgen/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,34 @@ func main() {
})
}

gasDimensionsFilePaths, err := filepath.Glob(filepath.Join(parent, "contracts", "out", "gas-dimensions", "*.sol", "*.json"))
if err != nil {
log.Fatal(err)
}
gasDimensionsModInfo := modules["gas_dimensionsgen"]
if gasDimensionsModInfo == nil {
gasDimensionsModInfo = &moduleInfo{}
modules["gas_dimensionsgen"] = gasDimensionsModInfo
}
for _, path := range gasDimensionsFilePaths {
_, file := filepath.Split(path)
name := file[:len(file)-5]

data, err := os.ReadFile(path)
if err != nil {
log.Fatal("could not read", path, "for contract", name, err)
}
artifact := FoundryArtifact{}
if err := json.Unmarshal(data, &artifact); err != nil {
log.Fatal("failed to parse contract", name, err)
}
gasDimensionsModInfo.addArtifact(HardHatArtifact{
ContractName: name,
Abi: artifact.Abi,
Bytecode: artifact.Bytecode.Object,
})
}

// add upgrade executor module which is not compiled locally, but imported from 'nitro-contracts' depedencies
upgExecutorPath := filepath.Join(parent, "contracts", "node_modules", "@offchainlabs", "upgrade-executor", "build", "contracts", "src", "UpgradeExecutor.sol", "UpgradeExecutor.json")
_, err = os.Stat(upgExecutorPath)
Expand Down
314 changes: 314 additions & 0 deletions system_tests/gas_dim_log_a_common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
package arbtest

import (
"context"
"encoding/json"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/tracers/native"
"github.com/ethereum/go-ethereum/params"

"github.com/offchainlabs/nitro/solgen/go/gas_dimensionsgen"
)

type DimensionLogRes = native.DimensionLogRes
type TraceResult = native.ExecutionResult

const (
ColdMinusWarmAccountAccessCost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
ColdMinusWarmSloadCost = params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929
)

// #########################################################################################################
// #########################################################################################################
// REGULAR COMPUTATION OPCODES (ADD, SWAP, ETC)
// #########################################################################################################
// #########################################################################################################

// Run a test where we set up an L2, then send a transaction
// that only has computation-only opcodes. Then call debug_traceTransaction
// with the txGasDimensionLogger tracer.
//
// we expect in this case to get back a json response, with the gas dimension logs
// containing only the computation-only opcodes and that the gas in the computation
// only opcodes is equal to the OneDimensionalGasCost.
func TestDimLogComputationOnlyOpcodes(t *testing.T) {
ctx, cancel, builder, auth, cleanup := gasDimensionTestSetup(t)
defer cancel()
defer cleanup()

_, contract := deployGasDimensionTestContract(t, builder, auth, gas_dimensionsgen.DeployCounter)
_, receipt := callOnContract(t, builder, auth, contract.NoSpecials)
traceResult := callDebugTraceTransactionWithLogger(t, ctx, builder, receipt.TxHash)

// Validate each log entry
for i, log := range traceResult.DimensionLogs {
// Basic field validation
if log.Op == "" {
t.Errorf("Log entry %d: Expected non-empty opcode", i)
}
if log.Depth < 1 {
t.Errorf("Log entry %d: Expected depth >= 1, got %d", i, log.Depth)
}

// Check that OneDimensionalGasCost equals Computation for computation-only opcodes
if log.OneDimensionalGasCost != log.Computation {
t.Errorf("Log entry %d: For computation-only opcode %s pc %d, expected OneDimensionalGasCost (%d) to equal Computation (%d): %v",
i, log.Op, log.Pc, log.OneDimensionalGasCost, log.Computation, log)
}
// check that there are only computation-only opcodes
if log.StateAccess != 0 || log.StateGrowth != 0 || log.HistoryGrowth != 0 {
t.Errorf("Log entry %d: For computation-only opcode %s pc %d, expected StateAccess (%d), StateGrowth (%d), HistoryGrowth (%d) to be 0: %v",
i, log.Op, log.Pc, log.StateAccess, log.StateGrowth, log.HistoryGrowth, log)
}

// Validate error field
if log.Err != nil {
t.Errorf("Log entry %d: Unexpected error: %v", i, log.Err)
}
}
}

// #########################################################################################################
// #########################################################################################################
// HELPER FUNCTIONS
// #########################################################################################################
// #########################################################################################################

// common setup for all gas_dimension_logger tests
func gasDimensionTestSetup(t *testing.T) (
ctx context.Context,
cancel context.CancelFunc,
builder *NodeBuilder,
auth bind.TransactOpts,
cleanup func(),
) {
t.Helper()
ctx, cancel = context.WithCancel(context.Background())
builder = NewNodeBuilder(ctx).DefaultConfig(t, true)
builder.execConfig.Caching.Archive = true
// For now Archive node should use HashScheme
builder.execConfig.Caching.StateScheme = rawdb.HashScheme
cleanup = builder.Build(t)
auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx)
return ctx, cancel, builder, auth, cleanup
}

// deploy the contract we want to deploy for this test
// wait for it to be included
func deployGasDimensionTestContract[C any](
t *testing.T,
builder *NodeBuilder,
auth bind.TransactOpts,
deployFunc func(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, C, error),
) (
address common.Address,
contract C,
) {
t.Helper()
address, tx, contract, err := deployFunc(
&auth, // Transaction options
builder.L2.Client, // Ethereum client
)
Require(t, err)

// 3. Wait for deployment to succeed
_, err = builder.L2.EnsureTxSucceeded(tx)
Require(t, err)

return address, contract
}

// call whatever test function is required for the test on the contract
func callOnContract[F func(auth *bind.TransactOpts) (*types.Transaction, error)](
t *testing.T,
builder *NodeBuilder,
auth bind.TransactOpts,
testFunc F,
) (tx *types.Transaction, receipt *types.Receipt) {
t.Helper()
tx, err := testFunc(&auth) // For write operations
Require(t, err)
receipt, err = builder.L2.EnsureTxSucceeded(tx)
Require(t, err)
return tx, receipt
}

// call whatever test function is required for the test on the contract
// pass in the argument provided to the test function call as its first argument
func callOnContractWithOneArg[A any, F func(auth *bind.TransactOpts, arg1 A) (*types.Transaction, error)](
t *testing.T,
builder *NodeBuilder,
auth bind.TransactOpts,
testFunc F,
arg1 A,
) (receipt *types.Receipt) {
t.Helper()
tx, err := testFunc(&auth, arg1)
Require(t, err)
receipt, err = builder.L2.EnsureTxSucceeded(tx)
Require(t, err)
return receipt
}

// call debug_traceTransaction with txGasDimensionLogger tracer
// do very light sanity checks on the result
func callDebugTraceTransactionWithLogger(
t *testing.T,
ctx context.Context,
builder *NodeBuilder,
txHash common.Hash,
) TraceResult {
t.Helper()
// Call debug_traceTransaction with txGasDimensionLogger tracer
rpcClient := builder.L2.ConsensusNode.Stack.Attach()
var result json.RawMessage
err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", txHash, map[string]interface{}{
"tracer": "txGasDimensionLogger",
})
Require(t, err)

// Parse the result
var traceResult TraceResult
if err := json.Unmarshal(result, &traceResult); err != nil {
Fatal(t, err)
}

// Validate basic structure
if traceResult.GasUsed == 0 {
Fatal(t, "Expected non-zero gas usage")
}
if traceResult.GasUsedForL1 == 0 {
Fatal(t, "Expected non-zero gas usage for L1")
}
if traceResult.GasUsedForL2 == 0 {
Fatal(t, "Expected non-zero gas usage for L2")
}
if traceResult.IntrinsicGas == 0 {
Fatal(t, "Expected non-zero intrinsic gas")
}
if traceResult.Failed {
Fatal(t, "Transaction should not have failed")
}
txHashHex := txHash.Hex()
if traceResult.TxHash != txHashHex {
Fatal(t, "Expected txHash %s, got %s", txHashHex, traceResult.TxHash)
}
if len(traceResult.DimensionLogs) == 0 {
Fatal(t, "Expected non-empty dimension logs")
}
return traceResult
}

// get dimension log at position index of that opcode
// desiredIndex is 0-indexed
func getSpecificDimensionLogAtIndex(
t *testing.T,
dimensionLogs []DimensionLogRes,
expectedOpcode string,
expectedCount uint64,
desiredIndex uint64,
) (
specificDimensionLog *DimensionLogRes,
) {
t.Helper()
specificDimensionLog = nil
var observedOpcodeCount uint64 = 0

for i, log := range dimensionLogs {
// Basic field validation
if log.Op == "" {
Fatal(t, "Log entry ", i, " Expected non-empty opcode")
}
if log.Depth < 1 {
Fatal(t, "Log entry ", i, " Expected depth >= 1, got", log.Depth)
}
if log.Err != nil {
Fatal(t, "Log entry ", i, " Unexpected error:", log.Err)
}
if log.Op == expectedOpcode {
if observedOpcodeCount == desiredIndex {
specificDimensionLog = &log
}
observedOpcodeCount++
}
}
if observedOpcodeCount != expectedCount {
Fatal(t, "Expected ", expectedCount, " ", expectedOpcode, " got ", observedOpcodeCount)
}
if specificDimensionLog == nil {
Fatal(t, "Expected to find log at index ", desiredIndex, " of ", expectedOpcode, " got nil")
}
return specificDimensionLog
}

// highlight one specific dimension log you want to get out of the
// dimension logs and return it. Make some basic field validation checks on the
// log while you iterate through it.
func getSpecificDimensionLog(t *testing.T, dimensionLogs []DimensionLogRes, expectedOpcode string) (
specificDimensionLog *DimensionLogRes,
) {
t.Helper()
return getSpecificDimensionLogAtIndex(t, dimensionLogs, expectedOpcode, 1, 0)
}

// for the sstore multiple tests, we need to get the second sstore in the transaction
// but there should only ever be two sstores in the transaction.
func getLastOfTwoDimensionLogs(t *testing.T, dimensionLogs []DimensionLogRes, expectedOpcode string) (
specificDimensionLog *DimensionLogRes,
) {
t.Helper()
return getSpecificDimensionLogAtIndex(t, dimensionLogs, expectedOpcode, 2, 1)
}

// just to reduce visual clutter in parameters
type ExpectedGasCosts struct {
OneDimensionalGasCost uint64
Computation uint64
StateAccess uint64
StateGrowth uint64
HistoryGrowth uint64
StateGrowthRefund int64
ChildExecutionCost uint64
}

// checks that all of the fields of the expected and actual dimension logs are equal
func checkGasDimensionsMatch(t *testing.T, expected ExpectedGasCosts, actual *DimensionLogRes) {
t.Helper()
if actual.Computation != expected.Computation {
Fatal(t, "Expected Computation ", expected.Computation, " got ", actual.Computation, " actual: ", actual.DebugString())
}
if actual.StateAccess != expected.StateAccess {
Fatal(t, "Expected StateAccess ", expected.StateAccess, " got ", actual.StateAccess, " actual: ", actual.DebugString())
}
if actual.StateGrowth != expected.StateGrowth {
Fatal(t, "Expected StateGrowth ", expected.StateGrowth, " got ", actual.StateGrowth, " actual: ", actual.DebugString())
}
if actual.HistoryGrowth != expected.HistoryGrowth {
Fatal(t, "Expected HistoryGrowth ", expected.HistoryGrowth, " got ", actual.HistoryGrowth, " actual: ", actual.DebugString())
}
if actual.StateGrowthRefund != expected.StateGrowthRefund {
Fatal(t, "Expected StateGrowthRefund ", expected.StateGrowthRefund, " got ", actual.StateGrowthRefund, " actual: ", actual.DebugString())
}
if actual.OneDimensionalGasCost != expected.OneDimensionalGasCost {
Fatal(t, "Expected OneDimensionalGasCost ", expected.OneDimensionalGasCost, " got ", actual.OneDimensionalGasCost, " actual: ", actual.DebugString())
}
if actual.ChildExecutionCost != expected.ChildExecutionCost {
Fatal(t, "Expected ChildExecutionCost ", expected.ChildExecutionCost, " got ", actual.ChildExecutionCost, " actual: ", actual.DebugString())
}
}

// check that the one dimensional gas cost is equal to the sum of the other gas dimensions
func checkGasDimensionsEqualOneDimensionalGas(
t *testing.T,
l *DimensionLogRes,
) {
t.Helper()
if l.OneDimensionalGasCost != l.Computation+l.StateAccess+l.StateGrowth+l.HistoryGrowth {
Fatal(t, "Expected OneDimensionalGasCost to equal sum of gas dimensions", l.DebugString())
}
}
Loading
Loading