Merge tag 'v1.15.7' into release/geth-1.x-fh3.0

This commit is contained in:
Matthieu Vachon 2025-03-31 16:28:56 -04:00
commit da61ce6491
42 changed files with 901 additions and 445 deletions

13
.gitignore vendored
View file

@ -43,3 +43,16 @@ profile.cov
.vscode .vscode
tests/spec-tests/ tests/spec-tests/
# binaries
cmd/abidump/abidump
cmd/abigen/abigen
cmd/blsync/blsync
cmd/clef/clef
cmd/devp2p/devp2p
cmd/era/era
cmd/ethkey/ethkey
cmd/evm/evm
cmd/geth/geth
cmd/rlpdump/rlpdump
cmd/workload/workload

View file

@ -2,12 +2,6 @@ language: go
go_import_path: github.com/ethereum/go-ethereum go_import_path: github.com/ethereum/go-ethereum
sudo: false sudo: false
jobs: jobs:
allow_failures:
- stage: build
os: osx
env:
- azure-osx
include: include:
# This builder create and push the Docker images for all architectures # This builder create and push the Docker images for all architectures
- stage: build - stage: build
@ -62,23 +56,6 @@ jobs:
- go run build/ci.go install -dlgo -arch arm64 -cc aarch64-linux-gnu-gcc - go run build/ci.go install -dlgo -arch arm64 -cc aarch64-linux-gnu-gcc
- go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds - go run build/ci.go archive -arch arm64 -type tar -signer LINUX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds
# This builder does the OSX Azure uploads
- stage: build
if: type = push
os: osx
osx_image: xcode14.2
go: 1.23.1 # See https://github.com/ethereum/go-ethereum/pull/30478
env:
- azure-osx
git:
submodules: false # avoid cloning ethereum/tests
script:
- ln -sf /Users/travis/gopath/bin/go1.23.1 /usr/local/bin/go # Work around travis go-setup bug
- go run build/ci.go install -dlgo
- go run build/ci.go archive -type tar -signer OSX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds
- go run build/ci.go install -dlgo -arch arm64
- go run build/ci.go archive -arch arm64 -type tar -signer OSX_SIGNING_KEY -signify SIGNIFY_KEY -upload gethstore/builds
# These builders run the tests # These builders run the tests
- stage: build - stage: build
if: type = push if: type = push

View file

@ -390,6 +390,7 @@ func (c *BoundContract) estimateGasLimit(opts *TransactOpts, contract *common.Ad
GasFeeCap: gasFeeCap, GasFeeCap: gasFeeCap,
Value: value, Value: value,
Data: input, Data: input,
AccessList: opts.AccessList,
} }
return c.transactor.EstimateGas(ensureContext(opts.Context), msg) return c.transactor.EstimateGas(ensureContext(opts.Context), msg)
} }

View file

