mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-19 11:20:45 +00:00
Merge remote-tracking branch 'upstream/master' into bs/cell-blobpool/sparse-v2
This commit is contained in:
commit
c796080770
58 changed files with 4476 additions and 429 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
614
core/eip8037_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
core/rawdb/eradb/testdata/noreceipts/sepolia-00000-deadbeef-noreceipts.ere
vendored
Normal file
BIN
core/rawdb/eradb/testdata/noreceipts/sepolia-00000-deadbeef-noreceipts.ere
vendored
Normal file
Binary file not shown.
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
739
core/vm/eip8037_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
753
ethdb/pebble/pebble_v1.go
Normal 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
146
ethdb/pebble/version.go
Normal 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
|
||||
}
|
||||
135
ethdb/pebble/version_test.go
Normal file
135
ethdb/pebble/version_test.go
Normal 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
14
go.mod
|
|
@ -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
38
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
160
trie/bintrie/bitarray.go
Normal 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
|
||||
}
|
||||
}
|
||||
205
trie/bintrie/bitarray_test.go
Normal file
205
trie/bintrie/bitarray_test.go
Normal 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
445
trie/bintrie/format_test.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue