Merge remote-tracking branch 'upstream/master' into bs/cell-blobpool/sparse-v2

This commit is contained in:
healthykim 2026-06-26 13:44:35 +02:00
commit c796080770
58 changed files with 4476 additions and 429 deletions

View file

@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/pebble"
"github.com/ethereum/go-ethereum/internal/tablewriter"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
@ -102,6 +103,7 @@ Remove blockchain and state databases`,
dbMetadataCmd,
dbCheckStateContentCmd,
dbInspectHistoryCmd,
dbPebbleUpgradeCmd,
},
}
dbInspectCmd = &cli.Command{
@ -242,6 +244,17 @@ WARNING: This is a low-level operation which may cause database corruption!`,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command queries the history of the account or storage slot within the specified block range",
}
dbPebbleUpgradeCmd = &cli.Command{
Action: dbPebbleUpgrade,
Name: "pebble-upgrade",
Usage: "Upgrade a legacy pebble v1 database to pebble v2 format",
Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
Description: `This command upgrades a legacy Pebble v1 database so
that it becomes compatible with Pebble v2. The upgrade process converts the
database format to the oldest format supported by Pebble v2. It's not the
one-way operation, instead, the database can still be opened by older versions
of geth that use the pebble v1 library.`,
}
)
func removeDB(ctx *cli.Context) error {
@ -543,6 +556,21 @@ func dbCompact(ctx *cli.Context) error {
return nil
}
func dbPebbleUpgrade(ctx *cli.Context) error {
stack, _ := makeConfigNode(ctx)
defer stack.Close()
path := stack.ResolvePath("chaindata")
dbType := rawdb.PreexistingDatabase(path)
if dbType == "" {
return fmt.Errorf("no database found at %s", path)
}
if dbType != rawdb.DBPebble {
return fmt.Errorf("database at %s is %s, not pebble", path, dbType)
}
return pebble.Upgrade(path)
}
// dbGet shows the value of a given database key
func dbGet(ctx *cli.Context) error {
if ctx.NArg() != 1 {

View file

@ -922,6 +922,7 @@ func dumpState(ctx *cli.Context) error {
for stIt.Next() {
da.Storage[stIt.Hash()] = common.Bytes2Hex(stIt.Slot())
}
stIt.Release()
}
enc.Encode(da)
accounts++

View file

@ -1,7 +1,11 @@
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8=
github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y=
github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk=
github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
@ -14,6 +18,8 @@ github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3M
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU=
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
@ -22,8 +28,12 @@ github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZe
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhKtJ6bOk=
github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI=
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI=
@ -72,8 +82,8 @@ github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZ
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@ -87,6 +97,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw=
github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
@ -95,14 +107,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4=
github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=

View file

@ -70,6 +70,8 @@ func newBALTestEnv(extra types.GenesisAlloc) *balTestEnv {
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
params.BuilderDepositAddress: {Nonce: 1, Code: params.BuilderDepositCode, Balance: common.Big0},
params.BuilderExitAddress: {Nonce: 1, Code: params.BuilderExitCode, Balance: common.Big0},
}
maps.Copy(alloc, extra)
return &balTestEnv{
@ -992,10 +994,12 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) {
// means all four of the post-merge system contracts touched by every
// Amsterdam block:
//
// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set)
// - EIP-2935 history storage (pre-execution)
// - EIP-7002 withdrawal queue (post-execution)
// - EIP-7251 consolidation queue (post-execution)
// - EIP-4788 beacon roots (pre-execution, when ParentBeaconRoot is set)
// - EIP-2935 history storage (pre-execution)
// - EIP-7002 withdrawal queue (post-execution)
// - EIP-7251 consolidation queue (post-execution)
// - EIP-8282 builder-deposit queue (post-execution)
// - EIP-8282 builder-exit queue (post-execution)
func TestBALSystemContractsPresent(t *testing.T) {
env := newBALTestEnv(nil)
@ -1013,6 +1017,8 @@ func TestBALSystemContractsPresent(t *testing.T) {
{"HistoryStorage (2935)", params.HistoryStorageAddress},
{"WithdrawalQueue (7002)", params.WithdrawalQueueAddress},
{"ConsolidationQueue (7251)", params.ConsolidationQueueAddress},
{"BuilderDepositQueue (8282)", params.BuilderDepositAddress},
{"BuilderExitQueue (8282)", params.BuilderExitAddress},
} {
if findAccount(b, sys.addr) == nil {
t.Errorf("%s (%x) MUST appear in BAL but is missing\n%s", sys.name, sys.addr, b.PrettyPrint())

View file

@ -85,6 +85,8 @@ func TestProcessUBT(t *testing.T) {
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
params.BuilderDepositAddress: {Nonce: 1, Code: params.BuilderDepositCode, Balance: common.Big0},
params.BuilderExitAddress: {Nonce: 1, Code: params.BuilderExitCode, Balance: common.Big0},
},
}
)

View file

@ -1540,27 +1540,12 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
// existing local chain segments (reorg around the chain tip). The reorganized part
// will be included in the provided chain segment, and stale canonical markers will be
// silently rewritten. Therefore, no explicit reorg logic is needed.
writeLive := func(blockChain types.Blocks, receiptChain []rlp.RawValue) (int, error) {
var (
skipPresenceCheck = false
batch = bc.db.NewBatch()
)
writeLive := func(blockChain types.Blocks, receiptChain []rlp.RawValue) error {
batch := bc.db.NewBatch()
for i, block := range blockChain {
// Short circuit insertion if shutting down or processing failed
if bc.insertStopped() {
return 0, errInsertionInterrupted
}
if !skipPresenceCheck {
// Ignore if the entire data is already known
if bc.HasBlock(block.Hash(), block.NumberU64()) {
stats.ignored++
continue
} else {
// If block N is not present, neither are the later blocks.
// This should be true, but if we are mistaken, the shortcut
// here will only cause overwriting of some existing data
skipPresenceCheck = true
}
return errInsertionInterrupted
}
// Write all the data out into the database
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
@ -1572,7 +1557,7 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
// except transaction indexes(will be created once sync is finished).
if batch.ValueSize() >= ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
return 0, err
return err
}
size += int64(batch.ValueSize())
batch.Reset()
@ -1585,13 +1570,10 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
if batch.ValueSize() > 0 {
size += int64(batch.ValueSize())
if err := batch.Write(); err != nil {
return 0, err
return err
}
}
if err := updateHead(blockChain[len(blockChain)-1].Header()); err != nil {
return 0, err
}
return 0, nil
return updateHead(blockChain[len(blockChain)-1].Header())
}
// Split the supplied blocks into two groups, according to the
@ -1608,11 +1590,11 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
}
}
if index != len(blockChain) {
if n, err := writeLive(blockChain[index:], receiptChain[index:]); err != nil {
if err := writeLive(blockChain[index:], receiptChain[index:]); err != nil {
if err == errInsertionInterrupted {
return 0, nil
}
return n, err
return 0, err
}
}
var (

614
core/eip8037_test.go Normal file
View file

@ -0,0 +1,614 @@
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
// Transaction- and block-level tests for EIP-8037 (multidimensional state-gas
// metering). They apply whole transactions and inspect the 2D block gas pool
// (cumulativeRegular / cumulativeState) and the receipt/peak figures.
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/state"
"github.com/ethereum/go-ethereum/core/tracing"
"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"
)
var (
cfg8037 = balChainConfig()
signer8037 = types.LatestSigner(cfg8037)
rules8037 = cfg8037.Rules(big.NewInt(0), true, 0)
senderKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
senderAddr = crypto.PubkeyToAddress(senderKey.PublicKey)
// state-gas charges in units (CPSB applied).
newAccountState = uint64(params.AccountCreationSize * params.CostPerStateByte) // 183,600
newSlotState = uint64(params.StorageCreationSize * params.CostPerStateByte) // 97,920
authBaseState = uint64(params.AuthorizationCreationSize * params.CostPerStateByte) // 35,190
authWorstState = newAccountState + authBaseState // 218,790
)
// mkState builds an in-memory StateDB from a genesis allocation.
func mkState(alloc types.GenesisAlloc) *state.StateDB {
sdb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
for addr, acc := range alloc {
sdb.CreateAccount(addr)
if acc.Balance != nil {
sdb.AddBalance(addr, uint256.MustFromBig(acc.Balance), tracing.BalanceChangeUnspecified)
}
if acc.Nonce != 0 {
sdb.SetNonce(addr, acc.Nonce, tracing.NonceChangeGenesis)
}
if len(acc.Code) != 0 {
sdb.SetCode(addr, acc.Code, tracing.CodeChangeUnspecified)
}
for k, v := range acc.Storage {
sdb.SetState(addr, k, v)
}
}
sdb.Finalise(true)
return sdb
}
// amsterdamCoreEVM builds an Amsterdam EVM over statedb with fees disabled.
func amsterdamCoreEVM(sdb *state.StateDB) *vm.EVM {
ctx := vm.BlockContext{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: func(uint64) common.Hash { return common.Hash{} },
BlockNumber: big.NewInt(0),
Random: &common.Hash{},
Difficulty: big.NewInt(0),
BaseFee: big.NewInt(0),
BlobBaseFee: big.NewInt(0),
GasLimit: 60_000_000,
CostPerStateByte: params.CostPerStateByte,
}
return vm.NewEVM(ctx, sdb, cfg8037, vm.Config{NoBaseFee: true})
}
// applyMsg applies one transaction with a fresh block gas pool and returns the
// execution result, the gas pool (for the 2D split) and any consensus error.
func applyMsg(t *testing.T, sdb *state.StateDB, tx *types.Transaction) (*ExecutionResult, *GasPool, error) {
t.Helper()
evm := amsterdamCoreEVM(sdb)
msg, err := TransactionToMessage(tx, signer8037, evm.Context.BaseFee)
if err != nil {
t.Fatalf("to message: %v", err)
}
gp := NewGasPool(evm.Context.GasLimit)
// Drive the stateTransition directly (as ApplyMessage does) so the test can
// inspect the final tx-level GasBudget vector via st.gasRemaining.
evm.SetTxContext(NewEVMTxContext(msg))
st := newStateTransition(evm, msg, gp)
res, err := st.execute()
if err == nil && res != nil {
assertPoolSane(t, res, gp)
limit := min(msg.GasLimit, params.MaxTxGas)
assertBudgetSane(t, vm.NewGasBudget(limit, msg.GasLimit-limit), st.gasRemaining)
}
return res, gp, err
}
// assertBudgetSane validates the final tx-level GasBudget vector:
//
// regular: RegularGas + UsedRegularGas + Spilled == initial.RegularGas
// state: StateGas + UsedStateGas == initial.StateGas + Spilled
// scalar: Used(initial) == UsedRegularGas + UsedStateGas
func assertBudgetSane(t *testing.T, initial, got vm.GasBudget) {
t.Helper()
if got.RegularGas+got.UsedRegularGas+got.Spilled != initial.RegularGas {
t.Fatalf("regular not conserved: R=%d usedR=%d spilled=%d, want sum %d",
got.RegularGas, got.UsedRegularGas, got.Spilled, initial.RegularGas)
}
if int64(got.StateGas)+got.UsedStateGas != int64(initial.StateGas)+int64(got.Spilled) {
t.Fatalf("state not conserved: S=%d usedS=%d spilled=%d, want %d+spilled",
got.StateGas, got.UsedStateGas, got.Spilled, initial.StateGas)
}
if int64(got.Used(initial)) != int64(got.UsedRegularGas)+got.UsedStateGas {
t.Fatalf("scalar mismatch: used=%d, usedR=%d usedS=%d",
got.Used(initial), got.UsedRegularGas, got.UsedStateGas)
}
}
// assertPoolSane validates the whole 2D block-gas-pool vector after a single tx.
//
// receipt: cumulativeUsed == res.UsedGas <= res.MaxUsedGas
// pre-refund: cumulativeRegular + cumulativeState <= res.MaxUsedGas (peak)
// bottleneck: Used() == max(cumulativeRegular, cumulativeState) <= initial
func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool) {
t.Helper()
if gp.cumulativeUsed != res.UsedGas {
t.Fatalf("receipt scalar = %d, want UsedGas %d", gp.cumulativeUsed, res.UsedGas)
}
if res.UsedGas > res.MaxUsedGas {
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
}
if sum := gp.cumulativeRegular + gp.cumulativeState; sum > res.MaxUsedGas {
t.Fatalf("regular+state %d exceeds peak %d", sum, res.MaxUsedGas)
}
if gp.Used() != max(gp.cumulativeRegular, gp.cumulativeState) {
t.Fatalf("block used %d != max(%d,%d)", gp.Used(), gp.cumulativeRegular, gp.cumulativeState)
}
if gp.Used() > gp.initial {
t.Fatalf("block used %d exceeds limit %d", gp.Used(), gp.initial)
}
}
// senderAlloc funds the sender with the given extra accounts merged in.
func senderAlloc(extra types.GenesisAlloc) types.GenesisAlloc {
alloc := types.GenesisAlloc{senderAddr: {Balance: big.NewInt(1e18)}}
for a, acc := range extra {
alloc[a] = acc
}
return alloc
}
// callTx builds a signed dynamic-fee call to `to` with zero fees.
func callTx(nonce uint64, to common.Address, value int64, gas uint64, data []byte) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
ChainID: cfg8037.ChainID, Nonce: nonce, To: &to, Value: big.NewInt(value),
Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: data,
})
}
// createTx builds a signed contract-creation transaction.
func createTx(nonce, gas uint64, initCode []byte) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
ChainID: cfg8037.ChainID, Nonce: nonce, To: nil, Value: big.NewInt(0),
Gas: gas, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), Data: initCode,
})
}
var (
deploy3 = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // init: return 3 bytes of code
revertI = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // init: REVERT
)
// ===================== Top-level create transaction ======================
// A creation tx's intrinsic gas pre-charges one account creation as state gas.
func TestCreateTxIntrinsicChargesAccountUnconditionally(t *testing.T) {
cost, err := IntrinsicGas(nil, nil, nil, true, rules8037, params.CostPerStateByte)
if err != nil {
t.Fatal(err)
}
if cost.StateGas != newAccountState {
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, newAccountState)
}
}
// Creating onto a pre-existing (balance-only) address refills the account
// portion; only the code deposit is charged as state gas.
func TestCreateTxPreexistingDestRefill(t *testing.T) {
derived := crypto.CreateAddress(senderAddr, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Balance: big.NewInt(1)}}))
_, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, deploy3))
if err != nil {
t.Fatal(err)
}
if want := uint64(3 * params.CostPerStateByte); gp.cumulativeState != want {
t.Fatalf("state gas = %d, want %d", gp.cumulativeState, want)
}
}
// A creation tx that reverts refills the account-creation charge.
func TestCreateTxRevertRefill(t *testing.T) {
sdb := mkState(senderAlloc(nil))
res, gp, err := applyMsg(t, sdb, createTx(0, 1_000_000, revertI))
if err != nil {
t.Fatal(err)
}
if !res.Failed() {
t.Fatal("expected failed creation")
}
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
}
}
// An address collision burns gas_left while refilling the account charge.
func TestCreateTxCollisionConsumesGasLeft(t *testing.T) {
const gas = 1_000_000
derived := crypto.CreateAddress(senderAddr, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{derived: {Nonce: 1}}))
res, gp, err := applyMsg(t, sdb, createTx(0, gas, deploy3))
if err != nil {
t.Fatal(err)
}
if !res.Failed() {
t.Fatal("expected collision failure")
}
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", gp.cumulativeState)
}
// All forwarded gas_left is burned; only the refilled account charge (which
// had spilled into regular) returns to gas_left. So regular gas consumed is
// exactly tx.gas - newAccountState, with no other refund.
if want := uint64(gas) - newAccountState; gp.cumulativeRegular != want {
t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
}
}
// ======================== Transaction validation =========================
// The regular dimension must have room for min(tx.gas, MaxTxGas).
func TestValidationRegularGasAvailable(t *testing.T) {
gp := NewGasPool(30_000_000)
gp.cumulativeRegular = 29_000_000
if gp.CheckGasAmsterdam(2_000_000, 0) == nil {
t.Fatal("expected regular dimension full")
}
if err := gp.CheckGasAmsterdam(1_000_000, 0); err != nil {
t.Fatalf("regular fits but rejected: %v", err)
}
}
// The state dimension must have room for the whole tx.gas.
func TestValidationStateGasAvailable(t *testing.T) {
gp := NewGasPool(30_000_000)
gp.cumulativeState = 29_000_000
if gp.CheckGasAmsterdam(0, 2_000_000) == nil {
t.Fatal("expected state dimension full")
}
if err := gp.CheckGasAmsterdam(0, 1_000_000); err != nil {
t.Fatalf("state fits but rejected: %v", err)
}
}
// tx.gas may exceed MaxTxGas: regular is capped at MaxTxGas while the state
// dimension reserves the full tx.gas (the excess lands in the reservoir).
func TestValidationStateGasOverflowAllowed(t *testing.T) {
gas := params.MaxTxGas + 5_000_000
gp := NewGasPool(40_000_000)
if err := gp.CheckGasAmsterdam(min(gas, params.MaxTxGas), gas); err != nil {
t.Fatalf("overflow tx rejected at pool: %v", err)
}
// A real transfer with gas above MaxTxGas is accepted under Amsterdam.
sdb := mkState(senderAlloc(nil))
to := common.HexToAddress("0xc0ffee")
if _, _, err := applyMsg(t, sdb, callTx(0, to, 1, gas, nil)); err != nil {
t.Fatalf("tx with gas > MaxTxGas rejected: %v", err)
}
}
// Intrinsic regular gas above MaxTxGas (EIP-7825 cap) is rejected.
func TestValidationIntrinsicRegularCap(t *testing.T) {
al := make(types.AccessList, 8000) // ~19.2M regular, over the 16.77M cap
for i := range al {
al[i].Address = common.BigToAddress(big.NewInt(int64(i + 1)))
}
tx := types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
ChainID: cfg8037.ChainID, Nonce: 0, To: &senderAddr, Value: big.NewInt(0),
Gas: 25_000_000, GasFeeCap: big.NewInt(0), GasTipCap: big.NewInt(0), AccessList: al,
})
if _, _, err := applyMsg(t, mkState(senderAlloc(nil)), tx); err == nil {
t.Fatal("expected rejection for intrinsic regular over MaxTxGas")
}
}
// ========================= Refund and gas used ===========================
// clearSlots deploys a contract that zeroes slots 1..n, each preset to 1.
func clearSlots(addr common.Address, n int) (types.GenesisAlloc, []byte) {
var code []byte
storage := make(map[common.Hash]common.Hash, n)
for s := 1; s <= n; s++ {
code = append(code, 0x60, 0x00, 0x60, byte(s), 0x55) // PUSH1 0; PUSH1 s; SSTORE
storage[common.BytesToHash([]byte{byte(s)})] = common.BytesToHash([]byte{1})
}
return types.GenesisAlloc{addr: {Code: append(code, 0x00), Storage: storage}}, nil
}
// tx_gas_used_before_refund (peak) exceeds the post-refund gas used.
func TestGasUsedBeforeRefund(t *testing.T) {
c := common.HexToAddress("0xc1ea0")
alloc, _ := clearSlots(c, 1)
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
if err != nil {
t.Fatal(err)
}
if res.MaxUsedGas <= res.UsedGas {
t.Fatalf("peak %d must exceed post-refund %d", res.MaxUsedGas, res.UsedGas)
}
}
// The refund is capped at 20% of gas used before refund.
func TestRefundCappedAt20Percent(t *testing.T) {
c := common.HexToAddress("0xc1ea3")
alloc, _ := clearSlots(c, 3) // refund (3x4800) exceeds the 20% cap
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, nil))
if err != nil {
t.Fatal(err)
}
if want := res.MaxUsedGas - res.MaxUsedGas/5; res.UsedGas != want {
t.Fatalf("gas used = %d, want capped %d", res.UsedGas, want)
}
}
// The EIP-7623 calldata floor is applied after the refund.
func TestRefundCalldataFloorAfterRefund(t *testing.T) {
data := make([]byte, 1000) // all-zero calldata: floor dominates a bare call
floor, _ := FloorDataGas(rules8037, data, nil)
to := common.HexToAddress("0xeeee")
res, _, err := applyMsg(t, mkState(senderAlloc(nil)), callTx(0, to, 0, 1_000_000, data))
if err != nil {
t.Fatal(err)
}
if res.UsedGas != floor {
t.Fatalf("gas used = %d, want floor %d", res.UsedGas, floor)
}
}
// When the floor exceeds the post-refund gas, it negates part of the refund.
func TestRefundFloorNegatesRefund(t *testing.T) {
c := common.HexToAddress("0xc1ea1")
alloc, _ := clearSlots(c, 1)
data := make([]byte, 1000)
floor, _ := FloorDataGas(rules8037, data, nil)
res, _, err := applyMsg(t, mkState(senderAlloc(alloc)), callTx(0, c, 0, 1_000_000, data))
if err != nil {
t.Fatal(err)
}
if res.UsedGas != floor {
t.Fatalf("gas used = %d, want floor %d (refund negated)", res.UsedGas, floor)
}
}
// ========================= Block-level accounting ========================
// The pool tracks regular and state cumulatively in separate counters.
func TestBlockTracksTwoCounters(t *testing.T) {
gp := NewGasPool(60_000_000)
if err := gp.ChargeGasAmsterdam(100, 200, 300); err != nil {
t.Fatal(err)
}
if gp.cumulativeRegular != 100 || gp.cumulativeState != 200 {
t.Fatalf("counters = (%d,%d), want (100,200)", gp.cumulativeRegular, gp.cumulativeState)
}
}
// Block gas used is the max of the two dimensions.
func TestBlockGasUsedIsMax(t *testing.T) {
gp := NewGasPool(60_000_000)
gp.ChargeGasAmsterdam(100, 200, 300)
if gp.Used() != 200 {
t.Fatalf("block used = %d, want 200", gp.Used())
}
}
// Block validity is checked against the max dimension, not the sum.
func TestBlockValidityAgainstMax(t *testing.T) {
gp := NewGasPool(150)
// regular 100 + state 120: sum 220 > 150 but max 120 <= 150 is valid.
if err := gp.ChargeGasAmsterdam(100, 120, 0); err != nil {
t.Fatalf("max within limit but rejected: %v", err)
}
// state 200 alone exceeds the limit.
if err := gp.ChargeGasAmsterdam(0, 200, 0); err == nil {
t.Fatal("expected block overflow on state dimension")
}
}
// The block header gas_used reflects the bottleneck dimension (here, state),
// which the base-fee update then equilibrates against.
func TestBlockBaseFeeUsesMax(t *testing.T) {
c := common.HexToAddress("0x5707e5")
var code []byte
for s := 1; s <= 5; s++ {
code = append(code, 0x60, byte(s), 0x60, byte(s), 0x55) // SSTORE new slot s
}
env := newBALTestEnv(types.GenesisAlloc{c: {Code: append(code, 0x00)}})
engine := beacon.New(ethash.NewFaker())
_, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {
b.AddTx(env.tx(0, &c, big.NewInt(0), 1_000_000, 0, nil))
})
if want := 5 * newSlotState; blocks[0].GasUsed() != want {
t.Fatalf("block gas used = %d, want %d (state bottleneck)", blocks[0].GasUsed(), want)
}
}
// Receipt cumulative_gas_used is the running sum of per-tx gas (post-refund,
// post-floor), so consecutive receipts differ by exactly that tx's gas.
func TestReceiptCumulativeGasUsed(t *testing.T) {
env := newBALTestEnv(nil)
a, b := common.HexToAddress("0xaaaa"), common.HexToAddress("0xbbbb")
engine := beacon.New(ethash.NewFaker())
_, _, receipts := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) {
g.AddTx(env.tx(0, &a, big.NewInt(1), txGasNewAccount, 0, nil))
g.AddTx(env.tx(1, &b, big.NewInt(1), txGasNewAccount, 0, nil))
})
r := receipts[0]
if got := r[1].CumulativeGasUsed - r[0].CumulativeGasUsed; got != r[1].GasUsed {
t.Fatalf("cumulative delta = %d, want tx gas %d", got, r[1].GasUsed)
}
}
// ======================= EIP-7702 authorizations =========================
// signAuth signs an authorization from authKey for the given delegate and nonce.
func signAuth(t *testing.T, authKey string, delegate common.Address, nonce uint64) (types.SetCodeAuthorization, common.Address) {
t.Helper()
k, _ := crypto.HexToECDSA(authKey)
auth, err := types.SignSetCode(k, types.SetCodeAuthorization{
ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate, Nonce: nonce,
})
if err != nil {
t.Fatalf("sign auth: %v", err)
}
return auth, crypto.PubkeyToAddress(k.PublicKey)
}
func setCodeTx(nonce uint64, to common.Address, auths []types.SetCodeAuthorization) *types.Transaction {
return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: nonce, To: to, Value: new(uint256.Int),
Gas: 1_000_000, GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
})
}
const authKeyA = "0202020202020202020202020202020202020202020202020202002020202020"
var delegate8037 = common.HexToAddress("0xde1e8a7e")
// Intrinsic gas pre-charges the worst-case (account + indicator) per auth.
func TestAuthIntrinsicWorstCase(t *testing.T) {
cost, err := IntrinsicGas(nil, nil, []types.SetCodeAuthorization{{}}, false, rules8037, params.CostPerStateByte)
if err != nil {
t.Fatal(err)
}
if cost.StateGas != authWorstState {
t.Fatalf("intrinsic state gas = %d, want %d", cost.StateGas, authWorstState)
}
}
// An invalid authorization refills its entire intrinsic state-gas charge.
func TestAuthInvalidRefillFull(t *testing.T) {
k, _ := crypto.HexToECDSA(authKeyA)
bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
})
sdb := mkState(senderAlloc(nil))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{bad}))
if err != nil {
t.Fatal(err)
}
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (fully refilled)", gp.cumulativeState)
}
}
// A pre-existing authority refills the account portion (indicator stands).
func TestAuthAccountExistsRefill(t *testing.T) {
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}}))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
if err != nil {
t.Fatal(err)
}
if gp.cumulativeState != authBaseState {
t.Fatalf("state gas = %d, want %d (account refilled)", gp.cumulativeState, authBaseState)
}
}
// Setting a delegation on an already-delegated authority refills the indicator
// portion (and the account portion, since the authority already exists).
func TestAuthSetOnDelegatedRefillBase(t *testing.T) {
auth, authority := signAuth(t, authKeyA, delegate8037, 0)
pre := types.AddressToDelegation(common.HexToAddress("0xabcd"))
sdb := mkState(senderAlloc(types.GenesisAlloc{authority: {Code: pre}}))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
if err != nil {
t.Fatal(err)
}
if gp.cumulativeState != 0 {
t.Fatalf("state gas = %d, want 0 (account+indicator refilled)", gp.cumulativeState)
}
}
// A net-new delegation on a fresh authority keeps the full worst-case charge.
func TestAuthSetNetNewNoRefill(t *testing.T) {
auth, _ := signAuth(t, authKeyA, delegate8037, 0)
sdb := mkState(senderAlloc(nil))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
if err != nil {
t.Fatal(err)
}
if gp.cumulativeState != authWorstState {
t.Fatalf("state gas = %d, want %d (no refill)", gp.cumulativeState, authWorstState)
}
}
// Clearing a delegation writes no indicator, so the indicator portion refills.
func TestAuthClearRefillBase(t *testing.T) {
auth, _ := signAuth(t, authKeyA, common.Address{}, 0) // clear (address ZERO)
sdb := mkState(senderAlloc(nil))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{auth}))
if err != nil {
t.Fatal(err)
}
if want := newAccountState; gp.cumulativeState != want {
t.Fatalf("state gas = %d, want %d (indicator refilled)", gp.cumulativeState, want)
}
}
// 0->a->0 in one tx: the indicator created by an earlier auth and cleared by a
// later one writes zero net bytes, so both indicator charges refill.
func TestAuthClearSameTxDoubleRefill(t *testing.T) {
set, authority := signAuth(t, authKeyA, delegate8037, 0)
clr, _ := signAuth(t, authKeyA, common.Address{}, 1)
sdb := mkState(senderAlloc(nil))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{set, clr}))
if err != nil {
t.Fatal(err)
}
_ = authority
if want := newAccountState; gp.cumulativeState != want {
t.Fatalf("state gas = %d, want %d (net-zero delegation)", gp.cumulativeState, want)
}
}
// The same authority across two auths is charged for its account only once.
func TestAuthDuplicateAuthorityOnce(t *testing.T) {
a0, _ := signAuth(t, authKeyA, delegate8037, 0)
a1, _ := signAuth(t, authKeyA, delegate8037, 1)
sdb := mkState(senderAlloc(nil))
_, gp, err := applyMsg(t, sdb, setCodeTx(0, senderAddr, []types.SetCodeAuthorization{a0, a1}))
if err != nil {
t.Fatal(err)
}
if gp.cumulativeState != authWorstState {
t.Fatalf("state gas = %d, want %d (leaf+indicator once)", gp.cumulativeState, authWorstState)
}
}
// ===================== System contracts / system calls ===================
// System call gas limit keeps 30M regular plus a state reservoir for new slots.
func TestSystemCallGasLimit(t *testing.T) {
limit, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
if limit != 30_000_000 || budget.RegularGas != 30_000_000 {
t.Fatalf("limit/regular = %d/%d, want 30M/30M", limit, budget.RegularGas)
}
}
// The extra system budget is placed in the state reservoir (16 new slots).
func TestSystemCallExtraInReservoir(t *testing.T) {
_, budget := systemCallGasBudget(amsterdamCoreEVM(mkState(nil)))
want := uint64(params.SystemMaxSStoresPerCall * params.CostPerStateByte * params.StorageCreationSize)
if budget.StateGas != want {
t.Fatalf("reservoir = %d, want %d", budget.StateGas, want)
}
}
// System calls do not contribute to either block dimension: an empty block
// (whose system calls still write state) reports zero gas used.
func TestSystemCallNotCountedInBlock(t *testing.T) {
env := newBALTestEnv(nil)
engine := beacon.New(ethash.NewFaker())
_, blocks, _ := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, b *BlockGen) {})
if blocks[0].GasUsed() != 0 {
t.Fatalf("block gas used = %d, want 0 (system calls excluded)", blocks[0].GasUsed())
}
}

