From 7a0e89b4f47ab0b5766d14f1fb1cac658906dff7 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 23 Jan 2026 02:22:56 +0100 Subject: [PATCH] core: add comprehensive execution metrics tests Add integration tests verifying cross-client execution metrics: - Account metrics (loaded, updated, deleted) - Storage metrics (loaded, updated, deleted) - Code metrics (loaded, updated, bytes written) --- core/blockchain_metrics_test.go | 576 ++++++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 core/blockchain_metrics_test.go diff --git a/core/blockchain_metrics_test.go b/core/blockchain_metrics_test.go new file mode 100644 index 0000000000..59349df8ca --- /dev/null +++ b/core/blockchain_metrics_test.go @@ -0,0 +1,576 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/rawdb" + "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/params" + "github.com/holiman/uint256" +) + +// TestAccountMetrics verifies that AccountLoaded and AccountUpdated are correctly +// tracked when processing a simple ETH transfer transaction. +func TestAccountMetrics(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + receiver = common.HexToAddress("0x1111111111111111111111111111111111111111") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + receiver: {Balance: big.NewInt(1)}, // Pre-existing account + }, + } + + // Generate block with simple ETH transfer + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, // nonce + receiver, // to + big.NewInt(1000), // value + 21000, // gas + uint256.MustFromBig(newGwei(5)).ToBig(), // gasPrice + nil, // data + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + // Process block and get stats + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // Both sender and receiver should be loaded from the database + if stats.AccountLoaded < 2 { + t.Errorf("Expected AccountLoaded >= 2, got %d", stats.AccountLoaded) + } + + // Both sender (nonce+balance) and receiver (balance) should be updated + if stats.AccountUpdated < 2 { + t.Errorf("Expected AccountUpdated >= 2, got %d", stats.AccountUpdated) + } +} + +// TestStorageLoadedMetric verifies that StorageLoaded is correctly incremented +// when a contract reads from storage that was set in genesis. +func TestStorageLoadedMetric(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x2222222222222222222222222222222222222222") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Contract that reads slot 0 via SLOAD and returns it + // PUSH1 0x00 SLOAD POP STOP + contractCode := []byte{ + byte(vm.PUSH1), 0x00, + byte(vm.SLOAD), + byte(vm.POP), + byte(vm.STOP), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + contract: { + Code: contractCode, + Balance: big.NewInt(0), + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x00"): common.HexToHash("0x42"), // Pre-populated storage + }, + }, + }, + } + + // Generate block that calls the contract + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, + contract, + big.NewInt(0), + 50000, + uint256.MustFromBig(newGwei(5)).ToBig(), + nil, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // Storage slot 0 should be loaded from DB + if stats.StorageLoaded < 1 { + t.Errorf("Expected StorageLoaded >= 1, got %d", stats.StorageLoaded) + } +} + +// TestStorageUpdatedMetric verifies that StorageUpdated is correctly incremented +// when a contract writes to storage. +func TestStorageUpdatedMetric(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x3333333333333333333333333333333333333333") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Contract that writes 0x42 to slot 0x01 + // PUSH1 0x42 PUSH1 0x01 SSTORE STOP + contractCode := []byte{ + byte(vm.PUSH1), 0x42, // value + byte(vm.PUSH1), 0x01, // key + byte(vm.SSTORE), + byte(vm.STOP), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + contract: {Code: contractCode, Balance: big.NewInt(0)}, + }, + } + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, + contract, + big.NewInt(0), + 50000, + uint256.MustFromBig(newGwei(5)).ToBig(), + nil, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // Storage slot 1 should be updated + if stats.StorageUpdated < 1 { + t.Errorf("Expected StorageUpdated >= 1, got %d", stats.StorageUpdated) + } +} + +// TestStorageDeletedMetric verifies that StorageDeleted is correctly incremented +// when a contract clears a storage slot (sets to zero). +func TestStorageDeletedMetric(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x4444444444444444444444444444444444444444") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Contract that clears slot 0x01 (sets to zero) + // PUSH1 0x00 PUSH1 0x01 SSTORE STOP + contractCode := []byte{ + byte(vm.PUSH1), 0x00, // value (zero = delete) + byte(vm.PUSH1), 0x01, // key + byte(vm.SSTORE), + byte(vm.STOP), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + contract: { + Code: contractCode, + Balance: big.NewInt(0), + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x01"): common.HexToHash("0x42"), // Pre-existing non-zero value + }, + }, + }, + } + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, + contract, + big.NewInt(0), + 50000, + uint256.MustFromBig(newGwei(5)).ToBig(), + nil, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // Storage slot 1 should be deleted (cleared) + if stats.StorageDeleted < 1 { + t.Errorf("Expected StorageDeleted >= 1, got %d", stats.StorageDeleted) + } +} + +// TestCodeLoadedMetric verifies that CodeLoaded is correctly incremented +// when a contract's code is fetched to execute it. +func TestCodeLoadedMetric(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x5555555555555555555555555555555555555555") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Simple contract: PUSH1 0x42 POP STOP + contractCode := []byte{ + byte(vm.PUSH1), 0x42, + byte(vm.POP), + byte(vm.STOP), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + contract: {Code: contractCode, Balance: big.NewInt(0)}, + }, + } + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, + contract, + big.NewInt(0), + 50000, + uint256.MustFromBig(newGwei(5)).ToBig(), + nil, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // Contract code should be loaded to execute + if stats.CodeLoaded < 1 { + t.Errorf("Expected CodeLoaded >= 1, got %d", stats.CodeLoaded) + } +} + +// TestCodeUpdatedMetricCREATE verifies that CodeUpdated and CodeBytesWrite are correctly +// incremented when a contract is deployed via a creation transaction. +func TestCodeUpdatedMetricCREATE(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Runtime code: PUSH1 0x42 POP STOP (4 bytes) + runtimeCode := []byte{ + byte(vm.PUSH1), 0x42, + byte(vm.POP), + byte(vm.STOP), + } + runtimeLen := len(runtimeCode) + + // Initcode that returns the runtime code + // PUSH4 PUSH1 0x00 MSTORE PUSH1 PUSH1 0x1c RETURN + initCode := []byte{ + byte(vm.PUSH4), + runtimeCode[0], runtimeCode[1], runtimeCode[2], runtimeCode[3], + byte(vm.PUSH1), 0x00, + byte(vm.MSTORE), + byte(vm.PUSH1), byte(runtimeLen), // size + byte(vm.PUSH1), 0x1c, // offset (32 - 4 = 28 = 0x1c) + byte(vm.RETURN), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + }, + } + + // Contract creation transaction (to = nil) + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewContractCreation( + 0, + big.NewInt(0), + 100000, + uint256.MustFromBig(newGwei(5)).ToBig(), + initCode, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // One contract should be deployed + if stats.CodeUpdated != 1 { + t.Errorf("Expected CodeUpdated = 1, got %d", stats.CodeUpdated) + } + + // Runtime code is 4 bytes + if stats.CodeBytesWrite != runtimeLen { + t.Errorf("Expected CodeBytesWrite = %d, got %d", runtimeLen, stats.CodeBytesWrite) + } +} + +// TestAccountDeletedMetric verifies that AccountDeleted is correctly incremented +// when a contract self-destructs. Post-Cancun (EIP-6780), this only works if the +// contract is created and destroyed in the same transaction. +func TestAccountDeletedMetric(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + beneficiary = common.HexToAddress("0x6666666666666666666666666666666666666666") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Initcode that: + // 1. Deploys runtime code that will never execute (we self-destruct in initcode) + // 2. Immediately calls SELFDESTRUCT to beneficiary during contract creation + // This is EIP-6780 compatible because we self-destruct in the same tx as creation. + // + // PUSH20 SELFDESTRUCT + initCode := make([]byte, 0, 22) + initCode = append(initCode, byte(vm.PUSH20)) + initCode = append(initCode, beneficiary.Bytes()...) + initCode = append(initCode, byte(vm.SELFDESTRUCT)) + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + beneficiary: {Balance: big.NewInt(1)}, // Pre-existing beneficiary + }, + } + + // Contract creation that immediately self-destructs + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewContractCreation( + 0, + big.NewInt(1000), // Send some ETH to the contract + 100000, + uint256.MustFromBig(newGwei(5)).ToBig(), + initCode, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // The newly created contract should be deleted (self-destructed) + if stats.AccountDeleted < 1 { + t.Errorf("Expected AccountDeleted >= 1, got %d", stats.AccountDeleted) + } +} + +// TestMultipleStorageOperationsMetrics verifies that storage metrics correctly +// accumulate when multiple operations occur. +func TestMultipleStorageOperationsMetrics(t *testing.T) { + var ( + config = *params.MergedTestChainConfig + signer = types.LatestSigner(&config) + engine = beacon.New(ethash.NewFaker()) + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + sender = crypto.PubkeyToAddress(key.PublicKey) + contract = common.HexToAddress("0x7777777777777777777777777777777777777777") + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + ) + + // Contract that: + // 1. Reads slots 0, 1, 2 (3 SLOADs) + // 2. Writes to slots 3, 4 (2 SSTOREs with non-zero values) + // 3. Clears slot 5 (1 SSTORE with zero, existing value was non-zero) + contractCode := []byte{ + // SLOAD slot 0 + byte(vm.PUSH1), 0x00, byte(vm.SLOAD), byte(vm.POP), + // SLOAD slot 1 + byte(vm.PUSH1), 0x01, byte(vm.SLOAD), byte(vm.POP), + // SLOAD slot 2 + byte(vm.PUSH1), 0x02, byte(vm.SLOAD), byte(vm.POP), + // SSTORE slot 3 = 0xFF + byte(vm.PUSH1), 0xFF, byte(vm.PUSH1), 0x03, byte(vm.SSTORE), + // SSTORE slot 4 = 0xEE + byte(vm.PUSH1), 0xEE, byte(vm.PUSH1), 0x04, byte(vm.SSTORE), + // SSTORE slot 5 = 0x00 (delete) + byte(vm.PUSH1), 0x00, byte(vm.PUSH1), 0x05, byte(vm.SSTORE), + byte(vm.STOP), + } + + gspec := &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + sender: {Balance: funds}, + contract: { + Code: contractCode, + Balance: big.NewInt(0), + Storage: map[common.Hash]common.Hash{ + common.HexToHash("0x00"): common.HexToHash("0x01"), + common.HexToHash("0x01"): common.HexToHash("0x02"), + common.HexToHash("0x02"): common.HexToHash("0x03"), + common.HexToHash("0x05"): common.HexToHash("0xAA"), // Will be cleared + }, + }, + }, + } + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + tx, _ := types.SignTx(types.NewTransaction( + 0, + contract, + big.NewInt(0), + 100000, + uint256.MustFromBig(newGwei(5)).ToBig(), + nil, + ), signer, key) + b.AddTx(tx) + }) + + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), gspec, engine, nil) + if err != nil { + t.Fatalf("failed to create chain: %v", err) + } + defer chain.Stop() + + result, err := chain.ProcessBlock(chain.CurrentBlock().Root, blocks[0], true, false) + if err != nil { + t.Fatalf("failed to process block: %v", err) + } + + stats := result.Stats() + + // 3 slots read: 0, 1, 2 + if stats.StorageLoaded < 3 { + t.Errorf("Expected StorageLoaded >= 3, got %d", stats.StorageLoaded) + } + + // 2 slots written with non-zero values: 3, 4 + if stats.StorageUpdated < 2 { + t.Errorf("Expected StorageUpdated >= 2, got %d", stats.StorageUpdated) + } + + // 1 slot cleared: 5 + if stats.StorageDeleted < 1 { + t.Errorf("Expected StorageDeleted >= 1, got %d", stats.StorageDeleted) + } +}