@ -37,7 +37,9 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/era" "github.com/ethereum/go-ethereum/internal/era"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -66,7 +68,7 @@ It expects the genesis file as argument.`,
Name: "dumpgenesis", Name: "dumpgenesis",
Usage: "Dumps genesis block JSON configuration to stdout", Usage: "Dumps genesis block JSON configuration to stdout",
ArgsUsage: "", ArgsUsage: "",
Flags: append([]cli.Flag{utils.DataDirFlag}, utils.NetworkFlags...), Flags: slices.Concat([]cli.Flag{utils.DataDirFlag}, utils.NetworkFlags),
Description: ` Description: `
The dumpgenesis command prints the genesis configuration of the network preset The dumpgenesis command prints the genesis configuration of the network preset
if one is set. Otherwise it prints the genesis from the datadir.`, if one is set. Otherwise it prints the genesis from the datadir.`,
@ -78,11 +80,11 @@ if one is set. Otherwise it prints the genesis from the datadir.`,
ArgsUsage: "<filename> (<filename 2> ... <filename N>) ", ArgsUsage: "<filename> (<filename 2> ... <filename N>) ",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{
utils.CacheFlag, utils.CacheFlag,
utils.SyncModeFlag,
utils.GCModeFlag, utils.GCModeFlag,
utils.SnapshotFlag, utils.SnapshotFlag,
utils.CacheDatabaseFlag, utils.CacheDatabaseFlag,
utils.CacheGCFlag, utils.CacheGCFlag,
utils.NoCompactionFlag,
utils.MetricsEnabledFlag, utils.MetricsEnabledFlag,
utils.MetricsEnabledExpensiveFlag, utils.MetricsEnabledExpensiveFlag,
utils.MetricsHTTPFlag, utils.MetricsHTTPFlag,
@ -105,7 +107,11 @@ if one is set. Otherwise it prints the genesis from the datadir.`,
utils.LogNoHistoryFlag, utils.LogNoHistoryFlag,
utils.LogExportCheckpointsFlag, utils.LogExportCheckpointsFlag,
utils.StateHistoryFlag, utils.StateHistoryFlag,
}, utils.DatabaseFlags), }, utils.DatabaseFlags, debug.Flags),
Before: func(ctx *cli.Context) error {
flags.MigrateGlobalFlags(ctx)
return debug.Setup(ctx)
},
Description: ` Description: `
The import command allows the import of blocks from an RLP-encoded format. This format can be a single file The import command allows the import of blocks from an RLP-encoded format. This format can be a single file
containing multiple RLP-encoded blocks, or multiple files can be given. containing multiple RLP-encoded blocks, or multiple files can be given.
@ -119,10 +125,7 @@ to import successfully.`,
Name: "export", Name: "export",
Usage: "Export blockchain into file", Usage: "Export blockchain into file",
ArgsUsage: "<filename> [<blockNumFirst> <blockNumLast>]", ArgsUsage: "<filename> [<blockNumFirst> <blockNumLast>]",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{utils.CacheFlag}, utils.DatabaseFlags),
utils.CacheFlag,
utils.SyncModeFlag,
}, utils.DatabaseFlags),
Description: ` Description: `
Requires a first argument of the file to write to. Requires a first argument of the file to write to.
Optional second and third arguments control the first and Optional second and third arguments control the first and
@ -135,12 +138,7 @@ be gzipped.`,
Name: "import-history", Name: "import-history",
Usage: "Import an Era archive", Usage: "Import an Era archive",
ArgsUsage: "<dir>", ArgsUsage: "<dir>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{utils.TxLookupLimitFlag, utils.TransactionHistoryFlag}, utils.DatabaseFlags, utils.NetworkFlags),
utils.TxLookupLimitFlag,
},
utils.DatabaseFlags,
utils.NetworkFlags,
),
Description: ` Description: `
The import-history command will import blocks and their corresponding receipts The import-history command will import blocks and their corresponding receipts
from Era archives. from Era archives.
@ -151,7 +149,7 @@ from Era archives.
Name: "export-history", Name: "export-history",
Usage: "Export blockchain history to Era archives", Usage: "Export blockchain history to Era archives",
ArgsUsage: "<dir> <first> <last>", ArgsUsage: "<dir> <first> <last>",
Flags: slices.Concat(utils.DatabaseFlags), Flags: utils.DatabaseFlags,
Description: ` Description: `
The export-history command will export blocks and their corresponding receipts The export-history command will export blocks and their corresponding receipts
into Era archives. Eras are typically packaged in steps of 8192 blocks. into Era archives. Eras are typically packaged in steps of 8192 blocks.
@ -162,10 +160,7 @@ into Era archives. Eras are typically packaged in steps of 8192 blocks.
Name: "import-preimages", Name: "import-preimages",
Usage: "Import the preimage database from an RLP stream", Usage: "Import the preimage database from an RLP stream",
ArgsUsage: "<datafile>", ArgsUsage: "<datafile>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{utils.CacheFlag}, utils.DatabaseFlags),
utils.CacheFlag,
utils.SyncModeFlag,
}, utils.DatabaseFlags),
Description: ` Description: `
The import-preimages command imports hash preimages from an RLP encoded stream. The import-preimages command imports hash preimages from an RLP encoded stream.
It's deprecated, please use "geth db import" instead. It's deprecated, please use "geth db import" instead.
@ -435,6 +430,10 @@ func importHistory(ctx *cli.Context) error {
network = "mainnet" network = "mainnet"
case ctx.Bool(utils.SepoliaFlag.Name): case ctx.Bool(utils.SepoliaFlag.Name):
network = "sepolia" network = "sepolia"
case ctx.Bool(utils.HoleskyFlag.Name):
network = "holesky"
case ctx.Bool(utils.HoodiFlag.Name):
network = "hoodi"
} }
} else { } else {
// No network flag set, try to determine network based on files // No network flag set, try to determine network based on files

View file

@ -89,9 +89,7 @@ Remove blockchain and state databases`,
Action: inspect, Action: inspect,
Name: "inspect", Name: "inspect",
ArgsUsage: "<prefix> <start>", ArgsUsage: "<prefix> <start>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Usage: "Inspect the storage size for each type of data in the database", Usage: "Inspect the storage size for each type of data in the database",
Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`,
} }
@ -109,16 +107,13 @@ a data corruption.`,
Action: dbStats, Action: dbStats,
Name: "stats", Name: "stats",
Usage: "Print leveldb statistics", Usage: "Print leveldb statistics",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
} }
dbCompactCmd = &cli.Command{ dbCompactCmd = &cli.Command{
Action: dbCompact, Action: dbCompact,
Name: "compact", Name: "compact",
Usage: "Compact leveldb database. WARNING: May take a very long time", Usage: "Compact leveldb database. WARNING: May take a very long time",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{
utils.SyncModeFlag,
utils.CacheFlag, utils.CacheFlag,
utils.CacheDatabaseFlag, utils.CacheDatabaseFlag,
}, utils.NetworkFlags, utils.DatabaseFlags), }, utils.NetworkFlags, utils.DatabaseFlags),
@ -131,9 +126,7 @@ corruption if it is aborted during execution'!`,
Name: "get", Name: "get",
Usage: "Show the value of a database key", Usage: "Show the value of a database key",
ArgsUsage: "<hex-encoded key>", ArgsUsage: "<hex-encoded key>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command looks up the specified database key from the database.", Description: "This command looks up the specified database key from the database.",
} }
dbDeleteCmd = &cli.Command{ dbDeleteCmd = &cli.Command{
@ -141,9 +134,7 @@ corruption if it is aborted during execution'!`,
Name: "delete", Name: "delete",
Usage: "Delete a database key (WARNING: may corrupt your database)", Usage: "Delete a database key (WARNING: may corrupt your database)",
ArgsUsage: "<hex-encoded key>", ArgsUsage: "<hex-encoded key>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: `This command deletes the specified database key from the database. Description: `This command deletes the specified database key from the database.
WARNING: This is a low-level operation which may cause database corruption!`, WARNING: This is a low-level operation which may cause database corruption!`,
} }
@ -152,9 +143,7 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Name: "put", Name: "put",
Usage: "Set the value of a database key (WARNING: may corrupt your database)", Usage: "Set the value of a database key (WARNING: may corrupt your database)",
ArgsUsage: "<hex-encoded key> <hex-encoded value>", ArgsUsage: "<hex-encoded key> <hex-encoded value>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: `This command sets a given database key to the given value. Description: `This command sets a given database key to the given value.
WARNING: This is a low-level operation which may cause database corruption!`, WARNING: This is a low-level operation which may cause database corruption!`,
} }
@ -163,9 +152,7 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Name: "dumptrie", Name: "dumptrie",
Usage: "Show the storage key/values of a given storage trie", Usage: "Show the storage key/values of a given storage trie",
ArgsUsage: "<hex-encoded state root> <hex-encoded account hash> <hex-encoded storage trie root> <hex-encoded start (optional)> <int max elements (optional)>", ArgsUsage: "<hex-encoded state root> <hex-encoded account hash> <hex-encoded storage trie root> <hex-encoded start (optional)> <int max elements (optional)>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command looks up the specified database key from the database.", Description: "This command looks up the specified database key from the database.",
} }
dbDumpFreezerIndex = &cli.Command{ dbDumpFreezerIndex = &cli.Command{
@ -173,9 +160,7 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Name: "freezer-index", Name: "freezer-index",
Usage: "Dump out the index of a specific freezer table", Usage: "Dump out the index of a specific freezer table",
ArgsUsage: "<freezer-type> <table-type> <start (int)> <end (int)>", ArgsUsage: "<freezer-type> <table-type> <start (int)> <end (int)>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "This command displays information about the freezer index.", Description: "This command displays information about the freezer index.",
} }
dbImportCmd = &cli.Command{ dbImportCmd = &cli.Command{
@ -183,9 +168,7 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Name: "import", Name: "import",
Usage: "Imports leveldb-data from an exported RLP dump.", Usage: "Imports leveldb-data from an exported RLP dump.",
ArgsUsage: "<dumpfile> <start (optional)", ArgsUsage: "<dumpfile> <start (optional)",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "The import command imports the specific chain data from an RLP encoded stream.", Description: "The import command imports the specific chain data from an RLP encoded stream.",
} }
dbExportCmd = &cli.Command{ dbExportCmd = &cli.Command{
@ -193,18 +176,14 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Name: "export", Name: "export",
Usage: "Exports the chain data into an RLP dump. If the <dumpfile> has .gz suffix, gzip compression will be used.", Usage: "Exports the chain data into an RLP dump. If the <dumpfile> has .gz suffix, gzip compression will be used.",
ArgsUsage: "<type> <dumpfile>", ArgsUsage: "<type> <dumpfile>",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.", Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.",
} }
dbMetadataCmd = &cli.Command{ dbMetadataCmd = &cli.Command{
Action: showMetaData, Action: showMetaData,
Name: "metadata", Name: "metadata",
Usage: "Shows metadata about the chain status.", Usage: "Shows metadata about the chain status.",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat(utils.NetworkFlags, utils.DatabaseFlags),
utils.SyncModeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Description: "Shows metadata about the chain status.", Description: "Shows metadata about the chain status.",
} }
dbInspectHistoryCmd = &cli.Command{ dbInspectHistoryCmd = &cli.Command{
@ -213,7 +192,6 @@ WARNING: This is a low-level operation which may cause database corruption!`,
Usage: "Inspect the state history within block range", Usage: "Inspect the state history within block range",
ArgsUsage: "<address> [OPTIONAL <storage-slot>]", ArgsUsage: "<address> [OPTIONAL <storage-slot>]",
Flags: slices.Concat([]cli.Flag{ Flags: slices.Concat([]cli.Flag{
utils.SyncModeFlag,
&cli.Uint64Flag{ &cli.Uint64Flag{
Name: "start", Name: "start",
Usage: "block number of the range start, zero means earliest history", Usage: "block number of the range start, zero means earliest history",

View file

@ -142,7 +142,6 @@ var (
utils.VMTraceJsonConfigFlag, utils.VMTraceJsonConfigFlag,
utils.NetworkIdFlag, utils.NetworkIdFlag,
utils.EthStatsURLFlag, utils.EthStatsURLFlag,
utils.NoCompactionFlag,
utils.GpoBlocksFlag, utils.GpoBlocksFlag,
utils.GpoPercentileFlag, utils.GpoPercentileFlag,
utils.GpoMaxGasPriceFlag, utils.GpoMaxGasPriceFlag,

View file

@ -54,8 +54,8 @@ func (iter *testIterator) Next() (byte, []byte, []byte, bool) {
if iter.index == 42 { if iter.index == 42 {
iter.index += 1 iter.index += 1
} }
return OpBatchAdd, []byte(fmt.Sprintf("key-%04d", iter.index)), return OpBatchAdd, fmt.Appendf(nil, "key-%04d", iter.index),
[]byte(fmt.Sprintf("value %d", iter.index)), true fmt.Appendf(nil, "value %d", iter.index), true
} }
func (iter *testIterator) Release() {} func (iter *testIterator) Release() {}
@ -72,7 +72,7 @@ func testExport(t *testing.T, f string) {
} }
// verify // verify
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
v, err := db.Get([]byte(fmt.Sprintf("key-%04d", i))) v, err := db.Get(fmt.Appendf(nil, "key-%04d", i))
if (i < 5 || i == 42) && err == nil { if (i < 5 || i == 42) && err == nil {
t.Fatalf("expected no element at idx %d, got '%v'", i, string(v)) t.Fatalf("expected no element at idx %d, got '%v'", i, string(v))
} }
@ -85,7 +85,7 @@ func testExport(t *testing.T, f string) {
} }
} }
} }
v, err := db.Get([]byte(fmt.Sprintf("key-%04d", 1000))) v, err := db.Get(fmt.Appendf(nil, "key-%04d", 1000))
if err == nil { if err == nil {
t.Fatalf("expected no element at idx %d, got '%v'", 1000, string(v)) t.Fatalf("expected no element at idx %d, got '%v'", 1000, string(v))
} }
@ -120,7 +120,7 @@ func (iter *deletionIterator) Next() (byte, []byte, []byte, bool) {
if iter.index == 42 { if iter.index == 42 {
iter.index += 1 iter.index += 1
} }
return OpBatchDel, []byte(fmt.Sprintf("key-%04d", iter.index)), nil, true return OpBatchDel, fmt.Appendf(nil, "key-%04d", iter.index), nil, true
} }
func (iter *deletionIterator) Release() {} func (iter *deletionIterator) Release() {}
@ -132,14 +132,14 @@ func testDeletion(t *testing.T, f string) {
} }
db := rawdb.NewMemoryDatabase() db := rawdb.NewMemoryDatabase()
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
db.Put([]byte(fmt.Sprintf("key-%04d", i)), []byte(fmt.Sprintf("value %d", i))) db.Put(fmt.Appendf(nil, "key-%04d", i), fmt.Appendf(nil, "value %d", i))
} }
err = ImportLDBData(db, f, 5, make(chan struct{})) err = ImportLDBData(db, f, 5, make(chan struct{}))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
v, err := db.Get([]byte(fmt.Sprintf("key-%04d", i))) v, err := db.Get(fmt.Appendf(nil, "key-%04d", i))
if i < 5 || i == 42 { if i < 5 || i == 42 {
if err != nil { if err != nil {
t.Fatalf("expected element at idx %d, got '%v'", i, err) t.Fatalf("expected element at idx %d, got '%v'", i, err)

View file

@ -72,7 +72,7 @@ func (i *HexOrDecimal256) MarshalText() ([]byte, error) {
if i == nil { if i == nil {
return []byte("0x0"), nil return []byte("0x0"), nil
} }
return []byte(fmt.Sprintf("%#x", (*big.Int)(i))), nil return fmt.Appendf(nil, "%#x", (*big.Int)(i)), nil
} }
// Decimal256 unmarshals big.Int as a decimal string. When unmarshalling, // Decimal256 unmarshals big.Int as a decimal string. When unmarshalling,

View file

@ -48,7 +48,7 @@ func (i *HexOrDecimal64) UnmarshalText(input []byte) error {
// MarshalText implements encoding.TextMarshaler. // MarshalText implements encoding.TextMarshaler.
func (i HexOrDecimal64) MarshalText() ([]byte, error) { func (i HexOrDecimal64) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%#x", uint64(i))), nil return fmt.Appendf(nil, "%#x", uint64(i)), nil
} }
// ParseUint64 parses s as an integer in decimal or hexadecimal syntax. // ParseUint64 parses s as an integer in decimal or hexadecimal syntax.

View file

@ -504,6 +504,13 @@ func GenerateVerkleChain(config *params.ChainConfig, parent *types.Block, engine
if gen != nil { if gen != nil {
gen(i, b) gen(i, b)
} }
requests := b.collectRequests(false)
if requests != nil {
reqHash := types.CalcRequestsHash(requests)
b.header.RequestsHash = &reqHash
}
body := &types.Body{ body := &types.Body{
Transactions: b.txs, Transactions: b.txs,
Uncles: b.uncles, Uncles: b.uncles,

View file

@ -83,7 +83,11 @@ func (cv *ChainView) getReceipts(number uint64) types.Receipts {
if number > cv.headNumber { if number > cv.headNumber {
panic("invalid block number") panic("invalid block number")
} }
return cv.chain.GetReceiptsByHash(cv.blockHash(number)) blockHash := cv.blockHash(number)
if blockHash == (common.Hash{}) {
log.Error("Chain view: block hash unavailable", "number", number, "head", cv.headNumber)
}
return cv.chain.GetReceiptsByHash(blockHash)
} }
// limitedView returns a new chain view that is a truncated version of the parent view. // limitedView returns a new chain view that is a truncated version of the parent view.

View file

@ -29,7 +29,6 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/leveldb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -59,6 +58,7 @@ type FilterMaps struct {
closeCh chan struct{} closeCh chan struct{}
closeWg sync.WaitGroup closeWg sync.WaitGroup
history uint64 history uint64
hashScheme bool // use hashdb-safe delete range method
exportFileName string exportFileName string
Params Params
@ -69,6 +69,7 @@ type FilterMaps struct {
// Matcher backend can read them under indexLock read lock. // Matcher backend can read them under indexLock read lock.
indexLock sync.RWMutex indexLock sync.RWMutex
indexedRange filterMapsRange indexedRange filterMapsRange
cleanedEpochsBefore uint32 // all unindexed data cleaned before this point
indexedView *ChainView // always consistent with the log index indexedView *ChainView // always consistent with the log index
hasTempRange bool hasTempRange bool
@ -180,6 +181,10 @@ type Config struct {
// This option enables the checkpoint JSON file generator. // This option enables the checkpoint JSON file generator.
// If set, the given file will be updated with checkpoint information. // If set, the given file will be updated with checkpoint information.
ExportFileName string ExportFileName string
// expect trie nodes of hash based state scheme in the filtermaps key range;
// use safe iterator based implementation of DeleteRange that skips them
HashScheme bool
} }
// NewFilterMaps creates a new FilterMaps and starts the indexer. // NewFilterMaps creates a new FilterMaps and starts the indexer.
@ -197,6 +202,7 @@ func NewFilterMaps(db ethdb.KeyValueStore, initView *ChainView, historyCutoff, f
blockProcessingCh: make(chan bool, 1), blockProcessingCh: make(chan bool, 1),
history: config.History, history: config.History,
disabled: config.Disabled, disabled: config.Disabled,
hashScheme: config.HashScheme,
disabledCh: make(chan struct{}), disabledCh: make(chan struct{}),
exportFileName: config.ExportFileName, exportFileName: config.ExportFileName,
Params: params, Params: params,
@ -208,6 +214,8 @@ func NewFilterMaps(db ethdb.KeyValueStore, initView *ChainView, historyCutoff, f
maps: common.NewRange(rs.MapsFirst, rs.MapsAfterLast-rs.MapsFirst), maps: common.NewRange(rs.MapsFirst, rs.MapsAfterLast-rs.MapsFirst),
tailPartialEpoch: rs.TailPartialEpoch, tailPartialEpoch: rs.TailPartialEpoch,
}, },
// deleting last unindexed epoch might have been interrupted by shutdown
cleanedEpochsBefore: max(rs.MapsFirst>>params.logMapsPerEpoch, 1) - 1,
historyCutoff: historyCutoff, historyCutoff: historyCutoff,
finalBlock: finalBlock, finalBlock: finalBlock,
matcherSyncCh: make(chan *FilterMapsMatcherBackend), matcherSyncCh: make(chan *FilterMapsMatcherBackend),
@ -301,14 +309,24 @@ func (f *FilterMaps) reset() {
// deleting the range first ensures that resetDb will be called again at next // deleting the range first ensures that resetDb will be called again at next
// startup and any leftover data will be removed even if it cannot finish now. // startup and any leftover data will be removed even if it cannot finish now.
rawdb.DeleteFilterMapsRange(f.db) rawdb.DeleteFilterMapsRange(f.db)
f.safeDeleteRange(rawdb.DeleteFilterMapsDb, "Resetting log index database") f.safeDeleteWithLogs(rawdb.DeleteFilterMapsDb, "Resetting log index database", f.isShuttingDown)
}
// isShuttingDown returns true if FilterMaps is shutting down.
func (f *FilterMaps) isShuttingDown() bool {
select {
case <-f.closeCh:
return true
default:
return false
}
} }
// init initializes an empty log index according to the current targetView. // init initializes an empty log index according to the current targetView.
func (f *FilterMaps) init() error { func (f *FilterMaps) init() error {
// ensure that there is no remaining data in the filter maps key range // ensure that there is no remaining data in the filter maps key range
if !f.safeDeleteRange(rawdb.DeleteFilterMapsDb, "Resetting log index database") { if err := f.safeDeleteWithLogs(rawdb.DeleteFilterMapsDb, "Resetting log index database", f.isShuttingDown); err != nil {
return errors.New("could not reset log index database") return err
} }
f.indexLock.Lock() f.indexLock.Lock()
@ -358,38 +376,37 @@ func (f *FilterMaps) init() error {
// removeBloomBits removes old bloom bits data from the database. // removeBloomBits removes old bloom bits data from the database.
func (f *FilterMaps) removeBloomBits() { func (f *FilterMaps) removeBloomBits() {
f.safeDeleteRange(rawdb.DeleteBloomBitsDb, "Removing old bloom bits database") f.safeDeleteWithLogs(rawdb.DeleteBloomBitsDb, "Removing old bloom bits database", f.isShuttingDown)
f.closeWg.Done() f.closeWg.Done()
} }
// safeDeleteRange calls the specified database range deleter function // safeDeleteWithLogs is a wrapper for a function that performs a safe range
// repeatedly as long as it returns leveldb.ErrTooManyKeys. // delete operation using rawdb.SafeDeleteRange. It emits log messages if the
// This wrapper is necessary because of the leveldb fallback implementation // process takes long enough to call the stop callback.
// of DeleteRange. func (f *FilterMaps) safeDeleteWithLogs(deleteFn func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error, action string, stopCb func() bool) error {
func (f *FilterMaps) safeDeleteRange(removeFn func(ethdb.KeyValueRangeDeleter) error, action string) bool { var (
start := time.Now() start = time.Now()
var retry bool logPrinted bool
for { lastLogPrinted = start
err := removeFn(f.db) )
if err == nil { switch err := deleteFn(f.db, f.hashScheme, func(deleted bool) bool {
if retry { if deleted && !logPrinted || time.Since(lastLogPrinted) > time.Second*10 {
log.Info(action+" finished", "elapsed", time.Since(start)) log.Info(action+" in progress...", "elapsed", common.PrettyDuration(time.Since(start)))
logPrinted, lastLogPrinted = true, time.Now()
} }
return true return stopCb()
}); {
case err == nil:
if logPrinted {
log.Info(action+" finished", "elapsed", common.PrettyDuration(time.Since(start)))
} }
if err != leveldb.ErrTooManyKeys { return nil
log.Error(action+" failed", "error", err) case errors.Is(err, rawdb.ErrDeleteRangeInterrupted):
return false log.Warn(action+" interrupted", "elapsed", common.PrettyDuration(time.Since(start)))
} return err
select {
case <-f.closeCh:
return false
default: default:
} log.Error(action+" failed", "error", err)
if !retry { return err
log.Info(action+" in progress...", "elapsed", time.Since(start))
retry = true
}
} }
} }
@ -658,54 +675,97 @@ func (f *FilterMaps) deleteLastBlockOfMap(batch ethdb.Batch, mapIndex uint32) {
rawdb.DeleteFilterMapLastBlock(batch, mapIndex) rawdb.DeleteFilterMapLastBlock(batch, mapIndex)
} }
// deleteTailEpoch deletes index data from the earliest, either fully or partially // deleteTailEpoch deletes index data from the specified epoch. The last block
// indexed epoch. The last block pointer for the last map of the epoch and the // pointer for the last map of the epoch and the corresponding block log value
// corresponding block log value pointer are retained as these are always assumed // pointer are retained as these are always assumed to be available for each
// to be available for each epoch. // epoch as boundary markers.
func (f *FilterMaps) deleteTailEpoch(epoch uint32) error { // The function returns true if all index data related to the epoch (except for
// the boundary markers) has been fully removed.
func (f *FilterMaps) deleteTailEpoch(epoch uint32) (bool, error) {
f.indexLock.Lock() f.indexLock.Lock()
defer f.indexLock.Unlock() defer f.indexLock.Unlock()
// determine epoch boundaries
firstMap := epoch << f.logMapsPerEpoch firstMap := epoch << f.logMapsPerEpoch
lastBlock, _, err := f.getLastBlockOfMap(firstMap + f.mapsPerEpoch - 1) lastBlock, _, err := f.getLastBlockOfMap(firstMap + f.mapsPerEpoch - 1)
if err != nil { if err != nil {
return fmt.Errorf("failed to retrieve last block of deleted epoch %d: %v", epoch, err) return false, fmt.Errorf("failed to retrieve last block of deleted epoch %d: %v", epoch, err)
} }
var firstBlock uint64 var firstBlock uint64
if epoch > 0 { if epoch > 0 {
firstBlock, _, err = f.getLastBlockOfMap(firstMap - 1) firstBlock, _, err = f.getLastBlockOfMap(firstMap - 1)
if err != nil { if err != nil {
return fmt.Errorf("failed to retrieve last block before deleted epoch %d: %v", epoch, err) return false, fmt.Errorf("failed to retrieve last block before deleted epoch %d: %v", epoch, err)
} }
firstBlock++ firstBlock++
} }
fmr := f.indexedRange // update rendered range if necessary
if f.indexedRange.maps.First() == firstMap && var (
f.indexedRange.maps.AfterLast() > firstMap+f.mapsPerEpoch && fmr = f.indexedRange
f.indexedRange.tailPartialEpoch == 0 { firstEpoch = f.indexedRange.maps.First() >> f.logMapsPerEpoch
fmr.maps.SetFirst(firstMap + f.mapsPerEpoch) afterLastEpoch = (f.indexedRange.maps.AfterLast() + f.mapsPerEpoch - 1) >> f.logMapsPerEpoch
fmr.blocks.SetFirst(lastBlock + 1) )
} else if f.indexedRange.maps.First() == firstMap+f.mapsPerEpoch { if f.indexedRange.tailPartialEpoch != 0 && firstEpoch > 0 {
fmr.tailPartialEpoch = 0 firstEpoch--
} else {
return errors.New("invalid tail epoch number")
} }
switch {
case epoch < firstEpoch:
// cleanup of already unindexed epoch; range not affected
case epoch == firstEpoch && epoch+1 < afterLastEpoch:
// first fully or partially rendered epoch and there is at least one
// rendered map in the next epoch; remove from indexed range
fmr.tailPartialEpoch = 0
fmr.maps.SetFirst((epoch + 1) << f.logMapsPerEpoch)
fmr.blocks.SetFirst(lastBlock + 1)
f.setRange(f.db, f.indexedView, fmr, false) f.setRange(f.db, f.indexedView, fmr, false)
default:
// cannot be cleaned or unindexed; return with error
return false, errors.New("invalid tail epoch number")
}
// remove index data
if err := f.safeDeleteWithLogs(func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error {
first := f.mapRowIndex(firstMap, 0) first := f.mapRowIndex(firstMap, 0)
count := f.mapRowIndex(firstMap+f.mapsPerEpoch, 0) - first count := f.mapRowIndex(firstMap+f.mapsPerEpoch, 0) - first
rawdb.DeleteFilterMapRows(f.db, common.NewRange(first, count)) if err := rawdb.DeleteFilterMapRows(f.db, common.NewRange(first, count), hashScheme, stopCb); err != nil {
return err
}
for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch; mapIndex++ { for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch; mapIndex++ {
f.filterMapCache.Remove(mapIndex) f.filterMapCache.Remove(mapIndex)
} }
rawdb.DeleteFilterMapLastBlocks(f.db, common.NewRange(firstMap, f.mapsPerEpoch-1)) // keep last enrty delMapRange := common.NewRange(firstMap, f.mapsPerEpoch-1) // keep last entry
if err := rawdb.DeleteFilterMapLastBlocks(f.db, delMapRange, hashScheme, stopCb); err != nil {
return err
}
for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch-1; mapIndex++ { for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch-1; mapIndex++ {
f.lastBlockCache.Remove(mapIndex) f.lastBlockCache.Remove(mapIndex)
} }
rawdb.DeleteBlockLvPointers(f.db, common.NewRange(firstBlock, lastBlock-firstBlock)) // keep last enrty delBlockRange := common.NewRange(firstBlock, lastBlock-firstBlock) // keep last entry
if err := rawdb.DeleteBlockLvPointers(f.db, delBlockRange, hashScheme, stopCb); err != nil {
return err
}
for blockNumber := firstBlock; blockNumber < lastBlock; blockNumber++ { for blockNumber := firstBlock; blockNumber < lastBlock; blockNumber++ {
f.lvPointerCache.Remove(blockNumber) f.lvPointerCache.Remove(blockNumber)
} }
return nil return nil
}, fmt.Sprintf("Deleting tail epoch #%d", epoch), func() bool {
f.processEvents()
return f.stop || !f.targetHeadIndexed()
}); err == nil {
// everything removed; mark as cleaned and report success
if f.cleanedEpochsBefore == epoch {
f.cleanedEpochsBefore = epoch + 1
}
return true, nil
} else {
// more data left in epoch range; mark as dirty and report unfinished
if f.cleanedEpochsBefore > epoch {
f.cleanedEpochsBefore = epoch
}
if errors.Is(err, rawdb.ErrDeleteRangeInterrupted) {
return false, nil
}
return false, err
}
} }
// exportCheckpoints exports epoch checkpoints in the format used by checkpoints.go. // exportCheckpoints exports epoch checkpoints in the format used by checkpoints.go.

View file

@ -17,6 +17,7 @@
package filtermaps package filtermaps
import ( import (
"errors"
"math" "math"
"time" "time"
@ -49,18 +50,15 @@ func (f *FilterMaps) indexerLoop() {
continue continue
} }
if err := f.init(); err != nil { if err := f.init(); err != nil {
log.Error("Error initializing log index; reverting to unindexed mode", "error", err) f.disableForError("initialization", err)
f.reset() f.reset() // remove broken index from DB
f.disabled = true
close(f.disabledCh)
return return
} }
} }
if !f.targetHeadIndexed() { if !f.targetHeadIndexed() {
if !f.tryIndexHead() { if err := f.tryIndexHead(); err != nil && err != errChainUpdate {
// either shutdown or unexpected error; in the latter case ensure f.disableForError("head rendering", err)
// that proper shutdown is still possible. return
f.processSingleEvent(true)
} }
} else { } else {
if f.finalBlock != f.lastFinal { if f.finalBlock != f.lastFinal {
@ -69,11 +67,37 @@ func (f *FilterMaps) indexerLoop() {
} }
f.lastFinal = f.finalBlock f.lastFinal = f.finalBlock
} }
if f.tryIndexTail() && f.tryUnindexTail() { // always attempt unindexing before indexing the tail in order to
// ensure that a potentially dirty previously unindexed epoch is
// always cleaned up before any new maps are rendered.
if done, err := f.tryUnindexTail(); err != nil {
f.disableForError("tail unindexing", err)
return
} else if !done {
continue
}
if done, err := f.tryIndexTail(); err != nil {
f.disableForError("tail rendering", err)
return
} else if !done {
continue
}
// tail indexing/unindexing is done; if head is also indexed then
// wait here until there is a new head
f.waitForNewHead() f.waitForNewHead()
} }
} }
} }
// disableForError is called when the indexer encounters a database error, for example a
// missing receipt. We can't continue operating when the database is broken, so the
// indexer goes into disabled state.
// Note that the partial index is left in disk; maybe a client update can fix the
// issue without reindexing.
func (f *FilterMaps) disableForError(op string, err error) {
log.Error("Log index "+op+" failed, reverting to unindexed mode", "error", err)
f.disabled = true
close(f.disabledCh)
} }
type targetUpdate struct { type targetUpdate struct {
@ -116,16 +140,17 @@ func (f *FilterMaps) SetBlockProcessing(blockProcessing bool) {
// WaitIdle blocks until the indexer is in an idle state while synced up to the // WaitIdle blocks until the indexer is in an idle state while synced up to the
// latest targetView. // latest targetView.
func (f *FilterMaps) WaitIdle() { func (f *FilterMaps) WaitIdle() {
if f.disabled {
f.closeWg.Wait()
return
}
for { for {
ch := make(chan bool) ch := make(chan bool)
f.waitIdleCh <- ch select {
case f.waitIdleCh <- ch:
if <-ch { if <-ch {
return return
} }
case <-f.disabledCh:
f.closeWg.Wait()
return
}
} }
} }
@ -196,16 +221,16 @@ func (f *FilterMaps) setTarget(target targetUpdate) {
f.finalBlock = target.finalBlock f.finalBlock = target.finalBlock
} }
// tryIndexHead tries to render head maps according to the current targetView // tryIndexHead tries to render head maps according to the current targetView.
// and returns true if successful. // Should be called when targetHeadIndexed returns false. If this function
func (f *FilterMaps) tryIndexHead() bool { // returns no error then either stop is true or head indexing is finished.
func (f *FilterMaps) tryIndexHead() error {
headRenderer, err := f.renderMapsBefore(math.MaxUint32) headRenderer, err := f.renderMapsBefore(math.MaxUint32)
if err != nil { if err != nil {
log.Error("Error creating log index head renderer", "error", err) return err
return false
} }
if headRenderer == nil { if headRenderer == nil {
return true return errors.New("head indexer has nothing to do") // tryIndexHead should be called when head is not indexed
} }
if !f.startedHeadIndex { if !f.startedHeadIndex {
f.lastLogHeadIndex = time.Now() f.lastLogHeadIndex = time.Now()
@ -230,8 +255,7 @@ func (f *FilterMaps) tryIndexHead() bool {
f.lastLogHeadIndex = time.Now() f.lastLogHeadIndex = time.Now()
} }
}); err != nil { }); err != nil {
log.Error("Log index head rendering failed", "error", err) return err
return false
} }
if f.loggedHeadIndex && f.indexedRange.hasIndexedBlocks() { if f.loggedHeadIndex && f.indexedRange.hasIndexedBlocks() {
log.Info("Log index head rendering finished", log.Info("Log index head rendering finished",
@ -240,7 +264,7 @@ func (f *FilterMaps) tryIndexHead() bool {
"elapsed", common.PrettyDuration(time.Since(f.startedHeadIndexAt))) "elapsed", common.PrettyDuration(time.Since(f.startedHeadIndexAt)))
} }
f.loggedHeadIndex, f.startedHeadIndex = false, false f.loggedHeadIndex, f.startedHeadIndex = false, false
return true return nil
} }
// tryIndexTail tries to render tail epochs until the tail target block is // tryIndexTail tries to render tail epochs until the tail target block is
@ -248,7 +272,7 @@ func (f *FilterMaps) tryIndexHead() bool {
// Note that tail indexing is only started if the log index head is fully // Note that tail indexing is only started if the log index head is fully
// rendered according to targetView and is suspended as soon as the targetView // rendered according to targetView and is suspended as soon as the targetView
// is changed. // is changed.
func (f *FilterMaps) tryIndexTail() bool { func (f *FilterMaps) tryIndexTail() (bool, error) {
for { for {
firstEpoch := f.indexedRange.maps.First() >> f.logMapsPerEpoch firstEpoch := f.indexedRange.maps.First() >> f.logMapsPerEpoch
if firstEpoch == 0 || !f.needTailEpoch(firstEpoch-1) { if firstEpoch == 0 || !f.needTailEpoch(firstEpoch-1) {
@ -256,7 +280,7 @@ func (f *FilterMaps) tryIndexTail() bool {
} }
f.processEvents() f.processEvents()
if f.stop || !f.targetHeadIndexed() { if f.stop || !f.targetHeadIndexed() {
return false return false, nil
} }
// resume process if tail rendering was interrupted because of head rendering // resume process if tail rendering was interrupted because of head rendering
tailRenderer := f.tailRenderer tailRenderer := f.tailRenderer
@ -268,8 +292,7 @@ func (f *FilterMaps) tryIndexTail() bool {
var err error var err error
tailRenderer, err = f.renderMapsBefore(f.indexedRange.maps.First()) tailRenderer, err = f.renderMapsBefore(f.indexedRange.maps.First())
if err != nil { if err != nil {
log.Error("Error creating log index tail renderer", "error", err) return false, err
return false
} }
} }
if tailRenderer == nil { if tailRenderer == nil {
@ -302,13 +325,16 @@ func (f *FilterMaps) tryIndexTail() bool {
f.lastLogTailIndex = time.Now() f.lastLogTailIndex = time.Now()
} }
}) })
if err != nil && f.needTailEpoch(firstEpoch-1) { if err != nil && !f.needTailEpoch(firstEpoch-1) {
// stop silently if cutoff point has move beyond epoch boundary while rendering // stop silently if cutoff point has move beyond epoch boundary while rendering
log.Error("Log index tail rendering failed", "error", err) return true, nil
}
if err != nil {
return false, err
} }
if !done { if !done {
f.tailRenderer = tailRenderer // only keep tail renderer if interrupted by stopCb f.tailRenderer = tailRenderer // only keep tail renderer if interrupted by stopCb
return false return false, nil
} }
} }
if f.loggedTailIndex && f.indexedRange.hasIndexedBlocks() { if f.loggedTailIndex && f.indexedRange.hasIndexedBlocks() {
@ -318,32 +344,31 @@ func (f *FilterMaps) tryIndexTail() bool {
"elapsed", common.PrettyDuration(time.Since(f.startedTailIndexAt))) "elapsed", common.PrettyDuration(time.Since(f.startedTailIndexAt)))
f.loggedTailIndex = false f.loggedTailIndex = false
} }
return true return true, nil
} }
// tryUnindexTail removes entire epochs of log index data as long as the first // tryUnindexTail removes entire epochs of log index data as long as the first
// fully indexed block is at least as old as the tail target. // fully indexed block is at least as old as the tail target.
// Note that unindexing is very quick as it only removes continuous ranges of // Note that unindexing is very quick as it only removes continuous ranges of
// data from the database and is also called while running head indexing. // data from the database and is also called while running head indexing.
func (f *FilterMaps) tryUnindexTail() bool { func (f *FilterMaps) tryUnindexTail() (bool, error) {
for { firstEpoch := f.indexedRange.maps.First() >> f.logMapsPerEpoch
firstEpoch := (f.indexedRange.maps.First() - f.indexedRange.tailPartialEpoch) >> f.logMapsPerEpoch if f.indexedRange.tailPartialEpoch > 0 && firstEpoch > 0 {
if f.needTailEpoch(firstEpoch) { firstEpoch--
break
}
f.processEvents()
if f.stop {
return false
} }
for epoch := min(firstEpoch, f.cleanedEpochsBefore); !f.needTailEpoch(epoch); epoch++ {
if !f.startedTailUnindex { if !f.startedTailUnindex {
f.startedTailUnindexAt = time.Now() f.startedTailUnindexAt = time.Now()
f.startedTailUnindex = true f.startedTailUnindex = true
f.ptrTailUnindexMap = f.indexedRange.maps.First() - f.indexedRange.tailPartialEpoch f.ptrTailUnindexMap = f.indexedRange.maps.First() - f.indexedRange.tailPartialEpoch
f.ptrTailUnindexBlock = f.indexedRange.blocks.First() - f.tailPartialBlocks() f.ptrTailUnindexBlock = f.indexedRange.blocks.First() - f.tailPartialBlocks()
} }
if err := f.deleteTailEpoch(firstEpoch); err != nil { if done, err := f.deleteTailEpoch(epoch); !done {
log.Error("Log index tail epoch unindexing failed", "error", err) return false, err
return false }
f.processEvents()
if f.stop || !f.targetHeadIndexed() {
return false, nil
} }
} }
if f.startedTailUnindex && f.indexedRange.hasIndexedBlocks() { if f.startedTailUnindex && f.indexedRange.hasIndexedBlocks() {
@ -354,7 +379,7 @@ func (f *FilterMaps) tryUnindexTail() bool {
"elapsed", common.PrettyDuration(time.Since(f.startedTailUnindexAt))) "elapsed", common.PrettyDuration(time.Since(f.startedTailUnindexAt)))
f.startedTailUnindex = false f.startedTailUnindex = false
} }
return true return true, nil
} }
// needTailEpoch returns true if the given tail epoch needs to be kept // needTailEpoch returns true if the given tail epoch needs to be kept

View file

@ -354,10 +354,8 @@ func WriteFilterMapBaseRows(db ethdb.KeyValueWriter, mapRowIndex uint64, rows []
} }
} }
func DeleteFilterMapRows(db ethdb.KeyValueRangeDeleter, mapRows common.Range[uint64]) { func DeleteFilterMapRows(db ethdb.KeyValueStore, mapRows common.Range[uint64], hashScheme bool, stopCallback func(bool) bool) error {
if err := db.DeleteRange(filterMapRowKey(mapRows.First(), false), filterMapRowKey(mapRows.AfterLast(), false)); err != nil { return SafeDeleteRange(db, filterMapRowKey(mapRows.First(), false), filterMapRowKey(mapRows.AfterLast(), false), hashScheme, stopCallback)
log.Crit("Failed to delete range of filter map rows", "err", err)
}
} }
// ReadFilterMapLastBlock retrieves the number of the block that generated the // ReadFilterMapLastBlock retrieves the number of the block that generated the
@ -368,7 +366,7 @@ func ReadFilterMapLastBlock(db ethdb.KeyValueReader, mapIndex uint32) (uint64, c
return 0, common.Hash{}, err return 0, common.Hash{}, err
} }
if len(enc) != 40 { if len(enc) != 40 {
return 0, common.Hash{}, errors.New("Invalid block number and id encoding") return 0, common.Hash{}, errors.New("invalid block number and id encoding")
} }
var id common.Hash var id common.Hash
copy(id[:], enc[8:]) copy(id[:], enc[8:])
@ -394,10 +392,8 @@ func DeleteFilterMapLastBlock(db ethdb.KeyValueWriter, mapIndex uint32) {
} }
} }
func DeleteFilterMapLastBlocks(db ethdb.KeyValueRangeDeleter, maps common.Range[uint32]) { func DeleteFilterMapLastBlocks(db ethdb.KeyValueStore, maps common.Range[uint32], hashScheme bool, stopCallback func(bool) bool) error {
if err := db.DeleteRange(filterMapLastBlockKey(maps.First()), filterMapLastBlockKey(maps.AfterLast())); err != nil { return SafeDeleteRange(db, filterMapLastBlockKey(maps.First()), filterMapLastBlockKey(maps.AfterLast()), hashScheme, stopCallback)
log.Crit("Failed to delete range of filter map last block pointers", "err", err)
}
} }
// ReadBlockLvPointer retrieves the starting log value index where the log values // ReadBlockLvPointer retrieves the starting log value index where the log values
@ -408,7 +404,7 @@ func ReadBlockLvPointer(db ethdb.KeyValueReader, blockNumber uint64) (uint64, er
return 0, err return 0, err
} }
if len(encPtr) != 8 { if len(encPtr) != 8 {
return 0, errors.New("Invalid log value pointer encoding") return 0, errors.New("invalid log value pointer encoding")
} }
return binary.BigEndian.Uint64(encPtr), nil return binary.BigEndian.Uint64(encPtr), nil
} }
@ -431,10 +427,8 @@ func DeleteBlockLvPointer(db ethdb.KeyValueWriter, blockNumber uint64) {
} }
} }
func DeleteBlockLvPointers(db ethdb.KeyValueRangeDeleter, blocks common.Range[uint64]) { func DeleteBlockLvPointers(db ethdb.KeyValueStore, blocks common.Range[uint64], hashScheme bool, stopCallback func(bool) bool) error {
if err := db.DeleteRange(filterMapBlockLVKey(blocks.First()), filterMapBlockLVKey(blocks.AfterLast())); err != nil { return SafeDeleteRange(db, filterMapBlockLVKey(blocks.First()), filterMapBlockLVKey(blocks.AfterLast()), hashScheme, stopCallback)
log.Crit("Failed to delete range of block log value pointers", "err", err)
}
} }
// FilterMapsRange is a storage representation of the block range covered by the // FilterMapsRange is a storage representation of the block range covered by the
@ -485,22 +479,22 @@ func DeleteFilterMapsRange(db ethdb.KeyValueWriter) {
} }
// deletePrefixRange deletes everything with the given prefix from the database. // deletePrefixRange deletes everything with the given prefix from the database.
func deletePrefixRange(db ethdb.KeyValueRangeDeleter, prefix []byte) error { func deletePrefixRange(db ethdb.KeyValueStore, prefix []byte, hashScheme bool, stopCallback func(bool) bool) error {
end := bytes.Clone(prefix) end := bytes.Clone(prefix)
end[len(end)-1]++ end[len(end)-1]++
return db.DeleteRange(prefix, end) return SafeDeleteRange(db, prefix, end, hashScheme, stopCallback)
} }
// DeleteFilterMapsDb removes the entire filter maps database // DeleteFilterMapsDb removes the entire filter maps database
func DeleteFilterMapsDb(db ethdb.KeyValueRangeDeleter) error { func DeleteFilterMapsDb(db ethdb.KeyValueStore, hashScheme bool, stopCallback func(bool) bool) error {
return deletePrefixRange(db, []byte(filterMapsPrefix)) return deletePrefixRange(db, []byte(filterMapsPrefix), hashScheme, stopCallback)
} }
// DeleteFilterMapsDb removes the old bloombits database and the associated // DeleteBloomBitsDb removes the old bloombits database and the associated
// chain indexer database. // chain indexer database.
func DeleteBloomBitsDb(db ethdb.KeyValueRangeDeleter) error { func DeleteBloomBitsDb(db ethdb.KeyValueStore, hashScheme bool, stopCallback func(bool) bool) error {
if err := deletePrefixRange(db, bloomBitsPrefix); err != nil { if err := deletePrefixRange(db, bloomBitsPrefix, hashScheme, stopCallback); err != nil {
return err return err
} }
return deletePrefixRange(db, bloomBitsMetaPrefix) return deletePrefixRange(db, bloomBitsMetaPrefix, hashScheme, stopCallback)
} }

View file

@ -28,12 +28,15 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
) )
var ErrDeleteRangeInterrupted = errors.New("safe delete range operation interrupted")
// freezerdb is a database wrapper that enables ancient chain segment freezing. // freezerdb is a database wrapper that enables ancient chain segment freezing.
type freezerdb struct { type freezerdb struct {
ethdb.KeyValueStore ethdb.KeyValueStore
@ -388,10 +391,6 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
verkleTries stat verkleTries stat
verkleStateLookups stat verkleStateLookups stat
// Les statistic
chtTrieNodes stat
bloomTrieNodes stat
// Meta- and unaccounted data // Meta- and unaccounted data
metadata stat metadata stat
unaccounted stat unaccounted stat
@ -465,16 +464,6 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
case bytes.HasPrefix(key, bloomBitsMetaPrefix) && len(key) < len(bloomBitsMetaPrefix)+8: case bytes.HasPrefix(key, bloomBitsMetaPrefix) && len(key) < len(bloomBitsMetaPrefix)+8:
bloomBits.Add(size) bloomBits.Add(size)
// LES indexes (deprecated)
case bytes.HasPrefix(key, chtTablePrefix) ||
bytes.HasPrefix(key, chtIndexTablePrefix) ||
bytes.HasPrefix(key, chtPrefix): // Canonical hash trie
chtTrieNodes.Add(size)
case bytes.HasPrefix(key, bloomTrieTablePrefix) ||
bytes.HasPrefix(key, bloomTrieIndexPrefix) ||
bytes.HasPrefix(key, bloomTriePrefix): // Bloomtrie sub
bloomTrieNodes.Add(size)
// Verkle trie data is detected, determine the sub-category // Verkle trie data is detected, determine the sub-category
case bytes.HasPrefix(key, VerklePrefix): case bytes.HasPrefix(key, VerklePrefix):
remain := key[len(VerklePrefix):] remain := key[len(VerklePrefix):]
@ -538,8 +527,6 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
{"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()}, {"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()},
{"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()}, {"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()},
{"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()},
{"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()},
{"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()},
} }
// Inspect all registered append-only file store then. // Inspect all registered append-only file store then.
ancients, err := inspectFreezers(db) ancients, err := inspectFreezers(db)
@ -623,3 +610,73 @@ func ReadChainMetadata(db ethdb.KeyValueStore) [][]string {
} }
return data return data
} }
// SafeDeleteRange deletes all of the keys (and values) in the range
// [start,end) (inclusive on start, exclusive on end).
// If hashScheme is true then it always uses an iterator and skips hashdb trie
// node entries. If it is false and the backing db is pebble db then it uses
// the fast native range delete.
// In case of fallback mode (hashdb or leveldb) the range deletion might be
// very slow depending on the number of entries. In this case stopCallback
// is periodically called and if it returns an error then SafeDeleteRange
// stops and also returns that error. The callback is not called if native
// range delete is used or there are a small number of keys only. The bool
// argument passed to the callback is true if enrties have actually been
// deleted already.
func SafeDeleteRange(db ethdb.KeyValueStore, start, end []byte, hashScheme bool, stopCallback func(bool) bool) error {
if !hashScheme {
// delete entire range; use fast native range delete on pebble db
for {
switch err := db.DeleteRange(start, end); {
case err == nil:
return nil
case errors.Is(err, ethdb.ErrTooManyKeys):
if stopCallback(true) {
return ErrDeleteRangeInterrupted
}
default:
return err
}
}
}
var (
count, deleted, skipped int
buff = crypto.NewKeccakState()
startTime = time.Now()
)
batch := db.NewBatch()
it := db.NewIterator(nil, start)
defer func() {
it.Release() // it might be replaced during the process
log.Debug("SafeDeleteRange finished", "deleted", deleted, "skipped", skipped, "elapsed", common.PrettyDuration(time.Since(startTime)))
}()
for it.Next() && bytes.Compare(end, it.Key()) > 0 {
// Prevent deletion for trie nodes in hash mode
if len(it.Key()) != 32 || crypto.HashData(buff, it.Value()) != common.BytesToHash(it.Key()) {
if err := batch.Delete(it.Key()); err != nil {
return err
}
deleted++
} else {
skipped++
}
count++
if count > 10000 { // should not block for more than a second
if err := batch.Write(); err != nil {
return err
}
if stopCallback(deleted != 0) {
return ErrDeleteRangeInterrupted
}
start = append(bytes.Clone(it.Key()), 0) // appending a zero gives us the next possible key
it.Release()
batch = db.NewBatch()
it = db.NewIterator(nil, start)
count = 0
}
}
return batch.Write()
}

View file

@ -144,14 +144,6 @@ var (
// old log index // old log index
bloomBitsMetaPrefix = []byte("iB") bloomBitsMetaPrefix = []byte("iB")
// LES indexes
chtPrefix = []byte("chtRootV2-") // ChtPrefix + chtNum (uint64 big endian) -> trie root hash
chtTablePrefix = []byte("cht-")
chtIndexTablePrefix = []byte("chtIndexV2-")
bloomTriePrefix = []byte("bltRoot-") // BloomTriePrefix + bloomTrieNum (uint64 big endian) -> trie root hash
bloomTrieTablePrefix = []byte("blt-")
bloomTrieIndexPrefix = []byte("bltIndex-")
preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil) preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil)
preimageHitsCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) preimageHitsCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil)
preimageMissCounter = metrics.NewRegisteredCounter("db/preimage/miss", nil) preimageMissCounter = metrics.NewRegisteredCounter("db/preimage/miss", nil)

View file

@ -655,7 +655,7 @@ func testGenerateWithManyExtraAccounts(t *testing.T, scheme string) {
for i := 0; i < 1000; i++ { for i := 0; i < 1000; i++ {
acc := &types.StateAccount{Balance: uint256.NewInt(uint64(i)), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()} acc := &types.StateAccount{Balance: uint256.NewInt(uint64(i)), Root: types.EmptyRootHash, CodeHash: types.EmptyCodeHash.Bytes()}
val, _ := rlp.EncodeToBytes(acc) val, _ := rlp.EncodeToBytes(acc)
key := hashData([]byte(fmt.Sprintf("acc-%d", i))) key := hashData(fmt.Appendf(nil, "acc-%d", i))
rawdb.WriteAccountSnapshot(helper.diskdb, key, val) rawdb.WriteAccountSnapshot(helper.diskdb, key, val)
} }
} }

View file

@ -329,27 +329,27 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte) h = make(map[common.Hash][]byte)
) )
for i := byte(2); i < 0xff; i++ { for i := byte(2); i < 0xff; i++ {
a[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 0, i)) a[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 0, i)
if i > 20 && i%2 == 0 { if i > 20 && i%2 == 0 {
b[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 1, i)) b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i)
} }
if i%4 == 0 { if i%4 == 0 {
c[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 2, i)) c[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 2, i)
} }
if i%7 == 0 { if i%7 == 0 {
d[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 3, i)) d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i)
} }
if i%8 == 0 { if i%8 == 0 {
e[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 4, i)) e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
} }
if i > 50 || i < 85 { if i > 50 || i < 85 {
f[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 5, i)) f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
} }
if i%64 == 0 { if i%64 == 0 {
g[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 6, i)) g[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 6, i)
} }
if i%128 == 0 { if i%128 == 0 {
h[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 7, i)) h[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 7, i)
} }
} }
// Assemble a stack of snapshots from the account layers // Assemble a stack of snapshots from the account layers
@ -428,27 +428,27 @@ func TestStorageIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte) h = make(map[common.Hash][]byte)
) )
for i := byte(2); i < 0xff; i++ { for i := byte(2); i < 0xff; i++ {
a[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 0, i)) a[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 0, i)
if i > 20 && i%2 == 0 { if i > 20 && i%2 == 0 {
b[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 1, i)) b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i)
} }
if i%4 == 0 { if i%4 == 0 {
c[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 2, i)) c[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 2, i)
} }
if i%7 == 0 { if i%7 == 0 {
d[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 3, i)) d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i)
} }
if i%8 == 0 { if i%8 == 0 {
e[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 4, i)) e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
} }
if i > 50 || i < 85 { if i > 50 || i < 85 {
f[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 5, i)) f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
} }
if i%64 == 0 { if i%64 == 0 {
g[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 6, i)) g[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 6, i)
} }
if i%128 == 0 { if i%128 == 0 {
h[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 7, i)) h[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 7, i)
} }
} }
// Assemble a stack of snapshots from the account layers // Assemble a stack of snapshots from the account layers

View file

@ -603,7 +603,6 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
// Insert into the live set // Insert into the live set
obj := newObject(s, addr, acct) obj := newObject(s, addr, acct)
s.setStateObject(obj) s.setStateObject(obj)
s.AccountLoaded++
return obj return obj
} }

View file

@ -618,7 +618,7 @@ func (pool *LegacyPool) checkDelegationLimit(tx *types.Transaction) error {
from, _ := types.Sender(pool.signer, tx) // validated from, _ := types.Sender(pool.signer, tx) // validated
// Short circuit if the sender has neither delegation nor pending delegation. // Short circuit if the sender has neither delegation nor pending delegation.
if pool.currentState.GetCodeHash(from) == types.EmptyCodeHash && len(pool.all.auths[from]) == 0 { if pool.currentState.GetCodeHash(from) == types.EmptyCodeHash && pool.all.delegationTxsCount(from) == 0 {
return nil return nil
} }
pending := pool.pending[from] pending := pool.pending[from]
@ -1849,6 +1849,13 @@ func (t *lookup) removeAuthorities(tx *types.Transaction) {
} }
} }
// delegationTxsCount returns the number of pending authorizations for the specified address.
func (t *lookup) delegationTxsCount(addr common.Address) int {
t.lock.RLock()
defer t.lock.RUnlock()
return len(t.auths[addr])
}
// numSlots calculates the number of slots needed for a single transaction. // numSlots calculates the number of slots needed for a single transaction.
func numSlots(tx *types.Transaction) int { func numSlots(tx *types.Transaction) int {
return int((tx.Size() + txSlotSize - 1) / txSlotSize) return int((tx.Size() + txSlotSize - 1) / txSlotSize)

View file

@ -108,6 +108,19 @@ func (env *testEnv) makeTx(nonce uint64, gasPrice *big.Int) *types.Transaction {
return tx return tx
} }
func (env *testEnv) makeTxs(n int) []*types.Transaction {
head := env.chain.CurrentHeader()
state, _ := env.chain.StateAt(head.Root)
nonce := state.GetNonce(address)
var txs []*types.Transaction
for i := 0; i < n; i++ {
tx, _ := types.SignTx(types.NewTransaction(nonce+uint64(i), common.Address{0x00}, big.NewInt(1000), params.TxGas, big.NewInt(params.GWei), nil), signer, key)
txs = append(txs, tx)
}
return txs
}
func (env *testEnv) commit() { func (env *testEnv) commit() {
head := env.chain.CurrentBlock() head := env.chain.CurrentBlock()
block := env.chain.GetBlock(head.Hash(), head.Number.Uint64()) block := env.chain.GetBlock(head.Hash(), head.Number.Uint64())
@ -177,3 +190,29 @@ func TestRejectInvalids(t *testing.T) {
} }
} }
} }
func TestResubmit(t *testing.T) {
env := newTestEnv(t, 10, 0, "")
defer env.close()
txs := env.makeTxs(10)
txsA := txs[:len(txs)/2]
txsB := txs[len(txs)/2:]
env.pool.Add(txsA, true)
pending, queued := env.pool.ContentFrom(address)
if len(pending) != len(txsA) || len(queued) != 0 {
t.Fatalf("Unexpected txpool content: %d, %d", len(pending), len(queued))
}
env.tracker.TrackAll(txs)
resubmit, all := env.tracker.recheck(true)
if len(resubmit) != len(txsB) {
t.Fatalf("Unexpected transactions to resubmit, got: %d, want: %d", len(resubmit), len(txsB))
}
if len(all) == 0 || len(all[address]) == 0 {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", 0, len(txs))
}
if len(all[address]) != len(txs) {
t.Fatalf("Unexpected transactions being tracked, got: %d, want: %d", len(all[address]), len(txs))
}
}

View file

@ -466,9 +466,9 @@ func (p *TxPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool)
return p.subs.Track(event.JoinSubscriptions(subs...)) return p.subs.Track(event.JoinSubscriptions(subs...))
} }
// Nonce returns the next nonce of an account, with all transactions executable // PoolNonce returns the next nonce of an account, with all transactions executable
// by the pool already applied on top. // by the pool already applied on top.
func (p *TxPool) Nonce(addr common.Address) uint64 { func (p *TxPool) PoolNonce(addr common.Address) uint64 {
// Since (for now) accounts are unique to subpools, only one pool will have // Since (for now) accounts are unique to subpools, only one pool will have
// (at max) a non-state nonce. To avoid stateful lookups, just return the // (at max) a non-state nonce. To avoid stateful lookups, just return the
// highest nonce for now. // highest nonce for now.
@ -481,6 +481,15 @@ func (p *TxPool) Nonce(addr common.Address) uint64 {
return nonce return nonce
} }
// Nonce returns the next nonce of an account at the current chain head. Unlike
// PoolNonce, this function does not account for pending executable transactions.
func (p *TxPool) Nonce(addr common.Address) uint64 {
p.stateLock.RLock()
defer p.stateLock.RUnlock()
return p.state.GetNonce(addr)
}
// Stats retrieves the current pool stats, namely the number of pending and the // Stats retrieves the current pool stats, namely the number of pending and the
// number of queued (non-executable) transactions. // number of queued (non-executable) transactions.
func (p *TxPool) Stats() (int, int) { func (p *TxPool) Stats() (int, int) {
@ -556,6 +565,9 @@ func (p *TxPool) Sync() error {
// Clear removes all tracked txs from the subpools. // Clear removes all tracked txs from the subpools.
func (p *TxPool) Clear() { func (p *TxPool) Clear() {
// Invoke Sync to ensure that txs pending addition don't get added to the pool after
// the subpools are subsequently cleared
p.Sync()
for _, subpool := range p.subpools { for _, subpool := range p.subpools {
subpool.Clear() subpool.Clear()
} }

View file

@ -327,7 +327,7 @@ func (b *EthAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash)
} }
func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) {
return b.eth.txPool.Nonce(addr), nil return b.eth.txPool.PoolNonce(addr), nil
} }
func (b *EthAPIBackend) Stats() (runnable int, blocked int) { func (b *EthAPIBackend) Stats() (runnable int, blocked int) {

View file

@ -239,7 +239,12 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmConfig := filtermaps.Config{History: config.LogHistory, Disabled: config.LogNoHistory, ExportFileName: config.LogExportCheckpoints} fmConfig := filtermaps.Config{
History: config.LogHistory,
Disabled: config.LogNoHistory,
ExportFileName: config.LogExportCheckpoints,
HashScheme: scheme == rawdb.HashScheme,
}
chainView := eth.newChainView(eth.blockchain.CurrentBlock()) chainView := eth.newChainView(eth.blockchain.CurrentBlock())
historyCutoff := eth.blockchain.HistoryPruningCutoff() historyCutoff := eth.blockchain.HistoryPruningCutoff()
var finalBlock uint64 var finalBlock uint64

View file

@ -403,7 +403,7 @@ func (h *handler) unregisterPeer(id string) {
// Abort if the peer does not exist // Abort if the peer does not exist
peer := h.peers.peer(id) peer := h.peers.peer(id)
if peer == nil { if peer == nil {
logger.Error("Ethereum peer removal failed", "err", errPeerNotRegistered) logger.Warn("Ethereum peer removal failed", "err", errPeerNotRegistered)
return return
} }
// Remove the `eth` peer if it exists // Remove the `eth` peer if it exists

View file

@ -103,16 +103,10 @@ type AccessListTracer struct {
// NewAccessListTracer creates a new tracer that can generate AccessLists. // NewAccessListTracer creates a new tracer that can generate AccessLists.
// An optional AccessList can be specified to occupy slots and addresses in // An optional AccessList can be specified to occupy slots and addresses in
// the resulting accesslist. // the resulting accesslist.
func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompiles []common.Address) *AccessListTracer { func NewAccessListTracer(acl types.AccessList, addressesToExclude map[common.Address]struct{}) *AccessListTracer {
excl := map[common.Address]struct{}{
from: {}, to: {},
}
for _, addr := range precompiles {
excl[addr] = struct{}{}
}
list := newAccessList() list := newAccessList()
for _, al := range acl { for _, al := range acl {
if _, ok := excl[al.Address]; !ok { if _, ok := addressesToExclude[al.Address]; !ok {
list.addAddress(al.Address) list.addAddress(al.Address)
} }
for _, slot := range al.StorageKeys { for _, slot := range al.StorageKeys {
@ -120,7 +114,7 @@ func NewAccessListTracer(acl types.AccessList, from, to common.Address, precompi
} }
} }
return &AccessListTracer{ return &AccessListTracer{
excl: excl, excl: addressesToExclude,
list: list, list: list,
} }
} }

View file

@ -17,7 +17,10 @@
// Package ethdb defines the interfaces for an Ethereum data store. // Package ethdb defines the interfaces for an Ethereum data store.
package ethdb package ethdb
import "io" import (
"errors"
"io"
)
// KeyValueReader wraps the Has and Get method of a backing data store. // KeyValueReader wraps the Has and Get method of a backing data store.
type KeyValueReader interface { type KeyValueReader interface {
@ -37,10 +40,14 @@ type KeyValueWriter interface {
Delete(key []byte) error Delete(key []byte) error
} }
var ErrTooManyKeys = errors.New("too many keys in deleted range")
// KeyValueRangeDeleter wraps the DeleteRange method of a backing data store. // KeyValueRangeDeleter wraps the DeleteRange method of a backing data store.
type KeyValueRangeDeleter interface { type KeyValueRangeDeleter interface {
// DeleteRange deletes all of the keys (and values) in the range [start,end) // DeleteRange deletes all of the keys (and values) in the range [start,end)
// (inclusive on start, exclusive on end). // (inclusive on start, exclusive on end).
// Some implementations of DeleteRange may return ErrTooManyKeys after
// partially deleting entries in the given range.
DeleteRange(start, end []byte) error DeleteRange(start, end []byte) error
} }

View file

@ -207,8 +207,6 @@ func (db *Database) Delete(key []byte) error {
return db.db.Delete(key, nil) return db.db.Delete(key, nil)
} }
var ErrTooManyKeys = errors.New("too many keys in deleted range")
// DeleteRange deletes all of the keys (and values) in the range [start,end) // DeleteRange deletes all of the keys (and values) in the range [start,end)
// (inclusive on start, exclusive on end). // (inclusive on start, exclusive on end).
// Note that this is a fallback implementation as leveldb does not natively // Note that this is a fallback implementation as leveldb does not natively
@ -228,7 +226,7 @@ func (db *Database) DeleteRange(start, end []byte) error {
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
return err return err
} }
return ErrTooManyKeys return ethdb.ErrTooManyKeys
} }
if err := batch.Delete(it.Key()); err != nil { if err := batch.Delete(it.Key()); err != nil {
return err return err

View file

@ -1117,12 +1117,13 @@ type accessListResult struct {
// CreateAccessList creates an EIP-2930 type AccessList for the given transaction. // CreateAccessList creates an EIP-2930 type AccessList for the given transaction.
// Reexec and BlockNrOrHash can be specified to create the accessList on top of a certain state. // Reexec and BlockNrOrHash can be specified to create the accessList on top of a certain state.
func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash) (*accessListResult, error) { // StateOverrides can be used to create the accessList while taking into account state changes from previous transactions.
func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash, stateOverrides *override.StateOverride) (*accessListResult, error) {
bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
if blockNrOrHash != nil { if blockNrOrHash != nil {
bNrOrHash = *blockNrOrHash bNrOrHash = *blockNrOrHash
} }
acl, gasUsed, vmerr, err := AccessList(ctx, api.b, bNrOrHash, args) acl, gasUsed, vmerr, err := AccessList(ctx, api.b, bNrOrHash, args, stateOverrides)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1136,13 +1137,22 @@ func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args Transaction
// AccessList creates an access list for the given transaction. // AccessList creates an access list for the given transaction.
// If the accesslist creation fails an error is returned. // If the accesslist creation fails an error is returned.
// If the transaction itself fails, an vmErr is returned. // If the transaction itself fails, an vmErr is returned.
func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs) (acl types.AccessList, gasUsed uint64, vmErr error, err error) { func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash, args TransactionArgs, stateOverrides *override.StateOverride) (acl types.AccessList, gasUsed uint64, vmErr error, err error) {
// Retrieve the execution context // Retrieve the execution context
db, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) db, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if db == nil || err != nil { if db == nil || err != nil {
return nil, 0, nil, err return nil, 0, nil, err
} }
// Apply state overrides immediately after StateAndHeaderByNumberOrHash.
// If not applied here, there could be cases where user-specified overrides (e.g., nonce)
// may conflict with default values from the database, leading to inconsistencies.
if stateOverrides != nil {
if err := stateOverrides.Apply(db, nil); err != nil {
return nil, 0, nil, err
}
}
// Ensure any missing fields are filled, extract the recipient and input data // Ensure any missing fields are filled, extract the recipient and input data
if err = args.setFeeDefaults(ctx, b, header); err != nil { if err = args.setFeeDefaults(ctx, b, header); err != nil {
return nil, 0, nil, err return nil, 0, nil, err
@ -1166,10 +1176,33 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
// Retrieve the precompiles since they don't need to be added to the access list // Retrieve the precompiles since they don't need to be added to the access list
precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, isPostMerge, header.Time)) precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, isPostMerge, header.Time))
// addressesToExclude contains sender, receiver, precompiles and valid authorizations
addressesToExclude := map[common.Address]struct{}{args.from(): {}, to: {}}
for _, addr := range precompiles {
addressesToExclude[addr] = struct{}{}
}
// Prevent redundant operations if args contain more authorizations than EVM may handle
maxAuthorizations := uint64(*args.Gas) / params.CallNewAccountGas
if uint64(len(args.AuthorizationList)) > maxAuthorizations {
return nil, 0, nil, errors.New("insufficient gas to process all authorizations")
}
for _, auth := range args.AuthorizationList {
// Duplicating stateTransition.validateAuthorization() logic
if (!auth.ChainID.IsZero() && auth.ChainID.CmpBig(b.ChainConfig().ChainID) != 0) || auth.Nonce+1 < auth.Nonce {
continue
}
if authority, err := auth.Authority(); err == nil {
addressesToExclude[authority] = struct{}{}
}
}
// Create an initial tracer // Create an initial tracer
prevTracer := logger.NewAccessListTracer(nil, args.from(), to, precompiles) prevTracer := logger.NewAccessListTracer(nil, addressesToExclude)
if args.AccessList != nil { if args.AccessList != nil {
prevTracer = logger.NewAccessListTracer(*args.AccessList, args.from(), to, precompiles) prevTracer = logger.NewAccessListTracer(*args.AccessList, addressesToExclude)
} }
for { for {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@ -1186,7 +1219,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH
msg := args.ToMessage(header.BaseFee, true, true) msg := args.ToMessage(header.BaseFee, true, true)
// Apply the transaction with the access list tracer // Apply the transaction with the access list tracer
tracer := logger.NewAccessListTracer(accessList, args.from(), to, precompiles) tracer := logger.NewAccessListTracer(accessList, addressesToExclude)
config := vm.Config{Tracer: tracer.Hooks(), NoBaseFee: true} config := vm.Config{Tracer: tracer.Hooks(), NoBaseFee: true}
evm := b.GetEVM(ctx, statedb, header, &config, nil) evm := b.GetEVM(ctx, statedb, header, &config, nil)

View file

@ -3529,3 +3529,76 @@ func testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc s
func addressToHash(a common.Address) common.Hash { func addressToHash(a common.Address) common.Hash {
return common.BytesToHash(a.Bytes()) return common.BytesToHash(a.Bytes())
} }
func TestCreateAccessListWithStateOverrides(t *testing.T) {
// Initialize test backend
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7"): {Balance: big.NewInt(1000000000000000000)},
},
}
backend := newTestBackend(t, 1, genesis, ethash.NewFaker(), nil)
// Create a new BlockChainAPI instance
api := NewBlockChainAPI(backend)
// Create test contract code - a simple storage contract
//
// SPDX-License-Identifier: MIT
// pragma solidity ^0.8.0;
//
// contract SimpleStorage {
// uint256 private value;
//
// function retrieve() public view returns (uint256) {
// return value;
// }
// }
var (
contractCode = hexutil.Bytes(common.Hex2Bytes("6080604052348015600f57600080fd5b506004361060285760003560e01c80632e64cec114602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b60008054905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056"))
// Create state overrides with more complete state
contractAddr = common.HexToAddress("0x1234567890123456789012345678901234567890")
nonce = hexutil.Uint64(1)
overrides = &override.StateOverride{
contractAddr: override.OverrideAccount{
Code: &contractCode,
Balance: (*hexutil.Big)(big.NewInt(1000000000000000000)),
Nonce: &nonce,
State: map[common.Hash]common.Hash{
common.Hash{}: common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000002a"),
},
},
}
)
// Create transaction arguments with gas and value
var (
from = common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7")
data = hexutil.Bytes(common.Hex2Bytes("2e64cec1")) // retrieve()
gas = hexutil.Uint64(100000)
args = TransactionArgs{
From: &from,
To: &contractAddr,
Data: &data,
Gas: &gas,
Value: new(hexutil.Big),
}
)
// Call CreateAccessList
result, err := api.CreateAccessList(context.Background(), args, nil, overrides)
if err != nil {
t.Fatalf("Failed to create access list: %v", err)
}
if err != nil || result == nil {
t.Fatalf("Failed to create access list: %v", err)
}
require.NotNil(t, result.Accesslist)
// Verify access list contains the contract address and storage slot
expected := &types.AccessList{{
Address: contractAddr,
StorageKeys: []common.Hash{{}},
}}
require.Equal(t, expected, result.Accesslist)
}

View file

@ -140,7 +140,7 @@ type ExtIP net.IP
func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil }
func (n ExtIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } func (n ExtIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) }
func (n ExtIP) MarshalText() ([]byte, error) { return []byte(fmt.Sprintf("extip:%v", net.IP(n))), nil } func (n ExtIP) MarshalText() ([]byte, error) { return fmt.Appendf(nil, "extip:%v", net.IP(n)), nil }
// These do nothing. // These do nothing.

View file

@ -71,7 +71,7 @@ func (n *pmp) DeleteMapping(protocol string, extport, intport int) (err error) {
} }
func (n *pmp) MarshalText() ([]byte, error) { func (n *pmp) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("natpmp:%v", n.gw)), nil return fmt.Appendf(nil, "natpmp:%v", n.gw), nil
} }
func discoverPMP() Interface { func discoverPMP() Interface {

View file

@ -282,7 +282,7 @@ func TestTypedDataArrayValidate(t *testing.T) {
messageHash, tErr := td.HashStruct(td.PrimaryType, td.Message) messageHash, tErr := td.HashStruct(td.PrimaryType, td.Message)
assert.NoError(t, tErr, "failed to hash message: %v", tErr) assert.NoError(t, tErr, "failed to hash message: %v", tErr)
digest := crypto.Keccak256Hash([]byte(fmt.Sprintf("%s%s%s", "\x19\x01", string(domainSeparator), string(messageHash)))) digest := crypto.Keccak256Hash(fmt.Appendf(nil, "%s%s%s", "\x19\x01", string(domainSeparator), string(messageHash)))
assert.Equal(t, tc.Digest, digest.String(), "digest doesn't not match") assert.Equal(t, tc.Digest, digest.String(), "digest doesn't not match")
assert.NoError(t, td.validate(), "validation failed", tErr) assert.NoError(t, td.validate(), "validation failed", tErr)

View file

@ -369,7 +369,7 @@ func sign(typedData apitypes.TypedData) ([]byte, []byte, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) rawData := fmt.Appendf(nil, "\x19\x01%s%s", string(domainSeparator), string(typedDataHash))
sighash := crypto.Keccak256(rawData) sighash := crypto.Keccak256(rawData)
return typedDataHash, sighash, nil return typedDataHash, sighash, nil
} }

View file

@ -57,32 +57,26 @@ func (c *committer) commit(path []byte, n node, parallel bool) node {
// Commit children, then parent, and remove the dirty flag. // Commit children, then parent, and remove the dirty flag.
switch cn := n.(type) { switch cn := n.(type) {
case *shortNode: case *shortNode:
// Commit child
collapsed := cn.copy()
// If the child is fullNode, recursively commit, // If the child is fullNode, recursively commit,
// otherwise it can only be hashNode or valueNode. // otherwise it can only be hashNode or valueNode.
if _, ok := cn.Val.(*fullNode); ok { if _, ok := cn.Val.(*fullNode); ok {
collapsed.Val = c.commit(append(path, cn.Key...), cn.Val, false) cn.Val = c.commit(append(path, cn.Key...), cn.Val, false)
} }
// The key needs to be copied, since we're adding it to the // The key needs to be copied, since we're adding it to the
// modified nodeset. // modified nodeset.
collapsed.Key = hexToCompact(cn.Key) cn.Key = hexToCompact(cn.Key)
hashedNode := c.store(path, collapsed) hashedNode := c.store(path, cn)
if hn, ok := hashedNode.(hashNode); ok { if hn, ok := hashedNode.(hashNode); ok {
return hn return hn
} }
return collapsed return cn
case *fullNode: case *fullNode:
hashedKids := c.commitChildren(path, cn, parallel) c.commitChildren(path, cn, parallel)
collapsed := cn.copy() hashedNode := c.store(path, cn)
collapsed.Children = hashedKids
hashedNode := c.store(path, collapsed)
if hn, ok := hashedNode.(hashNode); ok { if hn, ok := hashedNode.(hashNode); ok {
return hn return hn
} }
return collapsed return cn
case hashNode: case hashNode:
return cn return cn
default: default:
@ -92,11 +86,10 @@ func (c *committer) commit(path []byte, n node, parallel bool) node {
} }
// commitChildren commits the children of the given fullnode // commitChildren commits the children of the given fullnode
func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) [17]node { func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) {
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
nodesMu sync.Mutex nodesMu sync.Mutex
children [17]node
) )
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
child := n.Children[i] child := n.Children[i]
@ -106,22 +99,21 @@ func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) [17]
// If it's the hashed child, save the hash value directly. // If it's the hashed child, save the hash value directly.
// Note: it's impossible that the child in range [0, 15] // Note: it's impossible that the child in range [0, 15]
// is a valueNode. // is a valueNode.
if hn, ok := child.(hashNode); ok { if _, ok := child.(hashNode); ok {
children[i] = hn
continue continue
} }
// Commit the child recursively and store the "hashed" value. // Commit the child recursively and store the "hashed" value.
// Note the returned node can be some embedded nodes, so it's // Note the returned node can be some embedded nodes, so it's
// possible the type is not hashNode. // possible the type is not hashNode.
if !parallel { if !parallel {
children[i] = c.commit(append(path, byte(i)), child, false) n.Children[i] = c.commit(append(path, byte(i)), child, false)
} else { } else {
wg.Add(1) wg.Add(1)
go func(index int) { go func(index int) {
p := append(path, byte(index)) p := append(path, byte(index))
childSet := trienode.NewNodeSet(c.nodes.Owner) childSet := trienode.NewNodeSet(c.nodes.Owner)
childCommitter := newCommitter(childSet, c.tracer, c.collectLeaf) childCommitter := newCommitter(childSet, c.tracer, c.collectLeaf)
children[index] = childCommitter.commit(p, child, false) n.Children[index] = childCommitter.commit(p, child, false)
nodesMu.Lock() nodesMu.Lock()
c.nodes.MergeSet(childSet) c.nodes.MergeSet(childSet)
nodesMu.Unlock() nodesMu.Unlock()
@ -132,11 +124,6 @@ func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) [17]
if parallel { if parallel {
wg.Wait() wg.Wait()
} }
// For the 17th child, it's possible the type is valuenode.
if n.Children[16] != nil {
children[16] = n.Children[16]
}
return children
} }
// store hashes the node n and adds it to the modified nodeset. If leaf collection // store hashes the node n and adds it to the modified nodeset. If leaf collection

View file

@ -53,62 +53,56 @@ func returnHasherToPool(h *hasher) {
hasherPool.Put(h) hasherPool.Put(h)
} }
// hash collapses a node down into a hash node, also returning a copy of the // hash collapses a node down into a hash node.
// original node initialized with the computed hash to replace the original one. func (h *hasher) hash(n node, force bool) node {
func (h *hasher) hash(n node, force bool) (hashed node, cached node) {
// Return the cached hash if it's available // Return the cached hash if it's available
if hash, _ := n.cache(); hash != nil { if hash, _ := n.cache(); hash != nil {
return hash, n return hash
} }
// Trie not processed yet, walk the children // Trie not processed yet, walk the children
switch n := n.(type) { switch n := n.(type) {
case *shortNode: case *shortNode:
collapsed, cached := h.hashShortNodeChildren(n) collapsed := h.hashShortNodeChildren(n)
hashed := h.shortnodeToHash(collapsed, force) hashed := h.shortnodeToHash(collapsed, force)
// We need to retain the possibly _not_ hashed node, in case it was too
// small to be hashed
if hn, ok := hashed.(hashNode); ok { if hn, ok := hashed.(hashNode); ok {
cached.flags.hash = hn n.flags.hash = hn
} else { } else {
cached.flags.hash = nil n.flags.hash = nil
} }
return hashed, cached return hashed
case *fullNode: case *fullNode:
collapsed, cached := h.hashFullNodeChildren(n) collapsed := h.hashFullNodeChildren(n)
hashed = h.fullnodeToHash(collapsed, force) hashed := h.fullnodeToHash(collapsed, force)
if hn, ok := hashed.(hashNode); ok { if hn, ok := hashed.(hashNode); ok {
cached.flags.hash = hn n.flags.hash = hn
} else { } else {
cached.flags.hash = nil n.flags.hash = nil
} }
return hashed, cached return hashed
default: default:
// Value and hash nodes don't have children, so they're left as were // Value and hash nodes don't have children, so they're left as were
return n, n return n
} }
} }
// hashShortNodeChildren collapses the short node. The returned collapsed node // hashShortNodeChildren returns a copy of the supplied shortNode, with its child
// holds a live reference to the Key, and must not be modified. // being replaced by either the hash or an embedded node if the child is small.
func (h *hasher) hashShortNodeChildren(n *shortNode) (collapsed, cached *shortNode) { func (h *hasher) hashShortNodeChildren(n *shortNode) *shortNode {
// Hash the short node's child, caching the newly hashed subtree var collapsed shortNode
collapsed, cached = n.copy(), n.copy()
// Previously, we did copy this one. We don't seem to need to actually
// do that, since we don't overwrite/reuse keys
// cached.Key = common.CopyBytes(n.Key)
collapsed.Key = hexToCompact(n.Key) collapsed.Key = hexToCompact(n.Key)
// Unless the child is a valuenode or hashnode, hash it
switch n.Val.(type) { switch n.Val.(type) {
case *fullNode, *shortNode: case *fullNode, *shortNode:
collapsed.Val, cached.Val = h.hash(n.Val, false) collapsed.Val = h.hash(n.Val, false)
default:
collapsed.Val = n.Val
} }
return collapsed, cached return &collapsed
} }
func (h *hasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached *fullNode) { // hashFullNodeChildren returns a copy of the supplied fullNode, with its child
// Hash the full node's children, caching the newly hashed subtrees // being replaced by either the hash or an embedded node if the child is small.
cached = n.copy() func (h *hasher) hashFullNodeChildren(n *fullNode) *fullNode {
collapsed = n.copy() var children [17]node
if h.parallel { if h.parallel {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(16) wg.Add(16)
@ -116,9 +110,9 @@ func (h *hasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached
go func(i int) { go func(i int) {
hasher := newHasher(false) hasher := newHasher(false)
if child := n.Children[i]; child != nil { if child := n.Children[i]; child != nil {
collapsed.Children[i], cached.Children[i] = hasher.hash(child, false) children[i] = hasher.hash(child, false)
} else { } else {
collapsed.Children[i] = nilValueNode children[i] = nilValueNode
} }
returnHasherToPool(hasher) returnHasherToPool(hasher)
wg.Done() wg.Done()
@ -128,19 +122,21 @@ func (h *hasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached
} else { } else {
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
if child := n.Children[i]; child != nil { if child := n.Children[i]; child != nil {
collapsed.Children[i], cached.Children[i] = h.hash(child, false) children[i] = h.hash(child, false)
} else { } else {
collapsed.Children[i] = nilValueNode children[i] = nilValueNode
} }
} }
} }
return collapsed, cached if n.Children[16] != nil {
children[16] = n.Children[16]
}
return &fullNode{flags: nodeFlag{}, Children: children}
} }
// shortnodeToHash creates a hashNode from a shortNode. The supplied shortnode // shortNodeToHash computes the hash of the given shortNode. The shortNode must
// should have hex-type Key, which will be converted (without modification) // first be collapsed, with its key converted to compact form. If the RLP-encoded
// into compact form for RLP encoding. // node data is smaller than 32 bytes, the node itself is returned.
// If the rlp data is smaller than 32 bytes, `nil` is returned.
func (h *hasher) shortnodeToHash(n *shortNode, force bool) node { func (h *hasher) shortnodeToHash(n *shortNode, force bool) node {
n.encode(h.encbuf) n.encode(h.encbuf)
enc := h.encodedBytes() enc := h.encodedBytes()
@ -151,8 +147,8 @@ func (h *hasher) shortnodeToHash(n *shortNode, force bool) node {
return h.hashData(enc) return h.hashData(enc)
} }
// fullnodeToHash is used to create a hashNode from a fullNode, (which // fullnodeToHash computes the hash of the given fullNode. If the RLP-encoded
// may contain nil values) // node data is smaller than 32 bytes, the node itself is returned.
func (h *hasher) fullnodeToHash(n *fullNode, force bool) node { func (h *hasher) fullnodeToHash(n *fullNode, force bool) node {
n.encode(h.encbuf) n.encode(h.encbuf)
enc := h.encodedBytes() enc := h.encodedBytes()
@ -203,10 +199,10 @@ func (h *hasher) hashDataTo(dst, data []byte) {
func (h *hasher) proofHash(original node) (collapsed, hashed node) { func (h *hasher) proofHash(original node) (collapsed, hashed node) {
switch n := original.(type) { switch n := original.(type) {
case *shortNode: case *shortNode:
sn, _ := h.hashShortNodeChildren(n) sn := h.hashShortNodeChildren(n)
return sn, h.shortnodeToHash(sn, false) return sn, h.shortnodeToHash(sn, false)
case *fullNode: case *fullNode:
fn, _ := h.hashFullNodeChildren(n) fn := h.hashFullNodeChildren(n)
return fn, h.fullnodeToHash(fn, false) return fn, h.fullnodeToHash(fn, false)
default: default:
// Value and hash nodes don't have children, so they're left as were // Value and hash nodes don't have children, so they're left as were

View file

@ -79,15 +79,19 @@ func (n *fullNode) EncodeRLP(w io.Writer) error {
return eb.Flush() return eb.Flush()
} }
func (n *fullNode) copy() *fullNode { copy := *n; return &copy }
func (n *shortNode) copy() *shortNode { copy := *n; return &copy }
// nodeFlag contains caching-related metadata about a node. // nodeFlag contains caching-related metadata about a node.
type nodeFlag struct { type nodeFlag struct {
hash hashNode // cached hash of the node (may be nil) hash hashNode // cached hash of the node (may be nil)
dirty bool // whether the node has changes that must be written to the database dirty bool // whether the node has changes that must be written to the database
} }
func (n nodeFlag) copy() nodeFlag {
return nodeFlag{
hash: common.CopyBytes(n.hash),
dirty: n.dirty,
}
}
func (n *fullNode) cache() (hashNode, bool) { return n.flags.hash, n.flags.dirty } func (n *fullNode) cache() (hashNode, bool) { return n.flags.hash, n.flags.dirty }
func (n *shortNode) cache() (hashNode, bool) { return n.flags.hash, n.flags.dirty } func (n *shortNode) cache() (hashNode, bool) { return n.flags.hash, n.flags.dirty }
func (n hashNode) cache() (hashNode, bool) { return nil, true } func (n hashNode) cache() (hashNode, bool) { return nil, true }
@ -228,7 +232,9 @@ func decodeRef(buf []byte) (node, []byte, error) {
err := fmt.Errorf("oversized embedded node (size is %d bytes, want size < %d)", size, hashLen) err := fmt.Errorf("oversized embedded node (size is %d bytes, want size < %d)", size, hashLen)
return nil, buf, err return nil, buf, err
} }
n, err := decodeNode(nil, buf) // The buffer content has already been copied or is safe to use;
// no additional copy is required.
n, err := decodeNodeUnsafe(nil, buf)
return n, rest, err return n, rest, err
case kind == rlp.String && len(val) == 0: case kind == rlp.String && len(val) == 0:
// empty node // empty node

View file

@ -29,11 +29,11 @@ import (
"github.com/ethereum/go-ethereum/triedb/database" "github.com/ethereum/go-ethereum/triedb/database"
) )
// Trie is a Merkle Patricia Trie. Use New to create a trie that sits on // Trie represents a Merkle Patricia Trie. Use New to create a trie that operates
// top of a database. Whenever trie performs a commit operation, the generated // on top of a node database. During a commit operation, the trie collects all
// nodes will be gathered and returned in a set. Once the trie is committed, // modified nodes into a set for return. After committing, the trie becomes
// it's not usable anymore. Callers have to re-create the trie with new root // unusable, and callers must recreate it with the new root based on the updated
// based on the updated trie database. // trie database.
// //
// Trie is not safe for concurrent use. // Trie is not safe for concurrent use.
type Trie struct { type Trie struct {
@ -67,13 +67,13 @@ func (t *Trie) newFlag() nodeFlag {
// Copy returns a copy of Trie. // Copy returns a copy of Trie.
func (t *Trie) Copy() *Trie { func (t *Trie) Copy() *Trie {
return &Trie{ return &Trie{
root: t.root, root: copyNode(t.root),
owner: t.owner, owner: t.owner,
committed: t.committed, committed: t.committed,
unhashed: t.unhashed,
uncommitted: t.uncommitted,
reader: t.reader, reader: t.reader,
tracer: t.tracer.copy(), tracer: t.tracer.copy(),
uncommitted: t.uncommitted,
unhashed: t.unhashed,
} }
} }
@ -169,14 +169,12 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no
} }
value, newnode, didResolve, err = t.get(n.Val, key, pos+len(n.Key)) value, newnode, didResolve, err = t.get(n.Val, key, pos+len(n.Key))
if err == nil && didResolve { if err == nil && didResolve {
n = n.copy()
n.Val = newnode n.Val = newnode
} }
return value, n, didResolve, err return value, n, didResolve, err
case *fullNode: case *fullNode:
value, newnode, didResolve, err = t.get(n.Children[key[pos]], key, pos+1) value, newnode, didResolve, err = t.get(n.Children[key[pos]], key, pos+1)
if err == nil && didResolve { if err == nil && didResolve {
n = n.copy()
n.Children[key[pos]] = newnode n.Children[key[pos]] = newnode
} }
return value, n, didResolve, err return value, n, didResolve, err
@ -257,7 +255,6 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
} }
item, newnode, resolved, err = t.getNode(n.Val, path, pos+len(n.Key)) item, newnode, resolved, err = t.getNode(n.Val, path, pos+len(n.Key))
if err == nil && resolved > 0 { if err == nil && resolved > 0 {
n = n.copy()
n.Val = newnode n.Val = newnode
} }
return item, n, resolved, err return item, n, resolved, err
@ -265,7 +262,6 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
case *fullNode: case *fullNode:
item, newnode, resolved, err = t.getNode(n.Children[path[pos]], path, pos+1) item, newnode, resolved, err = t.getNode(n.Children[path[pos]], path, pos+1)
if err == nil && resolved > 0 { if err == nil && resolved > 0 {
n = n.copy()
n.Children[path[pos]] = newnode n.Children[path[pos]] = newnode
} }
return item, n, resolved, err return item, n, resolved, err
@ -375,7 +371,6 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
if !dirty || err != nil { if !dirty || err != nil {
return false, n, err return false, n, err
} }
n = n.copy()
n.flags = t.newFlag() n.flags = t.newFlag()
n.Children[key[0]] = nn n.Children[key[0]] = nn
return true, n, nil return true, n, nil
@ -483,7 +478,6 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) {
if !dirty || err != nil { if !dirty || err != nil {
return false, n, err return false, n, err
} }
n = n.copy()
n.flags = t.newFlag() n.flags = t.newFlag()
n.Children[key[0]] = nn n.Children[key[0]] = nn
@ -576,6 +570,36 @@ func concat(s1 []byte, s2 ...byte) []byte {
return r return r
} }
// copyNode deep-copies the supplied node along with its children recursively.
func copyNode(n node) node {
switch n := (n).(type) {
case nil:
return nil
case valueNode:
return valueNode(common.CopyBytes(n))
case *shortNode:
return &shortNode{
flags: n.flags.copy(),
Key: common.CopyBytes(n.Key),
Val: copyNode(n.Val),
}
case *fullNode:
var children [17]node
for i, cn := range n.Children {
children[i] = copyNode(cn)
}
return &fullNode{
flags: n.flags.copy(),
Children: children,
}
case hashNode:
return n
default:
panic(fmt.Sprintf("%T: unknown node type", n))
}
}
func (t *Trie) resolve(n node, prefix []byte) (node, error) { func (t *Trie) resolve(n node, prefix []byte) (node, error) {
if n, ok := n.(hashNode); ok { if n, ok := n.(hashNode); ok {
return t.resolveAndTrack(n, prefix) return t.resolveAndTrack(n, prefix)
@ -593,15 +617,16 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) {
return nil, err return nil, err
} }
t.tracer.onRead(prefix, blob) t.tracer.onRead(prefix, blob)
return mustDecodeNode(n, blob), nil
// The returned node blob won't be changed afterward. No need to
// deep-copy the slice.
return decodeNodeUnsafe(n, blob)
} }
// Hash returns the root hash of the trie. It does not write to the // Hash returns the root hash of the trie. It does not write to the
// database and can be used even if the trie doesn't have one. // database and can be used even if the trie doesn't have one.
func (t *Trie) Hash() common.Hash { func (t *Trie) Hash() common.Hash {
hash, cached := t.hashRoot() return common.BytesToHash(t.hashRoot().(hashNode))
t.root = cached
return common.BytesToHash(hash.(hashNode))
} }
// Commit collects all dirty nodes in the trie and replaces them with the // Commit collects all dirty nodes in the trie and replaces them with the
@ -652,9 +677,9 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
} }
// hashRoot calculates the root hash of the given trie // hashRoot calculates the root hash of the given trie
func (t *Trie) hashRoot() (node, node) { func (t *Trie) hashRoot() node {
if t.root == nil { if t.root == nil {
return hashNode(types.EmptyRootHash.Bytes()), nil return hashNode(types.EmptyRootHash.Bytes())
} }
// If the number of changes is below 100, we let one thread handle it // If the number of changes is below 100, we let one thread handle it
h := newHasher(t.unhashed >= 100) h := newHasher(t.unhashed >= 100)
@ -662,8 +687,7 @@ func (t *Trie) hashRoot() (node, node) {
returnHasherToPool(h) returnHasherToPool(h)
t.unhashed = 0 t.unhashed = 0
}() }()
hashed, cached := h.hash(t.root, true) return h.hash(t.root, true)
return hashed, cached
} }
// Witness returns a set containing all trie nodes that have been accessed. // Witness returns a set containing all trie nodes that have been accessed.

View file

@ -1330,3 +1330,171 @@ func printSet(set *trienode.NodeSet) string {
} }
return out.String() return out.String()
} }
func TestTrieCopy(t *testing.T) {
testTrieCopy(t, []kv{
{k: []byte("do"), v: []byte("verb")},
{k: []byte("ether"), v: []byte("wookiedoo")},
{k: []byte("horse"), v: []byte("stallion")},
{k: []byte("shaman"), v: []byte("horse")},
{k: []byte("doge"), v: []byte("coin")},
{k: []byte("dog"), v: []byte("puppy")},
})
var entries []kv
for i := 0; i < 256; i++ {
entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)})
}
testTrieCopy(t, entries)
}
func testTrieCopy(t *testing.T, entries []kv) {
tr := NewEmpty(nil)
for _, entry := range entries {
tr.Update(entry.k, entry.v)
}
trCpy := tr.Copy()
if tr.Hash() != trCpy.Hash() {
t.Errorf("Hash mismatch: old %v, copy %v", tr.Hash(), trCpy.Hash())
}
// Check iterator
it, _ := tr.NodeIterator(nil)
itCpy, _ := trCpy.NodeIterator(nil)
for it.Next(false) {
hasNext := itCpy.Next(false)
if !hasNext {
t.Fatal("Iterator is not matched")
}
if !bytes.Equal(it.Path(), itCpy.Path()) {
t.Fatal("Iterator is not matched")
}
if it.Leaf() != itCpy.Leaf() {
t.Fatal("Iterator is not matched")
}
if it.Leaf() && !bytes.Equal(it.LeafBlob(), itCpy.LeafBlob()) {
t.Fatal("Iterator is not matched")
}
}
// Check commit
root, nodes := tr.Commit(false)
rootCpy, nodesCpy := trCpy.Commit(false)
if root != rootCpy {
t.Fatal("root mismatch")
}
if len(nodes.Nodes) != len(nodesCpy.Nodes) {
t.Fatal("commit node mismatch")
}
for p, n := range nodes.Nodes {
nn, exists := nodesCpy.Nodes[p]
if !exists {
t.Fatalf("node not exists: %v", p)
}
if !reflect.DeepEqual(n, nn) {
t.Fatalf("node mismatch: %v", p)
}
}
}
func TestTrieCopyOldTrie(t *testing.T) {
testTrieCopyOldTrie(t, []kv{
{k: []byte("do"), v: []byte("verb")},
{k: []byte("ether"), v: []byte("wookiedoo")},
{k: []byte("horse"), v: []byte("stallion")},
{k: []byte("shaman"), v: []byte("horse")},
{k: []byte("doge"), v: []byte("coin")},
{k: []byte("dog"), v: []byte("puppy")},
})
var entries []kv
for i := 0; i < 256; i++ {
entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)})
}
testTrieCopyOldTrie(t, entries)
}
func testTrieCopyOldTrie(t *testing.T, entries []kv) {
tr := NewEmpty(nil)
for _, entry := range entries {
tr.Update(entry.k, entry.v)
}
hash := tr.Hash()
trCpy := tr.Copy()
for _, val := range entries {
if rand.Intn(2) == 0 {
trCpy.Delete(val.k)
} else {
trCpy.Update(val.k, testrand.Bytes(32))
}
}
for i := 0; i < 10; i++ {
trCpy.Update(testrand.Bytes(32), testrand.Bytes(32))
}
trCpy.Hash()
trCpy.Commit(false)
// Traverse the original tree, the changes made on the copy one shouldn't
// affect the old one
for _, entry := range entries {
d, _ := tr.Get(entry.k)
if !bytes.Equal(d, entry.v) {
t.Errorf("Unexpected data, key: %v, want: %v, got: %v", entry.k, entry.v, d)
}
}
if tr.Hash() != hash {
t.Errorf("Hash mismatch: old %v, new %v", hash, tr.Hash())
}
}
func TestTrieCopyNewTrie(t *testing.T) {
testTrieCopyNewTrie(t, []kv{
{k: []byte("do"), v: []byte("verb")},
{k: []byte("ether"), v: []byte("wookiedoo")},
{k: []byte("horse"), v: []byte("stallion")},
{k: []byte("shaman"), v: []byte("horse")},
{k: []byte("doge"), v: []byte("coin")},
{k: []byte("dog"), v: []byte("puppy")},
})
var entries []kv
for i := 0; i < 256; i++ {
entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)})
}
testTrieCopyNewTrie(t, entries)
}
func testTrieCopyNewTrie(t *testing.T, entries []kv) {
tr := NewEmpty(nil)
for _, entry := range entries {
tr.Update(entry.k, entry.v)
}
trCpy := tr.Copy()
hash := trCpy.Hash()
for _, val := range entries {
if rand.Intn(2) == 0 {
tr.Delete(val.k)
} else {
tr.Update(val.k, testrand.Bytes(32))
}
}
for i := 0; i < 10; i++ {
tr.Update(testrand.Bytes(32), testrand.Bytes(32))
}
// Traverse the original tree, the changes made on the copy one shouldn't
// affect the old one
for _, entry := range entries {
d, _ := trCpy.Get(entry.k)
if !bytes.Equal(d, entry.v) {
t.Errorf("Unexpected data, key: %v, want: %v, got: %v", entry.k, entry.v, d)
}
}
if trCpy.Hash() != hash {
t.Errorf("Hash mismatch: old %v, new %v", hash, tr.Hash())
}
}

View file

@ -27,6 +27,8 @@ type NodeReader interface {
// node path and the corresponding node hash. No error will be returned // node path and the corresponding node hash. No error will be returned
// if the node is not found. // if the node is not found.
// //
// The returned node content won't be changed after the call.
//
// Don't modify the returned byte slice since it's not deep-copied and // Don't modify the returned byte slice since it's not deep-copied and
// still be referenced by database. // still be referenced by database.
Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error)

View file

@ -371,27 +371,27 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte) h = make(map[common.Hash][]byte)
) )
for i := byte(2); i < 0xff; i++ { for i := byte(2); i < 0xff; i++ {
a[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 0, i)) a[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 0, i)
if i > 20 && i%2 == 0 { if i > 20 && i%2 == 0 {
b[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 1, i)) b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i)
} }
if i%4 == 0 { if i%4 == 0 {
c[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 2, i)) c[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 2, i)
} }
if i%7 == 0 { if i%7 == 0 {
d[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 3, i)) d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i)
} }
if i%8 == 0 { if i%8 == 0 {
e[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 4, i)) e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
} }
if i > 50 || i < 85 { if i > 50 || i < 85 {
f[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 5, i)) f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
} }
if i%64 == 0 { if i%64 == 0 {
g[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 6, i)) g[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 6, i)
} }
if i%128 == 0 { if i%128 == 0 {
h[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 7, i)) h[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 7, i)
} }
} }
// Assemble a stack of snapshots from the account layers // Assemble a stack of snapshots from the account layers
@ -480,27 +480,27 @@ func TestStorageIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte) h = make(map[common.Hash][]byte)
) )
for i := byte(2); i < 0xff; i++ { for i := byte(2); i < 0xff; i++ {
a[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 0, i)) a[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 0, i)
if i > 20 && i%2 == 0 { if i > 20 && i%2 == 0 {
b[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 1, i)) b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i)
} }
if i%4 == 0 { if i%4 == 0 {
c[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 2, i)) c[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 2, i)
} }
if i%7 == 0 { if i%7 == 0 {
d[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 3, i)) d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i)
} }
if i%8 == 0 { if i%8 == 0 {
e[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 4, i)) e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i)
} }
if i > 50 || i < 85 { if i > 50 || i < 85 {
f[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 5, i)) f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
} }
if i%64 == 0 { if i%64 == 0 {
g[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 6, i)) g[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 6, i)
} }
if i%128 == 0 { if i%128 == 0 {
h[common.Hash{i}] = []byte(fmt.Sprintf("layer-%d, key %d", 7, i)) h[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 7, i)
} }
} }
// Assemble a stack of snapshots from the account layers // Assemble a stack of snapshots from the account layers

View file

@ -19,6 +19,6 @@ package version
const ( const (
Major = 1 // Major version component of the current release Major = 1 // Major version component of the current release
Minor = 15 // Minor version component of the current release Minor = 15 // Minor version component of the current release
Patch = 6 // Patch version component of the current release Patch = 7 // Patch version component of the current release
Meta = "stable" // Version metadata to append to the version string Meta = "stable" // Version metadata to append to the version string
) )