View file

@ -718,6 +718,9 @@ func DeveloperGenesisBlock(gasLimit uint64, faucet *common.Address) *Genesis {
params.HistoryStorageAddress: {Nonce: 1, Code: params.HistoryStorageCode, Balance: common.Big0},
params.WithdrawalQueueAddress: {Nonce: 1, Code: params.WithdrawalQueueCode, Balance: common.Big0},
params.ConsolidationQueueAddress: {Nonce: 1, Code: params.ConsolidationQueueCode, Balance: common.Big0},
// EIP-8282 - Builder Execution Requests
params.BuilderDepositAddress: {Nonce: 1, Code: params.BuilderDepositCode, Balance: common.Big0},
params.BuilderExitAddress: {Nonce: 1, Code: params.BuilderExitCode, Balance: common.Big0},
},
}
if faucet != nil {

View file

@ -311,7 +311,10 @@ func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hash
// Retrieve all the components of the canonical block.
hash := ReadCanonicalHash(nfdb, number)
if hash == (common.Hash{}) {
return fmt.Errorf("canonical hash missing, can't freeze block %d", number)
// A missing canonical mapping at the freeze frontier is almost
// always an orphaned block left by an unclean stop (header/body
// present by hash, but no number->hash mapping).
return fmt.Errorf("canonical hash missing, can't freeze block %d (block data present at height: %v)", number, ReadAllHashes(nfdb, number))
}
header := ReadHeaderRLP(nfdb, hash, number)
if len(header) == 0 {

View file

@ -352,17 +352,40 @@ const (
// PreexistingDatabase checks the given data directory whether a database is already
// instantiated at that location, and if so, returns the type of database (or the
// empty string).
//
// The database flavors are told apart by their on-disk file layout:
//
// CURRENT marker.manifest.* OPTIONS*
// leveldb x
// pebble v1 x x
// pebble v2 x x
func PreexistingDatabase(path string) string {
if _, err := os.Stat(filepath.Join(path, "CURRENT")); err != nil {
var (
hasCurrent = fileExists(filepath.Join(path, "CURRENT"))
hasMarker = anyFileMatches(filepath.Join(path, "marker.manifest.*"))
hasOptions = anyFileMatches(filepath.Join(path, "OPTIONS*"))
)
switch {
case hasMarker, hasCurrent && hasOptions:
return DBPebble
case hasCurrent:
return DBLeveldb
default:
return "" // No pre-existing db
}
if matches, err := filepath.Glob(filepath.Join(path, "OPTIONS*")); len(matches) > 0 || err != nil {
if err != nil {
panic(err) // only possible if the pattern is malformed
}
return DBPebble
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func anyFileMatches(pattern string) bool {
matches, err := filepath.Glob(pattern)
if err != nil {
panic(err) // only possible if the pattern is malformed
}
return DBLeveldb
return len(matches) > 0
}
type counter uint64

View file

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package eradb implements a history backend using era1 files.
// Package eradb implements a history backend using era1 or ere files.
package eradb
import (
@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/internal/era"
"github.com/ethereum/go-ethereum/internal/era/execdb"
"github.com/ethereum/go-ethereum/internal/era/onedb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
@ -36,7 +37,7 @@ const openFileLimit = 64
var errClosed = errors.New("era store is closed")
// Store manages read access to a directory of era1 files.
// Store manages read access to a directory of era1 or ere files.
// The getter methods are thread-safe.
type Store struct {
datadir string
@ -52,7 +53,8 @@ type Store struct {
type fileCacheEntry struct {
refcount int // reference count. This is protected by Store.mu!
opened chan struct{} // signals opening of file has completed
file *onedb.Era // the file
file era.Era // the file
slim bool // true if receipts are stored in the ere slim encoding
err error // error from opening the file
}
@ -77,7 +79,7 @@ func New(datadir string) (*Store, error) {
return db, nil
}
// Close closes all open era1 files in the cache.
// Close closes all open era files in the cache.
func (db *Store) Close() {
db.mu.Lock()
defer db.mu.Unlock()
@ -132,12 +134,14 @@ func (db *Store) GetRawReceipts(number uint64) ([]byte, error) {
if err != nil {
return nil, err
}
return convertReceipts(data)
return convertReceipts(data, entry.slim)
}
// convertReceipts transforms an encoded block receipts list from the format
// used by era1 into the 'storage' format used by the go-ethereum ancients database.
func convertReceipts(input []byte) ([]byte, error) {
// convertReceipts transforms an encoded block receipts list into the 'storage'
// format used by the go-ethereum ancients database, i.e. a list of
// [status, gas-used, logs]. The input uses the era1 network encoding, or the
// ere slim encoding when slim is true.
func convertReceipts(input []byte, slim bool) ([]byte, error) {
var (
out bytes.Buffer
enc = rlp.NewEncoderBuffer(&out)
@ -148,32 +152,41 @@ func convertReceipts(input []byte) ([]byte, error) {
}
outerList := enc.List()
for i := 0; blockListIter.Next(); i++ {
kind, content, _, err := rlp.Split(blockListIter.Value())
if err != nil {
return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
}
var receiptData []byte
switch kind {
case rlp.Byte:
return nil, fmt.Errorf("receipt %d is single byte", i)
case rlp.String:
// Typed receipt - skip type.
receiptData = content[1:]
case rlp.List:
// Legacy receipt
var (
receiptData []byte
skip int
)
if slim {
// Slim receipt is [tx-type, status, gas-used, logs]: skip the tx-type.
receiptData = blockListIter.Value()
skip = 0
} else {
// Era1 receipt is [status, gas-used, bloom, logs], prefixed by the
// tx type if non-legacy: skip the bloom.
kind, content, _, err := rlp.Split(blockListIter.Value())
if err != nil {
return nil, fmt.Errorf("receipt %d invalid: %v", i, err)
}
switch kind {
case rlp.Byte:
return nil, fmt.Errorf("receipt %d is single byte", i)
case rlp.String:
// Typed receipt - skip type.
receiptData = content[1:]
case rlp.List:
// Legacy receipt
receiptData = blockListIter.Value()
}
skip = 2
}
// Convert data list.
// Input is [status, gas-used, bloom, logs]
// Output is [status, gas-used, logs], i.e. we need to skip the bloom.
dataIter, err := rlp.NewListIterator(receiptData)
if err != nil {
return nil, fmt.Errorf("receipt %d has invalid data: %v", i, err)
}
innerList := enc.List()
for field := 0; dataIter.Next(); field++ {
if field == 2 {
continue // skip bloom
if field == skip {
continue
}
enc.Write(dataIter.Value())
}
@ -202,11 +215,11 @@ func (db *Store) getEraByEpoch(epoch uint64) *fileCacheEntry {
case fileIsNew:
// Open the file and put it into the cache.
e, err := db.openEraFile(epoch)
e, slim, err := db.openEraFile(epoch)
if err != nil {
db.fileFailedToOpen(epoch, entry, err)
} else {
db.fileOpened(epoch, entry, e)
db.fileOpened(epoch, entry, e, slim)
}
close(entry.opened)
@ -250,7 +263,7 @@ func (db *Store) getCacheEntry(epoch uint64) (stat fileCacheStatus, entry *fileC
}
// fileOpened is called after an era file has been successfully opened.
func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era) {
func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file era.Era, slim bool) {
db.mu.Lock()
defer db.mu.Unlock()
@ -267,6 +280,7 @@ func (db *Store) fileOpened(epoch uint64, entry *fileCacheEntry, file *onedb.Era
// Add it to the LRU. This may evict an existing item, which we have to close.
entry.file = file
entry.slim = slim
evictedEpoch, evictedEntry, _ := db.lru.Add3(epoch, entry)
if evictedEntry != nil {
evictedEntry.derefAndClose(evictedEpoch)
@ -283,32 +297,61 @@ func (db *Store) fileFailedToOpen(epoch uint64, entry *fileCacheEntry, err error
entry.err = err
}
func (db *Store) openEraFile(epoch uint64) (*onedb.Era, error) {
// File name scheme is <network>-<epoch>-<root>.
glob := fmt.Sprintf("*-%05d-*.era1", epoch)
matches, err := filepath.Glob(filepath.Join(db.datadir, glob))
if err != nil {
return nil, err
// openEraFile opens the era file of the given epoch. The second return value
// signals whether the receipts in the file use the ere slim encoding.
func (db *Store) openEraFile(epoch uint64) (era.Era, bool, error) {
// File name scheme is <network>-<epoch>-<root> for era1 files and
// <network>-<epoch>-<root>(-<profile>)* for ere files.
var matches []string
for _, glob := range []string{
fmt.Sprintf("*-%05d-*.era1", epoch),
fmt.Sprintf("*-%05d-*.ere", epoch),
} {
m, err := filepath.Glob(filepath.Join(db.datadir, glob))
if err != nil {
return nil, false, err
}
matches = append(matches, m...)
}
if len(matches) > 1 {
return nil, fmt.Errorf("multiple era1 files found for epoch %d", epoch)
return nil, false, fmt.Errorf("multiple era files found for epoch %d: %v", epoch, matches)
}
if len(matches) == 0 {
return nil, fs.ErrNotExist
return nil, false, fs.ErrNotExist
}
filename := matches[0]
e, err := onedb.Open(filename)
if err != nil {
return nil, err
var (
e era.Era
err error
slim = filepath.Ext(filename) == ".ere"
)
if slim {
var f *execdb.Era
f, err = execdb.Open(filename)
if err != nil {
return nil, false, err
}
// Files written with the "noreceipts" profile cannot serve as a
// history backend, since the receipts cannot be retrieved.
if !f.HasReceipts() {
f.Close()
return nil, false, fmt.Errorf("ere file %s contains no receipts", filepath.Base(filename))
}
e = f
} else {
e, err = onedb.Open(filename)
if err != nil {
return nil, false, err
}
}
// Sanity-check start block.
if e.Start()%uint64(era.MaxSize) != 0 {
if e.Start() != epoch*uint64(era.MaxSize) {
e.Close()
return nil, fmt.Errorf("pre-merge era1 file has invalid boundary. %d %% %d != 0", e.Start(), era.MaxSize)
return nil, false, fmt.Errorf("era file %s has wrong start block %d for epoch %d", filepath.Base(filename), e.Start(), epoch)
}
log.Debug("Opened era1 file", "epoch", epoch)
return e.(*onedb.Era), nil
log.Debug("Opened era file", "epoch", epoch, "name", filepath.Base(filename))
return e, slim, nil
}
// doneWithFile signals that the caller has finished using a file.
@ -339,9 +382,9 @@ func (entry *fileCacheEntry) derefAndClose(epoch uint64) (closed bool) {
closeErr := entry.file.Close()
if closeErr == nil {
log.Debug("Closed era1 file", "epoch", epoch)
log.Debug("Closed era file", "epoch", epoch)
} else {
log.Warn("Error closing era1 file", "epoch", epoch, "err", closeErr)
log.Warn("Error closing era file", "epoch", epoch, "err", closeErr)
}
return true
}

View file

@ -17,10 +17,15 @@
package eradb
import (
"math/big"
"os"
"path/filepath"
"sync"
"testing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/internal/era/execdb"
"github.com/ethereum/go-ethereum/internal/era/onedb"
"github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -48,6 +53,101 @@ func TestEraDatabase(t *testing.T) {
assert.Equal(t, 3, len(receipts), "receipts length mismatch")
}
// TestEreDatabase checks that the store can serve bodies and receipts from a
// directory of ere files, and that the receipts returned are byte-identical to
// the ones derived from the equivalent era1 files.
func TestEreDatabase(t *testing.T) {
dir := t.TempDir()
convertEra1ToEre(t, "testdata/sepolia-00000-643a00f7.era1", dir, "sepolia", 0)
convertEra1ToEre(t, "testdata/sepolia-00021-b8814b14.era1", dir, "sepolia", 21)
db, err := New(dir)
require.NoError(t, err)
defer db.Close()
r, err := db.GetRawBody(175881)
require.NoError(t, err)
var body *types.Body
err = rlp.DecodeBytes(r, &body)
require.NoError(t, err)
require.NotNil(t, body, "block body not found")
assert.Equal(t, 3, len(body.Transactions))
r, err = db.GetRawReceipts(175881)
require.NoError(t, err)
var receipts []*types.ReceiptForStorage
err = rlp.DecodeBytes(r, &receipts)
require.NoError(t, err)
require.NotNil(t, receipts, "receipts not found")
assert.Equal(t, 3, len(receipts), "receipts length mismatch")
// Cross-check against the era1 store: both backends must return the same
// storage encoding.
eraDB, err := New("testdata")
require.NoError(t, err)
defer eraDB.Close()
for _, num := range []uint64{0, 1024, 172032, 175881, 180223} {
want, err := eraDB.GetRawReceipts(num)
require.NoError(t, err)
got, err := db.GetRawReceipts(num)
require.NoError(t, err)
assert.Equal(t, want, got, "receipts mismatch at block %d", num)
wantBody, err := eraDB.GetRawBody(num)
require.NoError(t, err)
gotBody, err := db.GetRawBody(num)
require.NoError(t, err)
assert.Equal(t, wantBody, gotBody, "body mismatch at block %d", num)
}
}
// TestEreDatabaseNoReceipts checks that ere files written with the
// "noreceipts" profile are rejected by the store. The testdata fixture is a
// minimal single-block ere file whose index has no receipts component.
func TestEreDatabaseNoReceipts(t *testing.T) {
db, err := New("testdata/noreceipts")
require.NoError(t, err)
defer db.Close()
_, err = db.GetRawBody(0)
require.ErrorContains(t, err, "no receipts")
_, err = db.GetRawReceipts(0)
require.ErrorContains(t, err, "no receipts")
}
// convertEra1ToEre reads an era1 file and writes its contents as an ere file
// into dir, using the canonical ere file name.
func convertEra1ToEre(t *testing.T, era1Path, dir, network string, epoch int) {
t.Helper()
e, err := onedb.Open(era1Path)
require.NoError(t, err)
defer e.Close()
f, err := os.CreateTemp(dir, "ere-convert-*")
require.NoError(t, err)
defer f.Close()
builder := execdb.NewBuilder(f)
td, err := e.InitialTD()
require.NoError(t, err)
it, err := e.Iterator()
require.NoError(t, err)
for it.Next() {
block, receipts, err := it.BlockAndReceipts()
require.NoError(t, err)
td.Add(td, block.Difficulty())
require.NoError(t, builder.Add(block, receipts, new(big.Int).Set(td)))
}
require.NoError(t, it.Error())
lastHash, err := builder.Finalize()
require.NoError(t, err)
require.NoError(t, f.Close())
require.NoError(t, os.Rename(f.Name(), filepath.Join(dir, execdb.Filename(network, epoch, lastHash))))
}
func TestEraDatabaseConcurrentOpen(t *testing.T) {
db, err := New("testdata")
require.NoError(t, err)

View file

@ -351,6 +351,9 @@ func (f *Freezer) TruncateTail(group string, tail uint64) (uint64, error) {
// SyncAncient flushes all data tables to disk.
func (f *Freezer) SyncAncient() error {
f.writeLock.RLock()
defer f.writeLock.RUnlock()
var errs []error
for _, table := range f.tables {
if err := table.Sync(); err != nil {

View file

@ -167,10 +167,9 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.
if config.IsAmsterdam(number, time) {
blockAccessList = bal.NewConstructionBlockAccessList()
}
rules := config.Rules(number, true, time) // IsMerge is always true
// Read requests if Prague is enabled.
if config.IsPrague(number, time) {
rules := config.Rules(number, true, time) // IsMerge is always true
requests = [][]byte{}
// EIP-6110
if err := ParseDepositLogs(&requests, allLogs, config); err != nil {
@ -185,6 +184,16 @@ func PostExecution(ctx context.Context, config *params.ChainConfig, number *big.
return nil, nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
}
if config.IsAmsterdam(number, time) {
// EIP-8282
if err := ProcessBuilderDepositQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
return nil, nil, fmt.Errorf("failed to process builder deposit queue: %w", err)
}
if err := ProcessBuilderExitQueue(&requests, rules, evm, blockAccessIndex, blockAccessList); err != nil {
return nil, nil, fmt.Errorf("failed to process builder exit queue: %w", err)
}
}
return requests, blockAccessList, nil
}
@ -361,6 +370,18 @@ func ProcessConsolidationQueue(requests *[][]byte, rules params.Rules, evm *vm.E
return processRequestsSystemCall(requests, rules, evm, 0x02, params.ConsolidationQueueAddress, blockAccessIndex, blockAccessList)
}
// ProcessBuilderDepositQueue calls the EIP-8282 builder deposit contract.
// It returns the opaque request data returned by the contract.
func ProcessBuilderDepositQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
return processRequestsSystemCall(requests, rules, evm, 0x03, params.BuilderDepositAddress, blockAccessIndex, blockAccessList)
}
// ProcessBuilderExitQueue calls the EIP-8282 builder exit contract.
// It returns the opaque request data returned by the contract.
func ProcessBuilderExitQueue(requests *[][]byte, rules params.Rules, evm *vm.EVM, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
return processRequestsSystemCall(requests, rules, evm, 0x04, params.BuilderExitAddress, blockAccessIndex, blockAccessList)
}
func processRequestsSystemCall(requests *[][]byte, rules params.Rules, evm *vm.EVM, requestType byte, addr common.Address, blockAccessIndex uint32, blockAccessList *bal.ConstructionBlockAccessList) error {
if tracer := evm.Config.Tracer; tracer != nil {
onSystemCallStart(tracer, evm.GetVMContext())

View file

@ -674,11 +674,6 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret []byte
vmerr error // vm errors do not effect consensus and are therefore not assigned to err
result vm.GasBudget
// Capture the forwarded regular-gas amount BEFORE ForwardAll consumes
// it, so Absorb can back out state-gas spillover from UsedRegularGas
// per EIP-8037.
forwarded = st.gasRemaining.RegularGas
)
if contractCreation {
// Check whether the init code size has been exceeded.
@ -686,12 +681,13 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
return nil, err
}
// Execute the transaction's creation.
ret, _, result, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
st.gasRemaining.Absorb(result, forwarded)
var creation bool
ret, _, result, creation, vmerr = st.evm.Create(msg.From, msg.Data, st.gasRemaining.ForwardAll(), value)
st.gasRemaining.Absorb(result)
// If the contract creation failed, refund the account-creation state
// gas pre-charged in IntrinsicGas.
if rules.IsAmsterdam && vmerr != nil {
// If the contract creation failed, or the destination was pre-existing,
// refund the account-creation state gas pre-charged in IntrinsicGas.
if rules.IsAmsterdam && !creation {
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
}
} else {
@ -711,7 +707,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
}
// Execute the transaction's call.
ret, result, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining.ForwardAll(), value)
st.gasRemaining.Absorb(result, forwarded)
st.gasRemaining.Absorb(result)
}
// Settle down the gas usage and refund the ETH back if any remaining

View file

@ -28,17 +28,17 @@ func _() {
_ = x[GasChangeWitnessCodeChunk-17]
_ = x[GasChangeWitnessContractCollisionCheck-18]
_ = x[GasChangeTxDataFloor-19]
_ = x[GasChangeAccountCreation-20]
_ = x[GasChangeRefundAccountCreation-20]
_ = x[GasChangeIgnored-255]
}
const (
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorAccountCreation"
_GasChangeReason_name_0 = "UnspecifiedTxInitialBalanceTxIntrinsicGasTxRefundsTxLeftOverReturnedCallInitialBalanceCallLeftOverReturnedCallLeftOverRefundedCallContractCreationCallContractCreation2CallCodeStorageCallOpCodeCallPrecompiledContractCallStorageColdAccessCallFailedExecutionWitnessContractInitWitnessContractCreationWitnessCodeChunkWitnessContractCollisionCheckTxDataFloorRefundAccountCreation"
_GasChangeReason_name_1 = "Ignored"
)
var (
_GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 368}
_GasChangeReason_index_0 = [...]uint16{0, 11, 27, 41, 50, 68, 86, 106, 126, 146, 167, 182, 192, 215, 236, 255, 274, 297, 313, 342, 353, 374}
)
func (i GasChangeReason) String() string {

View file

@ -472,9 +472,9 @@ const (
// transaction data. This change will always be a negative change.
GasChangeTxDataFloor GasChangeReason = 19
// GasChangeAccountCreation represents the state gas charging for account
// creation inside the call/create frame.
GasChangeAccountCreation GasChangeReason = 20
// GasChangeRefundAccountCreation represents the cancellation of a
// pre-charged account-creation cost when no account is created.
GasChangeRefundAccountCreation GasChangeReason = 20
// GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as
// it will be "manually" tracked by a direct emit of the gas change event.

View file

@ -152,10 +152,20 @@ func (c *Contract) chargeState(s uint64, logger *tracing.Hooks, reason tracing.G
return true
}
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
func (c *Contract) refundGas(child GasBudget, forwarded uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
// refundState refunds the pre-charged state gas back to state reservoir.
func (c *Contract) refundState(s uint64, logger *tracing.Hooks, reason tracing.GasChangeReason) {
prior := c.Gas
c.Gas.Absorb(child, forwarded)
c.Gas.RefundState(s)
if s != 0 && logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
}
// refundGas absorbs a sub-call's leftover GasBudget into this contract's gas state.
func (c *Contract) refundGas(child GasBudget, logger *tracing.Hooks, reason tracing.GasChangeReason) {
prior := c.Gas
c.Gas.Absorb(child)
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}

739
core/vm/eip8037_test.go Normal file
View file

@ -0,0 +1,739 @@
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
// Opcode-level tests for EIP-8037 (multidimensional state-gas metering).
// They drive a single frame via evm.Call and assert the state-gas accounting
// exposed by the returned GasBudget (UsedStateGas / StateGas / Spilled).
package vm
import (
"math"
"math/big"
"math/rand"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
)
// state-gas charges in units (CPSB applied).
var (
stateGasNewAccount = int64(params.AccountCreationSize * params.CostPerStateByte) // 183,600
stateGasNewSlot = int64(params.StorageCreationSize * params.CostPerStateByte) // 97,920
)
// amsterdam8037Config clones MergedTestChainConfig with Amsterdam (EIP-8037) live.
func amsterdam8037Config() *params.ChainConfig {
cfg := *params.MergedTestChainConfig
cfg.AmsterdamTime = new(uint64)
blob := *cfg.BlobScheduleConfig
blob.Amsterdam = blob.Osaka
cfg.BlobScheduleConfig = &blob
return &cfg
}
// amsterdam8037EVM builds an EVM with real value transfers and CPSB wired in.
func amsterdam8037EVM(statedb StateDB) *EVM {
ctx := BlockContext{
CanTransfer: func(db StateDB, addr common.Address, amount *uint256.Int) bool {
return db.GetBalance(addr).Cmp(amount) >= 0
},
Transfer: func(db StateDB, sender, recipient common.Address, amount *uint256.Int, _ *params.Rules) {
db.SubBalance(sender, amount, tracing.BalanceChangeTransfer)
db.AddBalance(recipient, amount, tracing.BalanceChangeTransfer)
},
BlockNumber: big.NewInt(0),
Random: &common.Hash{},
CostPerStateByte: params.CostPerStateByte,
}
return NewEVM(ctx, statedb, amsterdam8037Config(), Config{})
}
// run8037 executes code at a contract address and returns the call's return
// data and the resulting budget. setup mutates the pre-state (before Finalise)
// and may fund the contract.
func run8037(t *testing.T, code []byte, gas GasBudget, value *uint256.Int, setup func(db *state.StateDB, self common.Address)) ([]byte, GasBudget, error) {
t.Helper()
self := common.BytesToAddress([]byte("self"))
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.CreateAccount(self)
statedb.SetCode(self, code, tracing.CodeChangeUnspecified)
if setup != nil {
setup(statedb, self)
}
statedb.Finalise(true)
ret, result, err := amsterdam8037EVM(statedb).Call(common.Address{}, self, nil, gas, value)
assertBudgetSane(t, gas, result)
return ret, result, err
}
// assertBudgetSane verifies the GasBudget conservation identities that must hold
// for any frame exit (success, revert or halt), validating the whole vector.
//
// regular: RegularGas + UsedRegularGas + Spilled == initial.RegularGas
// state: StateGas + UsedStateGas == initial.StateGas + Spilled
// scalar: Used(initial) == UsedRegularGas + UsedStateGas
func assertBudgetSane(t *testing.T, initial, got GasBudget) {
t.Helper()
if got.RegularGas+got.UsedRegularGas+got.Spilled != initial.RegularGas {
t.Fatalf("regular not conserved: R=%d usedR=%d spilled=%d, want sum %d",
got.RegularGas, got.UsedRegularGas, got.Spilled, initial.RegularGas)
}
if int64(got.StateGas)+got.UsedStateGas != int64(initial.StateGas)+int64(got.Spilled) {
t.Fatalf("state not conserved: S=%d usedS=%d spilled=%d, want %d+spilled",
got.StateGas, got.UsedStateGas, got.Spilled, initial.StateGas)
}
if int64(got.Used(initial)) != int64(got.UsedRegularGas)+got.UsedStateGas {
t.Fatalf("scalar mismatch: used=%d, usedR=%d usedS=%d",
got.Used(initial), got.UsedRegularGas, got.UsedStateGas)
}
}
// hugeBudget is a budget that never runs out, with a separate state reservoir.
func hugeBudget() GasBudget { return NewGasBudget(math.MaxUint64/2, math.MaxUint64/2) }
// sstore returns "PUSH val; PUSH slot; SSTORE" bytecode.
func sstore(slot, val byte) []byte { return []byte{0x60, val, 0x60, slot, 0x55} }
// setSlot commits an original (pre-tx) value into a storage slot.
func setSlot(slot, val byte) func(*state.StateDB, common.Address) {
return func(db *state.StateDB, self common.Address) {
db.SetState(self, common.BytesToHash([]byte{slot}), common.BytesToHash([]byte{val}))
}
}
// ============================ SSTORE state-gas =============================
// 0 -> 0 -> x: brand-new slot is charged one storage-creation.
func TestSStoreNewSlot(t *testing.T) {
_, res, err := run8037(t, sstore(0, 1), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != stateGasNewSlot {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewSlot)
}
}
// 0 -> x -> 0: slot created then cleared in-tx, net charge refilled to zero.
func TestSStoreClearZeroAtStart(t *testing.T) {
code := append(sstore(0, 1), sstore(0, 0)...)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// x -> x -> 0: clearing a slot non-zero at tx start makes no state adjustment.
func TestSStoreClearOriginalNonzero(t *testing.T) {
_, res, err := run8037(t, sstore(0, 0), hugeBudget(), new(uint256.Int), setSlot(0, 1))
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// x -> 0 -> x: clearing then restoring the original value makes no adjustment.
func TestSStoreRestoreOriginal(t *testing.T) {
code := append(sstore(0, 0), sstore(0, 1)...)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setSlot(0, 1))
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// x -> y: overwriting an existing slot with another value makes no adjustment.
func TestSStoreOtherWrite(t *testing.T) {
_, res, err := run8037(t, sstore(0, 2), hugeBudget(), new(uint256.Int), setSlot(0, 1))
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// New-slot charge is metered at the opcode: with a reservoir smaller than the
// charge it spills into regular gas exactly at the SSTORE.
func TestSStoreChargedAtOpcodeEnd(t *testing.T) {
_, res, err := run8037(t, sstore(0, 1), NewGasBudget(1_000_000, 100), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if want := uint64(stateGasNewSlot) - 100; res.Spilled != want {
t.Fatalf("spilled = %d, want %d", res.Spilled, want)
}
}
// The SSTORE reentrancy sentry checks gas_left only; the reservoir is excluded.
// Uses a noop write (1->1->1) so the sentry is the sole gate.
func TestSStoreStipendExcludesReservoir(t *testing.T) {
// regular at the sentry, huge reservoir: must still fail.
if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2306, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil {
t.Fatal("expected sentry failure with regular gas at the limit")
}
// one more regular gas clears the sentry.
if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2307, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil {
t.Fatalf("unexpected failure above sentry: %v", err)
}
}
// ---- CALL / CREATE bytecode helpers ----
var (
freshAddr = common.BytesToAddress([]byte("fresh-target"))
existAddr = common.BytesToAddress([]byte("exist-target"))
balanceAddr = common.BytesToAddress([]byte("balance-only"))
childAddr = common.BytesToAddress([]byte("child-frame"))
revertInit = []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // PUSH1 0; PUSH1 0; REVERT
invalidInit = []byte{0xfe} // INVALID
deploy3Init = []byte{0x60, 0x03, 0x60, 0x00, 0xf3} // return 3 bytes of code
deploy0Init = []byte{0x60, 0x00, 0x60, 0x00, 0xf3} // return 0 bytes of code
stop = []byte{0x00}
revertTail = []byte{0x60, 0x00, 0x60, 0x00, 0xfd}
invalidTail = []byte{0xfe}
stateDeposit = int64(3 * params.CostPerStateByte) // 3-byte code deposit (4,590)
)
// callCode builds bytecode that CALLs `to` forwarding `value` wei and all gas,
// followed by `tail`.
func callCode(to common.Address, value byte, tail []byte) []byte {
b := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, value, 0x73}
b = append(b, to.Bytes()...)
b = append(b, 0x5a, 0xf1) // GAS; CALL
return append(b, tail...)
}
// deployCode builds bytecode that MSTOREs init and runs CREATE/CREATE2 with value.
func deployCode(init []byte, create2 bool, value byte) []byte {
word := make([]byte, 32)
copy(word[32-len(init):], init)
off, sz := byte(32-len(init)), byte(len(init))
b := append([]byte{0x7f}, word...) // PUSH32 init-word
b = append(b, 0x60, 0x00, 0x52) // PUSH1 0; MSTORE
if create2 {
b = append(b, 0x60, 0x00, 0x60, sz, 0x60, off, 0x60, value, 0xf5) // salt,size,off,value; CREATE2
} else {
b = append(b, 0x60, sz, 0x60, off, 0x60, value, 0xf0) // size,off,value; CREATE
}
return append(b, 0x00) // STOP
}
func fund(addr common.Address, wei int64) func(*state.StateDB, common.Address) {
return func(db *state.StateDB, _ common.Address) {
db.AddBalance(addr, uint256.NewInt(uint64(wei)), tracing.BalanceChangeUnspecified)
}
}
// ====================== CALL* new-account state-gas =======================
// CALL with value to a non-existent account charges one account creation.
func TestCallValueToNewAccount(t *testing.T) {
_, res, err := run8037(t, callCode(freshAddr, 1, stop), hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != stateGasNewAccount {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewAccount)
}
}
// CALL with value to an existing (code-bearing) account is not charged.
func TestCallValueToExistingAccount(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
db.CreateAccount(existAddr)
db.SetCode(existAddr, stop, tracing.CodeChangeUnspecified)
db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
}
_, res, err := run8037(t, callCode(existAddr, 1, stop), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// CALL with zero value creates no account, so nothing is charged.
func TestCallZeroValueToNewAccount(t *testing.T) {
_, res, err := run8037(t, callCode(freshAddr, 0, stop), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// CALL that fails before the child frame (insufficient balance) refills the charge.
func TestCallInsufficientBalanceRefill(t *testing.T) {
// self has no balance, so the value transfer fails the CanTransfer check.
_, res, err := run8037(t, callCode(freshAddr, 1, stop), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// A new-account charge is refilled when its frame reverts.
func TestCallChildRevertRefill(t *testing.T) {
code := callCode(freshAddr, 1, revertTail)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
if err != ErrExecutionReverted {
t.Fatalf("err = %v, want revert", err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// A new-account charge is refilled when its frame halts exceptionally.
func TestCallChildExceptionalHaltRefill(t *testing.T) {
code := callCode(freshAddr, 1, invalidTail)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
if err == nil || err == ErrExecutionReverted {
t.Fatalf("err = %v, want exceptional halt", err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// An account with balance but no code/nonce is existent: no account charge.
func TestCallBalanceOnlyAccountIsExistent(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
db.AddBalance(balanceAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
}
_, res, err := run8037(t, callCode(balanceAddr, 1, stop), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// ===================== CREATE / CREATE2 state-gas =========================
// CREATE to a fresh address charges account creation plus code deposit.
func TestCreateNewAccount(t *testing.T) {
_, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if want := stateGasNewAccount + stateDeposit; res.UsedStateGas != want {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want)
}
}
// CREATE onto a pre-existing (balance-only) leaf refills the account portion;
// only the code deposit is charged.
func TestCreatePreexistingTarget(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
derived := crypto.CreateAddress(self, db.GetNonce(self))
db.AddBalance(derived, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
}
_, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != stateDeposit {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateDeposit)
}
}
// CREATE whose init code reverts refills the account charge and deposits nothing.
func TestCreateInitRevertRefill(t *testing.T) {
_, res, err := run8037(t, deployCode(revertInit, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// CREATE whose init code halts exceptionally refills the account charge.
func TestCreateInitOOGRefill(t *testing.T) {
_, res, err := run8037(t, deployCode(invalidInit, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// CREATE onto an address collision (existing nonce) refills the account charge.
func TestCreateAddressCollisionRefill(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
derived := crypto.CreateAddress(self, db.GetNonce(self))
db.SetNonce(derived, 1, tracing.NonceChangeUnspecified)
}
_, res, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// CREATE with value exceeding balance fails before the frame and is refilled.
func TestCreateInsufficientBalanceRefill(t *testing.T) {
// self has no balance; CREATE forwards value 1.
_, res, err := run8037(t, deployCode(deploy3Init, false, 1), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled)", res.UsedStateGas)
}
}
// CREATE2 charges account creation plus code deposit identically to CREATE.
func TestCreate2SameSemantics(t *testing.T) {
_, res, err := run8037(t, deployCode(deploy3Init, true, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if want := stateGasNewAccount + stateDeposit; res.UsedStateGas != want {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want)
}
}
// The code-deposit portion is charged per byte independently of the account
// charge: the delta between a 3-byte and 0-byte deploy is exactly 3 x CPSB.
func TestCreateCodeDepositChargedSeparately(t *testing.T) {
_, big3, err := run8037(t, deployCode(deploy3Init, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
_, big0, err := run8037(t, deployCode(deploy0Init, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if got := big3.UsedStateGas - big0.UsedStateGas; got != stateDeposit {
t.Fatalf("deposit delta = %d, want %d", got, stateDeposit)
}
}
// ========================= SELFDESTRUCT state-gas =========================
// selfdestruct sending balance to a non-existent beneficiary creates it.
func TestSelfdestructCreatesNewAccount(t *testing.T) {
code := append([]byte{0x73}, freshAddr.Bytes()...) // PUSH20 beneficiary
code = append(code, 0xff) // SELFDESTRUCT
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), fund(common.BytesToAddress([]byte("self")), 10))
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != stateGasNewAccount {
t.Fatalf("state gas = %d, want %d", res.UsedStateGas, stateGasNewAccount)
}
}
// selfdestruct to an existing beneficiary creates no account.
func TestSelfdestructToExistingAccount(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
db.AddBalance(existAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
db.AddBalance(self, uint256.NewInt(10), tracing.BalanceChangeUnspecified)
}
code := append([]byte{0x73}, existAddr.Bytes()...)
code = append(code, 0xff)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// A contract created and self-destructed in the same tx gets no refill: the
// account-creation charge stands.
func TestSelfdestructSameTxAccountNoRefill(t *testing.T) {
// init code selfdestructs to self (existing), so only the create charges.
self := common.BytesToAddress([]byte("self"))
init := append([]byte{0x73}, self.Bytes()...)
init = append(init, 0xff)
_, res, err := run8037(t, deployCode(init, false, 0), hugeBudget(), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != stateGasNewAccount {
t.Fatalf("state gas = %d, want %d (no refill)", res.UsedStateGas, stateGasNewAccount)
}
}
// selfdestruct of a pre-existing account refills nothing (EIP-6780: not removed).
func TestSelfdestructPreexistingNoRefill(t *testing.T) {
setup := func(db *state.StateDB, self common.Address) {
db.AddBalance(existAddr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
}
code := append([]byte{0x73}, existAddr.Bytes()...)
code = append(code, 0xff)
_, res, err := run8037(t, code, hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0", res.UsedStateGas)
}
}
// ===================== Reservoir / gas_left mechanics =====================
// State-gas is drawn from the reservoir first: a charge within reservoir size
// does not spill into regular gas.
func TestReservoirDrawnFirst(t *testing.T) {
_, res, err := run8037(t, sstore(0, 1), NewGasBudget(1_000_000, 200_000), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.Spilled != 0 {
t.Fatalf("spilled = %d, want 0", res.Spilled)
}
if want := uint64(200_000 - stateGasNewSlot); res.StateGas != want {
t.Fatalf("reservoir left = %d, want %d", res.StateGas, want)
}
}
// The GAS opcode returns gas_left only, excluding the reservoir.
func TestGasOpcodeExcludesReservoir(t *testing.T) {
code := []byte{0x5a, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3} // GAS; MSTORE; RETURN(32)
ret, _, err := run8037(t, code, NewGasBudget(1_000_000, 500_000), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if got := new(uint256.Int).SetBytes(ret).Uint64(); got != 1_000_000-GasQuickStep {
t.Fatalf("GAS = %d, want %d (reservoir excluded)", got, 1_000_000-GasQuickStep)
}
}
// Refills are LIFO: borrowed regular gas is repaid before the reservoir. With a
// zero reservoir, a 0->x->0 SSTORE repays the spill and leaves the reservoir at 0.
func TestLIFORefillOrder(t *testing.T) {
code := append(sstore(0, 1), sstore(0, 0)...)
_, res, err := run8037(t, code, NewGasBudget(1_000_000, 0), new(uint256.Int), nil)
if err != nil {
t.Fatal(err)
}
if res.Spilled != 0 || res.StateGas != 0 || res.UsedStateGas != 0 {
t.Fatalf("after LIFO refill: spilled=%d reservoir=%d used=%d, want 0/0/0", res.Spilled, res.StateGas, res.UsedStateGas)
}
}
// State-gas charged inside a child frame is refilled at the frame boundary when
// the child reverts or halts.
func TestStateGasMeteredAtFrameBoundary(t *testing.T) {
for _, tt := range []struct {
name string
tail []byte
}{
{"revert", revertTail},
{"halt", invalidTail},
} {
t.Run(tt.name, func(t *testing.T) {
childCode := append(sstore(0, 1), tt.tail...)
setup := func(db *state.StateDB, self common.Address) {
db.CreateAccount(childAddr)
db.SetCode(childAddr, childCode, tracing.CodeChangeUnspecified)
}
_, res, err := run8037(t, callCode(childAddr, 0, stop), hugeBudget(), new(uint256.Int), setup)
if err != nil {
t.Fatal(err)
}
if res.UsedStateGas != 0 {
t.Fatalf("state gas = %d, want 0 (refilled at boundary)", res.UsedStateGas)
}
})
}
}
// ===================== LIFO refill vector invariant =========================
// Charge A then B (both spilling into regular because the reservoir is too
// small), then refill only A. The refill must repay the borrowed regular gas
// first (Spilled -> 0) before crediting the reservoir, leaving B outstanding.
func TestLIFORefillRepaysRegularBeforeReservoir(t *testing.T) {
initial := NewGasBudget(1000, 100) // reservoir covers only 100 of state gas
b := initial
b.ChargeState(150) // A: 100 from reservoir, 50 spills into regular
b.ChargeState(30) // B: reservoir empty, all 30 spills
if b.Spilled != 80 || b.StateGas != 0 {
t.Fatalf("after A+B: spilled=%d reservoir=%d, want 80/0", b.Spilled, b.StateGas)
}
b.RefundState(150) // refill A: repay 80 to regular first, 70 tops reservoir
if b.Spilled != 0 {
t.Fatalf("spilled=%d, want 0 (regular repaid before reservoir)", b.Spilled)
}
if b.StateGas != 70 {
t.Fatalf("reservoir=%d, want 70 (remainder after repaying regular)", b.StateGas)
}
assertBudgetSane(t, initial, b)
}
// Fuzz arbitrary sequences of state/regular charges and LIFO refills around the
// reservoir/spill boundary: the GasBudget vector must stay self-consistent after
// every op and across all three frame-exit forms, and refilling every charge
// must restore the state side exactly (reservoir to initial, nothing borrowed).
func TestLIFOVectorInvariantUnderRandomOps(t *testing.T) {
rng := rand.New(rand.NewSource(8037))
for trial := 0; trial < 2000; trial++ {
initial := NewGasBudget(1_000_000, uint64(rng.Intn(1000)))
b := initial
outstanding := int64(0) // state-gas charged but not yet refilled
for step := 0; step < 40; step++ {
switch rng.Intn(3) {
case 0: // state charge (may spill into regular)
if s := uint64(rng.Intn(400)); b.CanAfford(GasCosts{StateGas: s}) {
b.ChargeState(s)
outstanding += int64(s)
}
case 1: // regular charge
if r := uint64(rng.Intn(400)); b.CanAfford(GasCosts{RegularGas: r}) {
b.ChargeRegular(r)
}
case 2: // LIFO refill of part of the outstanding state gas
if outstanding > 0 {
s := uint64(rng.Int63n(outstanding) + 1)
b.RefundState(s)
outstanding -= int64(s)
}
}
assertBudgetSane(t, initial, b)
assertBudgetSane(t, initial, b.ExitSuccess())
assertBudgetSane(t, initial, b.ExitRevert())
assertBudgetSane(t, initial, b.ExitHalt())
}
if outstanding > 0 {
b.RefundState(uint64(outstanding))
}
if b.Spilled != 0 || b.StateGas != initial.StateGas || b.UsedStateGas != 0 {
t.Fatalf("trial %d: after full refill spilled=%d reservoir=%d used=%d, want 0/%d/0",
trial, b.Spilled, b.StateGas, b.UsedStateGas, initial.StateGas)
}
}
}
// ================== Halting frame terminal state (nested) ===================
func concat(parts ...[]byte) []byte {
var b []byte
for _, p := range parts {
b = append(b, p...)
}
return b
}
// assertHalted checks the predictable terminal budget of an exceptionally
// halted frame: regular gas fully consumed, state restored to the frame's
// initial reservoir, and no net state-gas used.
func assertHalted(t *testing.T, initial, got GasBudget) {
t.Helper()
if got.RegularGas != 0 {
t.Fatalf("RegularGas = %d, want 0 (gas_left consumed on halt)", got.RegularGas)
}
if got.StateGas != initial.StateGas {
t.Fatalf("StateGas = %d, want %d (reservoir restored)", got.StateGas, initial.StateGas)
}
if got.UsedStateGas != 0 {
t.Fatalf("UsedStateGas = %d, want 0 (all refilled)", got.UsedStateGas)
}
}
var (
haltGrandchild = common.BytesToAddress([]byte("grandchild"))
haltOKChild = common.BytesToAddress([]byte("child-ok")) // succeeds, calls grandchild
haltBadChild = common.BytesToAddress([]byte("child-halt")) // SSTOREs then INVALID
)
// haltFrameChildren is a run8037 setup that funds self and deploys a 3-level
// child set: a success child that itself calls a grandchild, and a halting child.
func haltFrameChildren(db *state.StateDB, self common.Address) {
db.AddBalance(self, uint256.NewInt(1000), tracing.BalanceChangeUnspecified)
db.CreateAccount(haltGrandchild)
db.SetCode(haltGrandchild, concat(sstore(5, 5), []byte{0x00}), tracing.CodeChangeUnspecified) // new slot; STOP
db.CreateAccount(haltOKChild)
db.SetCode(haltOKChild, concat(sstore(1, 1), callCode(haltGrandchild, 0, nil), []byte{0x00}), tracing.CodeChangeUnspecified)
db.CreateAccount(haltBadChild)
db.SetCode(haltBadChild, concat(sstore(3, 3), []byte{0xfe}), tracing.CodeChangeUnspecified) // new slot; INVALID
}
// A frame that charges state, drives a successful child (with a grandchild), a
// halting child and a new-account call, then halts, returns the predictable
// terminal budget regardless of all the descendant activity.
func TestHaltFrameTerminalState(t *testing.T) {
top := concat(
sstore(0, 1), // self: new slot
callCode(haltOKChild, 0, nil), // child + grandchild succeed
callCode(haltBadChild, 0, nil), // descendant halts (contained)
callCode(freshAddr, 1, nil), // new-account charge
[]byte{0xfe}, // this frame halts
)
initial := NewGasBudget(2_000_000, 300_000)
_, res, err := run8037(t, top, initial, new(uint256.Int), haltFrameChildren)
if err == nil || err == ErrExecutionReverted {
t.Fatalf("err = %v, want exceptional halt", err)
}
assertHalted(t, initial, res)
}
// Fuzz: arbitrary sequences of state writes, child calls (success / halting) and
// new-account calls, always terminated by INVALID. However the descendants
// behave, a halted frame's terminal budget is always (0, initial reservoir, 0).
func TestHaltFrameTerminalStateFuzz(t *testing.T) {
rng := rand.New(rand.NewSource(80371))
for trial := 0; trial < 400; trial++ {
steps := [][]byte{
sstore(byte(1+rng.Intn(20)), 1),
callCode(haltOKChild, 0, nil),
callCode(haltBadChild, 0, nil),
callCode(freshAddr, 1, nil),
}
var code []byte
for n := 1 + rng.Intn(8); n > 0; n-- {
code = append(code, steps[rng.Intn(len(steps))]...)
}
code = append(code, 0xfe) // halt
initial := NewGasBudget(3_000_000, uint64(rng.Intn(400_000)))
_, res, err := run8037(t, code, initial, new(uint256.Int), haltFrameChildren)
if err == nil || err == ErrExecutionReverted {
t.Fatalf("trial %d: err = %v, want halt", trial, err)
}
assertHalted(t, initial, res)
}
}

View file

@ -587,7 +587,10 @@ func enable7843(jt *JumpTable) {
// enable8037 enables the multidimensional-metering as specified in EIP-8037.
func enable8037(jt *JumpTable) {
jt[CREATE].constantGas = params.CreateGasAmsterdam
jt[CREATE].dynamicGas = gasCreateEip8037
jt[CREATE2].constantGas = params.CreateGasAmsterdam
jt[CREATE2].dynamicGas = gasCreate2Eip8037
jt[CALL].dynamicGas = gasCallEIP8037
jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
jt[SSTORE].dynamicGas = gasSStore8037
}

View file

@ -265,7 +265,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if !syscall && !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)
if !evm.StateDB.Exist(addr) {
if !isPrecompile && evm.chainRules.IsEIP4762 && !isSystemCall(caller) {
@ -279,7 +279,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
wgas := evm.AccessEvents.CodeHashGas(addr, true, gas.RegularGas, false)
if _, ok := gas.ChargeRegular(wgas); !ok {
evm.StateDB.RevertToSnapshot(snapshot)
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, gas.ExitHalt(), ErrOutOfGas
}
}
@ -289,16 +289,6 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
evm.StateDB.CreateAccount(addr)
}
if evm.chainRules.IsAmsterdam && !value.IsZero() && evm.StateDB.Empty(addr) {
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
if !ok {
evm.StateDB.RevertToSnapshot(snapshot)
return nil, gas.ExitHalt(reservoir), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
}
}
// Perform the value transfer only in non-syscall mode.
// Calling this is required even for zero-value transfers,
// to ensure the state clearing mechanism is applied.
@ -324,7 +314,7 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -360,7 +350,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
if !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
@ -375,7 +365,7 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -406,7 +396,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
if evm.depth > int(params.CallCreateDepth) {
return nil, gas, ErrDepth
}
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
@ -419,7 +409,7 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
@ -453,7 +443,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
// after all empty accounts were deleted, so this is not required. However, if we omit this,
// then certain tests start failing; stRevertTest/RevertPrecompiledTouchExactOOG.json.
// We could change this, but for now it's left for legacy reasons
snapshot, reservoir := evm.StateDB.Snapshot(), gas.StateGas
snapshot := evm.StateDB.Snapshot()
// We do an AddBalance of zero here, just in order to trigger a touch.
// This doesn't matter on Mainnet, where all empties are gone at the time of Byzantium,
@ -471,7 +461,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
}
// Calculate the remaining gas at the end of frame
exitGas := gas.Exit(err, reservoir)
exitGas := gas.Exit(err)
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
@ -484,7 +474,7 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
}
// create creates a new contract using code as deployment code.
func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, err error) {
func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int, address common.Address, typ OpCode) (ret []byte, createAddress common.Address, result GasBudget, creation bool, err error) {
// Depth check execution. Fail if we're trying to execute above the
// limit.
var nonce uint64
@ -505,18 +495,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
}(gas)
}
if err != nil {
return nil, common.Address{}, gas, err
return nil, common.Address{}, gas, false, err
}
// Increment the caller's nonce after passing all validations
evm.StateDB.SetNonce(caller, nonce+1, tracing.NonceChangeContractCreator)
reservoir := gas.StateGas
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
statelessGas := evm.AccessEvents.ContractCreatePreCheckGas(address, gas.RegularGas)
prior, ok := gas.Charge(GasCosts{RegularGas: statelessGas})
if !ok {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
@ -537,13 +526,13 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
halt := gas.ExitHalt(reservoir)
halt := gas.ExitHalt()
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), halt.AsTracing(), tracing.GasChangeCallFailedExecution)
}
// EIP-8037 collision rule: the state reservoir is fully preserved on
// address collision while regular gas is burnt.
return nil, common.Address{}, halt, ErrContractAddressCollision
return nil, common.Address{}, halt, false, ErrContractAddressCollision
}
// Create a new account on the state only if the object was not present.
// It might be possible the contract code is deployed to a pre-existent
@ -551,18 +540,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
snapshot := evm.StateDB.Snapshot()
if !evm.StateDB.Exist(address) {
evm.StateDB.CreateAccount(address)
if evm.chainRules.IsAmsterdam && evm.depth > 0 {
// Only charge state gas if we are not doing a create transaction.
// Prevents double charging with IntrinsicGas.
prev, ok := gas.ChargeState(params.AccountCreationSize * evm.Context.CostPerStateByte)
if !ok {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
}
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prev.AsTracing(), gas.AsTracing(), tracing.GasChangeAccountCreation)
}
}
creation = true
}
// CreateContract means that regardless of whether the account previously existed
// in the state trie or not, it _now_ becomes created as a _contract_ account.
@ -577,7 +555,7 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.chainRules.IsEIP4762 {
consumed, wanted := evm.AccessEvents.ContractCreateInitGas(address, gas.RegularGas)
if consumed < wanted {
return nil, common.Address{}, gas.ExitHalt(reservoir), ErrOutOfGas
return nil, common.Address{}, gas.ExitHalt(), false, ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer.HasGasHook() {
@ -602,17 +580,17 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
evm.StateDB.RevertToSnapshot(snapshot)
exit := contract.Gas.Exit(err, reservoir)
exit := contract.Gas.Exit(err)
if err != ErrExecutionReverted {
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(contract.Gas.AsTracing(), exit.AsTracing(), tracing.GasChangeCallFailedExecution)
}
}
return ret, address, exit, err
return ret, address, exit, false, err
}
// Either success, or pre-Homestead ErrCodeStoreOutOfGas (gas preserved).
// Both packaged as a success-form GasBudget.
return ret, address, contract.Gas.ExitSuccess(), err
return ret, address, contract.Gas.ExitSuccess(), creation, err
}
// initNewContract runs a new contract's creation code, performs checks on the
@ -668,7 +646,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
}
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) {
contractAddr = crypto.CreateAddress(caller, evm.StateDB.GetNonce(caller))
return evm.create(caller, code, gas, value, contractAddr, CREATE)
}
@ -677,7 +655,7 @@ func (evm *EVM) Create(caller common.Address, code []byte, gas GasBudget, value
//
// The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:]
// instead of the usual sender-and-nonce-hash as the address where the contract is initialized at.
func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, err error) {
func (evm *EVM) Create2(caller common.Address, code []byte, gas GasBudget, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, result GasBudget, creation bool, err error) {
inithash := crypto.Keccak256Hash(code)
contractAddr = crypto.CreateAddress2(caller, salt.Bytes32(), inithash[:])
return evm.create(caller, code, gas, endowment, contractAddr, CREATE2)

View file

@ -519,6 +519,117 @@ func gasSelfdestruct(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me
return GasCosts{RegularGas: gas}, nil
}
func gasCreateEip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return GasCosts{}, err
}
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow
words := (size + 31) / 32
wordGas := params.InitCodeWordGas * words
// Unconditionally pre-charge the account creation and refunds if the creation
// doesn't happen after the create-frame.
return GasCosts{
RegularGas: gas + wordGas,
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
}, nil
}
func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
return GasCosts{}, err
}
size, overflow := stack.back(2).Uint64WithOverflow()
if overflow {
return GasCosts{}, ErrGasUintOverflow
}
if err := CheckMaxInitCodeSize(&evm.chainRules, size); err != nil {
return GasCosts{}, err
}
// Since size <= MaxInitCodeSizeAmsterdam, these multiplications cannot overflow
words := (size + 31) / 32
// CREATE2 charges both InitCodeWordGas (EIP-3860) and Keccak256WordGas
// (for address hashing).
wordGas := (params.InitCodeWordGas + params.Keccak256WordGas) * words
// Unconditionally pre-charge the account creation and refunds if the creation
// doesn't happen after the create-frame.
return GasCosts{
RegularGas: gas + wordGas,
StateGas: params.AccountCreationSize * evm.Context.CostPerStateByte,
}, nil
}
// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam.
// It computes memory expansion + value transfer gas but excludes new account
// creation, which is handled as state gas by the wrapper.
func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.back(2).IsZero()
)
if evm.readOnly && transfersValue {
return 0, ErrWriteProtection
}
memoryGas, err := memoryGasCost(mem, memorySize)
if err != nil {
return 0, err
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
transferGas = params.CallValueTransferGas
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
return 0, ErrGasUintOverflow
}
return gas, nil
}
// stateGasCall8037 is the stateful gas calculator for CALL in Amsterdam (EIP-8037).
// It only returns the state-dependent gas (account creation as state gas).
// Memory gas, transfer gas, and callGas are handled by gasCallStateless and
// makeCallVariantGasCall.
func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error) {
var (
gas uint64
transfersValue = !stack.back(2).IsZero()
address = common.Address(stack.back(1).Bytes20())
)
// TODO(rjl, marius), can EIP8037 implicitly means the EIP158 is also activated?
// It's technically possible to skip the EIP158 but very unlikely in practice.
if evm.chainRules.IsEIP158 {
// Important: use StateDB.Empty instead of !StateDB.Exist. An account may exist
// in the current state yet still be considered non-existent by EIP-161 if its
// nonce, balance, and code are all zero. Such accounts can appear temporarily
// during execution (e.g. via SELFDESTRUCT) and are removed at tx end.
//
// Funding such an account makes it permanent state growth and must be charged.
if transfersValue && evm.StateDB.Empty(address) {
gas += params.AccountCreationSize * evm.Context.CostPerStateByte
}
} else if !evm.StateDB.Exist(address) {
gas += params.AccountCreationSize * evm.Context.CostPerStateByte
}
return gas, nil
}
func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection

View file

@ -65,6 +65,11 @@ type GasBudget struct {
StateGas uint64 // remaining state-gas reservoir (or leftover for caller to absorb)
UsedRegularGas uint64 // gross regular gas consumed in this frame
UsedStateGas int64 // signed net state-gas consumed in this frame
// Spilled tracks how much of this frame's regular gas (gas_left)
// has been borrowed to cover state-gas charges that exceeded the
// reservoir.
Spilled uint64
}
// NewGasBudget initializes a fresh GasBudget for execution / forwarding,
@ -82,7 +87,7 @@ func (g GasBudget) Used(initial GasBudget) uint64 {
// String returns a visual representation of the budget.
func (g GasBudget) String() string {
return fmt.Sprintf("<%v,%v,used=<%v,%v>>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas)
return fmt.Sprintf("<%v,%v,used=<%v,%v>,borrowed=%v>", g.RegularGas, g.StateGas, g.UsedRegularGas, g.UsedStateGas, g.Spilled)
}
// Charge deducts a combined regular+state cost from the running balance and
@ -104,6 +109,21 @@ func (g *GasBudget) chargeRegularOnly(r uint64) bool {
return true
}
// CanAfford reports whether the running budget can cover the given cost vector
// without going out of gas. The regular cost must fit in the regular balance,
// and any state gas in excess of the reservoir must be coverable by the
// remaining regular gas (the spillover), mirroring charge without mutating.
func (g GasBudget) CanAfford(cost GasCosts) bool {
if g.RegularGas < cost.RegularGas {
return false
}
regular := g.RegularGas - cost.RegularGas
if cost.StateGas > g.StateGas {
return cost.StateGas-g.StateGas <= regular
}
return true
}
// charge deducts both the state and regular cost.
func (g *GasBudget) charge(cost GasCosts) bool {
if g.RegularGas < cost.RegularGas {
@ -111,6 +131,7 @@ func (g *GasBudget) charge(cost GasCosts) bool {
}
regular := g.RegularGas - cost.RegularGas
state := g.StateGas
spilled := g.Spilled
if cost.StateGas > state {
spillover := cost.StateGas - state
@ -119,6 +140,7 @@ func (g *GasBudget) charge(cost GasCosts) bool {
}
regular -= spillover
state = 0
spilled += spillover
} else {
state -= cost.StateGas
}
@ -126,6 +148,7 @@ func (g *GasBudget) charge(cost GasCosts) bool {
g.StateGas = state
g.UsedRegularGas += cost.RegularGas
g.UsedStateGas += int64(cost.StateGas)
g.Spilled = spilled
return true
}
@ -151,14 +174,25 @@ func (g *GasBudget) IsZero() bool {
}
// RefundState applies an inline state-gas refund (e.g., SSTORE 0->A->0).
// The reservoir is credited and the signed usage counter is decremented
// in lockstep, preserving the per-frame invariant:
//
// StateGas + UsedStateGas == initialStateGas + spillover_so_far
// Per EIP-8037, the refund repays the regular gas previously borrowed for
// state-gas spillover (tracked by Spilled) before crediting the
// reservoir: it is returned to RegularGas up to the outstanding borrowed
// amount, and only the remainder tops up StateGas.
//
// which the revert path relies on for the correct gross refund.
// The signed usage counter is decremented by the full refund regardless of the
// split, preserving the per-frame invariant:
//
// StateGas + UsedStateGas == initialStateGas + Spilled
//
// which the revert and halt paths rely on for the correct gross refund.
func (g *GasBudget) RefundState(s uint64) {
g.StateGas += s
repay := min(s, g.Spilled)
g.RegularGas += repay
g.Spilled -= repay
// Whatever is left tops up the reservoir.
g.StateGas += s - repay
g.UsedStateGas -= int64(s)
}
@ -206,54 +240,49 @@ func (g GasBudget) ExitSuccess() GasBudget {
return g
}
// ExitRevert produces the leftover for a REVERT exit. Per EIP-8037, all state
// gas charged by the reverted frame is refunded to the caller's reservoir:
//
// leftover.StateGas = StateGas + UsedStateGas
//
// UsedStateGas is reset since the frame's state changes are discarded.
// ExitRevert produces the leftover for a REVERT exit. The frame's state
// changes are discarded, so all state gas it charged is refilled to its origin
// (EIP-8037): up to Spilled is returned to RegularGas (the regular
// gas it borrowed), and the remainder restores the reservoir. Because the
// borrowed regular gas is repaid first, the reservoir is made whole back to its
// start-of-frame value.
func (g GasBudget) ExitRevert() GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)
if reservoir < 0 {
// Reservoir should never be negative. By construction it equals
// the initial state-gas allocation plus any spillover to regular
// gas.
// the initial state-gas allocation.
reservoir = 0
log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas)
log.Warn("Negative reservoir at revert", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.Spilled)
}
return GasBudget{
RegularGas: g.RegularGas,
RegularGas: g.RegularGas + g.Spilled,
StateGas: uint64(reservoir),
UsedRegularGas: g.UsedRegularGas,
UsedStateGas: 0,
Spilled: 0,
}
}
// ExitHalt produces the leftover for an exceptional halt.
//
// - state_gas_reservoir is reset back to its value at the start of the child frame
// - the gas_left initially given to the child is consumed (set to zero)
func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas
// ExitHalt produces the leftover for an exceptional halt. As with a revert, the
// frame's state changes are rolled back and its state gas is refilled to origin
// (EIP-8037); the difference is that the frame's gas_left is consumed rather
// than returned. The portion refilled to RegularGas is therefore burned along
// with the rest of gas_left, leaving only the reservoir portion to survive,
// which equals the reservoir's value at the start of the frame.
func (g GasBudget) ExitHalt() GasBudget {
reservoir := int64(g.StateGas) + g.UsedStateGas - int64(g.Spilled)
if reservoir < 0 {
// Reservoir should never be negative. By construction it equals
// the initial state-gas allocation plus any spillover to regular
// gas.
// the initial state-gas allocation.
reservoir = 0
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas)
}
// The portion of state gas charged from regular gas is also burned
// together with the regular gas, rather than being returned to the
// parent's state-gas reservoir.
var spilled uint64
if uint64(reservoir) > initStateReservoir {
spilled = uint64(reservoir) - initStateReservoir
log.Warn("Negative reservoir at halt", "remaining", g.StateGas, "used", g.UsedStateGas, "borrowed", g.Spilled)
}
return GasBudget{
RegularGas: 0,
StateGas: initStateReservoir,
UsedRegularGas: g.UsedRegularGas + g.RegularGas + spilled,
StateGas: uint64(reservoir),
UsedRegularGas: g.UsedRegularGas + g.RegularGas + g.Spilled,
UsedStateGas: 0,
Spilled: 0,
}
}
@ -263,17 +292,14 @@ func (g GasBudget) ExitHalt(initStateReservoir uint64) GasBudget {
// - err == nil → ExitSuccess
// - err == ErrExecutionReverted → ExitRevert
// - any other err → ExitHalt
//
// Soft validation failures (occurring BEFORE evm.Run) should call Preserved
// directly instead of going through this dispatcher.
func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
func (g GasBudget) Exit(err error) GasBudget {
switch {
case err == nil:
return g.ExitSuccess()
case err == ErrExecutionReverted:
return g.ExitRevert()
default:
return g.ExitHalt(initStateReservoir)
return g.ExitHalt()
}
}
@ -281,18 +307,12 @@ func (g GasBudget) Exit(err error, initStateReservoir uint64) GasBudget {
// budget. Additionally, it does an EIP-8037 spillover correction:
// state-gas that spilled into the regular pool inside the child frame is
// excluded from the UsedRegularGas.
//
// spillover = forwarded - child.RegularGas - child.UsedRegularGas
//
// forwarded is the regular-gas amount that was passed to the child at call
// entry (i.e., the regular initial of the child's GasBudget).
func (g *GasBudget) Absorb(child GasBudget, forwarded uint64) {
spillover := forwarded - child.RegularGas - child.UsedRegularGas
func (g *GasBudget) Absorb(child GasBudget) {
g.UsedRegularGas -= child.RegularGas
g.RegularGas += child.RegularGas
g.StateGas = child.StateGas
g.UsedStateGas += child.UsedStateGas
g.UsedRegularGas -= spillover
g.UsedRegularGas -= child.Spilled
g.Spilled += child.Spilled
}

View file

@ -646,7 +646,7 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
stackvalue := size
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation)
res, addr, result, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
res, addr, result, creation, suberr := evm.Create(scope.Contract.Address(), input, child, &value)
// Push item on the stack based on the returned error. If the ruleset is
// homestead we must check for CodeStoreOutOfGasError (homestead only
// rule) and treat as an error, if the ruleset is frontier we must
@ -661,8 +661,12 @@ func opCreate(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(&stackvalue)
// Refund the leftover gas back to current frame
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// Refund the state gas of account-creation if creation doesn't happen
if evm.GetRules().IsAmsterdam && !creation {
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
}
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
return res, nil
@ -685,7 +689,7 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
// reuse size int for stackvalue
stackvalue := size
child := scope.Contract.forwardGas(forward, evm.Config.Tracer, tracing.GasChangeCallContractCreation2)
res, addr, result, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
res, addr, result, creation, suberr := evm.Create2(scope.Contract.Address(), input, child, &endowment, &salt)
// Push item on the stack based on the returned error.
if suberr != nil {
stackvalue.Clear()
@ -695,8 +699,12 @@ func opCreate2(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Stack.push(&stackvalue)
// Refund the leftover gas back to current frame
scope.Contract.refundGas(result, forward, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// Refund the state gas of account-creation if creation doesn't happen
if evm.GetRules().IsAmsterdam && !creation {
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
}
if suberr == ErrExecutionReverted {
evm.returnData = res // set REVERT data to return data buffer
return res, nil
@ -740,8 +748,13 @@ func opCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
// If the call frame reverts or halts exceptionally, the charged state-gas
// is refilled back to the state reservoir in Amsterdam.
if evm.chainRules.IsAmsterdam && err != nil && !value.IsZero() && evm.StateDB.Empty(toAddr) {
scope.Contract.refundState(params.AccountCreationSize*evm.Context.CostPerStateByte, evm.Config.Tracer, tracing.GasChangeRefundAccountCreation)
}
evm.returnData = ret
return ret, nil
}
@ -776,7 +789,7 @@ func opCallCode(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@ -808,7 +821,7 @@ func opDelegateCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil
@ -841,7 +854,7 @@ func opStaticCall(pc *uint64, evm *EVM, scope *ScopeContext) ([]byte, error) {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.refundGas(result, gas, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
scope.Contract.refundGas(result, evm.Config.Tracer, tracing.GasChangeCallLeftOverRefunded)
evm.returnData = ret
return ret, nil

View file

@ -28,6 +28,9 @@ type (
intrinsicGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error) // last parameter is the requested memory size as a uint64
// memorySizeFunc returns the required size, and whether the operation overflowed a uint64
memorySizeFunc func(*Stack) (size uint64, overflow bool)
regularGasFunc func(*EVM, *Contract, *Stack, *Memory, uint64) (uint64, error)
stateGasFunc func(*EVM, *Contract, *Stack) (uint64, error)
)
type operation struct {

View file

@ -262,6 +262,7 @@ var (
gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037)
)
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
@ -276,6 +277,14 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
transfersValue := !stack.back(2).IsZero()
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
}
return innerGasCallEIP8037(evm, contract, stack, mem, memorySize)
}
func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
@ -362,3 +371,89 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
return GasCosts{RegularGas: totalCost}, nil
}
}
// makeCallVariantGasCallEIP8037 creates a call gas function for Amsterdam (EIP-8037).
// It extends the EIP-7702 pattern with state gas handling and GasUsed tracking.
// intrinsicFunc computes the regular gas (memory + transfer, no new account creation).
// stateGasFunc computes the state gas (new account creation as state gas).
func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
eip7702Cost uint64
addr = common.Address(stack.back(1).Bytes20())
)
// EIP-2929 cold access check.
if !evm.StateDB.AddressInAccessList(addr) {
evm.StateDB.AddAddressToAccessList(addr)
eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Compute regular cost (memory + transfer, no new account creation).
regularCost, err := regularFunc(evm, contract, stack, mem, memorySize)
if err != nil {
return GasCosts{}, err
}
// Charge intrinsic cost directly (regular gas). This must happen
// BEFORE state gas to prevent reservoir inflation, and also serves
// as the OOG guard before stateful operations.
if !contract.chargeRegular(regularCost, evm.Config.Tracer, tracing.GasChangeCallOpCode) {
return GasCosts{}, ErrOutOfGas
}
// EIP-7702 delegation check.
if target, ok := types.ParseDelegation(evm.StateDB.GetCode(addr)); ok {
if evm.StateDB.AddressInAccessList(target) {
eip7702Cost = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
eip7702Cost = params.ColdAccountAccessCostEIP2929
}
if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
}
// Compute and charge state gas (new account creation) AFTER regular gas.
stateGas, err := stateGasFunc(evm, contract, stack)
if err != nil {
return GasCosts{}, err
}
if stateGas > 0 {
if _, ok := contract.Gas.ChargeState(stateGas); !ok {
return GasCosts{}, ErrOutOfGas
}
}
// Calculate the gas budget for the nested call (63/64 rule).
evm.callGasTemp, err = callGas(evm.chainRules.IsEIP150, contract.Gas.RegularGas, 0, stack.back(0))
if err != nil {
return GasCosts{}, err
}
// Temporarily undo direct regular charges for tracer reporting.
// The interpreter will charge the returned totalCost.
contract.Gas.RegularGas += eip2929Cost + eip7702Cost + regularCost
contract.Gas.UsedRegularGas -= eip2929Cost + eip7702Cost + regularCost
// Aggregate total cost.
var (
overflow bool
totalCost uint64
)
if totalCost, overflow = math.SafeAdd(eip2929Cost, eip7702Cost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, regularCost); overflow {
return GasCosts{}, ErrGasUintOverflow
}
if totalCost, overflow = math.SafeAdd(totalCost, evm.callGasTemp); overflow {
return GasCosts{}, ErrGasUintOverflow
}
return GasCosts{RegularGas: totalCost}, nil
}
}

View file

@ -179,7 +179,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) {
// - reset transient storage(eip 1153)
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, nil, vm.ActivePrecompiles(rules), nil)
// Call the code with the given configuration.
code, address, result, err := vmenv.Create(
code, address, result, _, err := vmenv.Create(
cfg.Origin,
input,
vm.NewGasBudget(cfg.GasLimit, 0),

View file

@ -62,9 +62,12 @@ func NewDownloaderAPI(d *Downloader, chain *core.BlockChain) *DownloaderAPI {
// If the node is already synced up, then only a single event subscribers will
// receive is {false}.
func (api *DownloaderAPI) eventLoop() {
events := make(chan SyncEvent, 16)
sub := api.d.SubscribeSyncEvents(events)
if sub == nil {
return
}
var (
events = make(chan SyncEvent, 16)
sub = api.d.SubscribeSyncEvents(events)
syncSubscriptions = make(map[chan interface{}]struct{})
checkInterval = time.Second * 60
checkTimer = time.NewTimer(checkInterval)

View file

@ -188,12 +188,17 @@ func (d *Downloader) findBeaconAncestor() (uint64, error) {
}
log.Debug("Searching beacon ancestor", "local", number, "beaconhead", beaconHead.Number, "beacontail", beaconTail.Number)
var linked bool
// Require the canonical mapping, not just presence by hash, so orphans and
// side chains are re-delivered instead of left in place.
var (
linked bool
num = beaconTail.Number.Uint64() - 1
)
switch d.getMode() {
case ethconfig.FullSync:
linked = d.blockchain.HasBlock(beaconTail.ParentHash, beaconTail.Number.Uint64()-1)
linked = d.blockchain.GetCanonicalHash(num) == beaconTail.ParentHash && d.blockchain.HasBlock(beaconTail.ParentHash, num)
case ethconfig.SnapSync:
linked = d.blockchain.HasFastBlock(beaconTail.ParentHash, beaconTail.Number.Uint64()-1)
linked = d.blockchain.GetCanonicalHash(num) == beaconTail.ParentHash && d.blockchain.HasFastBlock(beaconTail.ParentHash, num)
default:
panic("unknown sync mode")
}
@ -226,12 +231,14 @@ func (d *Downloader) findBeaconAncestor() (uint64, error) {
}
n := h.Number.Uint64()
// Require the canonical mapping, not just presence by hash, so orphans
// and side chains are re-synced instead of treated as already owned.
var known bool
switch d.getMode() {
case ethconfig.FullSync:
known = d.blockchain.HasBlock(h.Hash(), n)
known = d.blockchain.GetCanonicalHash(n) == h.Hash() && d.blockchain.HasBlock(h.Hash(), n)
case ethconfig.SnapSync:
known = d.blockchain.HasFastBlock(h.Hash(), n)
known = d.blockchain.GetCanonicalHash(n) == h.Hash() && d.blockchain.HasFastBlock(h.Hash(), n)
default:
panic("unknown sync mode")
}

View file

@ -187,6 +187,10 @@ type BlockChain interface {
// HasFastBlock verifies a snap block's presence in the local chain.
HasFastBlock(common.Hash, uint64) bool
// GetCanonicalHash returns the canonical hash for the block at the given
// number, or the zero hash if no canonical block is present at that height.
GetCanonicalHash(uint64) common.Hash
// GetBlockByHash retrieves a block from the local chain.
GetBlockByHash(common.Hash) *types.Block

View file

@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/eth/protocols/snap"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
@ -40,6 +41,7 @@ import (
// downloadTester is a test simulator for mocking out local block chain.
type downloadTester struct {
db ethdb.Database
chain *core.BlockChain
downloader *Downloader
@ -77,6 +79,7 @@ func newTesterWithSnap(t *testing.T, mode ethconfig.SyncMode, success func(), sn
panic(err)
}
tester := &downloadTester{
db: db,
chain: chain,
peers: make(map[string]*downloadTesterPeer),
}
@ -683,6 +686,78 @@ func testBeaconSync(t *testing.T, protocol uint, mode SyncMode) {
}
}
// TestBeaconSyncRepairFork verifies the end-to-end repair of non-canonical block
// data. The local node sits on fork A, but fork B's blocks below the local head
// are also present by hash (no canonical mapping), as if imported optimistically
// via the engine API. When the beacon chain switches to fork B, sync must not
// anchor on the non-canonical fork-B data; it has to descend to the real common
// ancestor and re-deliver everything, ending with the full fork-B chain present
// and canonical at every height - for both snap and full sync.
func TestBeaconSyncRepairForkFull(t *testing.T) { testBeaconSyncRepairFork(t, eth.ETH69, FullSync) }
func TestBeaconSyncRepairForkSnap(t *testing.T) { testBeaconSyncRepairFork(t, eth.ETH69, SnapSync) }
func testBeaconSyncRepairFork(t *testing.T, protocol uint, mode SyncMode) {
// Reuse the pre-generated fork chains (new chains can't be generated after the
// package init). Fork A and fork B share the whole testChainBase prefix and
// diverge at height len(testChainBase.blocks); fork B (the beacon target) is
// longer, so it wins the reorg. The exact shortenings used here are the ones
// registered as peer chains during init.
localChain := testChainForkLightA.shorten(len(testChainBase.blocks) + 80)
targetChain := testChainForkLightB.shorten(len(testChainBase.blocks) + MaxHeaderFetch)
forkPoint := uint64(len(testChainBase.blocks)) // first height the forks differ
localHead := uint64(len(localChain.blocks) - 1)
targetHead := uint64(len(targetChain.blocks) - 1)
success := make(chan struct{})
tester := newTesterWithNotification(t, mode, func() {
close(success)
})
defer tester.terminate()
tester.newPeer("peer", protocol, targetChain.blocks[1:])
// Make fork A the local canonical chain.
if _, err := tester.chain.InsertChain(localChain.blocks[1 : localHead+1]); err != nil {
t.Fatalf("failed to build local chain: %v", err)
}
// Seed fork B's divergent blocks that sit below the local head as scattered,
// non-canonical data: full block data present by hash, but the canonical
// mapping at those heights still points at fork A.
for n := forkPoint; n <= localHead; n++ {
b := targetChain.blocks[n]
rawdb.WriteBlock(tester.db, b)
rawdb.WriteReceipts(tester.db, b.Hash(), b.NumberU64(), types.Receipts{})
}
if err := tester.downloader.BeaconSync(targetChain.blocks[targetHead].Header(), nil); err != nil {
t.Fatalf("failed to beacon-sync chain: %v", err)
}
select {
case <-success:
case <-time.NewTimer(10 * time.Second).C:
t.Fatalf("failed to sync chain in ten seconds")
}
// The head must reach fork B's tip.
if got := tester.chain.CurrentBlock().Number.Uint64(); got != targetHead {
t.Fatalf("synced head mismatch: have %d, want %d", got, targetHead)
}
// Every height must be canonical to fork B and carry complete block data,
// proving the non-canonical fork-A / seed data was fully reorged out.
for n := uint64(1); n <= targetHead; n++ {
want := targetChain.blocks[n].Hash()
if got := rawdb.ReadCanonicalHash(tester.db, n); got != want {
t.Fatalf("canonical hash at %d: have %x, want %x", n, got, want)
}
if !rawdb.HasHeader(tester.db, want, n) || !rawdb.HasBody(tester.db, want, n) {
t.Fatalf("incomplete block data at %d after sync", n)
}
if !rawdb.HasReceipts(tester.db, want, n) {
t.Fatalf("missing receipts at %d after sync", n)
}
}
}
// Tests that synchronisation progress (origin block number, current block number
// and highest block number) is tracked and updated correctly.
func TestSyncProgressFull(t *testing.T) { testSyncProgress(t, eth.ETH69, FullSync) }

View file

@ -365,7 +365,13 @@ func (s *skeleton) Sync(head *types.Header, final *types.Header, force bool) err
// linked returns the flag indicating whether the skeleton has been linked with
// the local chain.
func (s *skeleton) linked(number uint64, hash common.Hash) bool {
linked := rawdb.HasHeader(s.db, hash, number) &&
// Require the canonical mapping, not just presence by hash. A block present
// only by hash (side chain or orphan from an unclean shutdown) must not be
// used as the link-up point, otherwise it's left in place forever without its
// canonical mapping ever being rewritten. Keep descending to a real canonical
// block.
linked := rawdb.ReadCanonicalHash(s.db, number) == hash &&
rawdb.HasHeader(s.db, hash, number) &&
rawdb.HasBody(s.db, hash, number) &&
rawdb.HasReceipts(s.db, hash, number)

View file

@ -841,6 +841,7 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
rawdb.WriteBlock(db, types.NewBlockWithHeader(chain[0]))
rawdb.WriteReceipts(db, chain[0].Hash(), chain[0].Number.Uint64(), types.Receipts{})
rawdb.WriteCanonicalHash(db, chain[0].Hash(), chain[0].Number.Uint64())
// Create a peer set to feed headers through
peerset := newPeerSet()
@ -871,6 +872,7 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
rawdb.WriteBlock(db, types.NewBlockWithHeader(header))
rawdb.WriteReceipts(db, header.Hash(), header.Number.Uint64(), types.Receipts{})
rawdb.WriteCanonicalHash(db, header.Hash(), header.Number.Uint64())
rawdb.DeleteSkeletonHeader(db, header.Number.Uint64())
@ -881,6 +883,7 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
rawdb.WriteBlock(db, types.NewBlockWithHeader(filled))
rawdb.WriteReceipts(db, filled.Hash(), filled.Number.Uint64(), types.Receipts{})
rawdb.WriteCanonicalHash(db, filled.Hash(), filled.Number.Uint64())
},
suspendHook: func() *types.Header {
@ -941,6 +944,70 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
}
}
// TestSkeletonLinkSkipsNonCanonical verifies that the skeleton only links to a
// local block that is canonical, and descends past block data that is present by
// hash but lacks a canonical number->hash mapping (e.g. blocks imported
// optimistically via the engine API, or orphans left by an unclean shutdown).
// Anchoring on such a block would leave it in place forever without its canonical
// mapping being rewritten, wedging the freezer later on.
func TestSkeletonLinkSkipsNonCanonical(t *testing.T) {
// Build a fake header chain; the skeleton only needs a parent-hash progression.
chain := []*types.Header{{Number: big.NewInt(0)}}
for i := 1; i < 200; i++ {
chain = append(chain, &types.Header{
ParentHash: chain[i-1].Hash(),
Number: big.NewInt(int64(i)),
})
}
const (
canon = 100 // blocks [0..canon] are present AND canonical locally
top = 150 // blocks [canon+1..top] are present by hash but NOT canonical
)
db := rawdb.NewMemoryDatabase()
// Canonical prefix: full block data plus the canonical number->hash mapping.
for i := 0; i <= canon; i++ {
rawdb.WriteBlock(db, types.NewBlockWithHeader(chain[i]))
rawdb.WriteReceipts(db, chain[i].Hash(), chain[i].Number.Uint64(), types.Receipts{})
rawdb.WriteCanonicalHash(db, chain[i].Hash(), chain[i].Number.Uint64())
}
// Scattered non-canonical data: full block data is present, but the canonical
// mapping is deliberately absent.
for i := canon + 1; i <= top; i++ {
rawdb.WriteBlock(db, types.NewBlockWithHeader(chain[i]))
rawdb.WriteReceipts(db, chain[i].Hash(), chain[i].Number.Uint64(), types.Receipts{})
}
// Feed the full chain through a single peer. The fakeChainReader reports an
// effectively infinite snap head, so the canonical mapping is the only thing
// that can prevent the skeleton from linking on a non-canonical block.
peerset := newPeerSet()
drop := func(peer string) { peerset.Unregister(peer) }
peer := newSkeletonTestPeer("peer", chain)
if err := peerset.Register(newPeerConnection(peer.id, eth.ETH69, peer, log.New("id", peer.id))); err != nil {
t.Fatalf("failed to register peer: %v", err)
}
skeleton := newSkeleton(db, peerset, drop, newHookedBackfiller(), &fakeChainReader{})
skeleton.Sync(chain[len(chain)-1], nil, true)
defer skeleton.Terminate()
// The skeleton must link at the canonical block, i.e. the resulting subchain
// tail must descend to canon+1 (covering the whole non-canonical region). With
// a hash-only "known" check it would instead stop at top+1.
wantTail := uint64(canon + 1)
var progress skeletonProgress
for deadline := time.Now().Add(2 * time.Second); ; {
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
if len(progress.Subchains) == 1 && progress.Subchains[0].Tail == wantTail {
break
}
if time.Now().After(deadline) {
t.Fatalf("skeleton did not link at canonical block: subchains=%+v, want single subchain with tail %d", progress.Subchains, wantTail)
}
time.Sleep(20 * time.Millisecond)
}
}
func checkSkeletonProgress(db ethdb.KeyValueReader, unpredictable bool, peers []*skeletonTestPeer, expected skeletonExpect) error {
var progress skeletonProgress
// Check the post-init end state if it matches the required results

View file

@ -18,6 +18,7 @@
package pebble
import (
"context"
"errors"
"fmt"
"runtime"
@ -26,8 +27,8 @@ import (
"sync/atomic"
"time"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/bloom"
"github.com/cockroachdb/pebble/v2"
"github.com/cockroachdb/pebble/v2/bloom"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
@ -52,7 +53,7 @@ const (
degradationWarnInterval = time.Minute
)
// Database is a persistent key-value store based on the pebble storage engine.
// Database is a persistent key-value store based on the pebble v2 storage engine.
// Apart from basic data storage functionality it also supports batch writes and
// iterating over the keyspace in binary-alphabetical order.
type Database struct {
@ -77,8 +78,8 @@ type Database struct {
zombieMemTablesGauge *metrics.Gauge // Gauge for tracking the number of zombie memory tables
blockCacheHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in the block cache
blockCacheMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in the block cache
tableCacheHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in the table cache
tableCacheMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in the table cache
tableCacheHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in the file cache
tableCacheMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in the file cache
filterHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in bloom filter
filterMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in bloom filter
estimatedCompDebtGauge *metrics.Gauge // Gauge for tracking the number of bytes that need to be compacted
@ -186,7 +187,7 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
handles = minHandles
}
logger := log.New("database", file)
logger.Info("Allocated cache and file handles", "cache", common.StorageSize(cache*1024*1024), "handles", handles)
logger.Info("Allocated cache and file handles", "cache", common.StorageSize(cache*1024*1024), "handles", handles, "version", "v2")
// The max memtable size is limited by the uint32 offsets stored in
// internal/arenaskl.node, DeferredBatchOp, and flushableBatchEntry.
@ -232,6 +233,7 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
// of course). Geth is expected to handle recovery from an unclean shutdown.
writeOptions: pebble.NoSync,
}
numCPU := runtime.NumCPU()
opt := &pebble.Options{
// Pebble has a single combined cache area and the write
// buffers are taken from this too. Assign all available
@ -256,20 +258,30 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
// The default compaction concurrency(1 thread),
// Here use all available CPUs for faster compaction.
MaxConcurrentCompactions: runtime.NumCPU,
CompactionConcurrencyRange: func() (int, int) { return 1, numCPU },
// Per-level options. Options for at least one level must be specified. The
// options for the last level are used for all subsequent levels.
Levels: []pebble.LevelOptions{
{TargetFileSize: 2 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 4 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 8 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 16 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 32 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 64 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
Levels: [7]pebble.LevelOptions{
{FilterPolicy: bloom.FilterPolicy(10)},
{FilterPolicy: bloom.FilterPolicy(10)},
{FilterPolicy: bloom.FilterPolicy(10)},
{FilterPolicy: bloom.FilterPolicy(10)},
{FilterPolicy: bloom.FilterPolicy(10)},
{FilterPolicy: bloom.FilterPolicy(10)},
// Pebble doesn't use the Bloom filter at level6 for read efficiency.
{TargetFileSize: 128 * 1024 * 1024},
{},
},
// Per-level target file sizes (replaces LevelOptions.TargetFileSize in v2).
TargetFileSizes: [7]int64{
2 * 1024 * 1024,
4 * 1024 * 1024,
8 * 1024 * 1024,
16 * 1024 * 1024,
32 * 1024 * 1024,
64 * 1024 * 1024,
128 * 1024 * 1024,
},
ReadOnly: readonly,
EventListener: &pebble.EventListener{
@ -299,6 +311,14 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
// the compaction debt as around 10GB. By reducing it to 2, the compaction
// debt will be less than 1GB, but with more frequent compactions scheduled.
L0CompactionThreshold: 2,
// FormatFlushableIngest is the minimum FormatMajorVersion supported by
// pebble v2. The more advanced version can be enabled later.
//
// This version is supported by both v1 and v2. It serves as the natural
// bridge point: a v1 database can be ratcheted up to FormatFlushableIngest
// using pebble v1, and then pebble v2 can open it since that's its minimum.
FormatMajorVersion: formatMinV2,
}
// Disable seek compaction explicitly. Check https://github.com/ethereum/go-ethereum/pull/20130
// for more details.
@ -309,7 +329,7 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
// - there is one more overlapping sub-level0;
// - there is an additional 256 MB of compaction debt;
//
// The maximum concurrency is still capped by MaxConcurrentCompactions, but with
// The maximum concurrency is still capped by CompactionConcurrencyRange, but with
// these settings compactions can scale up more readily.
opt.Experimental.L0CompactionConcurrency = 1
opt.Experimental.CompactionDebtConcurrency = 1 << 28 // 256MB
@ -506,7 +526,7 @@ func (d *Database) Compact(start []byte, limit []byte) error {
if limit == nil {
limit = ethdb.MaximumKey
}
return d.db.Compact(start, limit, true) // Parallelization is preferred
return d.db.Compact(context.Background(), start, limit, true) // Parallelization is preferred
}
// Path returns the path to the database directory.
@ -565,10 +585,10 @@ func (d *Database) meter(refresh time.Duration, namespace string) {
compTimes[i%2] = compTime
for _, levelMetrics := range stats.Levels {
nWrite += int64(levelMetrics.BytesCompacted)
nWrite += int64(levelMetrics.BytesFlushed)
compWrite += int64(levelMetrics.BytesCompacted)
compRead += int64(levelMetrics.BytesRead)
nWrite += int64(levelMetrics.TableBytesCompacted)
nWrite += int64(levelMetrics.TableBytesFlushed)
compWrite += int64(levelMetrics.TableBytesCompacted)
compRead += int64(levelMetrics.TableBytesRead)
}
nWrite += int64(stats.WAL.BytesWritten)
@ -607,8 +627,8 @@ func (d *Database) meter(refresh time.Duration, namespace string) {
d.liveMemTablesGauge.Update(stats.MemTable.Count)
d.zombieMemTablesGauge.Update(stats.MemTable.ZombieCount)
d.estimatedCompDebtGauge.Update(int64(stats.Compact.EstimatedDebt))
d.tableCacheHitGauge.Update(stats.TableCache.Hits)
d.tableCacheMissGauge.Update(stats.TableCache.Misses)
d.tableCacheHitGauge.Update(stats.FileCache.Hits)
d.tableCacheMissGauge.Update(stats.FileCache.Misses)
d.blockCacheHitGauge.Update(stats.BlockCache.Hits)
d.blockCacheMissGauge.Update(stats.BlockCache.Misses)
d.filterHitGauge.Update(stats.Filter.Hits)
@ -619,7 +639,7 @@ func (d *Database) meter(refresh time.Duration, namespace string) {
if i >= len(d.levelsGauge) {
d.levelsGauge = append(d.levelsGauge, metrics.GetOrRegisterGauge(namespace+fmt.Sprintf("tables/level%v", i), nil))
}
d.levelsGauge[i].Update(level.NumFiles)
d.levelsGauge[i].Update(level.TablesCount)
}
// Sleep a bit, then repeat the stats collection

View file

@ -21,7 +21,11 @@ import (
"testing"
"github.com/cockroachdb/pebble"
pebblev1 "github.com/cockroachdb/pebble"
pebblev2 "github.com/cockroachdb/pebble/v2"
vfsv2 "github.com/cockroachdb/pebble/v2/vfs"
"github.com/cockroachdb/pebble/vfs"
vfsv1 "github.com/cockroachdb/pebble/vfs"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/dbtest"
)
@ -29,8 +33,8 @@ import (
func TestPebbleDB(t *testing.T) {
t.Run("DatabaseSuite", func(t *testing.T) {
dbtest.TestDatabaseSuite(t, func() ethdb.KeyValueStore {
db, err := pebble.Open("", &pebble.Options{
FS: vfs.NewMem(),
db, err := pebblev2.Open("", &pebblev2.Options{
FS: vfsv2.NewMem(),
})
if err != nil {
t.Fatal(err)
@ -39,13 +43,24 @@ func TestPebbleDB(t *testing.T) {
db: db,
}
})
dbtest.TestDatabaseSuite(t, func() ethdb.KeyValueStore {
db, err := pebblev1.Open("", &pebblev1.Options{
FS: vfsv1.NewMem(),
})
if err != nil {
t.Fatal(err)
}
return &V1Database{
db: db,
}
})
})
}
func BenchmarkPebbleDB(b *testing.B) {
dbtest.BenchDatabaseSuite(b, func() ethdb.KeyValueStore {
db, err := pebble.Open("", &pebble.Options{
FS: vfs.NewMem(),
db, err := pebblev2.Open("", &pebblev2.Options{
FS: vfsv2.NewMem(),
})
if err != nil {
b.Fatal(err)
@ -54,9 +69,20 @@ func BenchmarkPebbleDB(b *testing.B) {
db: db,
}
})
dbtest.BenchDatabaseSuite(b, func() ethdb.KeyValueStore {
db, err := pebblev1.Open("", &pebblev1.Options{
FS: vfsv1.NewMem(),
})
if err != nil {
b.Fatal(err)
}
return &V1Database{
db: db,
}
})
}
func TestPebbleLogData(t *testing.T) {
func TestPebbleLogDataV1(t *testing.T) {
db, err := pebble.Open("", &pebble.Options{
FS: vfs.NewMem(),
})
@ -78,3 +104,26 @@ func TestPebbleLogData(t *testing.T) {
t.Fatal("Unknown database entry")
}
}
func TestPebbleLogDataV2(t *testing.T) {
db, err := pebblev2.Open("", &pebblev2.Options{
FS: vfsv2.NewMem(),
})
if err != nil {
t.Fatal(err)
}
_, _, err = db.Get(nil)
if !errors.Is(err, pebblev2.ErrNotFound) {
t.Fatal("Unknown database entry")
}
b := db.NewBatch()
b.LogData(nil, nil)
db.Apply(b, pebblev2.Sync)
_, _, err = db.Get(nil)
if !errors.Is(err, pebblev2.ErrNotFound) {
t.Fatal("Unknown database entry")
}
}

753
ethdb/pebble/pebble_v1.go Normal file
View file

@ -0,0 +1,753 @@
// Copyright 2023 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 <http://www.gnu.org/licenses/>.
// Legacy pebble v1 wrapper. This file mirrors pebble.go but with V1-prefixed
// types so that it can coexist alongside a future v2 variant in the same package.
package pebble
import (
"errors"
"fmt"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/bloom"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
// V1Database is a persistent key-value store based on the pebble v1 storage engine.
// Apart from basic data storage functionality it also supports batch writes and
// iterating over the keyspace in binary-alphabetical order.
type V1Database struct {
fn string // filename for reporting
db *pebble.DB // Underlying pebble storage engine
namespace string // Namespace for metrics
compTimeMeter *metrics.Meter // Meter for measuring the total time spent in database compaction
compReadMeter *metrics.Meter // Meter for measuring the data read during compaction
compWriteMeter *metrics.Meter // Meter for measuring the data written during compaction
writeDelayNMeter *metrics.Meter // Meter for measuring the write delay number due to database compaction
writeDelayMeter *metrics.Meter // Meter for measuring the write delay duration due to database compaction
diskSizeGauge *metrics.Gauge // Gauge for tracking the size of all the levels in the database
diskReadMeter *metrics.Meter // Meter for measuring the effective amount of data read
diskWriteMeter *metrics.Meter // Meter for measuring the effective amount of data written
memCompGauge *metrics.Gauge // Gauge for tracking the number of memory compaction
level0CompGauge *metrics.Gauge // Gauge for tracking the number of table compaction in level0
nonlevel0CompGauge *metrics.Gauge // Gauge for tracking the number of table compaction in non0 level
seekCompGauge *metrics.Gauge // Gauge for tracking the number of table compaction caused by read opt
manualMemAllocGauge *metrics.Gauge // Gauge for tracking amount of non-managed memory currently allocated
liveMemTablesGauge *metrics.Gauge // Gauge for tracking the number of live memory tables
zombieMemTablesGauge *metrics.Gauge // Gauge for tracking the number of zombie memory tables
blockCacheHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in the block cache
blockCacheMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in the block cache
tableCacheHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in the table cache
tableCacheMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in the table cache
filterHitGauge *metrics.Gauge // Gauge for tracking the number of total hit in bloom filter
filterMissGauge *metrics.Gauge // Gauge for tracking the number of total miss in bloom filter
estimatedCompDebtGauge *metrics.Gauge // Gauge for tracking the number of bytes that need to be compacted
liveCompGauge *metrics.Gauge // Gauge for tracking the number of in-progress compactions
liveCompSizeGauge *metrics.Gauge // Gauge for tracking the size of in-progress compactions
liveIterGauge *metrics.Gauge // Gauge for tracking the number of live database iterators
levelsGauge []*metrics.Gauge // Gauge for tracking the number of tables in levels
quitLock sync.RWMutex // Mutex protecting the quit channel and the closed flag
quitChan chan chan error // Quit channel to stop the metrics collection before closing the database
closed bool // keep track of whether we're Closed
log log.Logger // Contextual logger tracking the database path
activeComp int // Current number of active compactions
compStartTime time.Time // The start time of the earliest currently-active compaction
compTime atomic.Int64 // Total time spent in compaction in ns
level0Comp atomic.Uint32 // Total number of level-zero compactions
nonLevel0Comp atomic.Uint32 // Total number of non level-zero compactions
writeStalled atomic.Bool // Flag whether the write is stalled
writeDelayStartTime time.Time // The start time of the latest write stall
writeDelayReason string // The reason of the latest write stall
writeDelayCount atomic.Int64 // Total number of write stall counts
writeDelayTime atomic.Int64 // Total time spent in write stalls
writeOptions *pebble.WriteOptions
}
func (d *V1Database) onCompactionBegin(info pebble.CompactionInfo) {
if d.activeComp == 0 {
d.compStartTime = time.Now()
}
l0 := info.Input[0]
if l0.Level == 0 {
d.level0Comp.Add(1)
} else {
d.nonLevel0Comp.Add(1)
}
d.activeComp++
}
func (d *V1Database) onCompactionEnd(info pebble.CompactionInfo) {
if d.activeComp == 1 {
d.compTime.Add(int64(time.Since(d.compStartTime)))
} else if d.activeComp == 0 {
panic("should not happen")
}
d.activeComp--
}
func (d *V1Database) onWriteStallBegin(b pebble.WriteStallBeginInfo) {
d.writeDelayStartTime = time.Now()
d.writeDelayCount.Add(1)
d.writeStalled.Store(true)
// Take just the first word of the reason. These are two potential
// reasons for the write stall:
// - memtable count limit reached
// - L0 file count limit exceeded
reason := b.Reason
if i := strings.IndexByte(reason, ' '); i != -1 {
reason = reason[:i]
}
if reason == "L0" || reason == "memtable" {
d.writeDelayReason = reason
metrics.GetOrRegisterGauge(d.namespace+"stall/count/"+reason, nil).Inc(1)
}
}
func (d *V1Database) onWriteStallEnd() {
d.writeDelayTime.Add(int64(time.Since(d.writeDelayStartTime)))
d.writeStalled.Store(false)
if d.writeDelayReason != "" {
metrics.GetOrRegisterResettingTimer(d.namespace+"stall/time/"+d.writeDelayReason, nil).UpdateSince(d.writeDelayStartTime)
d.writeDelayReason = ""
}
d.writeDelayStartTime = time.Time{}
}
// NewV1 returns a wrapped pebble v1 DB object. The namespace is the prefix that the
// metrics reporting should use for surfacing internal stats.
func NewV1(file string, cache int, handles int, namespace string, readonly bool) (*V1Database, error) {
// Ensure we have some minimal caching and file guarantees
if cache < minCache {
cache = minCache
}
if handles < minHandles {
handles = minHandles
}
logger := log.New("database", file)
logger.Info("Allocated cache and file handles", "cache", common.StorageSize(cache*1024*1024), "handles", handles, "version", "v1")
// The max memtable size is limited by the uint32 offsets stored in
// internal/arenaskl.node, DeferredBatchOp, and flushableBatchEntry.
//
// - MaxUint32 on 64-bit platforms;
// - MaxInt on 32-bit platforms.
//
// It is used when slices are limited to Uint32 on 64-bit platforms (the
// length limit for slices is naturally MaxInt on 32-bit platforms).
//
// Taken from https://github.com/cockroachdb/pebble/blob/master/internal/constants/constants.go
maxMemTableSize := (1<<31)<<(^uint(0)>>63) - 1
// Four memory tables are configured, each with a default size of 256 MB.
// Having multiple smaller memory tables while keeping the total memory
// limit unchanged allows writes to be flushed more smoothly. This helps
// avoid compaction spikes and mitigates write stalls caused by heavy
// compaction workloads.
memTableNumber := 4
memTableSize := cache * 1024 * 1024 / 2 / memTableNumber
// The memory table size is currently capped at maxMemTableSize-1 due to a
// known bug in the pebble where maxMemTableSize is not recognized as a
// valid size.
//
// TODO use the maxMemTableSize as the maximum table size once the issue
// in pebble is fixed.
if memTableSize >= maxMemTableSize {
memTableSize = maxMemTableSize - 1
}
db := &V1Database{
fn: file,
log: logger,
quitChan: make(chan chan error),
namespace: namespace,
// Use asynchronous write mode by default. Otherwise, the overhead of frequent fsync
// operations can be significant, especially on platforms with slow fsync performance
// (e.g., macOS) or less capable SSDs.
//
// Note that enabling async writes means recent data may be lost in the event of an
// application-level panic (writes will also be lost on a machine-level failure,
// of course). Geth is expected to handle recovery from an unclean shutdown.
writeOptions: pebble.NoSync,
}
opt := &pebble.Options{
// Pebble has a single combined cache area and the write
// buffers are taken from this too. Assign all available
// memory allowance for cache.
Cache: pebble.NewCache(int64(cache * 1024 * 1024)),
MaxOpenFiles: handles,
// The size of memory table(as well as the write buffer).
// Note, there may have more than two memory tables in the system.
MemTableSize: uint64(memTableSize),
// MemTableStopWritesThreshold places a hard limit on the number
// of the existent MemTables(including the frozen one).
//
// Note, this must be the number of tables not the size of all memtables
// according to https://github.com/cockroachdb/pebble/blob/master/options.go#L738-L742
// and to https://github.com/cockroachdb/pebble/blob/master/db.go#L1892-L1903.
//
// MemTableStopWritesThreshold is set to twice the maximum number of
// allowed memtables to accommodate temporary spikes.
MemTableStopWritesThreshold: memTableNumber * 2,
// The default compaction concurrency(1 thread),
// Here use all available CPUs for faster compaction.
MaxConcurrentCompactions: runtime.NumCPU,
// Per-level options. Options for at least one level must be specified. The
// options for the last level are used for all subsequent levels.
Levels: []pebble.LevelOptions{
{TargetFileSize: 2 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 4 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 8 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 16 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 32 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
{TargetFileSize: 64 * 1024 * 1024, FilterPolicy: bloom.FilterPolicy(10)},
// Pebble doesn't use the Bloom filter at level6 for read efficiency.
{TargetFileSize: 128 * 1024 * 1024},
},
ReadOnly: readonly,
EventListener: &pebble.EventListener{
CompactionBegin: db.onCompactionBegin,
CompactionEnd: db.onCompactionEnd,
WriteStallBegin: db.onWriteStallBegin,
WriteStallEnd: db.onWriteStallEnd,
},
Logger: panicLogger{}, // TODO(karalabe): Delete when this is upstreamed in Pebble
// Pebble is configured to use asynchronous write mode, meaning write operations
// return as soon as the data is cached in memory, without waiting for the WAL
// to be written. This mode offers better write performance but risks losing
// recent writes if the application crashes or a power failure/system crash occurs.
//
// By setting the WALBytesPerSync, the cached WAL writes will be periodically
// flushed at the background if the accumulated size exceeds this threshold.
WALBytesPerSync: 5 * ethdb.IdealBatchSize,
// L0CompactionThreshold specifies the number of L0 read-amplification
// necessary to trigger an L0 compaction. It essentially refers to the
// number of sub-levels at the L0. For each sub-level, it contains several
// L0 files which are non-overlapping with each other, typically produced
// by a single memory-table flush.
//
// The default value in Pebble is 4, which is a bit too large to have
// the compaction debt as around 10GB. By reducing it to 2, the compaction
// debt will be less than 1GB, but with more frequent compactions scheduled.
L0CompactionThreshold: 2,
}
// Disable seek compaction explicitly. Check https://github.com/ethereum/go-ethereum/pull/20130
// for more details.
opt.Experimental.ReadSamplingMultiplier = -1
// These two settings define the conditions under which compaction concurrency
// is increased. Specifically, one additional compaction job will be enabled when:
// - there is one more overlapping sub-level0;
// - there is an additional 256 MB of compaction debt;
//
// The maximum concurrency is still capped by MaxConcurrentCompactions, but with
// these settings compactions can scale up more readily.
opt.Experimental.L0CompactionConcurrency = 1
opt.Experimental.CompactionDebtConcurrency = 1 << 28 // 256MB
// Open the db and recover any potential corruptions
innerDB, err := pebble.Open(file, opt)
if err != nil {
return nil, err
}
db.db = innerDB
db.compTimeMeter = metrics.GetOrRegisterMeter(namespace+"compact/time", nil)
db.compReadMeter = metrics.GetOrRegisterMeter(namespace+"compact/input", nil)
db.compWriteMeter = metrics.GetOrRegisterMeter(namespace+"compact/output", nil)
db.diskSizeGauge = metrics.GetOrRegisterGauge(namespace+"disk/size", nil)
db.diskReadMeter = metrics.GetOrRegisterMeter(namespace+"disk/read", nil)
db.diskWriteMeter = metrics.GetOrRegisterMeter(namespace+"disk/write", nil)
db.writeDelayMeter = metrics.GetOrRegisterMeter(namespace+"compact/writedelay/duration", nil)
db.writeDelayNMeter = metrics.GetOrRegisterMeter(namespace+"compact/writedelay/counter", nil)
db.memCompGauge = metrics.GetOrRegisterGauge(namespace+"compact/memory", nil)
db.level0CompGauge = metrics.GetOrRegisterGauge(namespace+"compact/level0", nil)
db.nonlevel0CompGauge = metrics.GetOrRegisterGauge(namespace+"compact/nonlevel0", nil)
db.seekCompGauge = metrics.GetOrRegisterGauge(namespace+"compact/seek", nil)
db.manualMemAllocGauge = metrics.GetOrRegisterGauge(namespace+"memory/manualalloc", nil)
db.liveMemTablesGauge = metrics.GetOrRegisterGauge(namespace+"table/live", nil)
db.zombieMemTablesGauge = metrics.GetOrRegisterGauge(namespace+"table/zombie", nil)
db.blockCacheHitGauge = metrics.GetOrRegisterGauge(namespace+"cache/block/hit", nil)
db.blockCacheMissGauge = metrics.GetOrRegisterGauge(namespace+"cache/block/miss", nil)
db.tableCacheHitGauge = metrics.GetOrRegisterGauge(namespace+"cache/table/hit", nil)
db.tableCacheMissGauge = metrics.GetOrRegisterGauge(namespace+"cache/table/miss", nil)
db.filterHitGauge = metrics.GetOrRegisterGauge(namespace+"filter/hit", nil)
db.filterMissGauge = metrics.GetOrRegisterGauge(namespace+"filter/miss", nil)
db.estimatedCompDebtGauge = metrics.GetOrRegisterGauge(namespace+"compact/estimateDebt", nil)
db.liveCompGauge = metrics.GetOrRegisterGauge(namespace+"compact/live/count", nil)
db.liveCompSizeGauge = metrics.GetOrRegisterGauge(namespace+"compact/live/size", nil)
db.liveIterGauge = metrics.GetOrRegisterGauge(namespace+"iter/count", nil)
// Start up the metrics gathering and return
go db.meter(metricsGatheringInterval, namespace)
return db, nil
}
// Close stops the metrics collection, flushes any pending data to disk and closes
// all io accesses to the underlying key-value store.
func (d *V1Database) Close() error {
d.quitLock.Lock()
defer d.quitLock.Unlock()
// Allow double closing, simplifies things
if d.closed {
return nil
}
d.closed = true
if d.quitChan != nil {
errc := make(chan error)
d.quitChan <- errc
if err := <-errc; err != nil {
d.log.Error("Metrics collection failed", "err", err)
}
d.quitChan = nil
}
return d.db.Close()
}
// Has retrieves if a key is present in the key-value store.
func (d *V1Database) Has(key []byte) (bool, error) {
d.quitLock.RLock()
defer d.quitLock.RUnlock()
if d.closed {
return false, pebble.ErrClosed
}
_, closer, err := d.db.Get(key)
if err == pebble.ErrNotFound {
return false, nil
} else if err != nil {
return false, err
}
if err = closer.Close(); err != nil {
return false, err
}
return true, nil
}
// Get retrieves the given key if it's present in the key-value store.
func (d *V1Database) Get(key []byte) ([]byte, error) {
d.quitLock.RLock()
defer d.quitLock.RUnlock()
if d.closed {
return nil, pebble.ErrClosed
}
dat, closer, err := d.db.Get(key)
if err != nil {
return nil, err
}
ret := make([]byte, len(dat))
copy(ret, dat)
if err = closer.Close(); err != nil {
return nil, err
}
return ret, nil
}
// Put inserts the given value into the key-value store.
func (d *V1Database) Put(key []byte, value []byte) error {
d.quitLock.RLock()
defer d.quitLock.RUnlock()
if d.closed {
return pebble.ErrClosed
}
return d.db.Set(key, value, d.writeOptions)
}
// Delete removes the key from the key-value store.
func (d *V1Database) Delete(key []byte) error {
d.quitLock.RLock()
defer d.quitLock.RUnlock()
if d.closed {
return pebble.ErrClosed
}
return d.db.Delete(key, d.writeOptions)
}
// DeleteRange deletes all of the keys (and values) in the range [start,end)
// (inclusive on start, exclusive on end).
func (d *V1Database) DeleteRange(start, end []byte) error {
d.quitLock.RLock()
defer d.quitLock.RUnlock()
if d.closed {
return pebble.ErrClosed
}
// There is no special flag to represent the end of key range
// in pebble(nil in leveldb). Use an ugly hack to construct a
// large key to represent it.
if end == nil {
end = ethdb.MaximumKey
}
return d.db.DeleteRange(start, end, d.writeOptions)
}
// NewBatch creates a write-only key-value store that buffers changes to its host
// database until a final write is called.
func (d *V1Database) NewBatch() ethdb.Batch {
return &v1batch{
b: d.db.NewBatch(),
db: d,
}
}
// NewBatchWithSize creates a write-only database batch with pre-allocated buffer.
func (d *V1Database) NewBatchWithSize(size int) ethdb.Batch {
return &v1batch{
b: d.db.NewBatchWithSize(size),
db: d,
}
}
// Stat returns the internal metrics of Pebble in a text format. It's a developer
// method to read everything there is to read, independent of Pebble version.
func (d *V1Database) Stat() (string, error) {
return d.db.Metrics().String(), nil
}
// Compact flattens the underlying data store for the given key range. In essence,
// deleted and overwritten versions are discarded, and the data is rearranged to
// reduce the cost of operations needed to access them.
//
// A nil start is treated as a key before all keys in the data store; a nil limit
// is treated as a key after all keys in the data store. If both is nil then it
// will compact entire data store.
func (d *V1Database) Compact(start []byte, limit []byte) error {
// There is no special flag to represent the end of key range
// in pebble(nil in leveldb). Use an ugly hack to construct a
// large key to represent it.
// Note any prefixed database entry will be smaller than this
// flag, as for trie nodes we need the 32 byte 0xff because
// there might be a shared prefix starting with a number of
// 0xff-s, so 32 ensures than only a hash collision could touch it.
// https://github.com/cockroachdb/pebble/issues/2359#issuecomment-1443995833
if limit == nil {
limit = ethdb.MaximumKey
}
return d.db.Compact(start, limit, true) // Parallelization is preferred
}
// Path returns the path to the database directory.
func (d *V1Database) Path() string {
return d.fn
}
// SyncKeyValue flushes all pending writes in the write-ahead-log to disk,
// ensuring data durability up to that point.
func (d *V1Database) SyncKeyValue() error {
// The entry (value=nil) is not written to the database; it is only
// added to the WAL. Writing this special log entry in sync mode
// automatically flushes all previous writes, ensuring database
// durability up to this point.
b := d.db.NewBatch()
b.LogData(nil, nil)
return d.db.Apply(b, pebble.Sync)
}
// NewIterator creates a binary-alphabetical iterator over a subset
// of database content with a particular key prefix, starting at a particular
// initial key (or after, if it does not exist).
func (d *V1Database) NewIterator(prefix []byte, start []byte) ethdb.Iterator {
iter, _ := d.db.NewIter(&pebble.IterOptions{
LowerBound: append(prefix, start...),
UpperBound: upperBound(prefix),
})
iter.First()
return &v1pebbleIterator{iter: iter, moved: true, released: false}
}
// meter periodically retrieves internal pebble counters and reports them to
// the metrics subsystem.
func (d *V1Database) meter(refresh time.Duration, namespace string) {
var errc chan error
timer := time.NewTimer(refresh)
defer timer.Stop()
// Create storage and warning log tracer for write delay.
var (
compTimes [2]int64
compWrites [2]int64
compReads [2]int64
nWrites [2]int64
writeDelayTimes [2]int64
writeDelayCounts [2]int64
lastWriteStallReport time.Time
)
// Iterate ad infinitum and collect the stats
for i := 1; errc == nil; i++ {
var (
compWrite int64
compRead int64
nWrite int64
stats = d.db.Metrics()
compTime = d.compTime.Load()
writeDelayCount = d.writeDelayCount.Load()
writeDelayTime = d.writeDelayTime.Load()
nonLevel0CompCount = int64(d.nonLevel0Comp.Load())
level0CompCount = int64(d.level0Comp.Load())
)
writeDelayTimes[i%2] = writeDelayTime
writeDelayCounts[i%2] = writeDelayCount
compTimes[i%2] = compTime
for _, levelMetrics := range stats.Levels {
nWrite += int64(levelMetrics.BytesCompacted)
nWrite += int64(levelMetrics.BytesFlushed)
compWrite += int64(levelMetrics.BytesCompacted)
compRead += int64(levelMetrics.BytesRead)
}
nWrite += int64(stats.WAL.BytesWritten)
compWrites[i%2] = compWrite
compReads[i%2] = compRead
nWrites[i%2] = nWrite
d.writeDelayNMeter.Mark(writeDelayCounts[i%2] - writeDelayCounts[(i-1)%2])
d.writeDelayMeter.Mark(writeDelayTimes[i%2] - writeDelayTimes[(i-1)%2])
// Print a warning log if writing has been stalled for a while. The log will
// be printed per minute to avoid overwhelming users.
if d.writeStalled.Load() && writeDelayCounts[i%2] == writeDelayCounts[(i-1)%2] &&
time.Now().After(lastWriteStallReport.Add(degradationWarnInterval)) {
d.log.Warn("Database compacting, degraded performance")
lastWriteStallReport = time.Now()
}
d.compTimeMeter.Mark(compTimes[i%2] - compTimes[(i-1)%2])
d.compReadMeter.Mark(compReads[i%2] - compReads[(i-1)%2])
d.compWriteMeter.Mark(compWrites[i%2] - compWrites[(i-1)%2])
d.diskSizeGauge.Update(int64(stats.DiskSpaceUsage()))
d.diskReadMeter.Mark(0) // pebble doesn't track non-compaction reads
d.diskWriteMeter.Mark(nWrites[i%2] - nWrites[(i-1)%2])
// See https://github.com/cockroachdb/pebble/pull/1628#pullrequestreview-1026664054
manuallyAllocated := stats.BlockCache.Size + int64(stats.MemTable.Size) + int64(stats.MemTable.ZombieSize)
d.manualMemAllocGauge.Update(manuallyAllocated)
d.memCompGauge.Update(stats.Flush.Count)
d.nonlevel0CompGauge.Update(nonLevel0CompCount)
d.level0CompGauge.Update(level0CompCount)
d.seekCompGauge.Update(stats.Compact.ReadCount)
d.liveCompGauge.Update(stats.Compact.NumInProgress)
d.liveCompSizeGauge.Update(stats.Compact.InProgressBytes)
d.liveIterGauge.Update(stats.TableIters)
d.liveMemTablesGauge.Update(stats.MemTable.Count)
d.zombieMemTablesGauge.Update(stats.MemTable.ZombieCount)
d.estimatedCompDebtGauge.Update(int64(stats.Compact.EstimatedDebt))
d.tableCacheHitGauge.Update(stats.TableCache.Hits)
d.tableCacheMissGauge.Update(stats.TableCache.Misses)
d.blockCacheHitGauge.Update(stats.BlockCache.Hits)
d.blockCacheMissGauge.Update(stats.BlockCache.Misses)
d.filterHitGauge.Update(stats.Filter.Hits)
d.filterMissGauge.Update(stats.Filter.Misses)
for i, level := range stats.Levels {
// Append metrics for additional layers
if i >= len(d.levelsGauge) {
d.levelsGauge = append(d.levelsGauge, metrics.GetOrRegisterGauge(namespace+fmt.Sprintf("tables/level%v", i), nil))
}
d.levelsGauge[i].Update(level.NumFiles)
}
// Sleep a bit, then repeat the stats collection
select {
case errc = <-d.quitChan:
// Quit requesting, stop hammering the database
case <-timer.C:
timer.Reset(refresh)
// Timeout, gather a new set of stats
}
}
errc <- nil
}
// v1batch is a write-only batch that commits changes to its host database
// when Write is called. A v1batch cannot be used concurrently.
type v1batch struct {
b *pebble.Batch
db *V1Database
size int
}
// Put inserts the given value into the batch for later committing.
func (b *v1batch) Put(key, value []byte) error {
if err := b.b.Set(key, value, nil); err != nil {
return err
}
b.size += len(key) + len(value)
return nil
}
// Delete inserts the key removal into the batch for later committing.
func (b *v1batch) Delete(key []byte) error {
if err := b.b.Delete(key, nil); err != nil {
return err
}
b.size += len(key)
return nil
}
// DeleteRange removes all keys in the range [start, end) from the batch for
// later committing, inclusive on start, exclusive on end.
func (b *v1batch) DeleteRange(start, end []byte) error {
// There is no special flag to represent the end of key range
// in pebble(nil in leveldb). Use an ugly hack to construct a
// large key to represent it.
if end == nil {
end = ethdb.MaximumKey
}
if err := b.b.DeleteRange(start, end, nil); err != nil {
return err
}
// Approximate size impact - just the keys
b.size += len(start) + len(end)
return nil
}
// ValueSize retrieves the amount of data queued up for writing.
func (b *v1batch) ValueSize() int {
return b.size
}
// Write flushes any accumulated data to disk.
func (b *v1batch) Write() error {
b.db.quitLock.RLock()
defer b.db.quitLock.RUnlock()
if b.db.closed {
return pebble.ErrClosed
}
return b.b.Commit(b.db.writeOptions)
}
// Reset resets the batch for reuse.
func (b *v1batch) Reset() {
b.b.Reset()
b.size = 0
}
// Replay replays the batch contents.
func (b *v1batch) Replay(w ethdb.KeyValueWriter) error {
reader := b.b.Reader()
for {
kind, k, v, ok, err := reader.Next()
if !ok || err != nil {
return err
}
// The (k,v) slices might be overwritten if the batch is reset/reused,
// and the receiver should copy them if they are to be retained long-term.
if kind == pebble.InternalKeyKindSet {
if err = w.Put(k, v); err != nil {
return err
}
} else if kind == pebble.InternalKeyKindDelete {
if err = w.Delete(k); err != nil {
return err
}
} else if kind == pebble.InternalKeyKindRangeDelete {
// For range deletion, k is the start key and v is the end key
if rangeDeleter, ok := w.(ethdb.KeyValueRangeDeleter); ok {
if err = rangeDeleter.DeleteRange(k, v); err != nil {
return err
}
} else {
return errors.New("ethdb.KeyValueWriter does not implement DeleteRange")
}
} else {
return fmt.Errorf("unhandled operation, keytype: %v", kind)
}
}
}
// Close closes the batch and releases all associated resources. After it is
// closed, any subsequent operations on this batch are undefined.
func (b *v1batch) Close() {
b.b.Close()
}
// v1pebbleIterator is a wrapper of underlying iterator in storage engine.
// The purpose of this structure is to implement the missing APIs.
//
// The v1pebbleIterator is not thread-safe.
type v1pebbleIterator struct {
iter *pebble.Iterator
moved bool
released bool
}
// Next moves the iterator to the next key/value pair. It returns whether the
// iterator is exhausted.
func (iter *v1pebbleIterator) Next() bool {
if iter.moved {
iter.moved = false
return iter.iter.Valid()
}
return iter.iter.Next()
}
// Error returns any accumulated error. Exhausting all the key/value pairs
// is not considered to be an error.
func (iter *v1pebbleIterator) Error() error {
return iter.iter.Error()
}
// Key returns the key of the current key/value pair, or nil if done. The caller
// should not modify the contents of the returned slice, and its contents may
// change on the next call to Next.
func (iter *v1pebbleIterator) Key() []byte {
return iter.iter.Key()
}
// Value returns the value of the current key/value pair, or nil if done. The
// caller should not modify the contents of the returned slice, and its contents
// may change on the next call to Next.
func (iter *v1pebbleIterator) Value() []byte {
return iter.iter.Value()
}
// Release releases associated resources. Release should always succeed and can
// be called multiple times without causing error.
func (iter *v1pebbleIterator) Release() {
if !iter.released {
iter.iter.Close()
iter.released = true
}
}

146
ethdb/pebble/version.go Normal file
View file

@ -0,0 +1,146 @@
// 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 <http://www.gnu.org/licenses/>.
package pebble
import (
"fmt"
"runtime"
pebblev1 "github.com/cockroachdb/pebble"
v1bloom "github.com/cockroachdb/pebble/bloom"
pebblev2 "github.com/cockroachdb/pebble/v2"
v2vfs "github.com/cockroachdb/pebble/v2/vfs"
v1vfs "github.com/cockroachdb/pebble/vfs"
"github.com/ethereum/go-ethereum/log"
)
// formatMinV2 is the minimum FormatMajorVersion supported by pebble v2.
// Databases with a lower format version must be opened with pebble v1.
const formatMinV2 = pebblev2.FormatFlushableIngest
// PeekFormatVersion reads the format version of an existing pebble database
// without opening it.
func PeekFormatVersion(file string) (bool, uint64, error) {
desc, err := pebblev2.Peek(file, v2vfs.Default)
if err == nil && desc.Exists {
return true, uint64(desc.FormatMajorVersion), nil
}
// Pebble v2 dropped support for the legacy FormatMostCompatible layout,
// which relies on the CURRENT file rather than a manifest marker.
//
// Databases created by older Geth (which never set FormatMajorVersion
// and therefore default to FormatMostCompatible) are not recognized by
// v2's Peek: it reports Exists=false with a nil error instead of failing.
// It may also fail outright on some old databases. In both cases fall
// back to v1's Peek, which still understands the CURRENT-file layout.
desc1, err1 := pebblev1.Peek(file, v1vfs.Default)
if err1 != nil {
// Surface the v2 error if there was one, otherwise the v1 error.
// Such as the folder is not existent, fs.ErrNotExist.
if err != nil {
return false, 0, err
}
return false, 0, err1
}
if !desc1.Exists {
// Neither version found a database; treat as a new/empty directory.
return false, 0, nil
}
return true, uint64(desc1.FormatMajorVersion), nil
}
// NeedsV1 returns true if the database at the given path requires pebble v1
// to open (format version too old for pebble v2).
func NeedsV1(file string) bool {
exists, ver, err := PeekFormatVersion(file)
if err != nil || !exists {
return false // New database or error; use v2
}
return pebblev2.FormatMajorVersion(ver) < formatMinV2
}
// Upgrade upgrades an existing pebble v1 database to be compatible with pebble v2.
// It opens the database with pebble v1 at its current format version, then uses
// RatchetFormatMajorVersion to migrate to FormatFlushableIngest (the minimum format
// version that pebble v2 supports).
//
// Notably, it's not an irreversible upgrade, the database can still be opened with
// legacy Geth binary.
func Upgrade(file string) error {
exists, ver, err := PeekFormatVersion(file)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("pebble database not found at %s", file)
}
if pebblev2.FormatMajorVersion(ver) >= formatMinV2 {
log.Info("Database format already compatible with pebble v2", "version", ver)
return nil
}
// FormatFlushableIngest exists in both pebble v1 and pebble v2 and it serves
// as the natural bridge point: a v1 database can be ratcheted up to
// FormatFlushableIngest using pebble v1, and then pebble v2 can open it since
// that's its minimum supported format.
v1Target := pebblev1.FormatFlushableIngest
log.Info("Upgrading pebble database format via v1", "from", ver, "to", v1Target)
numCPU := runtime.NumCPU()
opt := &pebblev1.Options{
// Open at the current on-disk format version; do not request a
// higher version here so that the upgrade happens explicitly via
// RatchetFormatMajorVersion below.
MaxConcurrentCompactions: func() int { return numCPU },
Levels: []pebblev1.LevelOptions{
{TargetFileSize: 2 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 4 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 8 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 16 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 32 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 64 * 1024 * 1024, FilterPolicy: v1bloom.FilterPolicy(10)},
{TargetFileSize: 128 * 1024 * 1024},
},
Logger: panicLogger{},
}
db, err := pebblev1.Open(file, opt)
if err != nil {
return fmt.Errorf("failed to open database with pebble v1 for upgrade: %w", err)
}
if err := db.RatchetFormatMajorVersion(v1Target); err != nil {
db.Close()
return fmt.Errorf("failed to ratchet format version to %d: %w", v1Target, err)
}
if err := db.Close(); err != nil {
return fmt.Errorf("failed to close database after v1 upgrade: %w", err)
}
log.Info("Pebble v1 format upgrade complete, verifying v2 compatibility")
// Verify that pebble v2 can open the upgraded database.
opt2 := &pebblev2.Options{
Logger: panicLogger{},
FormatMajorVersion: formatMinV2,
}
db2, err := pebblev2.Open(file, opt2)
if err != nil {
return fmt.Errorf("failed to open database with pebble v2 after upgrade: %w", err)
}
if err := db2.Close(); err != nil {
return fmt.Errorf("failed to close database after v2 verification: %w", err)
}
log.Info("Pebble database format upgrade complete")
return nil
}

View file

@ -0,0 +1,135 @@
// 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 <http://www.gnu.org/licenses/>.
package pebble
import (
"testing"
pebblev1 "github.com/cockroachdb/pebble"
pebblev2 "github.com/cockroachdb/pebble/v2"
v2vfs "github.com/cockroachdb/pebble/v2/vfs"
v1vfs "github.com/cockroachdb/pebble/vfs"
)
// TestPeekFormatVersionLegacyV1 verifies that PeekFormatVersion correctly
// detects a legacy pebble v1 database written in the FormatMostCompatible
// layout. Older Geth never set FormatMajorVersion, so pebble v1 defaulted to
// FormatMostCompatible, which uses the CURRENT file rather than a manifest
// marker. Pebble v2's Peek does not understand this layout and reports
// Exists=false with a nil error, so PeekFormatVersion must fall back to v1.
func TestPeekFormatVersionLegacyV1(t *testing.T) {
dir := t.TempDir()
// Create a v1 database with default options (no FormatMajorVersion set),
// which yields FormatMostCompatible, exactly as legacy Geth would.
db, err := pebblev1.Open(dir, &pebblev1.Options{})
if err != nil {
t.Fatalf("failed to create v1 database: %v", err)
}
if got := db.FormatMajorVersion(); got != pebblev1.FormatMostCompatible {
db.Close()
t.Fatalf("unexpected on-disk format version: have %d, want %d", got, pebblev1.FormatMostCompatible)
}
if err := db.Set([]byte("foo"), []byte("bar"), pebblev1.Sync); err != nil {
db.Close()
t.Fatalf("failed to write to v1 database: %v", err)
}
if err := db.Close(); err != nil {
t.Fatalf("failed to close v1 database: %v", err)
}
// Document the underlying pebble v2 behavior that motivates the v1
// fallback: v2's Peek silently fails to recognize this database.
if desc, err := pebblev2.Peek(dir, v2vfs.Default); err == nil && desc.Exists {
t.Fatal("expected pebble v2 Peek to not recognize a FormatMostCompatible database")
}
exists, ver, err := PeekFormatVersion(dir)
if err != nil {
t.Fatalf("PeekFormatVersion returned error: %v", err)
}
if !exists {
t.Fatal("expected legacy v1 database to be detected, got exists=false")
}
if ver != uint64(pebblev1.FormatMostCompatible) {
t.Fatalf("unexpected format version: have %d, want %d", ver, pebblev1.FormatMostCompatible)
}
// The database is too old for pebble v2, so it must be routed through v1.
if !NeedsV1(dir) {
t.Fatal("expected NeedsV1 to be true for a FormatMostCompatible database")
}
}
// TestPeekFormatVersionV2 verifies that PeekFormatVersion detects a database
// written at a pebble v2 compatible format version directly via v2's Peek.
func TestPeekFormatVersionV2(t *testing.T) {
dir := t.TempDir()
db, err := pebblev2.Open(dir, &pebblev2.Options{
FormatMajorVersion: formatMinV2,
})
if err != nil {
t.Fatalf("failed to create v2 database: %v", err)
}
if err := db.Set([]byte("foo"), []byte("bar"), pebblev2.Sync); err != nil {
db.Close()
t.Fatalf("failed to write to v2 database: %v", err)
}
if err := db.Close(); err != nil {
t.Fatalf("failed to close v2 database: %v", err)
}
exists, ver, err := PeekFormatVersion(dir)
if err != nil {
t.Fatalf("PeekFormatVersion returned error: %v", err)
}
if !exists {
t.Fatal("expected v2 database to be detected, got exists=false")
}
if ver != uint64(formatMinV2) {
t.Fatalf("unexpected format version: have %d, want %d", ver, formatMinV2)
}
if NeedsV1(dir) {
t.Fatal("expected NeedsV1 to be false for a v2 database")
}
}
// TestPeekFormatVersionEmpty verifies that an empty directory (a new database
// location) is reported as non-existent by both v2 and v1 Peek, rather than
// being misreported.
func TestPeekFormatVersionEmpty(t *testing.T) {
dir := t.TempDir()
// Sanity check that v1's Peek also reports a non-existent database here,
// so the test exercises the "neither version found a database" branch.
if desc, err := pebblev1.Peek(dir, v1vfs.Default); err != nil {
t.Fatalf("v1 Peek on empty directory returned error: %v", err)
} else if desc.Exists {
t.Fatal("expected v1 Peek to report no database in an empty directory")
}
exists, _, err := PeekFormatVersion(dir)
if err != nil {
t.Fatalf("PeekFormatVersion returned error: %v", err)
}
if exists {
t.Fatal("expected no database in an empty directory, got exists=true")
}
if NeedsV1(dir) {
t.Fatal("expected NeedsV1 to be false for an empty directory")
}
}

14
go.mod
View file

@ -13,6 +13,7 @@ require (
github.com/cespare/cp v0.1.0
github.com/cloudflare/cloudflare-go v0.114.0
github.com/cockroachdb/pebble v1.1.5
github.com/cockroachdb/pebble/v2 v2.1.4
github.com/consensys/gnark-crypto v0.18.1
github.com/crate-crypto/go-eth-kzg v1.5.0
github.com/davecgh/go-spew v1.1.1
@ -45,7 +46,6 @@ require (
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/jackpal/go-nat-pmp v1.0.2
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267
github.com/klauspost/compress v1.17.8
github.com/kylelemons/godebug v1.1.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.20
@ -83,10 +83,16 @@ require (
)
require (
github.com/RaduBerinde/axisds v0.1.0 // indirect
github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b // indirect
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
@ -101,7 +107,7 @@ require (
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/DataDog/zstd v1.4.5 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect
@ -153,10 +159,10 @@ require (
github.com/pion/logging v0.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.15.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect

38
go.sum
View file

@ -10,16 +10,22 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 h1:gggzg0SUMs6SQbEw+
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0/go.mod h1:+6KLcKIVgxoBDMqMO/Nvy7bZ9a0nbU3I1DtFQK3YvB4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8=
github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y=
github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk=
github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/aclements/go-perfevent v0.0.0-20240301234650-f7843625020f h1:JjxwchlOepwsUWcQwD2mLUAGE9aCp0/ehy6yCHFBOvo=
github.com/aclements/go-perfevent v0.0.0-20240301234650-f7843625020f/go.mod h1:tMDTce/yLLN/SK8gMOxQfnyeMeCg8KGzp0D1cbECEeo=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA=
@ -63,18 +69,26 @@ github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86c
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudflare/cloudflare-go v0.114.0 h1:ucoti4/7Exo0XQ+rzpn1H+IfVVe++zgiM+tyKtf0HUA=
github.com/cloudflare/cloudflare-go v0.114.0/go.mod h1:O7fYfFfA6wKqKFn2QIR9lhj7FDw6VQCGOY6hd2TBtd0=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU=
github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac=
github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5 h1:UycK/E0TkisVrQbSoxvU827FwgBBcZ95nRRmpj/12QI=
github.com/cockroachdb/datadriven v1.0.3-0.20250407164829-2945557346d5/go.mod h1:jsaKMvD3RBCATk1/jbUZM8C9idWBJME9+VRZ5+Liq1g=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895 h1:XANOgPYtvELQ/h4IrmPAohXqe2pWA8Bwhejr3VQoZsA=
github.com/cockroachdb/metamorphic v0.0.0-20231108215700-4ba948b56895/go.mod h1:aPd7gM9ov9M8v32Yy5NJrDyOcD8z642dqs+F0CeNXfA=
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhKtJ6bOk=
github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8=
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI=
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI=
@ -138,6 +152,8 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM=
github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
@ -233,8 +249,8 @@ github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4
github.com/kilic/bls12-381 v0.1.0/go.mod h1:vDTTHJONJ6G+P2R74EhnyotQDTliQDnFEwhdmfzw1ig=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@ -272,6 +288,8 @@ github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw=
github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
@ -315,14 +333,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/protolambda/bls12-381-util v0.1.0 h1:05DU2wJN7DTU7z28+Q+zejXkIsA/MF8JZQGhtBZZiWk=
github.com/protolambda/bls12-381-util v0.1.0/go.mod h1:cdkysJTRpeFeuUVx/TXGDQNMTiRAalk1vQw3TYTHcE4=
github.com/protolambda/zrnt v0.34.1 h1:qW55rnhZJDnOb3TwFiFRJZi3yTXFrJdGOFQM7vCwYGg=

View file

@ -26,7 +26,7 @@ import (
"github.com/ethereum/go-ethereum/internal/era"
"github.com/ethereum/go-ethereum/internal/era/e2store"
"github.com/ethereum/go-ethereum/rlp"
"github.com/klauspost/compress/snappy"
"github.com/golang/snappy"
)
type Iterator struct {

View file

@ -30,7 +30,7 @@ import (
"github.com/ethereum/go-ethereum/internal/era"
"github.com/ethereum/go-ethereum/internal/era/e2store"
"github.com/ethereum/go-ethereum/rlp"
"github.com/klauspost/compress/snappy"
"github.com/golang/snappy"
)
// Era object represents an era file that contains blocks and their components.
@ -204,6 +204,12 @@ func (e *Era) HasComponent(c componentType) bool {
return ok
}
// HasReceipts reports whether the file contains a receipts component. Files
// written with the "noreceipts" profile omit it.
func (e *Era) HasReceipts() bool {
return e.HasComponent(receipts)
}
// InitialTD returns initial total difficulty before the difficulty of the
// first block of the Era is applied. Returns an error if TD is not available
// (e.g., post-merge epoch).

View file

@ -114,7 +114,19 @@ func newLevelDBDatabase(file string, cache int, handles int, namespace string, r
// newPebbleDBDatabase creates a persistent key-value database without a freezer
// moving immutable chain segments into cold storage.
//
// If the database already exists with a legacy pebble v1 format, it is opened
// using pebble v1 for backward compatibility and a warning is logged directing
// the user to upgrade offline. New databases use pebble v2.
func newPebbleDBDatabase(file string, cache int, handles int, namespace string, readonly bool) (ethdb.KeyValueStore, error) {
if pebble.NeedsV1(file) {
log.Warn("Pebble database uses legacy v1 format; upgrade offline with 'geth db pebble-upgrade'")
db, err := pebble.NewV1(file, cache, handles, namespace, readonly)
if err != nil {
return nil, err
}
return db, nil
}
db, err := pebble.New(file, cache, handles, namespace, readonly)
if err != nil {
return nil, err

View file

@ -1198,6 +1198,11 @@ func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig {
func (c *ChainConfig) ActiveSystemContracts(time uint64) map[string]common.Address {
fork := c.LatestFork(time)
active := make(map[string]common.Address)
if fork >= forks.Amsterdam {
// EIP-8282 - Builder Execution Requests
active["BUILDER_DEPOSIT_CONTRACT_ADDRESS"] = BuilderDepositAddress
active["BUILDER_EXIT_CONTRACT_ADDRESS"] = BuilderExitAddress
}
if fork >= forks.Osaka {
// no new system contracts
}

View file

@ -139,7 +139,7 @@ const (
MaxCodeSize = 24576 // Maximum bytecode to permit for a contract
MaxInitCodeSize = 2 * MaxCodeSize // Maximum initcode to permit in a creation transaction and create instructions
MaxCodeSizeAmsterdam = 32768 // Maximum bytecode to permit for a contract post Amsterdam
MaxCodeSizeAmsterdam = 65536 // Maximum bytecode to permit for a contract post Amsterdam
MaxInitCodeSizeAmsterdam = 2 * MaxCodeSizeAmsterdam // Maximum initcode to permit in a creation transaction and create instructions post Amsterdam
// Precompiled contract gas prices
@ -240,6 +240,12 @@ var (
// EIP-7251 - Increase the MAX_EFFECTIVE_BALANCE
ConsolidationQueueAddress = common.HexToAddress("0x0000BBdDc7CE488642fb579F8B00f3a590007251")
ConsolidationQueueCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd")
// EIP-8282 - Builder Execution Requests
BuilderDepositAddress = common.HexToAddress("0x0000884d2AA32eAa155F59A2f24eFa73D9008282")
BuilderDepositCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe146101065760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461023457600182026001905f5b5f82111560695781019083028483029004916001019190604e565b90939004925050503660b814608957366102345734610234575f5260205ff35b8034106102345760383567ffffffffffffffff1680633b9aca001161023457633b9aca00029034031061023457600154600101600155600354806006026004015f358155600101602035815560010160403581556001016060358155600101608035815560010160a035905560b85f5f3760b85fa0600101600355005b600354600254808203806101001161011d57506101005b5f5b8181146101c3578281016006026004018160b8028154815260200181600101548152602001816002015480825260401c67ffffffffffffffff16816010018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360200181600301548152602001816004015481526020019060050154905260010161011f565b91018092146101d557906002556101e0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561020d57505f5b6001546020828201116102225750505f610228565b01602090035b5f555f60015560b8025ff35b5f5ffd")
BuilderExitAddress = common.HexToAddress("0x000014574A74c805590AFF9499fc7A690f008282")
BuilderExitCode = common.FromHex("0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd")
)
// System log events.

View file

@ -40,7 +40,10 @@ func TestSerializeDeserializeInternalNode(t *testing.T) {
s.root = rootRef
// Serialize the node — grouped format at groupDepth=1:
// [type(1)][groupDepth(1)][bitmap(1)][leftHash(32)][rightHash(32)] = 67 bytes
// [type(1)][groupDepth(1)][bitmap(1)][depths(1)][leftHash(32)][rightHash(32)] = 68 bytes.
// Both children are at depthOffset=groupDepth=1 (the bottom of the 1-level
// group). Each depth is stored as (offset-1)=0 in 3 bits, so the two entries
// pack into a single byte 0x00.
serialized := s.serializeNode(rootRef, 1)
if serialized[0] != nodeTypeInternal {
@ -50,7 +53,7 @@ func TestSerializeDeserializeInternalNode(t *testing.T) {
t.Errorf("Expected groupDepth byte to be 1, got %d", serialized[1])
}
expectedLen := NodeTypeBytes + 1 + 1 + 2*HashSize // type + groupDepth + bitmap + 2 hashes = 67
expectedLen := NodeTypeBytes + 1 + 1 + 1 + 2*HashSize // type + groupDepth + bitmap + packed depths + 2 hashes = 68
if len(serialized) != expectedLen {
t.Errorf("Expected serialized length to be %d, got %d", expectedLen, len(serialized))
}
@ -60,7 +63,13 @@ func TestSerializeDeserializeInternalNode(t *testing.T) {
t.Errorf("Expected bitmap byte 0xc0, got 0x%02x", serialized[2])
}
hashesStart := NodeTypeBytes + 1 + 1
depthsStart := NodeTypeBytes + 1 + 1
// Two depth offsets of 1 → stored as (1-1)=0 each → packed byte 0x00.
if serialized[depthsStart] != 0x00 {
t.Errorf("Expected packed depth byte 0x00, got 0x%02x", serialized[depthsStart])
}
hashesStart := depthsStart + 1
if !bytes.Equal(serialized[hashesStart:hashesStart+HashSize], leftHash[:]) {
t.Error("Left hash not found at expected position")
}
@ -244,29 +253,29 @@ func TestKeyToPath(t *testing.T) {
{
name: "depth 0",
depth: 0,
key: []byte{0x80}, // 10000000 in binary
expected: []byte{1},
key: []byte{0x80}, // 10000000 in binary
expected: []byte{0x80, 1}, // 1 bit "1", left-aligned, + length byte 1
wantErr: false,
},
{
name: "depth 7",
depth: 7,
key: []byte{0xFF}, // 11111111 in binary
expected: []byte{1, 1, 1, 1, 1, 1, 1, 1},
key: []byte{0xFF}, // 11111111 in binary
expected: []byte{0xFF, 8}, // 8-bit value 0xFF + length byte 8
wantErr: false,
},
{
name: "depth crossing byte boundary",
depth: 10,
key: []byte{0xFF, 0x00}, // 11111111 00000000 in binary
expected: []byte{1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0},
key: []byte{0xFF, 0x00}, // 11111111 00000000 in binary
expected: []byte{0xFF, 0x00, 11}, // top 11 bits "11111111 000", left-aligned, + length byte 11
wantErr: false,
},
{
name: "max valid depth",
depth: StemSize*8 - 1,
key: make([]byte, HashSize),
expected: make([]byte, StemSize*8),
expected: append(make([]byte, StemSize), StemSize*8), // 248 bits of zeros + length byte 248
wantErr: false,
},
{

160
trie/bintrie/bitarray.go Normal file
View file

@ -0,0 +1,160 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
package bintrie
// BitArray represents a trie path: the most significant `len` bits of a key,
// packed big-endian and MSB-first. Bit i (0 = most significant) lives at
// bytes[i/8] in mask 1<<(7-i%8). All bits at positions >= len are kept zero so
// that two paths are byte-equal iff they are logically equal.
//
// This mirrors the on-disk key layout, so path manipulation is plain slicing
// and copying: no shifting or endianness conversion is required. The maximum
// length is 248 bits (a 31-byte trie stem), and is a uint8 so the spare bits in
// the final byte are always available.
type BitArray struct {
len uint8
bytes [32]byte
}
// NewBitArray creates a bit array of the given length whose bits are the `length`
// least-significant bits of val, read most-significant-first. Used by tests to
// build expected paths; the value is interpreted as a number, not raw bytes.
func NewBitArray(length uint8, val uint64) BitArray {
var b BitArray
b.len = length
for p := uint8(0); p < length; p++ {
if (val>>(length-1-p))&1 == 1 {
b.bytes[p/8] |= 1 << (7 - p%8)
}
}
return b
}
// Len returns the number of used bits.
func (b *BitArray) Len() uint8 {
return b.len
}
// Bytes returns the packed big-endian, MSB-first representation. Bits beyond
// len are zero.
func (b *BitArray) Bytes() [32]byte {
return b.bytes
}
// AppendBit sets the bit array to x with a single bit appended, and returns the
// receiver. Safe when b and x alias the same value.
func (b *BitArray) AppendBit(x *BitArray, bit uint8) *BitArray {
*b = *x
if bit&1 == 1 {
// Position b.len is guaranteed zero by the all-bits-beyond-len-are-zero
// invariant, so a 1 only needs setting; a 0 is already in place.
b.bytes[b.len/8] |= 1 << (7 - b.len%8)
}
b.len++
return b
}
// MSBs sets the bit array to the most significant n bits of x and returns the
// receiver. If n >= x.len it is an exact copy of x. Think of it as x[:n].
func (b *BitArray) MSBs(x *BitArray, n uint8) *BitArray {
*b = *x
if n < b.len {
b.len = n
b.maskTail()
}
return b
}
// Equal reports whether two bit arrays hold the same path.
func (b *BitArray) Equal(x *BitArray) bool {
return b.len == x.len && b.bytes == x.bytes
}
// SetBytes sets the bit array to the most significant `length` bits of data,
// interpreted as big-endian bytes, and returns the receiver. At most 32 bytes
// of data are read; bits beyond length are zeroed.
func (b *BitArray) SetBytes(length uint8, data []byte) *BitArray {
b.bytes = [32]byte{}
copy(b.bytes[:], data)
b.len = length
b.maskTail()
return b
}
// SetBit sets the bit array to a single bit and returns the receiver.
func (b *BitArray) SetBit(bit uint8) *BitArray {
b.bytes = [32]byte{}
b.len = 1
if bit&1 == 1 {
b.bytes[0] = 0x80
}
return b
}
// Copy returns a value copy of the bit array.
func (b *BitArray) Copy() BitArray {
return *b
}
// Set sets the bit array to the same value as x and returns the receiver.
func (b *BitArray) Set(x *BitArray) *BitArray {
*b = *x
return b
}
// KeyBytes returns the path-to-DB-key encoding: the active bytes (the
// left-aligned MSB-first prefix) followed by a single trailing byte holding the
// bit-length. The trailing length disambiguates paths whose active bytes
// coincide (e.g. 1-bit "1" packs to [0x80, 0x01] and 8-bit "10000000" to
// [0x80, 0x08]). The empty path encodes as no bytes.
func (b *BitArray) KeyBytes() []byte {
if b.len == 0 {
return nil
}
bc := (int(b.len) + 7) / 8
res := make([]byte, bc+1)
copy(res[:bc], b.bytes[:bc])
res[bc] = b.len
return res
}
// PutKeyBytes writes the key encoding (active bytes followed by length byte)
// into dst and returns the populated sub-slice. The empty path returns dst[:0]
// without touching dst. For non-empty paths dst must have len >= 33 (32 packed
// bytes for 248 bits + 1 length byte).
func (b *BitArray) PutKeyBytes(dst []byte) []byte {
if b.len == 0 {
return dst[:0]
}
bc := (int(b.len) + 7) / 8
_ = dst[bc] // bounds check hint
copy(dst[:bc], b.bytes[:bc])
dst[bc] = b.len
return dst[:bc+1]
}
// maskTail zeroes every bit at a position >= len, preserving the invariant that
// equal paths are byte-equal.
func (b *BitArray) maskTail() {
full := int(b.len / 8)
if rem := b.len % 8; rem != 0 {
b.bytes[full] &= byte(0xFF) << (8 - rem)
full++
}
for i := full; i < len(b.bytes); i++ {
b.bytes[i] = 0
}
}

View file

@ -0,0 +1,205 @@
package bintrie
import (
"bytes"
"testing"
)
// ba builds a BitArray with the given length and leading bytes, for use as an
// expected value. Remaining bytes are zero.
func ba(length uint8, lead ...byte) BitArray {
var b BitArray
b.len = length
copy(b.bytes[:], lead)
return b
}
func TestNewBitArray(t *testing.T) {
tests := []struct {
name string
length uint8
val uint64
want BitArray
}{
{"empty", 0, 0, ba(0)},
{"single 1", 1, 1, ba(1, 0x80)},
{"single 0", 1, 0, ba(1, 0x00)},
{"101", 3, 0b101, ba(3, 0xA0)},
{"full byte", 8, 0xFF, ba(8, 0xFF)},
{"ten bits", 10, 0x3FF, ba(10, 0xFF, 0xC0)},
{"high bits ignored beyond length", 3, 0b11101, ba(3, 0xA0)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewBitArray(tt.length, tt.val)
if !got.Equal(&tt.want) {
t.Errorf("NewBitArray(%d, %#x) = %x (len %d), want %x (len %d)",
tt.length, tt.val, got.bytes, got.len, tt.want.bytes, tt.want.len)
}
})
}
}
func TestSetBytes(t *testing.T) {
tests := []struct {
name string
length uint8
data []byte
want BitArray
}{
{"empty", 0, []byte{0xFF}, ba(0)},
{"full byte", 8, []byte{0xAB}, ba(8, 0xAB)},
{"top 4 bits", 4, []byte{0xFF}, ba(4, 0xF0)},
{"11 bits masks tail", 11, []byte{0xFF, 0xFF}, ba(11, 0xFF, 0xE0)},
{"data longer than length", 4, []byte{0xFF, 0xFF}, ba(4, 0xF0)},
{"data shorter than length", 16, []byte{0xAB}, ba(16, 0xAB, 0x00)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := new(BitArray).SetBytes(tt.length, tt.data)
if !got.Equal(&tt.want) {
t.Errorf("SetBytes(%d, %x) = %x (len %d), want %x (len %d)",
tt.length, tt.data, got.bytes, got.len, tt.want.bytes, tt.want.len)
}
})
}
}
func TestSetBytesFull(t *testing.T) {
data := bytes.Repeat([]byte{0xFF}, 32)
got := new(BitArray).SetBytes(248, data)
want := ba(248)
for i := 0; i < 31; i++ {
want.bytes[i] = 0xFF
}
if !got.Equal(&want) {
t.Errorf("SetBytes(248, 0xFF*32): byte 31 must be zeroed; got %x", got.bytes)
}
}
func TestMSBs(t *testing.T) {
x := new(BitArray).SetBytes(16, []byte{0xAB, 0xCD})
tests := []struct {
name string
n uint8
want BitArray
}{
{"prefix byte", 8, ba(8, 0xAB)},
{"prefix nibble", 4, ba(4, 0xA0)},
{"zero", 0, ba(0)},
{"n equals len", 16, ba(16, 0xAB, 0xCD)},
{"n exceeds len copies x", 20, ba(16, 0xAB, 0xCD)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := new(BitArray).MSBs(x, tt.n)
if !got.Equal(&tt.want) {
t.Errorf("MSBs(x, %d) = %x (len %d), want %x (len %d)",
tt.n, got.bytes, got.len, tt.want.bytes, tt.want.len)
}
})
}
}
func TestAppendBit(t *testing.T) {
// Build "101" one bit at a time from empty.
var p BitArray
for _, bit := range []uint8{1, 0, 1} {
p.AppendBit(&p, bit) // receiver aliases argument
}
if want := ba(3, 0xA0); !p.Equal(&want) {
t.Fatalf("append 1,0,1 = %x (len %d), want %x (len 3)", p.bytes, p.len, want.bytes)
}
// Append across a byte boundary: 8 ones then a 1 → 9 bits.
var q BitArray
for i := 0; i < 9; i++ {
q.AppendBit(&q, 1)
}
if want := ba(9, 0xFF, 0x80); !q.Equal(&want) {
t.Fatalf("append nine 1s = %x (len %d), want %x (len 9)", q.bytes, q.len, want.bytes)
}
// Appending to a copy must not mutate the source.
src := new(BitArray).SetBytes(4, []byte{0xF0})
child := *src
child.AppendBit(&child, 0)
if want := ba(4, 0xF0); !src.Equal(&want) {
t.Errorf("source mutated by append on copy: %x", src.bytes)
}
if want := ba(5, 0xF0); !child.Equal(&want) {
t.Errorf("append 0 = %x (len %d), want %x (len 5)", child.bytes, child.len, want.bytes)
}
}
func TestSetBit(t *testing.T) {
if got, want := new(BitArray).SetBit(1), ba(1, 0x80); !got.Equal(&want) {
t.Errorf("SetBit(1) = %x (len %d), want %x", got.bytes, got.len, want.bytes)
}
if got, want := new(BitArray).SetBit(0), ba(1, 0x00); !got.Equal(&want) {
t.Errorf("SetBit(0) = %x (len %d), want %x", got.bytes, got.len, want.bytes)
}
}
func TestEqual(t *testing.T) {
a := NewBitArray(3, 0b101)
b := NewBitArray(3, 0b101)
if !a.Equal(&b) {
t.Error("equal arrays reported unequal")
}
// Same active bytes, different length must be unequal.
c := NewBitArray(2, 0b10) // "10" -> byte 0x80, len 2
d := ba(3, c.bytes[0]) // same byte, len 3
if c.Equal(&d) {
t.Error("arrays with different length reported equal")
}
}
func TestKeyBytesRoundTrip(t *testing.T) {
tests := []struct {
name string
length uint8
data []byte
want []byte // expected KeyBytes output
}{
{"empty", 0, nil, nil},
{"one bit", 1, []byte{0x80}, []byte{0x80, 1}},
{"full byte", 8, []byte{0x80}, []byte{0x80, 8}},
{"eleven bits", 11, []byte{0xFF, 0xFF}, []byte{0xFF, 0xE0, 11}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
src := new(BitArray).SetBytes(tt.length, tt.data)
key := src.KeyBytes()
if !bytes.Equal(key, tt.want) {
t.Fatalf("KeyBytes() = %x, want %x", key, tt.want)
}
// PutKeyBytes must agree with KeyBytes.
var buf [33]byte
if put := src.PutKeyBytes(buf[:]); !bytes.Equal(put, tt.want) {
t.Fatalf("PutKeyBytes() = %x, want %x", put, tt.want)
}
// Re-parse the active bytes and confirm the path round-trips.
if tt.length == 0 {
return
}
lengthByte := key[len(key)-1]
reparsed := new(BitArray).SetBytes(lengthByte, key[:len(key)-1])
if !reparsed.Equal(src) {
t.Fatalf("round-trip mismatch: %x (len %d) != %x (len %d)",
reparsed.bytes, reparsed.len, src.bytes, src.len)
}
})
}
}
func TestCopyIsIndependent(t *testing.T) {
src := new(BitArray).SetBytes(8, []byte{0xAB})
cp := src.Copy()
cp.AppendBit(&cp, 1)
if want := ba(8, 0xAB); !src.Equal(&want) {
t.Errorf("Copy not independent: source became %x (len %d)", src.bytes, src.len)
}
}

445
trie/bintrie/format_test.go Normal file
View file

@ -0,0 +1,445 @@
// Copyright 2026 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 <http://www.gnu.org/licenses/>.
package bintrie
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
)
// TestRootHashMatchesReadBackHash pins the round-trip invariant: the root
// hash a Commit advertises must be exactly the value a fresh reader computes
// from the on-disk blob. Before Option B the writer produced a natural-depth
// hash while DeserializeAndHash produced an extended-depth hash, so the two
// disagreed for any non-trivial subtree — this test failed. With the
// per-entry depth byte, the reader rebuilds the natural-shape tree and the
// hashes match for every groupDepth and every divergence bit.
func TestRootHashMatchesReadBackHash(t *testing.T) {
for groupDepth := 1; groupDepth <= MaxGroupDepth; groupDepth++ {
// divergeBit ∈ [0, groupDepth-1] places the two stems at natural
// depth (divergeBit+1) within the root group; we want to exercise
// every depth-offset value the new format must handle.
for divergeBit := 0; divergeBit < groupDepth; divergeBit++ {
t.Run(fmt.Sprintf("gd=%d/diverge=%d", groupDepth, divergeBit), func(t *testing.T) {
tr := &BinaryTrie{
store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
groupDepth: groupDepth,
}
stemL, stemR := stemsDivergingAt(divergeBit)
if err := tr.store.Insert(stemL, oneKey[:], nil); err != nil {
t.Fatalf("Insert stemL: %v", err)
}
if err := tr.store.Insert(stemR, twoKey[:], nil); err != nil {
t.Fatalf("Insert stemR: %v", err)
}
natural := tr.Hash()
_, ns := tr.Commit(false)
rootNode, ok := ns.Nodes[""]
if !ok {
t.Fatalf("Commit produced no root blob (path \"\")")
}
readBack, err := DeserializeAndHash(rootNode.Blob, 0)
if err != nil {
t.Fatalf("DeserializeAndHash: %v", err)
}
if natural != readBack {
t.Fatalf("round-trip hash mismatch:\n"+
" tr.Hash() = %x\n"+
" DeserializeAndHash(rootBlob) = %x\n"+
"the parent's stored root hash cannot be reproduced from its own blob",
natural, readBack)
}
})
}
}
}
// TestMultiStemMixedDepths inserts four stems that diverge at different
// depths within a single groupDepth=5 group, then round-trips the trie
// through Commit + fresh-read. Verifies that every stem is retrievable by
// key after reload — exercises the new format with several depth-offset
// values in the same blob (1, 2, 3, 4) and confirms attachInGroup builds
// the natural-shape tree correctly.
func TestMultiStemMixedDepths(t *testing.T) {
const groupDepth = 5
// Each stem diverges from `0x00…00` at a different bit, so naturally:
// - stem at bit-0 divergence → depth 1
// - stem at bit-1 divergence → depth 2
// - stem at bit-2 divergence → depth 3
// - stem at bit-3 divergence → depth 4
stems := [][]byte{
zeroKey[:],
bitFlipStem(0), // diverge at bit 0
bitFlipStem(1), // diverge at bit 1 (prefix "0" matches stem 0)
bitFlipStem(2), // diverge at bit 2 (prefix "00")
bitFlipStem(3), // diverge at bit 3 (prefix "000")
}
values := []common.Hash{oneKey, twoKey, threeKey, fourKey, ffKey}
tr := &BinaryTrie{
store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
groupDepth: groupDepth,
}
for i, stem := range stems {
if err := tr.store.Insert(stem, values[i][:], nil); err != nil {
t.Fatalf("Insert stem %d: %v", i, err)
}
}
before := tr.Hash()
_, ns := tr.Commit(false)
rootBlob, ok := ns.Nodes[""]
if !ok {
t.Fatalf("no root blob in NodeSet")
}
readBack, err := DeserializeAndHash(rootBlob.Blob, 0)
if err != nil {
t.Fatalf("DeserializeAndHash: %v", err)
}
if before != readBack {
t.Fatalf("hash mismatch: tr.Hash()=%x DeserializeAndHash(rootBlob)=%x", before, readBack)
}
// Reload the root blob into a fresh store and confirm structure.
fresh := newNodeStore()
ref, err := fresh.deserializeNodeWithHash(rootBlob.Blob, 0, before)
if err != nil {
t.Fatalf("deserializeNodeWithHash: %v", err)
}
if ref.Kind() != kindInternal {
t.Fatalf("expected root to be Internal, got kind %d", ref.Kind())
}
// Spot-check: the reload-tree's root hash equals the commit-time hash.
if got := fresh.computeHash(ref); got != before {
t.Fatalf("reload root hash mismatch: got %x, want %x", got, before)
}
}
// TestDecodeRejectsNonCanonicalPosition hand-crafts a blob where the bitmap
// position has nonzero trailing bits given its depth offset. Two
// implementations must produce byte-identical blobs for the same logical
// content, so a non-canonical position is unambiguously an invalid blob.
func TestDecodeRejectsNonCanonicalPosition(t *testing.T) {
// groupDepth=5, bitmap size = 4 bytes. Set bit at position 5 (binary
// 00101) and declare depthOffset=2. Top 2 bits of 00101 are 00 (path
// "00"), the trailing 3 bits should be zero — they're 101 here, so the
// reader must reject.
blob := []byte{nodeTypeInternal, 5}
// bitmap[0] = bit at position 5 → 1 << (7-5) = 0x04
blob = append(blob, 0x04, 0x00, 0x00, 0x00)
// depths[0] = 2, packed as (2-1)=1 in 3 bits MSB-first → 0b0010_0000 = 0x20
blob = append(blob, 0x20)
// hashes[0] = 32 zero bytes
blob = append(blob, make([]byte, HashSize)...)
s := newNodeStore()
_, err := s.deserializeNode(blob, 0)
if err == nil {
t.Fatal("expected non-canonical position error, got nil")
}
if err.Error() != "non-canonical bitmap position" {
t.Errorf("expected 'non-canonical bitmap position', got %q", err.Error())
}
}
// TestDecodeRejectsInvalidDepthOffset covers depthOffset>groupDepth (the entry
// would live below the group's bottom layer, impossible by construction). The
// old depthOffset=0 and depthOffset>MaxGroupDepth cases are gone: the 3-bit
// field stores (offset-1) ∈ [0,7], so offset 0 and offset 9 are unrepresentable
// and can no longer be hand-crafted into a blob. Only offset>groupDepth with
// groupDepth<MaxGroupDepth remains encodable and must still be rejected.
func TestDecodeRejectsInvalidDepthOffset(t *testing.T) {
makeBlob := func(groupDepth int, depthOffset uint8) []byte {
bitmapSize := bitmapSizeForDepth(groupDepth)
bitmap := make([]byte, bitmapSize)
bitmap[0] = 0x80 // bit at position 0
depths := make([]byte, packedDepthsLen(1))
writeDepth(depths, 0, depthOffset-1)
blob := []byte{nodeTypeInternal, byte(groupDepth)}
blob = append(blob, bitmap...)
blob = append(blob, depths...)
blob = append(blob, make([]byte, HashSize)...)
return blob
}
for _, tc := range []struct {
name string
groupDepth int
depthOffset uint8
}{
{"gd2/depth3", 2, 3},
{"gd3/depth4", 3, 4},
{"gd5/depth6", 5, 6},
{"gd7/depth8", 7, 8},
} {
t.Run(tc.name, func(t *testing.T) {
s := newNodeStore()
_, err := s.deserializeNode(makeBlob(tc.groupDepth, tc.depthOffset), 0)
if err == nil {
t.Fatal("expected invalid depth offset error, got nil")
}
if err.Error() != "invalid depth offset" {
t.Errorf("expected 'invalid depth offset', got %q", err.Error())
}
})
}
}
// TestDecodeRejectsNonCanonicalDepthPadding verifies the canonical-encoding
// check on the packed depth stream: when k*3 is not a multiple of 8, the unused
// low bits of the final packed byte must be zero. Here groupDepth=2 with a
// single entry uses 3 bits, leaving 5 pad bits; a stray pad bit must be
// rejected so that two encoders cannot produce differing blobs for the same
// content.
func TestDecodeRejectsNonCanonicalDepthPadding(t *testing.T) {
// groupDepth=2, one entry at bitmap position 0, depthOffset=2 (bottom layer).
// Packed depth = (2-1)=1 → 0b001_00000 = 0x20; set a stray low pad bit → 0x21.
blob := []byte{nodeTypeInternal, 2}
blob = append(blob, 0x80) // bitmap: bit at position 0
blob = append(blob, 0x21) // packed depths with a non-zero pad bit
blob = append(blob, make([]byte, HashSize)...)
s := newNodeStore()
_, err := s.deserializeNode(blob, 0)
if err == nil {
t.Fatal("expected non-canonical depth padding error, got nil")
}
if err.Error() != "non-canonical depth padding" {
t.Errorf("expected 'non-canonical depth padding', got %q", err.Error())
}
}
// TestRoundTripPersistence exercises the full commit-then-reload pipeline
// the way real geth does it: write blobs to a backing map, open a fresh
// nodeStore from the root blob, then resolve and read every value back
// through the resolver. Catches mismatches between the writer's storage
// path (collectChildGroups in store_commit.go) and the reader's lookup
// path (keyToPath in store_ops.go) — exactly the bug Option B's first
// implementation hit, where blobs were written at the bottom-layer-extended
// path but resolved at the natural-depth path. Also confirms the reloaded
// trie's root hash equals the original committed hash.
func TestRoundTripPersistence(t *testing.T) {
for _, groupDepth := range []int{1, 2, 3, 5, 8} {
t.Run(fmt.Sprintf("groupDepth=%d", groupDepth), func(t *testing.T) {
// 1. Build a trie with deterministically-distributed keys.
// 50 keys with FNV-style spread guarantees several stems
// land in the root group (natural depth ≤ groupDepth) and
// several land in sub-groups, exercising both the in-group
// resolve and the cross-group resolve paths.
writerTrie := &BinaryTrie{
store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
groupDepth: groupDepth,
}
const n = 50
keys := make([][HashSize]byte, n)
values := make([][HashSize]byte, n)
for i := range n {
binary.BigEndian.PutUint64(keys[i][:8], uint64(i+1)*0x9e3779b97f4a7c15)
binary.BigEndian.PutUint64(keys[i][8:16], uint64(i+1)*0xc2b2ae3d27d4eb4f)
binary.BigEndian.PutUint64(keys[i][16:24], uint64(i+1)*0x165667b19e3779f9)
binary.BigEndian.PutUint64(keys[i][24:32], uint64(i+1)*0x85ebca77c2b2ae63)
binary.BigEndian.PutUint64(values[i][:8], uint64(i+1))
if err := writerTrie.store.Insert(keys[i][:], values[i][:], nil); err != nil {
t.Fatalf("insert %d: %v", i, err)
}
}
// 2. Commit; capture every blob into an in-memory map keyed
// by its path. The NodeSet key is the BitArray.PutKeyBytes
// encoding — exactly the bytes the resolver gets from
// keyToPath, so map lookups by string(path) round-trip.
rootHash := writerTrie.Hash()
_, ns := writerTrie.Commit(false)
blobs := make(map[string][]byte, len(ns.Nodes))
for path, node := range ns.Nodes {
blobs[path] = node.Blob
}
// 3. Build a resolver that serves blobs from the map.
resolver := func(path []byte, hash common.Hash) ([]byte, error) {
blob, ok := blobs[string(path)]
if !ok {
return nil, fmt.Errorf("blob not found at path %x (hash %x)", path, hash)
}
return blob, nil
}
// 4. Open a fresh store, seeded only with the root blob.
// Everything else must be reached via the resolver.
readerStore := newNodeStore()
rootBlob, ok := blobs[""]
if !ok {
t.Fatalf("no root blob in NodeSet")
}
rootRef, err := readerStore.deserializeNodeWithHash(rootBlob, 0, rootHash)
if err != nil {
t.Fatalf("deserialize root: %v", err)
}
readerStore.root = rootRef
// 5. Read every key back through the resolver and verify.
// A mismatch here means either the storage path diverged
// from the lookup path, or deserialization corrupted data.
for i := range n {
got, err := readerStore.Get(keys[i][:], resolver)
if err != nil {
t.Fatalf("Get key %d (%x): %v", i, keys[i], err)
}
if !bytes.Equal(got, values[i][:]) {
t.Fatalf("Get key %d: got %x, want %x", i, got, values[i][:])
}
}
// 6. The reloaded trie's root hash must equal the original.
// This is the canonical-hash round-trip property: any
// independent reader walking the same blobs computes the
// same root, independent of in-memory layout choices.
if got := readerStore.Hash(); got != rootHash {
t.Fatalf("post-reload root hash: got %x, want %x", got, rootHash)
}
})
}
}
// TestNoOrphanBlobAfterStemPromotion targets gballet's store_ops.go review
// concern: when a second commit promotes an existing stem deeper, the stem's
// blob moves to a new path, and Commit emits only AddNode entries (never
// deletes). If a stem's old path were not reoccupied by the new ancestor node,
// the prior commit's blob would linger as an unreachable orphan.
//
// The test applies two commit deltas to a single backing map, then walks the
// trie from the new root and asserts every persisted blob is reachable — i.e.
// no orphan survives. The first batch establishes stems at group boundaries;
// the second batch shares prefixes with the first to force promotions.
func TestNoOrphanBlobAfterStemPromotion(t *testing.T) {
for _, groupDepth := range []int{1, 2, 3, 5} {
t.Run(fmt.Sprintf("groupDepth=%d", groupDepth), func(t *testing.T) {
tr := &BinaryTrie{
store: newNodeStore(),
tracer: trie.NewPrevalueTracer(),
groupDepth: groupDepth,
}
db := make(map[string][]byte)
apply := func(ns *trienode.NodeSet) {
for path, node := range ns.Nodes {
if node.IsDeleted() {
delete(db, path)
continue
}
db[path] = node.Blob
}
}
const n = 24
keys := make([][HashSize]byte, n)
values := make([][HashSize]byte, n)
for i := range n {
binary.BigEndian.PutUint64(keys[i][:8], uint64(i+1)*0x9e3779b97f4a7c15)
binary.BigEndian.PutUint64(keys[i][8:16], uint64(i+1)*0xc2b2ae3d27d4eb4f)
binary.BigEndian.PutUint64(values[i][:8], uint64(i+1))
}
// Commit 1: first half.
for i := 0; i < n/2; i++ {
if err := tr.store.Insert(keys[i][:], values[i][:], nil); err != nil {
t.Fatalf("insert %d: %v", i, err)
}
}
_, ns1 := tr.Commit(false)
apply(ns1)
// Commit 2: second half (shares prefixes, forces promotions).
for i := n / 2; i < n; i++ {
if err := tr.store.Insert(keys[i][:], values[i][:], nil); err != nil {
t.Fatalf("insert %d: %v", i, err)
}
}
rootHash, ns2 := tr.Commit(false)
apply(ns2)
// Walk from the new root, recording every blob the reader resolves.
resolved := make(map[string]bool)
resolver := func(path []byte, _ common.Hash) ([]byte, error) {
resolved[string(path)] = true
blob, ok := db[string(path)]
if !ok {
return nil, fmt.Errorf("missing blob at path %x", path)
}
return blob, nil
}
reader := newNodeStore()
rootRef, err := reader.deserializeNodeWithHash(db[""], 0, rootHash)
if err != nil {
t.Fatalf("deserialize root: %v", err)
}
reader.root = rootRef
for i := range n {
got, err := reader.Get(keys[i][:], resolver)
if err != nil {
t.Fatalf("Get key %d: %v", i, err)
}
if !bytes.Equal(got, values[i][:]) {
t.Fatalf("Get key %d: got %x, want %x", i, got, values[i][:])
}
}
// Every persisted blob must be reachable; the root ("") is seeded.
reachable := map[string]bool{"": true}
for path := range resolved {
reachable[path] = true
}
for path := range db {
if !reachable[path] {
t.Errorf("orphan blob at path %x is unreachable from the new root", []byte(path))
}
}
})
}
}
// stemsDivergingAt returns two 32-byte stems whose first `divergeBit` bits
// are zero and whose bit at index `divergeBit` differs (left=0, right=1).
// Useful for placing two stems at a known natural depth within a group.
func stemsDivergingAt(divergeBit int) (left, right []byte) {
left = make([]byte, HashSize)
right = make([]byte, HashSize)
// Bit `divergeBit` is in byte (divergeBit/8) at MSB position (7 - divergeBit%8).
right[divergeBit/8] = 1 << (7 - divergeBit%8)
return left, right
}
// bitFlipStem returns a 32-byte stem whose first `divergeBit` bits are zero,
// bit `divergeBit` is 1, and all subsequent bits are zero. Used together
// with the all-zero stem to force divergence at a specific bit.
func bitFlipStem(divergeBit int) []byte {
out := make([]byte, HashSize)
out[divergeBit/8] = 1 << (7 - divergeBit%8)
return out
}

View file

@ -26,12 +26,8 @@ func keyToPath(depth int, key []byte) ([]byte, error) {
if depth >= 31*8 {
return nil, errors.New("node too deep")
}
path := make([]byte, 0, depth+1)
for i := range depth + 1 {
bit := key[i/8] >> (7 - (i % 8)) & 1
path = append(path, bit)
}
return path, nil
path := new(BitArray).SetBytes(uint8(depth+1), key)
return path.KeyBytes(), nil
}
// Invariant: dirty=false implies mustRecompute=false. Every mutation that

View file

@ -283,14 +283,13 @@ func TestInternalNodeCollectNodes(t *testing.T) {
t.Fatal(err)
}
var collectedPaths [][]byte
flushFn := func(path []byte, hash common.Hash, serialized []byte) {
pathCopy := make([]byte, len(path))
copy(pathCopy, path)
collectedPaths = append(collectedPaths, pathCopy)
var collectedPaths []BitArray
flushFn := func(path BitArray, hash common.Hash, serialized []byte) {
collectedPaths = append(collectedPaths, path)
}
s.collectNodes(s.root, []byte{1}, flushFn, 8)
initialPath := NewBitArray(1, 1)
s.collectNodes(s.root, initialPath, flushFn, 8)
// Should have collected 3 nodes: left stem, right stem, and the internal node itself
if len(collectedPaths) != 3 {

View file

@ -188,20 +188,20 @@ func (it *binaryNodeIterator) Parent() common.Hash {
return it.store.computeHash(it.stack[len(it.stack)-2].Node)
}
// Path returns the bit-path to the current node.
// Path returns the bit-packed path to the current node.
// Callers must not retain references to the returned slice after calling Next.
func (it *binaryNodeIterator) Path() []byte {
if it.Leaf() {
return it.LeafKey()
}
var path []byte
var path BitArray
for i, state := range it.stack {
if i >= len(it.stack)-1 {
break
}
path = append(path, byte(state.Index))
path.AppendBit(&path, uint8(state.Index))
}
return path
return path.KeyBytes()
}
func (it *binaryNodeIterator) NodeBlob() []byte {

View file

@ -41,10 +41,15 @@ type nodeStore struct {
// stem-split keeps the old stem at a deeper position), so they don't
// have free lists.
freeHashed []uint32
// orphans holds on-disk paths whose committed blob has been abandoned by a
// stem depth-promotion since the last commit. Commit emits a deletion for
// each (unless a freshly flushed node reoccupies the path), then clears it.
orphans map[string]struct{}
}
func newNodeStore() *nodeStore {
return &nodeStore{root: emptyRef}
return &nodeStore{root: emptyRef, orphans: make(map[string]struct{})}
}
func (s *nodeStore) allocInternal() uint32 {
@ -179,6 +184,10 @@ func (s *nodeStore) Copy() *nodeStore {
ns.freeHashed = make([]uint32, len(s.freeHashed))
copy(ns.freeHashed, s.freeHashed)
}
ns.orphans = make(map[string]struct{}, len(s.orphans))
for path := range s.orphans {
ns.orphans[path] = struct{}{}
}
return ns
}

View file

@ -313,14 +313,13 @@ func TestStemNodeCollectNodes(t *testing.T) {
t.Fatal(err)
}
var collectedPaths [][]byte
flushFn := func(path []byte, hash common.Hash, serialized []byte) {
pathCopy := make([]byte, len(path))
copy(pathCopy, path)
collectedPaths = append(collectedPaths, pathCopy)
var collectedPaths []BitArray
flushFn := func(path BitArray, hash common.Hash, serialized []byte) {
collectedPaths = append(collectedPaths, path)
}
s.collectNodes(s.root, []byte{0, 1, 0}, flushFn, 8)
initialPath := NewBitArray(3, 0b010)
s.collectNodes(s.root, initialPath, flushFn, 8)
// Should have collected one node (itself)
if len(collectedPaths) != 1 {
@ -328,7 +327,8 @@ func TestStemNodeCollectNodes(t *testing.T) {
}
// Check the path
if !bytes.Equal(collectedPaths[0], []byte{0, 1, 0}) {
t.Errorf("Path mismatch: expected [0, 1, 0], got %v", collectedPaths[0])
expectedPath := NewBitArray(3, 0b010)
if !collectedPaths[0].Equal(&expectedPath) {
t.Errorf("Path mismatch: expected %v, got %v", expectedPath, collectedPaths[0])
}
}

View file

@ -27,7 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common"
)
type nodeFlushFn func(path []byte, hash common.Hash, serialized []byte)
type nodeFlushFn func(path BitArray, hash common.Hash, serialized []byte)
func (s *nodeStore) Hash() common.Hash {
return s.computeHash(s.root)
@ -111,7 +111,7 @@ func (s *nodeStore) hashInternal(idx uint32) common.Hash {
// It traverses up to `remainingDepth` levels, storing hashes of bottom-layer children.
// position tracks the current index (0 to 2^groupDepth - 1) for bitmap placement.
// hashes collects the hashes of present children, bitmap tracks which positions are present.
func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position int, absoluteDepth int, bitmap []byte, hashes *[]common.Hash) {
func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position int, groupDepth int, bitmap []byte, hashes *[]common.Hash, depths *[]uint8) {
if remainingDepth == 0 {
// Bottom layer: store hash if not empty
switch ref.Kind() {
@ -122,6 +122,7 @@ func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position i
// StemNode, HashedNode, or InternalNode at boundary: store hash
bitmap[position/8] |= 1 << (7 - (position % 8))
*hashes = append(*hashes, s.computeHash(ref))
*depths = append(*depths, uint8(groupDepth))
}
return
}
@ -130,57 +131,81 @@ func (s *nodeStore) serializeSubtree(ref nodeRef, remainingDepth int, position i
case kindInternal:
leftPos := position * 2
rightPos := position*2 + 1
s.serializeSubtree(s.getInternal(ref.Index()).left, remainingDepth-1, leftPos, absoluteDepth+1, bitmap, hashes)
s.serializeSubtree(s.getInternal(ref.Index()).right, remainingDepth-1, rightPos, absoluteDepth+1, bitmap, hashes)
s.serializeSubtree(s.getInternal(ref.Index()).left, remainingDepth-1, leftPos, groupDepth, bitmap, hashes, depths)
s.serializeSubtree(s.getInternal(ref.Index()).right, remainingDepth-1, rightPos, groupDepth, bitmap, hashes, depths)
case kindEmpty:
return
default:
// StemNode or HashedNode encountered before reaching the group's bottom
// layer. Compute the leaf bitmap position where this node's hash will
// be stored.
leafPos := position
switch ref.Kind() {
case kindStem:
sn := s.getStem(ref.Index())
// Extend position using the stem's key bits so that
// GetValuesAtStem traversal (which follows key bits) finds the hash.
for d := 0; d < remainingDepth; d++ {
bit := sn.Stem[(absoluteDepth+d)/8] >> (7 - ((absoluteDepth + d) % 8)) & 1
leafPos = leafPos*2 + int(bit)
}
default:
// HashedNode or unknown: extend all-left (no key bits available).
// This matches the all-zero path that resolveNode would follow.
leafPos = position << remainingDepth
}
bitmap[leafPos/8] |= 1 << (7 - (leafPos % 8))
bitmapPos := position << remainingDepth
bitmap[bitmapPos/8] |= 1 << (7 - (bitmapPos % 8))
*hashes = append(*hashes, s.computeHash(ref))
*depths = append(*depths, uint8(groupDepth-remainingDepth))
}
}
// depthBits is the number of bits used to encode one depth offset.
const depthBits = 3
// packedDepthsLen returns the byte length of k packed depth entries
func packedDepthsLen(k int) int {
return (k*depthBits + 7) / 8
}
// writeDepth writes a depth entry at idx into the buf, MSB-first.
func writeDepth(buf []byte, idx int, v uint8) {
pos := idx * depthBits
for i := range depthBits {
bit := (v >> (depthBits - 1 - i)) & 1
p := pos + i
buf[p>>3] |= bit << (7 - (p & 7))
}
}
// readDepth reads a depth for entry idx from buf.
func readDepth(buf []byte, idx int) uint8 {
pos := idx * depthBits
var v uint8
for i := range depthBits {
p := pos + i
bit := (buf[p>>3] >> (7 - (p & 7))) & 1
v = v<<1 | bit
}
return v
}
// SerializeNode serializes a node into the flat on-disk format.
func (s *nodeStore) serializeNode(ref nodeRef, groupDepth int) []byte {
switch ref.Kind() {
case kindInternal:
// InternalNode group: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes
// InternalNode group format:
// [type(1)] [groupDepth(1)] [bitmap (2^groupDepth bits)] [depths(3 bits × K, padded)] [hashes(32B × K)]
bitmapSize := bitmapSizeForDepth(groupDepth)
bitmap := make([]byte, bitmapSize)
var hashes []common.Hash
var depths []uint8
node := s.getInternal(ref.Index())
s.serializeSubtree(ref, groupDepth, 0, int(node.depth), bitmap, &hashes)
s.serializeSubtree(ref, groupDepth, 0, groupDepth, bitmap, &hashes, &depths)
// Build serialized output
serializedLen := NodeTypeBytes + 1 + bitmapSize + len(hashes)*HashSize
k := len(hashes)
depthsLen := packedDepthsLen(k)
serializedLen := NodeTypeBytes + 1 + bitmapSize + depthsLen + k*HashSize
serialized := make([]byte, serializedLen)
serialized[0] = nodeTypeInternal
serialized[1] = byte(groupDepth) // group depth => bitmap size for a sparse group
serialized[1] = byte(groupDepth)
copy(serialized[2:2+bitmapSize], bitmap)
offset := NodeTypeBytes + 1 + bitmapSize
for _, h := range hashes {
copy(serialized[offset:offset+HashSize], h.Bytes())
offset += HashSize
depthsOff := NodeTypeBytes + 1 + bitmapSize
for i, d := range depths {
writeDepth(serialized[depthsOff:depthsOff+depthsLen], i, d-1)
}
hashesOff := depthsOff + depthsLen
for i, h := range hashes {
copy(serialized[hashesOff+i*HashSize:hashesOff+(i+1)*HashSize], h.Bytes())
}
return serialized
@ -229,56 +254,90 @@ func (s *nodeStore) deserializeNodeWithHash(serialized []byte, depth int, hn com
}
// deserializeSubtree reconstructs an InternalNode subtree from grouped serialization.
// remainingDepth is how many more levels to build, position is current index in the bitmap,
// nodeDepth is the actual trie depth for the node being created.
// hashIdx tracks the current position in the hash data (incremented as hashes are consumed).
func (s *nodeStore) deserializeSubtree(hn common.Hash, remainingDepth int, position int, nodeDepth int, bitmap []byte, hashData []byte, hashIdx *int, mustRecompute bool, dirty bool) (nodeRef, error) {
if remainingDepth == 0 {
// Bottom layer: check bitmap and return HashedNode or Empty
if bitmap[position/8]>>(7-(position%8))&1 == 1 {
if len(hashData) < (*hashIdx+1)*HashSize {
return emptyRef, errInvalidSerializedLength
}
hash := common.BytesToHash(hashData[*hashIdx*HashSize : (*hashIdx+1)*HashSize])
*hashIdx++
return s.newHashedRef(hash), nil
}
func (s *nodeStore) deserializeSubtree(hn common.Hash, groupDepth int, nodeDepth int, bitmap []byte, depths []byte, hashData []byte, mustRecompute bool, dirty bool) (nodeRef, error) {
if len(hashData)%HashSize != 0 {
return emptyRef, errInvalidSerializedLength
}
k := len(hashData) / HashSize
if len(depths) != packedDepthsLen(k) {
return emptyRef, errInvalidSerializedLength
}
if k == 0 {
return emptyRef, nil
}
// Check if this entire subtree is empty by examining all relevant bitmap bits
leftPos := position * 2
rightPos := position*2 + 1
// note that the parent might not need root computations, but the children
// do, because their hash isn't saved. Hence `mustRecompute` is set to `true`.
left, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, leftPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty)
if err != nil {
return emptyRef, err
}
right, err := s.deserializeSubtree(common.Hash{}, remainingDepth-1, rightPos, nodeDepth+1, bitmap, hashData, hashIdx, true, dirty)
if err != nil {
return emptyRef, err
}
// If both children are empty, return Empty
if left.IsEmpty() && right.IsEmpty() {
return emptyRef, nil
}
ref := s.newInternalRef(nodeDepth)
node := s.getInternal(ref.Index())
node.left = left
node.right = right
node.mustRecompute = mustRecompute
rootRef := s.newInternalRef(nodeDepth)
rootNode := s.getInternal(rootRef.Index())
rootNode.mustRecompute = mustRecompute
if !mustRecompute {
// mustRecompute will only be false for the root of the subtree,
// for which we already know the hash.
node.hash = hn
node.mustRecompute = false
rootNode.hash = hn
}
node.dirty = dirty
return ref, nil
rootNode.dirty = dirty
bitmapBits := 1 << groupDepth
entryIdx := 0
for bit := 0; bit < bitmapBits; bit++ {
if bitmap[bit/8]>>(7-(bit%8))&1 == 0 {
continue
}
depthOffset := int(readDepth(depths, entryIdx)) + 1
if depthOffset > groupDepth {
return emptyRef, errors.New("invalid depth offset")
}
// Canonical-encoding check: trailing position bits must be zero.
mask := (1 << (groupDepth - depthOffset)) - 1
if bit&mask != 0 {
return emptyRef, errors.New("non-canonical bitmap position")
}
var hash common.Hash
copy(hash[:], hashData[entryIdx*HashSize:(entryIdx+1)*HashSize])
if err := s.attachInGroup(rootRef, nodeDepth, groupDepth, depthOffset, bit, hash, dirty); err != nil {
return emptyRef, err
}
entryIdx++
}
return rootRef, nil
}
func (s *nodeStore) attachInGroup(rootRef nodeRef, rootDepth, groupDepth, depthOffset, bitmapPos int, hash common.Hash, dirty bool) error {
cur := rootRef
for level := 0; level < depthOffset-1; level++ {
bit := (bitmapPos >> (groupDepth - 1 - level)) & 1
node := s.getInternal(cur.Index())
childRef := node.left
if bit == 1 {
childRef = node.right
}
if childRef.IsEmpty() {
newRef := s.newInternalRef(rootDepth + level + 1)
s.getInternal(newRef.Index()).dirty = dirty
if bit == 0 {
node.left = newRef
} else {
node.right = newRef
}
cur = newRef
continue
}
if childRef.Kind() != kindInternal {
return errors.New("overlapping entries in group blob")
}
cur = childRef
}
leafBit := (bitmapPos >> (groupDepth - depthOffset)) & 1
node := s.getInternal(cur.Index())
if leafBit == 0 {
if !node.left.IsEmpty() {
return errors.New("overlapping entries in group blob")
}
node.left = s.newHashedRef(hash)
} else {
if !node.right.IsEmpty() {
return errors.New("overlapping entries in group blob")
}
node.right = s.newHashedRef(hash)
}
return nil
}
func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mustRecompute, dirty bool) (nodeRef, error) {
@ -288,7 +347,9 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus
switch serialized[0] {
case nodeTypeInternal:
// Grouped format: 1 byte type + 1 byte group depth + variable bitmap + N×32 byte hashes
// Grouped format:
// [type(1)] [groupDepth(1)] [bitmap (2^groupDepth bits, padded to bitmapSize bytes)]
// [depthOffsets (3 bits × K, padded to bytes)] [hashes (32B × K)]
if len(serialized) < NodeTypeBytes+1 {
return emptyRef, errInvalidSerializedLength
}
@ -301,10 +362,38 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus
return 0, errInvalidSerializedLength
}
bitmap := serialized[2 : 2+bitmapSize]
hashData := serialized[2+bitmapSize:]
hashIdx := 0
return s.deserializeSubtree(hn, groupDepth, 0, depth, bitmap, hashData, &hashIdx, mustRecompute, dirty)
bitmapBits := 1 << groupDepth
if bitmapBits < 8 {
padMask := byte(0xFF) >> bitmapBits
if bitmap[0]&padMask != 0 {
return emptyRef, errors.New("non-canonical bitmap padding")
}
}
k := 0
for _, b := range bitmap {
k += bits.OnesCount8(b)
}
depthsLen := packedDepthsLen(k)
expectedLen := NodeTypeBytes + 1 + bitmapSize + depthsLen + k*HashSize
if len(serialized) != expectedLen {
return emptyRef, errInvalidSerializedLength
}
depthsOff := NodeTypeBytes + 1 + bitmapSize
depths := serialized[depthsOff : depthsOff+depthsLen]
hashData := serialized[depthsOff+depthsLen : depthsOff+depthsLen+k*HashSize]
// Canonical-encoding check: the unused low bits of the last packed
// depth byte must be zero.
if usedBits := k * depthBits; usedBits%8 != 0 {
padMask := byte(0xFF) >> (usedBits % 8)
if depths[depthsLen-1]&padMask != 0 {
return emptyRef, errors.New("non-canonical depth padding")
}
}
return s.deserializeSubtree(hn, groupDepth, depth, bitmap, depths, hashData, mustRecompute, dirty)
case nodeTypeStem:
if len(serialized) < NodeTypeBytes+StemSize+StemBitmapSize {
@ -340,7 +429,10 @@ func (s *nodeStore) decodeNode(serialized []byte, depth int, hn common.Hash, mus
// CollectNodes flushes every node that needs flushing via flushfn in post-order.
// Invariant: any ancestor of a node that needs flushing is itself marked, so a
// clean root means the whole subtree is clean.
func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn, groupDepth int) {
//
// BitArray is passed by value (33 bytes) to keep child paths on the stack.
// Passing by pointer causes escape to heap per recursive call.
func (s *nodeStore) collectNodes(ref nodeRef, path BitArray, flushfn nodeFlushFn, groupDepth int) {
switch ref.Kind() {
case kindInternal:
node := s.getInternal(ref.Index())
@ -375,77 +467,51 @@ func (s *nodeStore) collectNodes(ref nodeRef, path []byte, flushfn nodeFlushFn,
// collectChildGroups traverses within a group to find and collect nodes in the next group.
// remainingLevels is how many more levels below the current node until we reach the group boundary.
// When remainingLevels=0, the current node's children are at the next group boundary.
func (s *nodeStore) collectChildGroups(node *InternalNode, path []byte, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error {
func (s *nodeStore) collectChildGroups(node *InternalNode, path BitArray, flushfn nodeFlushFn, groupDepth int, remainingLevels int) error {
if remainingLevels == 0 {
// Current node is at depth (groupBoundary - 1), its children are at the next group boundary
if !node.left.IsEmpty() {
s.collectNodes(node.left, appendBit(path, 0), flushfn, groupDepth)
leftPath := path
leftPath.AppendBit(&leftPath, 0)
s.collectNodes(node.left, leftPath, flushfn, groupDepth)
}
if !node.right.IsEmpty() {
s.collectNodes(node.right, appendBit(path, 1), flushfn, groupDepth)
rightPath := path
rightPath.AppendBit(&rightPath, 1)
s.collectNodes(node.right, rightPath, flushfn, groupDepth)
}
return nil
}
if !node.left.IsEmpty() {
leftPath := path
leftPath.AppendBit(&leftPath, 0)
switch node.left.Kind() {
case kindInternal:
n := s.getInternal(node.left.Index())
if err := s.collectChildGroups(n, appendBit(path, 0), flushfn, groupDepth, remainingLevels-1); err != nil {
if err := s.collectChildGroups(n, leftPath, flushfn, groupDepth, remainingLevels-1); err != nil {
return err
}
default:
extPath := s.extendPathToGroupLeaf(appendBit(path, 0), node.left, remainingLevels)
s.collectNodes(node.left, extPath, flushfn, groupDepth)
s.collectNodes(node.left, leftPath, flushfn, groupDepth)
}
}
if !node.right.IsEmpty() {
rightPath := path
rightPath.AppendBit(&rightPath, 1)
switch node.right.Kind() {
case kindInternal:
n := s.getInternal(node.right.Index())
if err := s.collectChildGroups(n, appendBit(path, 1), flushfn, groupDepth, remainingLevels-1); err != nil {
if err := s.collectChildGroups(n, rightPath, flushfn, groupDepth, remainingLevels-1); err != nil {
return err
}
default:
extPath := s.extendPathToGroupLeaf(appendBit(path, 1), node.right, remainingLevels)
s.collectNodes(node.right, extPath, flushfn, groupDepth)
s.collectNodes(node.right, rightPath, flushfn, groupDepth)
}
}
return nil
}
// extendPathToGroupLeaf extends a storage path to the group's leaf boundary,
// matching the projection done by serializeSubtree. For StemNodes, the path
// is extended using the stem's key bits (same as serializeSubtree). For other
// node types, the path is extended with all-zero (left) bits.
func (s *nodeStore) extendPathToGroupLeaf(path []byte, node nodeRef, remainingLevels int) []byte {
if remainingLevels <= 0 {
return path
}
if node.Kind() == kindStem {
sn := s.getStem(node.Index())
for _ = range remainingLevels {
bit := sn.Stem[len(path)/8] >> (7 - (len(path) % 8)) & 1
path = appendBit(path, bit)
}
} else {
// HashedNode or other: all-left extension (matches serializeSubtree's
// position << remainingDepth behavior).
for _ = range remainingLevels {
path = appendBit(path, 0)
}
}
return path
}
// appendBit appends a bit to a path, returning a new slice
func appendBit(path []byte, bit byte) []byte {
var p [256]byte
copy(p[:], path)
result := p[:len(path)]
return append(result, bit)
}
func (s *nodeStore) toDot(ref nodeRef, parent, path string) string {
switch ref.Kind() {
case kindInternal:

View file

@ -262,7 +262,15 @@ func (s *nodeStore) splitStemValuesInsert(existingRef nodeRef, newStem []byte, v
bitStem := existing.Stem[existing.depth/8] >> (7 - (existing.depth % 8)) & 1
nRef := s.newInternalRef(int(existing.depth))
nNode := s.getInternal(nRef.Index())
if !existing.dirty {
var buf [33]byte
oldPath := new(BitArray).SetBytes(existing.depth, existing.Stem[:]).PutKeyBytes(buf[:])
s.orphans[string(oldPath)] = struct{}{}
}
existing.depth++
// The existing stem's on-disk path lengthens by one bit, which means
// the stem must be re-flushed at the longer new path.
existing.dirty = true
bitKey := newStem[nNode.depth/8] >> (7 - (nNode.depth % 8)) & 1
if bitKey == bitStem {

View file

@ -347,12 +347,35 @@ func (t *BinaryTrie) Hash() common.Hash {
func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{})
// Pre-size the path buffer: collectNodes reuses it in-place via
// append/truncate; 32 covers typical binary-trie depth without regrowth.
pathBuf := make([]byte, 0, 32)
t.store.collectNodes(t.store.root, pathBuf, func(path []byte, hash common.Hash, serialized []byte) {
nodeset.AddNode(path, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(path)))
// Stem depth-promotion abandons a committed blob and is the only source of
// orphans. When none are pending (the common case) we skip tracking flushed
// paths entirely, keeping Commit allocation-free beyond the node set.
var added map[string]struct{}
if len(t.store.orphans) > 0 {
added = make(map[string]struct{})
}
var rootPath BitArray
t.store.collectNodes(t.store.root, rootPath, func(path BitArray, hash common.Hash, serialized []byte) {
var buf [33]byte
pathBytes := path.PutKeyBytes(buf[:])
if added != nil {
added[string(pathBytes)] = struct{}{}
}
nodeset.AddNode(pathBytes, trienode.NewNodeWithPrev(hash, serialized, t.tracer.Get(pathBytes)))
}, t.groupDepth)
// Delete blobs abandoned by stem depth-promotion, unless a freshly flushed
// node already reoccupies the path (the group-boundary case).
if len(t.store.orphans) > 0 {
for path := range t.store.orphans {
if _, ok := added[path]; ok {
continue
}
nodeset.AddNode([]byte(path), trienode.NewDeletedWithPrev(t.tracer.Get([]byte(path))))
}
t.store.orphans = make(map[string]struct{})
}
return t.Hash(), nodeset
}