diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go
index 173fc97def..408d0e3777 100644
--- a/cmd/geth/dbcmd.go
+++ b/cmd/geth/dbcmd.go
@@ -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 {
diff --git a/cmd/keeper/go.sum b/cmd/keeper/go.sum
index 51a6a3fad2..1906759f12 100644
--- a/cmd/keeper/go.sum
+++ b/cmd/keeper/go.sum
@@ -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=
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index 8063bc6419..f4cb72fcec 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -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
diff --git a/ethdb/pebble/pebble.go b/ethdb/pebble/pebble.go
index 7654d582c4..41ac260c38 100644
--- a/ethdb/pebble/pebble.go
+++ b/ethdb/pebble/pebble.go
@@ -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
diff --git a/ethdb/pebble/pebble_test.go b/ethdb/pebble/pebble_test.go
index e703a8d0ce..7d96a6626c 100644
--- a/ethdb/pebble/pebble_test.go
+++ b/ethdb/pebble/pebble_test.go
@@ -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")
+ }
+}
diff --git a/ethdb/pebble/pebble_v1.go b/ethdb/pebble/pebble_v1.go
new file mode 100644
index 0000000000..4d0ed3f2de
--- /dev/null
+++ b/ethdb/pebble/pebble_v1.go
@@ -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 .
+
+// 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
+ }
+}
diff --git a/ethdb/pebble/version.go b/ethdb/pebble/version.go
new file mode 100644
index 0000000000..64a8d3e153
--- /dev/null
+++ b/ethdb/pebble/version.go
@@ -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 .
+
+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
+}
diff --git a/ethdb/pebble/version_test.go b/ethdb/pebble/version_test.go
new file mode 100644
index 0000000000..58d74cafaf
--- /dev/null
+++ b/ethdb/pebble/version_test.go
@@ -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 .
+
+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")
+ }
+}
diff --git a/go.mod b/go.mod
index cc3f7e7eb1..a81ad08926 100644
--- a/go.mod
+++ b/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,7 @@ 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/klauspost/compress v1.17.11
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 +84,15 @@ 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/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
diff --git a/go.sum b/go.sum
index 1bc679a9f6..2150365e17 100644
--- a/go.sum
+++ b/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=
diff --git a/node/database.go b/node/database.go
index 274ccbfa7e..9fbd7140f0 100644
--- a/node/database.go
+++ b/node/database.go
@@ -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