diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml
new file mode 100644
index 0000000000..8c9ae1688f
--- /dev/null
+++ b/.github/workflows/validate_pr.yml
@@ -0,0 +1,23 @@
+name: PR Format Validation
+
+on:
+ pull_request:
+ types: [opened, edited, synchronize]
+
+jobs:
+ validate-pr:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check PR Title Format
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prTitle = context.payload.pull_request.title;
+ const titleRegex = /^(\.?[\w\s,{}/]+): .+/;
+
+ if (!titleRegex.test(prTitle)) {
+ core.setFailed(`PR title "${prTitle}" does not match required format: directory, ...: description`);
+ return;
+ }
+
+ console.log('✅ PR title format is valid');
diff --git a/README.md b/README.md
index 3bc34ab87f..bf46892ef7 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ https://pkg.go.dev/badge/github.com/ethereum/go-ethereum
[](https://goreportcard.com/report/github.com/ethereum/go-ethereum)
[](https://app.travis-ci.com/github/ethereum/go-ethereum)
[](https://discord.gg/nthXNEv)
+[](https://x.com/go_ethereum)
Automated builds are available for stable releases and the unstable master branch. Binary
archives are published at https://geth.ethereum.org/downloads/.
diff --git a/accounts/abi/abigen/source2.go.tpl b/accounts/abi/abigen/source2.go.tpl
index 8ef906b8d6..3d98cbb700 100644
--- a/accounts/abi/abigen/source2.go.tpl
+++ b/accounts/abi/abigen/source2.go.tpl
@@ -183,7 +183,7 @@ var (
// Solidity: {{.Original.String}}
func ({{ decapitalise $contract.Type}} *{{$contract.Type}}) Unpack{{.Normalized.Name}}Event(log *types.Log) (*{{$contract.Type}}{{.Normalized.Name}}, error) {
event := "{{.Original.Name}}"
- if log.Topics[0] != {{ decapitalise $contract.Type}}.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != {{ decapitalise $contract.Type}}.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new({{$contract.Type}}{{.Normalized.Name}})
diff --git a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
index b2183c91ea..b548b6cdae 100644
--- a/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
+++ b/accounts/abi/abigen/testdata/v2/crowdsale.go.txt
@@ -360,7 +360,7 @@ func (CrowdsaleFundTransfer) ContractEventName() string {
// Solidity: event FundTransfer(address backer, uint256 amount, bool isContribution)
func (crowdsale *Crowdsale) UnpackFundTransferEvent(log *types.Log) (*CrowdsaleFundTransfer, error) {
event := "FundTransfer"
- if log.Topics[0] != crowdsale.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != crowdsale.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(CrowdsaleFundTransfer)
diff --git a/accounts/abi/abigen/testdata/v2/dao.go.txt b/accounts/abi/abigen/testdata/v2/dao.go.txt
index 75fa95df91..c246771d6d 100644
--- a/accounts/abi/abigen/testdata/v2/dao.go.txt
+++ b/accounts/abi/abigen/testdata/v2/dao.go.txt
@@ -606,7 +606,7 @@ func (DAOChangeOfRules) ContractEventName() string {
// Solidity: event ChangeOfRules(uint256 minimumQuorum, uint256 debatingPeriodInMinutes, int256 majorityMargin)
func (dAO *DAO) UnpackChangeOfRulesEvent(log *types.Log) (*DAOChangeOfRules, error) {
event := "ChangeOfRules"
- if log.Topics[0] != dAO.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DAOChangeOfRules)
@@ -648,7 +648,7 @@ func (DAOMembershipChanged) ContractEventName() string {
// Solidity: event MembershipChanged(address member, bool isMember)
func (dAO *DAO) UnpackMembershipChangedEvent(log *types.Log) (*DAOMembershipChanged, error) {
event := "MembershipChanged"
- if log.Topics[0] != dAO.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DAOMembershipChanged)
@@ -692,7 +692,7 @@ func (DAOProposalAdded) ContractEventName() string {
// Solidity: event ProposalAdded(uint256 proposalID, address recipient, uint256 amount, string description)
func (dAO *DAO) UnpackProposalAddedEvent(log *types.Log) (*DAOProposalAdded, error) {
event := "ProposalAdded"
- if log.Topics[0] != dAO.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DAOProposalAdded)
@@ -736,7 +736,7 @@ func (DAOProposalTallied) ContractEventName() string {
// Solidity: event ProposalTallied(uint256 proposalID, int256 result, uint256 quorum, bool active)
func (dAO *DAO) UnpackProposalTalliedEvent(log *types.Log) (*DAOProposalTallied, error) {
event := "ProposalTallied"
- if log.Topics[0] != dAO.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DAOProposalTallied)
@@ -780,7 +780,7 @@ func (DAOVoted) ContractEventName() string {
// Solidity: event Voted(uint256 proposalID, bool position, address voter, string justification)
func (dAO *DAO) UnpackVotedEvent(log *types.Log) (*DAOVoted, error) {
event := "Voted"
- if log.Topics[0] != dAO.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dAO.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DAOVoted)
diff --git a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
index 92558c5efe..8ad59e63b1 100644
--- a/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
+++ b/accounts/abi/abigen/testdata/v2/eventchecker.go.txt
@@ -72,7 +72,7 @@ func (EventCheckerDynamic) ContractEventName() string {
// Solidity: event dynamic(string indexed idxStr, bytes indexed idxDat, string str, bytes dat)
func (eventChecker *EventChecker) UnpackDynamicEvent(log *types.Log) (*EventCheckerDynamic, error) {
event := "dynamic"
- if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(EventCheckerDynamic)
@@ -112,7 +112,7 @@ func (EventCheckerEmpty) ContractEventName() string {
// Solidity: event empty()
func (eventChecker *EventChecker) UnpackEmptyEvent(log *types.Log) (*EventCheckerEmpty, error) {
event := "empty"
- if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(EventCheckerEmpty)
@@ -154,7 +154,7 @@ func (EventCheckerIndexed) ContractEventName() string {
// Solidity: event indexed(address indexed addr, int256 indexed num)
func (eventChecker *EventChecker) UnpackIndexedEvent(log *types.Log) (*EventCheckerIndexed, error) {
event := "indexed"
- if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(EventCheckerIndexed)
@@ -196,7 +196,7 @@ func (EventCheckerMixed) ContractEventName() string {
// Solidity: event mixed(address indexed addr, int256 num)
func (eventChecker *EventChecker) UnpackMixedEvent(log *types.Log) (*EventCheckerMixed, error) {
event := "mixed"
- if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(EventCheckerMixed)
@@ -238,7 +238,7 @@ func (EventCheckerUnnamed) ContractEventName() string {
// Solidity: event unnamed(uint256 indexed arg0, uint256 indexed arg1)
func (eventChecker *EventChecker) UnpackUnnamedEvent(log *types.Log) (*EventCheckerUnnamed, error) {
event := "unnamed"
- if log.Topics[0] != eventChecker.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != eventChecker.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(EventCheckerUnnamed)
diff --git a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
index fbc61a5c6c..3fbabee5a5 100644
--- a/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
+++ b/accounts/abi/abigen/testdata/v2/nameconflict.go.txt
@@ -134,7 +134,7 @@ func (NameConflictLog) ContractEventName() string {
// Solidity: event log(int256 msg, int256 _msg)
func (nameConflict *NameConflict) UnpackLogEvent(log *types.Log) (*NameConflictLog, error) {
event := "log"
- if log.Topics[0] != nameConflict.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != nameConflict.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(NameConflictLog)
diff --git a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
index 9d698a2657..d962583e48 100644
--- a/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
+++ b/accounts/abi/abigen/testdata/v2/numericmethodname.go.txt
@@ -136,7 +136,7 @@ func (NumericMethodNameE1TestEvent) ContractEventName() string {
// Solidity: event _1TestEvent(address _param)
func (numericMethodName *NumericMethodName) UnpackE1TestEventEvent(log *types.Log) (*NumericMethodNameE1TestEvent, error) {
event := "_1TestEvent"
- if log.Topics[0] != numericMethodName.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != numericMethodName.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(NumericMethodNameE1TestEvent)
diff --git a/accounts/abi/abigen/testdata/v2/overload.go.txt b/accounts/abi/abigen/testdata/v2/overload.go.txt
index 3b9a95a125..ddddd10186 100644
--- a/accounts/abi/abigen/testdata/v2/overload.go.txt
+++ b/accounts/abi/abigen/testdata/v2/overload.go.txt
@@ -114,7 +114,7 @@ func (OverloadBar) ContractEventName() string {
// Solidity: event bar(uint256 i)
func (overload *Overload) UnpackBarEvent(log *types.Log) (*OverloadBar, error) {
event := "bar"
- if log.Topics[0] != overload.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != overload.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(OverloadBar)
@@ -156,7 +156,7 @@ func (OverloadBar0) ContractEventName() string {
// Solidity: event bar(uint256 i, uint256 j)
func (overload *Overload) UnpackBar0Event(log *types.Log) (*OverloadBar0, error) {
event := "bar0"
- if log.Topics[0] != overload.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != overload.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(OverloadBar0)
diff --git a/accounts/abi/abigen/testdata/v2/token.go.txt b/accounts/abi/abigen/testdata/v2/token.go.txt
index 69294f375a..6ebc96861b 100644
--- a/accounts/abi/abigen/testdata/v2/token.go.txt
+++ b/accounts/abi/abigen/testdata/v2/token.go.txt
@@ -386,7 +386,7 @@ func (TokenTransfer) ContractEventName() string {
// Solidity: event Transfer(address indexed from, address indexed to, uint256 value)
func (token *Token) UnpackTransferEvent(log *types.Log) (*TokenTransfer, error) {
event := "Transfer"
- if log.Topics[0] != token.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != token.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(TokenTransfer)
diff --git a/accounts/abi/abigen/testdata/v2/tuple.go.txt b/accounts/abi/abigen/testdata/v2/tuple.go.txt
index 76a1f58d52..4724fdd351 100644
--- a/accounts/abi/abigen/testdata/v2/tuple.go.txt
+++ b/accounts/abi/abigen/testdata/v2/tuple.go.txt
@@ -193,7 +193,7 @@ func (TupleTupleEvent) ContractEventName() string {
// Solidity: event TupleEvent((uint256,uint256[],(uint256,uint256)[]) a, (uint256,uint256)[2][] b, (uint256,uint256)[][2] c, (uint256,uint256[],(uint256,uint256)[])[] d, uint256[] e)
func (tuple *Tuple) UnpackTupleEventEvent(log *types.Log) (*TupleTupleEvent, error) {
event := "TupleEvent"
- if log.Topics[0] != tuple.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != tuple.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(TupleTupleEvent)
@@ -234,7 +234,7 @@ func (TupleTupleEvent2) ContractEventName() string {
// Solidity: event TupleEvent2((uint8,uint8)[] arg0)
func (tuple *Tuple) UnpackTupleEvent2Event(log *types.Log) (*TupleTupleEvent2, error) {
event := "TupleEvent2"
- if log.Topics[0] != tuple.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != tuple.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(TupleTupleEvent2)
diff --git a/accounts/abi/bind/v2/internal/contracts/db/bindings.go b/accounts/abi/bind/v2/internal/contracts/db/bindings.go
index fc00a555b5..4ac1652ff7 100644
--- a/accounts/abi/bind/v2/internal/contracts/db/bindings.go
+++ b/accounts/abi/bind/v2/internal/contracts/db/bindings.go
@@ -276,7 +276,7 @@ func (DBInsert) ContractEventName() string {
// Solidity: event Insert(uint256 key, uint256 value, uint256 length)
func (dB *DB) UnpackInsertEvent(log *types.Log) (*DBInsert, error) {
event := "Insert"
- if log.Topics[0] != dB.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dB.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DBInsert)
@@ -318,7 +318,7 @@ func (DBKeyedInsert) ContractEventName() string {
// Solidity: event KeyedInsert(uint256 indexed key, uint256 value)
func (dB *DB) UnpackKeyedInsertEvent(log *types.Log) (*DBKeyedInsert, error) {
event := "KeyedInsert"
- if log.Topics[0] != dB.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != dB.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(DBKeyedInsert)
diff --git a/accounts/abi/bind/v2/internal/contracts/events/bindings.go b/accounts/abi/bind/v2/internal/contracts/events/bindings.go
index ba4fdc71e3..40d2c44a44 100644
--- a/accounts/abi/bind/v2/internal/contracts/events/bindings.go
+++ b/accounts/abi/bind/v2/internal/contracts/events/bindings.go
@@ -115,7 +115,7 @@ func (CBasic1) ContractEventName() string {
// Solidity: event basic1(uint256 indexed id, uint256 data)
func (c *C) UnpackBasic1Event(log *types.Log) (*CBasic1, error) {
event := "basic1"
- if log.Topics[0] != c.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != c.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(CBasic1)
@@ -157,7 +157,7 @@ func (CBasic2) ContractEventName() string {
// Solidity: event basic2(bool indexed flag, uint256 data)
func (c *C) UnpackBasic2Event(log *types.Log) (*CBasic2, error) {
event := "basic2"
- if log.Topics[0] != c.abi.Events[event].ID {
+ if len(log.Topics) == 0 || log.Topics[0] != c.abi.Events[event].ID {
return nil, errors.New("event signature mismatch")
}
out := new(CBasic2)
diff --git a/accounts/abi/bind/v2/lib_test.go b/accounts/abi/bind/v2/lib_test.go
index ee1db9cf86..11360fc7dd 100644
--- a/accounts/abi/bind/v2/lib_test.go
+++ b/accounts/abi/bind/v2/lib_test.go
@@ -367,3 +367,28 @@ func TestErrors(t *testing.T) {
t.Fatalf("bad unpacked error result: expected Arg4 to be false. got true")
}
}
+
+func TestEventUnpackEmptyTopics(t *testing.T) {
+ c := events.NewC()
+
+ for _, log := range []*types.Log{
+ {Topics: []common.Hash{}},
+ {Topics: nil},
+ } {
+ _, err := c.UnpackBasic1Event(log)
+ if err == nil {
+ t.Fatal("expected error when unpacking event with empty topics, got nil")
+ }
+ if err.Error() != "event signature mismatch" {
+ t.Fatalf("expected 'event signature mismatch' error, got: %v", err)
+ }
+
+ _, err = c.UnpackBasic2Event(log)
+ if err == nil {
+ t.Fatal("expected error when unpacking event with empty topics, got nil")
+ }
+ if err.Error() != "event signature mismatch" {
+ t.Fatalf("expected 'event signature mismatch' error, got: %v", err)
+ }
+ }
+}
diff --git a/accounts/scwallet/wallet.go b/accounts/scwallet/wallet.go
index 58cfc88301..7612953c22 100644
--- a/accounts/scwallet/wallet.go
+++ b/accounts/scwallet/wallet.go
@@ -472,6 +472,11 @@ func (w *Wallet) selfDerive() {
continue
}
pairing := w.Hub.pairing(w)
+ if pairing == nil {
+ w.lock.Unlock()
+ reqc <- struct{}{}
+ continue
+ }
// Device lock obtained, derive the next batch of accounts
var (
@@ -631,13 +636,13 @@ func (w *Wallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Accoun
}
if pin {
- pairing := w.Hub.pairing(w)
- pairing.Accounts[account.Address] = path
- if err := w.Hub.setPairing(w, pairing); err != nil {
- return accounts.Account{}, err
+ if pairing := w.Hub.pairing(w); pairing != nil {
+ pairing.Accounts[account.Address] = path
+ if err := w.Hub.setPairing(w, pairing); err != nil {
+ return accounts.Account{}, err
+ }
}
}
-
return account, nil
}
@@ -774,11 +779,11 @@ func (w *Wallet) SignTxWithPassphrase(account accounts.Account, passphrase strin
// It first checks for the address in the list of pinned accounts, and if it is
// not found, attempts to parse the derivation path from the account's URL.
func (w *Wallet) findAccountPath(account accounts.Account) (accounts.DerivationPath, error) {
- pairing := w.Hub.pairing(w)
- if path, ok := pairing.Accounts[account.Address]; ok {
- return path, nil
+ if pairing := w.Hub.pairing(w); pairing != nil {
+ if path, ok := pairing.Accounts[account.Address]; ok {
+ return path, nil
+ }
}
-
// Look for the path in the URL
if account.URL.Scheme != w.Hub.scheme {
return nil, fmt.Errorf("scheme %s does not match wallet scheme %s", account.URL.Scheme, w.Hub.scheme)
diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go
index 2be6edd44f..52595a1621 100644
--- a/accounts/usbwallet/ledger.go
+++ b/accounts/usbwallet/ledger.go
@@ -166,7 +166,7 @@ func (w *ledgerDriver) SignTx(path accounts.DerivationPath, tx *types.Transactio
return common.Address{}, nil, accounts.ErrWalletClosed
}
// Ensure the wallet is capable of signing the given transaction
- if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 {
+ if chainID != nil && (w.version[0] < 1 || (w.version[0] == 1 && w.version[1] == 0 && w.version[2] < 3)) {
//lint:ignore ST1005 brand name displayed on the console
return common.Address{}, nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", w.version[0], w.version[1], w.version[2])
}
diff --git a/beacon/light/request/scheduler.go b/beacon/light/request/scheduler.go
index e80daf805e..242ed56d28 100644
--- a/beacon/light/request/scheduler.go
+++ b/beacon/light/request/scheduler.go
@@ -269,7 +269,7 @@ func (s *Scheduler) addEvent(event Event) {
s.Trigger()
}
-// filterEvent sorts each Event either as a request event or a server event,
+// filterEvents sorts each Event either as a request event or a server event,
// depending on its type. Request events are also sorted in a map based on the
// module that originally initiated the request. It also ensures that no events
// related to a server are returned before EvRegistered or after EvUnregistered.
diff --git a/build/ci.go b/build/ci.go
index 85483c170f..57e4374c69 100644
--- a/build/ci.go
+++ b/build/ci.go
@@ -124,6 +124,7 @@ var (
"jammy", // 22.04, EOL: 04/2032
"noble", // 24.04, EOL: 04/2034
"oracular", // 24.10, EOL: 07/2025
+ "plucky", // 25.04, EOL: 01/2026
}
// This is where the tests should be unpacked.
diff --git a/cmd/bera-geth/chaincmd.go b/cmd/bera-geth/chaincmd.go
index 0206e30829..abd9c905f6 100644
--- a/cmd/bera-geth/chaincmd.go
+++ b/cmd/bera-geth/chaincmd.go
@@ -109,6 +109,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`,
utils.MetricsInfluxDBTokenFlag,
utils.MetricsInfluxDBBucketFlag,
utils.MetricsInfluxDBOrganizationFlag,
+ utils.StateSizeTrackingFlag,
utils.TxLookupLimitFlag,
utils.VMTraceFlag,
utils.VMTraceJsonConfigFlag,
@@ -292,7 +293,7 @@ func initGenesis(ctx *cli.Context) error {
// Only create triedb if we're not using PBSS or genesis is empty on disk. Refer to
// https://github.com/ethereum/go-ethereum/pull/25523 for why the triedb cannot be
// re-created when using PBSS.
- triedb = utils.MakeTrieDatabase(ctx, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle())
+ triedb = utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle())
defer triedb.Close()
}
@@ -651,7 +652,7 @@ func dump(ctx *cli.Context) error {
if err != nil {
return err
}
- triedb := utils.MakeTrieDatabase(ctx, db, true, true, false) // always enable preimage lookup
+ triedb := utils.MakeTrieDatabase(ctx, stack, db, true, true, false) // always enable preimage lookup
defer triedb.Close()
state, err := state.New(root, state.NewDatabase(triedb, nil))
diff --git a/cmd/bera-geth/config.go b/cmd/bera-geth/config.go
index f6e9e97172..26a3468769 100644
--- a/cmd/bera-geth/config.go
+++ b/cmd/bera-geth/config.go
@@ -215,7 +215,7 @@ func constructDevModeBanner(ctx *cli.Context, cfg gethConfig) string {
0x%x (10^49 ETH)
`, cfg.Eth.Miner.PendingFeeRecipient)
if cfg.Eth.Miner.PendingFeeRecipient == utils.DeveloperAddr {
- devModeBanner += fmt.Sprintf(`
+ devModeBanner += fmt.Sprintf(`
Private Key
------------------
0x%x
diff --git a/cmd/bera-geth/dbcmd.go b/cmd/bera-geth/dbcmd.go
index 44a52521f0..c57add0656 100644
--- a/cmd/bera-geth/dbcmd.go
+++ b/cmd/bera-geth/dbcmd.go
@@ -524,7 +524,7 @@ func dbDumpTrie(ctx *cli.Context) error {
db := utils.MakeChainDatabase(ctx, stack, true)
defer db.Close()
- triedb := utils.MakeTrieDatabase(ctx, db, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
defer triedb.Close()
var (
@@ -859,7 +859,7 @@ func inspectHistory(ctx *cli.Context) error {
db := utils.MakeChainDatabase(ctx, stack, true)
defer db.Close()
- triedb := utils.MakeTrieDatabase(ctx, db, false, false, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, db, false, false, false)
defer triedb.Close()
var (
diff --git a/cmd/bera-geth/main.go b/cmd/bera-geth/main.go
index 64d2f2d120..babb2b27fd 100644
--- a/cmd/bera-geth/main.go
+++ b/cmd/bera-geth/main.go
@@ -200,6 +200,7 @@ var (
utils.MetricsInfluxDBTokenFlag,
utils.MetricsInfluxDBBucketFlag,
utils.MetricsInfluxDBOrganizationFlag,
+ utils.StateSizeTrackingFlag,
}
)
diff --git a/cmd/bera-geth/snapshot.go b/cmd/bera-geth/snapshot.go
index aa9ae7087f..994cb149ce 100644
--- a/cmd/bera-geth/snapshot.go
+++ b/cmd/bera-geth/snapshot.go
@@ -217,7 +217,7 @@ func verifyState(ctx *cli.Context) error {
log.Error("Failed to load head block")
return errors.New("no head block")
}
- triedb := utils.MakeTrieDatabase(ctx, chaindb, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false)
defer triedb.Close()
var (
@@ -282,7 +282,7 @@ func traverseState(ctx *cli.Context) error {
chaindb := utils.MakeChainDatabase(ctx, stack, true)
defer chaindb.Close()
- triedb := utils.MakeTrieDatabase(ctx, chaindb, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false)
defer triedb.Close()
headBlock := rawdb.ReadHeadBlock(chaindb)
@@ -391,7 +391,7 @@ func traverseRawState(ctx *cli.Context) error {
chaindb := utils.MakeChainDatabase(ctx, stack, true)
defer chaindb.Close()
- triedb := utils.MakeTrieDatabase(ctx, chaindb, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false)
defer triedb.Close()
headBlock := rawdb.ReadHeadBlock(chaindb)
@@ -558,7 +558,7 @@ func dumpState(ctx *cli.Context) error {
if err != nil {
return err
}
- triedb := utils.MakeTrieDatabase(ctx, db, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
defer triedb.Close()
snapConfig := snapshot.Config{
@@ -640,7 +640,7 @@ func snapshotExportPreimages(ctx *cli.Context) error {
chaindb := utils.MakeChainDatabase(ctx, stack, true)
defer chaindb.Close()
- triedb := utils.MakeTrieDatabase(ctx, chaindb, false, true, false)
+ triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false)
defer triedb.Close()
var root common.Hash
diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go
index 47d00761f3..47327b6844 100644
--- a/cmd/devp2p/internal/ethtest/suite.go
+++ b/cmd/devp2p/internal/ethtest/suite.go
@@ -1133,7 +1133,10 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
// transmit the same tx but with correct sidecar from the good peer.
var req *eth.GetPooledTransactionsPacket
- req, err = readUntil[eth.GetPooledTransactionsPacket](context.Background(), conn)
+ ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
+ defer cancel()
+
+ req, err = readUntil[eth.GetPooledTransactionsPacket](ctx, conn)
if err != nil {
errc <- fmt.Errorf("reading pooled tx request failed: %v", err)
return
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 6891147177..57663abd4c 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -425,7 +425,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc) *state.StateDB
sdb := state.NewDatabase(tdb, nil)
statedb, _ := state.New(types.EmptyRootHash, sdb)
for addr, a := range accounts {
- statedb.SetCode(addr, a.Code)
+ statedb.SetCode(addr, a.Code, tracing.CodeChangeUnspecified)
statedb.SetNonce(addr, a.Nonce, tracing.NonceChangeGenesis)
statedb.SetBalance(addr, uint256.MustFromBig(a.Balance), tracing.BalanceIncreaseGenesisBalance)
for k, v := range a.Storage {
diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go
index b2cf28353b..ebb3e04461 100644
--- a/cmd/evm/runner.go
+++ b/cmd/evm/runner.go
@@ -322,7 +322,7 @@ func runCmd(ctx *cli.Context) error {
}
} else {
if len(code) > 0 {
- prestate.SetCode(receiver, code)
+ prestate.SetCode(receiver, code, tracing.CodeChangeUnspecified)
}
execFunc = func() ([]byte, uint64, error) {
// don't mutate the state!
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index d1825cd27c..b25cf9bb57 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -281,6 +281,12 @@ var (
Usage: "Scheme to use for storing ethereum state ('hash' or 'path')",
Category: flags.StateCategory,
}
+ StateSizeTrackingFlag = &cli.BoolFlag{
+ Name: "state.size-tracking",
+ Usage: "Enable state size tracking, retrieve state size with debug_stateSize.",
+ Value: ethconfig.Defaults.EnableStateSizeTracking,
+ Category: flags.StateCategory,
+ }
StateHistoryFlag = &cli.Uint64Flag{
Name: "history.state",
Usage: "Number of recent blocks to retain state history for, only relevant in state.scheme=path (default = 90,000 blocks, 0 = entire chain)",
@@ -1741,6 +1747,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
cfg.EthDiscoveryURLs = SplitAndTrim(urls)
}
}
+ if ctx.Bool(StateSizeTrackingFlag.Name) {
+ cfg.EnableStateSizeTracking = true
+ }
// Override any default configs for hard coded networks.
switch {
case ctx.Bool(MainnetFlag.Name):
@@ -2223,6 +2232,9 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh
// - DATADIR/triedb/merkle.journal
// - DATADIR/triedb/verkle.journal
TrieJournalDirectory: stack.ResolvePath("triedb"),
+
+ // Enable state size tracking if enabled
+ StateSizeTracking: ctx.Bool(StateSizeTrackingFlag.Name),
}
if options.ArchiveMode && !options.Preimages {
options.Preimages = true
@@ -2284,7 +2296,7 @@ func MakeConsolePreloads(ctx *cli.Context) []string {
}
// MakeTrieDatabase constructs a trie database based on the configured scheme.
-func MakeTrieDatabase(ctx *cli.Context, disk ethdb.Database, preimage bool, readOnly bool, isVerkle bool) *triedb.Database {
+func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, preimage bool, readOnly bool, isVerkle bool) *triedb.Database {
config := &triedb.Config{
Preimages: preimage,
IsVerkle: isVerkle,
@@ -2300,10 +2312,13 @@ func MakeTrieDatabase(ctx *cli.Context, disk ethdb.Database, preimage bool, read
config.HashDB = hashdb.Defaults
return triedb.NewDatabase(disk, config)
}
+ var pathConfig pathdb.Config
if readOnly {
- config.PathDB = pathdb.ReadOnly
+ pathConfig = *pathdb.ReadOnly
} else {
- config.PathDB = pathdb.Defaults
+ pathConfig = *pathdb.Defaults
}
+ pathConfig.JournalDirectory = stack.ResolvePath("triedb")
+ config.PathDB = &pathConfig
return triedb.NewDatabase(disk, config)
}
diff --git a/cmd/workload/filtertestperf.go b/cmd/workload/filtertestperf.go
index f13647683a..50cc6eaae2 100644
--- a/cmd/workload/filtertestperf.go
+++ b/cmd/workload/filtertestperf.go
@@ -154,7 +154,7 @@ func (st *bucketStats) print(name string) {
name, st.count, float64(st.blocks)/float64(st.count), float64(st.logs)/float64(st.count), st.runtime/time.Duration(st.count))
}
-// writeQueries serializes the generated errors to the error file.
+// writeErrors serializes the generated errors to the error file.
func writeErrors(errorFile string, errors []*filterQuery) {
file, err := os.Create(errorFile)
if err != nil {
diff --git a/common/eta.go b/common/eta.go
new file mode 100644
index 0000000000..72c838f93d
--- /dev/null
+++ b/common/eta.go
@@ -0,0 +1,30 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package common
+
+import "time"
+
+// CalculateETA calculates the estimated remaining time based on the
+// number of finished task, remaining task, and the time cost for finished task.
+func CalculateETA(done, left uint64, elapsed time.Duration) time.Duration {
+ if done == 0 || elapsed.Milliseconds() == 0 {
+ return 0
+ }
+
+ speed := float64(done) / float64(elapsed.Milliseconds())
+ return time.Duration(float64(left)/speed) * time.Millisecond
+}
diff --git a/common/eta_test.go b/common/eta_test.go
new file mode 100644
index 0000000000..b1dbb09e6c
--- /dev/null
+++ b/common/eta_test.go
@@ -0,0 +1,60 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package common
+
+import (
+ "testing"
+ "time"
+)
+
+func TestCalculateETA(t *testing.T) {
+ type args struct {
+ done uint64
+ left uint64
+ elapsed time.Duration
+ }
+ tests := []struct {
+ name string
+ args args
+ want time.Duration
+ }{
+ {
+ name: "zero done",
+ args: args{done: 0, left: 100, elapsed: time.Second},
+ want: 0,
+ },
+ {
+ name: "zero elapsed",
+ args: args{done: 1, left: 100, elapsed: 0},
+ want: 0,
+ },
+ {
+ name: "@Jolly23 's case",
+ args: args{done: 16858580, left: 41802252, elapsed: 66179848 * time.Millisecond},
+ want: 164098440 * time.Millisecond,
+ // wrong msg: msg="Indexing state history" processed=16858580 left=41802252 elapsed=18h22m59.848s eta=11h36m42.252s
+ // should be around 45.58 hours
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := CalculateETA(tt.args.done, tt.args.left, tt.args.elapsed); got != tt.want {
+ t.Errorf("CalculateETA() = %v ms, want %v ms", got.Milliseconds(), tt.want)
+ }
+ })
+ }
+}
diff --git a/core/blockchain.go b/core/blockchain.go
index 0b92a94b6c..c97897cd70 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -168,10 +168,13 @@ type BlockChainConfig struct {
TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed
TrieJournalDirectory string // Directory path to the journal used for persisting trie data across node restarts
- Preimages bool // Whether to store preimage of trie key to the disk
- StateHistory uint64 // Number of blocks from head whose state histories are reserved.
- StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top
- ArchiveMode bool // Whether to enable the archive mode
+ Preimages bool // Whether to store preimage of trie key to the disk
+ StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top
+ ArchiveMode bool // Whether to enable the archive mode
+
+ // Number of blocks from the chain head for which state histories are retained.
+ // If set to 0, all state histories across the entire chain will be retained;
+ StateHistory uint64
// State snapshot related options
SnapshotLimit int // Memory allowance (MB) to use for caching snapshot entries in memory
@@ -193,6 +196,9 @@ type BlockChainConfig struct {
// If the value is zero, all transactions of the entire chain will be indexed.
// If the value is -1, indexing is disabled.
TxLookupLimit int64
+
+ // StateSizeTracking indicates whether the state size tracking is enabled.
+ StateSizeTracking bool
}
// DefaultConfig returns the default config.
@@ -330,6 +336,7 @@ type BlockChain struct {
prefetcher Prefetcher
processor Processor // Block transaction processor interface
logger *tracing.Hooks
+ stateSizer *state.SizeTracker // State size tracking
lastForkReadyAlert time.Time // Last time there was a fork readiness print out
}
@@ -523,6 +530,17 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
if bc.cfg.TxLookupLimit >= 0 {
bc.txIndexer = newTxIndexer(uint64(bc.cfg.TxLookupLimit), bc)
}
+
+ // Start state size tracker
+ if bc.cfg.StateSizeTracking {
+ stateSizer, err := state.NewSizeTracker(bc.db, bc.triedb)
+ if err == nil {
+ bc.stateSizer = stateSizer
+ log.Info("Enabled state size metrics")
+ } else {
+ log.Info("Failed to setup size tracker", "err", err)
+ }
+ }
return bc, nil
}
@@ -1249,6 +1267,10 @@ func (bc *BlockChain) stopWithoutSaving() {
// Signal shutdown to all goroutines.
bc.InterruptInsert(true)
+ // Stop state size tracker
+ if bc.stateSizer != nil {
+ bc.stateSizer.Stop()
+ }
// Now wait for all chain modifications to end and persistent goroutines to exit.
//
// Note: Close waits for the mutex to become available, i.e. any running chain
@@ -1583,10 +1605,14 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
log.Crit("Failed to write block into disk", "err", err)
}
// Commit all cached state changes into underlying memory database.
- root, err := statedb.Commit(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number()), bc.chainConfig.IsCancun(block.Number(), block.Time()))
+ root, stateUpdate, err := statedb.CommitWithUpdate(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number()), bc.chainConfig.IsCancun(block.Number(), block.Time()))
if err != nil {
return err
}
+ // Emit the state update to the state sizestats if it's active
+ if bc.stateSizer != nil {
+ bc.stateSizer.Notify(stateUpdate)
+ }
// If node is running in path mode, skip explicit gc operation
// which is unnecessary in this mode.
if bc.triedb.Scheme() == rawdb.PathScheme {
@@ -2011,7 +2037,10 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
// If we are past Byzantium, enable prefetching to pull in trie node paths
// while processing transactions. Before Byzantium the prefetcher is mostly
// useless due to the intermediate root hashing after each transaction.
- var witness *stateless.Witness
+ var (
+ witness *stateless.Witness
+ witnessStats *stateless.WitnessStats
+ )
if bc.chainConfig.IsByzantium(block.Number()) {
// Generate witnesses either if we're self-testing, or if it's the
// only block being inserted. A bit crude, but witnesses are huge,
@@ -2021,8 +2050,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
if err != nil {
return nil, err
}
+ if bc.cfg.VmConfig.EnableWitnessStats {
+ witnessStats = stateless.NewWitnessStats()
+ }
}
- statedb.StartPrefetcher("chain", witness)
+ statedb.StartPrefetcher("chain", witness, witnessStats)
defer statedb.StopPrefetcher()
}
@@ -2083,6 +2115,7 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash())
}
}
+
xvtime := time.Since(xvstart)
proctime := time.Since(startTime) // processing + validation + cross validation
@@ -2118,6 +2151,11 @@ func (bc *BlockChain) processBlock(parentRoot common.Hash, block *types.Block, s
if err != nil {
return nil, err
}
+ // Report the collected witness statistics
+ if witnessStats != nil {
+ witnessStats.ReportMetrics()
+ }
+
// Update the metrics touched during block commit
accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them
storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them
@@ -2776,3 +2814,8 @@ func (bc *BlockChain) SetTrieFlushInterval(interval time.Duration) {
func (bc *BlockChain) GetTrieFlushInterval() time.Duration {
return time.Duration(bc.flushInterval.Load())
}
+
+// StateSizer returns the state size tracker, or nil if it's not initialized
+func (bc *BlockChain) StateSizer() *state.SizeTracker {
+ return bc.stateSizer
+}
diff --git a/core/filtermaps/indexer.go b/core/filtermaps/indexer.go
index 3571f9f375..ca50fb466c 100644
--- a/core/filtermaps/indexer.go
+++ b/core/filtermaps/indexer.go
@@ -30,7 +30,7 @@ const (
headLogDelay = time.Second // head indexing log info delay (do not log if finished faster)
)
-// updateLoop initializes and updates the log index structure according to the
+// indexerLoop initializes and updates the log index structure according to the
// current targetView.
func (f *FilterMaps) indexerLoop() {
defer f.closeWg.Done()
@@ -221,7 +221,7 @@ func (f *FilterMaps) processSingleEvent(blocking bool) bool {
return true
}
-// setTargetView updates the target chain view of the iterator.
+// setTarget updates the target chain view of the iterator.
func (f *FilterMaps) setTarget(target targetUpdate) {
f.targetView = target.targetView
f.historyCutoff = target.historyCutoff
diff --git a/core/genesis.go b/core/genesis.go
index 098de3cf24..b07837821d 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -153,7 +153,7 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
if account.Balance != nil {
statedb.AddBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceIncreaseGenesisBalance)
}
- statedb.SetCode(addr, account.Code)
+ statedb.SetCode(addr, account.Code, tracing.CodeChangeGenesis)
statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
@@ -179,7 +179,7 @@ func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, e
// already captures the allocations.
statedb.AddBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceIncreaseGenesisBalance)
}
- statedb.SetCode(addr, account.Code)
+ statedb.SetCode(addr, account.Code, tracing.CodeChangeGenesis)
statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go
index 859566f722..6996031be2 100644
--- a/core/rawdb/accessors_metadata.go
+++ b/core/rawdb/accessors_metadata.go
@@ -174,16 +174,3 @@ func UpdateUncleanShutdownMarker(db ethdb.KeyValueStore) {
log.Warn("Failed to write unclean-shutdown marker", "err", err)
}
}
-
-// ReadTransitionStatus retrieves the eth2 transition status from the database
-func ReadTransitionStatus(db ethdb.KeyValueReader) []byte {
- data, _ := db.Get(transitionStatusKey)
- return data
-}
-
-// WriteTransitionStatus stores the eth2 transition status to the database
-func WriteTransitionStatus(db ethdb.KeyValueWriter, data []byte) {
- if err := db.Put(transitionStatusKey, data); err != nil {
- log.Crit("Failed to store the eth2 transition status", "err", err)
- }
-}
diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go
index 44f041d82e..2359fb18f1 100644
--- a/core/rawdb/accessors_state.go
+++ b/core/rawdb/accessors_state.go
@@ -119,13 +119,6 @@ func WriteStateID(db ethdb.KeyValueWriter, root common.Hash, id uint64) {
}
}
-// DeleteStateID deletes the specified state lookup from the database.
-func DeleteStateID(db ethdb.KeyValueWriter, root common.Hash) {
- if err := db.Delete(stateIDKey(root)); err != nil {
- log.Crit("Failed to delete state ID", "err", err)
- }
-}
-
// ReadPersistentStateID retrieves the id of the persistent state from the database.
func ReadPersistentStateID(db ethdb.KeyValueReader) uint64 {
data, _ := db.Get(persistentStateIDKey)
diff --git a/core/rawdb/ancienttest/testsuite.go b/core/rawdb/ancienttest/testsuite.go
index e33e768947..7512c1f44b 100644
--- a/core/rawdb/ancienttest/testsuite.go
+++ b/core/rawdb/ancienttest/testsuite.go
@@ -48,12 +48,16 @@ func basicRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
)
defer db.Close()
- db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < len(data); i++ {
- op.AppendRaw("a", uint64(i), data[i])
+ if err := op.AppendRaw("a", uint64(i), data[i]); err != nil {
+ return err
+ }
}
return nil
- })
+ }); err != nil {
+ t.Fatalf("Failed to write ancient data %v", err)
+ }
db.TruncateTail(10)
db.TruncateHead(90)
@@ -109,12 +113,16 @@ func batchRead(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
)
defer db.Close()
- db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), data[i])
+ if err := op.AppendRaw("a", uint64(i), data[i]); err != nil {
+ return err
+ }
}
return nil
- })
+ }); err != nil {
+ t.Fatalf("Failed to write ancient data %v", err)
+ }
db.TruncateTail(10)
db.TruncateHead(90)
@@ -189,7 +197,9 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
// The ancient write to tables should be aligned
_, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), dataA[i])
+ if err := op.AppendRaw("a", uint64(i), dataA[i]); err != nil {
+ return err
+ }
}
return nil
})
@@ -200,8 +210,12 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
// Test normal ancient write
size, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), dataA[i])
- op.AppendRaw("b", uint64(i), dataB[i])
+ if err := op.AppendRaw("a", uint64(i), dataA[i]); err != nil {
+ return err
+ }
+ if err := op.AppendRaw("b", uint64(i), dataB[i]); err != nil {
+ return err
+ }
}
return nil
})
@@ -217,8 +231,12 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
db.TruncateHead(90)
_, err = db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 90; i < 100; i++ {
- op.AppendRaw("a", uint64(i), dataA[i])
- op.AppendRaw("b", uint64(i), dataB[i])
+ if err := op.AppendRaw("a", uint64(i), dataA[i]); err != nil {
+ return err
+ }
+ if err := op.AppendRaw("b", uint64(i), dataB[i]); err != nil {
+ return err
+ }
}
return nil
})
@@ -227,11 +245,15 @@ func basicWrite(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
}
// Write should work after truncating everything
- db.TruncateTail(0)
+ db.TruncateHead(0)
_, err = db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), dataA[i])
- op.AppendRaw("b", uint64(i), dataB[i])
+ if err := op.AppendRaw("a", uint64(i), dataA[i]); err != nil {
+ return err
+ }
+ if err := op.AppendRaw("b", uint64(i), dataB[i]); err != nil {
+ return err
+ }
}
return nil
})
@@ -245,14 +267,18 @@ func nonMutable(t *testing.T, newFn func(kinds []string) ethdb.AncientStore) {
defer db.Close()
// We write 100 zero-bytes to the freezer and immediately mutate the slice
- db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
data := make([]byte, 100)
- op.AppendRaw("a", uint64(0), data)
+ if err := op.AppendRaw("a", uint64(0), data); err != nil {
+ return err
+ }
for i := range data {
data[i] = 0xff
}
return nil
- })
+ }); err != nil {
+ t.Fatalf("Failed to write ancient data %v", err)
+ }
// Now read it.
data, err := db.Ancient("a", uint64(0))
if err != nil {
@@ -275,23 +301,31 @@ func TestResettableAncientSuite(t *testing.T, newFn func(kinds []string) ethdb.R
)
defer db.Close()
- db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), data[i])
+ if err := op.AppendRaw("a", uint64(i), data[i]); err != nil {
+ return err
+ }
}
return nil
- })
+ }); err != nil {
+ t.Fatalf("Failed to write ancient data %v", err)
+ }
db.TruncateTail(10)
db.TruncateHead(90)
// Ancient write should work after resetting
db.Reset()
- db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
+ if _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
for i := 0; i < 100; i++ {
- op.AppendRaw("a", uint64(i), data[i])
+ if err := op.AppendRaw("a", uint64(i), data[i]); err != nil {
+ return err
+ }
}
return nil
- })
+ }); err != nil {
+ t.Fatalf("Failed to write ancient data %v", err)
+ }
})
}
diff --git a/core/rawdb/database.go b/core/rawdb/database.go
index 25cd20d164..626d390c0d 100644
--- a/core/rawdb/database.go
+++ b/core/rawdb/database.go
@@ -18,13 +18,17 @@ package rawdb
import (
"bytes"
+ "context"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
+ "runtime"
"slices"
"strings"
+ "sync"
+ "sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
@@ -32,7 +36,7 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
"github.com/ethereum/go-ethereum/log"
- "github.com/olekukonko/tablewriter"
+ "golang.org/x/sync/errgroup"
)
var ErrDeleteRangeInterrupted = errors.New("safe delete range operation interrupted")
@@ -363,36 +367,36 @@ func (c counter) Percentage(current uint64) string {
return fmt.Sprintf("%d", current*100/uint64(c))
}
-// stat stores sizes and count for a parameter
+// stat provides lock-free statistics aggregation using atomic operations
type stat struct {
- size common.StorageSize
- count counter
+ size uint64
+ count uint64
}
-// Add size to the stat and increase the counter by 1
-func (s *stat) Add(size common.StorageSize) {
- s.size += size
- s.count++
+func (s *stat) empty() bool {
+ return atomic.LoadUint64(&s.count) == 0
}
-func (s *stat) Size() string {
- return s.size.String()
+func (s *stat) add(size common.StorageSize) {
+ atomic.AddUint64(&s.size, uint64(size))
+ atomic.AddUint64(&s.count, 1)
}
-func (s *stat) Count() string {
- return s.count.String()
+func (s *stat) sizeString() string {
+ return common.StorageSize(atomic.LoadUint64(&s.size)).String()
+}
+
+func (s *stat) countString() string {
+ return counter(atomic.LoadUint64(&s.count)).String()
}
// InspectDatabase traverses the entire database and checks the size
// of all different categories of data.
func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
- it := db.NewIterator(keyPrefix, keyStart)
- defer it.Release()
-
var (
- count int64
- start = time.Now()
- logged = time.Now()
+ start = time.Now()
+ count atomic.Int64
+ total atomic.Uint64
// Key-value store statistics
headers stat
@@ -428,144 +432,200 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
metadata stat
unaccounted stat
- // Totals
- total common.StorageSize
-
// This map tracks example keys for unaccounted data.
// For each unique two-byte prefix, the first unaccounted key encountered
// by the iterator will be stored.
unaccountedKeys = make(map[[2]byte][]byte)
+ unaccountedMu sync.Mutex
)
- // Inspect key-value database first.
- for it.Next() {
- var (
- key = it.Key()
- size = common.StorageSize(len(key) + len(it.Value()))
- )
- total += size
- switch {
- case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength):
- headers.Add(size)
- case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength):
- bodies.Add(size)
- case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength):
- receipts.Add(size)
- case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix):
- tds.Add(size)
- case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix):
- numHashPairings.Add(size)
- case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
- hashNumPairings.Add(size)
- case IsLegacyTrieNode(key, it.Value()):
- legacyTries.Add(size)
- case bytes.HasPrefix(key, stateIDPrefix) && len(key) == len(stateIDPrefix)+common.HashLength:
- stateLookups.Add(size)
- case IsAccountTrieNode(key):
- accountTries.Add(size)
- case IsStorageTrieNode(key):
- storageTries.Add(size)
- case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength:
- codes.Add(size)
- case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength):
- txLookups.Add(size)
- case bytes.HasPrefix(key, SnapshotAccountPrefix) && len(key) == (len(SnapshotAccountPrefix)+common.HashLength):
- accountSnaps.Add(size)
- case bytes.HasPrefix(key, SnapshotStoragePrefix) && len(key) == (len(SnapshotStoragePrefix)+2*common.HashLength):
- storageSnaps.Add(size)
- case bytes.HasPrefix(key, PreimagePrefix) && len(key) == (len(PreimagePrefix)+common.HashLength):
- preimages.Add(size)
- case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength):
- metadata.Add(size)
- case bytes.HasPrefix(key, genesisPrefix) && len(key) == (len(genesisPrefix)+common.HashLength):
- metadata.Add(size)
- case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8):
- beaconHeaders.Add(size)
- case bytes.HasPrefix(key, CliqueSnapshotPrefix) && len(key) == 7+common.HashLength:
- cliqueSnaps.Add(size)
- // new log index
- case bytes.HasPrefix(key, filterMapRowPrefix) && len(key) <= len(filterMapRowPrefix)+9:
- filterMapRows.Add(size)
- case bytes.HasPrefix(key, filterMapLastBlockPrefix) && len(key) == len(filterMapLastBlockPrefix)+4:
- filterMapLastBlock.Add(size)
- case bytes.HasPrefix(key, filterMapBlockLVPrefix) && len(key) == len(filterMapBlockLVPrefix)+8:
- filterMapBlockLV.Add(size)
-
- // old log index (deprecated)
- case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength):
- bloomBits.Add(size)
- case bytes.HasPrefix(key, bloomBitsMetaPrefix) && len(key) < len(bloomBitsMetaPrefix)+8:
- bloomBits.Add(size)
-
- // Path-based historic state indexes
- case bytes.HasPrefix(key, StateHistoryIndexPrefix) && len(key) >= len(StateHistoryIndexPrefix)+common.HashLength:
- stateIndex.Add(size)
-
- // Verkle trie data is detected, determine the sub-category
- case bytes.HasPrefix(key, VerklePrefix):
- remain := key[len(VerklePrefix):]
+ inspectRange := func(ctx context.Context, r byte) error {
+ var s []byte
+ if len(keyStart) > 0 {
switch {
- case IsAccountTrieNode(remain):
- verkleTries.Add(size)
- case bytes.HasPrefix(remain, stateIDPrefix) && len(remain) == len(stateIDPrefix)+common.HashLength:
- verkleStateLookups.Add(size)
- case bytes.Equal(remain, persistentStateIDKey):
- metadata.Add(size)
- case bytes.Equal(remain, trieJournalKey):
- metadata.Add(size)
- case bytes.Equal(remain, snapSyncStatusFlagKey):
- metadata.Add(size)
+ case r < keyStart[0]:
+ return nil
+ case r == keyStart[0]:
+ s = keyStart[1:]
default:
- unaccounted.Add(size)
+ // entire key range is included for inspection
}
+ }
+ it := db.NewIterator(append(keyPrefix, r), s)
+ defer it.Release()
- // Metadata keys
- case slices.ContainsFunc(knownMetadataKeys, func(x []byte) bool { return bytes.Equal(x, key) }):
- metadata.Add(size)
+ for it.Next() {
+ var (
+ key = it.Key()
+ size = common.StorageSize(len(key) + len(it.Value()))
+ )
+ total.Add(uint64(size))
+ count.Add(1)
- default:
- unaccounted.Add(size)
- if len(key) >= 2 {
- prefix := [2]byte(key[:2])
- if _, ok := unaccountedKeys[prefix]; !ok {
- unaccountedKeys[prefix] = bytes.Clone(key)
+ switch {
+ case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength):
+ headers.add(size)
+ case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength):
+ bodies.add(size)
+ case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength):
+ receipts.add(size)
+ case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix):
+ tds.add(size)
+ case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix):
+ numHashPairings.add(size)
+ case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength):
+ hashNumPairings.add(size)
+ case IsLegacyTrieNode(key, it.Value()):
+ legacyTries.add(size)
+ case bytes.HasPrefix(key, stateIDPrefix) && len(key) == len(stateIDPrefix)+common.HashLength:
+ stateLookups.add(size)
+ case IsAccountTrieNode(key):
+ accountTries.add(size)
+ case IsStorageTrieNode(key):
+ storageTries.add(size)
+ case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength:
+ codes.add(size)
+ case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength):
+ txLookups.add(size)
+ case bytes.HasPrefix(key, SnapshotAccountPrefix) && len(key) == (len(SnapshotAccountPrefix)+common.HashLength):
+ accountSnaps.add(size)
+ case bytes.HasPrefix(key, SnapshotStoragePrefix) && len(key) == (len(SnapshotStoragePrefix)+2*common.HashLength):
+ storageSnaps.add(size)
+ case bytes.HasPrefix(key, PreimagePrefix) && len(key) == (len(PreimagePrefix)+common.HashLength):
+ preimages.add(size)
+ case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength):
+ metadata.add(size)
+ case bytes.HasPrefix(key, genesisPrefix) && len(key) == (len(genesisPrefix)+common.HashLength):
+ metadata.add(size)
+ case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8):
+ beaconHeaders.add(size)
+ case bytes.HasPrefix(key, CliqueSnapshotPrefix) && len(key) == 7+common.HashLength:
+ cliqueSnaps.add(size)
+
+ // new log index
+ case bytes.HasPrefix(key, filterMapRowPrefix) && len(key) <= len(filterMapRowPrefix)+9:
+ filterMapRows.add(size)
+ case bytes.HasPrefix(key, filterMapLastBlockPrefix) && len(key) == len(filterMapLastBlockPrefix)+4:
+ filterMapLastBlock.add(size)
+ case bytes.HasPrefix(key, filterMapBlockLVPrefix) && len(key) == len(filterMapBlockLVPrefix)+8:
+ filterMapBlockLV.add(size)
+
+ // old log index (deprecated)
+ case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength):
+ bloomBits.add(size)
+ case bytes.HasPrefix(key, bloomBitsMetaPrefix) && len(key) < len(bloomBitsMetaPrefix)+8:
+ bloomBits.add(size)
+
+ // Path-based historic state indexes
+ case bytes.HasPrefix(key, StateHistoryIndexPrefix) && len(key) >= len(StateHistoryIndexPrefix)+common.HashLength:
+ stateIndex.add(size)
+
+ // Verkle trie data is detected, determine the sub-category
+ case bytes.HasPrefix(key, VerklePrefix):
+ remain := key[len(VerklePrefix):]
+ switch {
+ case IsAccountTrieNode(remain):
+ verkleTries.add(size)
+ case bytes.HasPrefix(remain, stateIDPrefix) && len(remain) == len(stateIDPrefix)+common.HashLength:
+ verkleStateLookups.add(size)
+ case bytes.Equal(remain, persistentStateIDKey):
+ metadata.add(size)
+ case bytes.Equal(remain, trieJournalKey):
+ metadata.add(size)
+ case bytes.Equal(remain, snapSyncStatusFlagKey):
+ metadata.add(size)
+ default:
+ unaccounted.add(size)
+ }
+
+ // Metadata keys
+ case slices.ContainsFunc(knownMetadataKeys, func(x []byte) bool { return bytes.Equal(x, key) }):
+ metadata.add(size)
+
+ default:
+ unaccounted.add(size)
+ if len(key) >= 2 {
+ prefix := [2]byte(key[:2])
+ unaccountedMu.Lock()
+ if _, ok := unaccountedKeys[prefix]; !ok {
+ unaccountedKeys[prefix] = bytes.Clone(key)
+ }
+ unaccountedMu.Unlock()
}
}
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
}
- count++
- if count%1000 == 0 && time.Since(logged) > 8*time.Second {
- log.Info("Inspecting database", "count", count, "elapsed", common.PrettyDuration(time.Since(start)))
- logged = time.Now()
- }
+
+ return it.Error()
}
+
+ var (
+ eg, ctx = errgroup.WithContext(context.Background())
+ workers = runtime.NumCPU()
+ )
+ eg.SetLimit(workers)
+
+ // Progress reporter
+ done := make(chan struct{})
+ go func() {
+ ticker := time.NewTicker(8 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ticker.C:
+ log.Info("Inspecting database", "count", count.Load(), "size", common.StorageSize(total.Load()), "elapsed", common.PrettyDuration(time.Since(start)))
+ case <-done:
+ return
+ }
+ }
+ }()
+
+ // Inspect key-value database in parallel.
+ for i := 0; i < 256; i++ {
+ eg.Go(func() error { return inspectRange(ctx, byte(i)) })
+ }
+
+ if err := eg.Wait(); err != nil {
+ close(done)
+ return err
+ }
+ close(done)
+
// Display the database statistic of key-value store.
stats := [][]string{
- {"Key-Value store", "Headers", headers.Size(), headers.Count()},
- {"Key-Value store", "Bodies", bodies.Size(), bodies.Count()},
- {"Key-Value store", "Receipt lists", receipts.Size(), receipts.Count()},
- {"Key-Value store", "Difficulties (deprecated)", tds.Size(), tds.Count()},
- {"Key-Value store", "Block number->hash", numHashPairings.Size(), numHashPairings.Count()},
- {"Key-Value store", "Block hash->number", hashNumPairings.Size(), hashNumPairings.Count()},
- {"Key-Value store", "Transaction index", txLookups.Size(), txLookups.Count()},
- {"Key-Value store", "Log index filter-map rows", filterMapRows.Size(), filterMapRows.Count()},
- {"Key-Value store", "Log index last-block-of-map", filterMapLastBlock.Size(), filterMapLastBlock.Count()},
- {"Key-Value store", "Log index block-lv", filterMapBlockLV.Size(), filterMapBlockLV.Count()},
- {"Key-Value store", "Log bloombits (deprecated)", bloomBits.Size(), bloomBits.Count()},
- {"Key-Value store", "Contract codes", codes.Size(), codes.Count()},
- {"Key-Value store", "Hash trie nodes", legacyTries.Size(), legacyTries.Count()},
- {"Key-Value store", "Path trie state lookups", stateLookups.Size(), stateLookups.Count()},
- {"Key-Value store", "Path trie account nodes", accountTries.Size(), accountTries.Count()},
- {"Key-Value store", "Path trie storage nodes", storageTries.Size(), storageTries.Count()},
- {"Key-Value store", "Path state history indexes", stateIndex.Size(), stateIndex.Count()},
- {"Key-Value store", "Verkle trie nodes", verkleTries.Size(), verkleTries.Count()},
- {"Key-Value store", "Verkle trie state lookups", verkleStateLookups.Size(), verkleStateLookups.Count()},
- {"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()},
- {"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()},
- {"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()},
- {"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()},
- {"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()},
- {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()},
+ {"Key-Value store", "Headers", headers.sizeString(), headers.countString()},
+ {"Key-Value store", "Bodies", bodies.sizeString(), bodies.countString()},
+ {"Key-Value store", "Receipt lists", receipts.sizeString(), receipts.countString()},
+ {"Key-Value store", "Difficulties (deprecated)", tds.sizeString(), tds.countString()},
+ {"Key-Value store", "Block number->hash", numHashPairings.sizeString(), numHashPairings.countString()},
+ {"Key-Value store", "Block hash->number", hashNumPairings.sizeString(), hashNumPairings.countString()},
+ {"Key-Value store", "Transaction index", txLookups.sizeString(), txLookups.countString()},
+ {"Key-Value store", "Log index filter-map rows", filterMapRows.sizeString(), filterMapRows.countString()},
+ {"Key-Value store", "Log index last-block-of-map", filterMapLastBlock.sizeString(), filterMapLastBlock.countString()},
+ {"Key-Value store", "Log index block-lv", filterMapBlockLV.sizeString(), filterMapBlockLV.countString()},
+ {"Key-Value store", "Log bloombits (deprecated)", bloomBits.sizeString(), bloomBits.countString()},
+ {"Key-Value store", "Contract codes", codes.sizeString(), codes.countString()},
+ {"Key-Value store", "Hash trie nodes", legacyTries.sizeString(), legacyTries.countString()},
+ {"Key-Value store", "Path trie state lookups", stateLookups.sizeString(), stateLookups.countString()},
+ {"Key-Value store", "Path trie account nodes", accountTries.sizeString(), accountTries.countString()},
+ {"Key-Value store", "Path trie storage nodes", storageTries.sizeString(), storageTries.countString()},
+ {"Key-Value store", "Path state history indexes", stateIndex.sizeString(), stateIndex.countString()},
+ {"Key-Value store", "Verkle trie nodes", verkleTries.sizeString(), verkleTries.countString()},
+ {"Key-Value store", "Verkle trie state lookups", verkleStateLookups.sizeString(), verkleStateLookups.countString()},
+ {"Key-Value store", "Trie preimages", preimages.sizeString(), preimages.countString()},
+ {"Key-Value store", "Account snapshot", accountSnaps.sizeString(), accountSnaps.countString()},
+ {"Key-Value store", "Storage snapshot", storageSnaps.sizeString(), storageSnaps.countString()},
+ {"Key-Value store", "Beacon sync headers", beaconHeaders.sizeString(), beaconHeaders.countString()},
+ {"Key-Value store", "Clique snapshots", cliqueSnaps.sizeString(), cliqueSnaps.countString()},
+ {"Key-Value store", "Singleton metadata", metadata.sizeString(), metadata.countString()},
}
+
// Inspect all registered append-only file store then.
ancients, err := inspectFreezers(db)
if err != nil {
@@ -580,16 +640,17 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
fmt.Sprintf("%d", ancient.count()),
})
}
- total += ancient.size()
+ total.Add(uint64(ancient.size()))
}
- table := tablewriter.NewWriter(os.Stdout)
+
+ table := newTableWriter(os.Stdout)
table.SetHeader([]string{"Database", "Category", "Size", "Items"})
- table.SetFooter([]string{"", "Total", total.String(), " "})
+ table.SetFooter([]string{"", "Total", common.StorageSize(total.Load()).String(), fmt.Sprintf("%d", count.Load())})
table.AppendBulk(stats)
table.Render()
- if unaccounted.size > 0 {
- log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count)
+ if !unaccounted.empty() {
+ log.Error("Database contains unaccounted data", "size", unaccounted.sizeString(), "count", unaccounted.countString())
for _, e := range slices.SortedFunc(maps.Values(unaccountedKeys), bytes.Compare) {
log.Error(fmt.Sprintf(" example key: %x", e))
}
diff --git a/core/rawdb/database_tablewriter_tinygo.go b/core/rawdb/database_tablewriter_tinygo.go
new file mode 100644
index 0000000000..2f8e456fd5
--- /dev/null
+++ b/core/rawdb/database_tablewriter_tinygo.go
@@ -0,0 +1,208 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// TODO: naive stub implementation for tablewriter
+
+//go:build tinygo
+// +build tinygo
+
+package rawdb
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+)
+
+const (
+ cellPadding = 1 // Number of spaces on each side of cell content
+ totalPadding = 2 * cellPadding // Total padding per cell. Its two because we pad equally on both sides
+)
+
+type Table struct {
+ out io.Writer
+ headers []string
+ footer []string
+ rows [][]string
+}
+
+func newTableWriter(w io.Writer) *Table {
+ return &Table{out: w}
+}
+
+// SetHeader sets the header row for the table. Headers define the column names
+// and determine the number of columns for the entire table.
+//
+// All data rows and footer must have the same number of columns as the headers.
+//
+// Note: Headers are required - tables without headers will fail validation.
+func (t *Table) SetHeader(headers []string) {
+ t.headers = headers
+}
+
+// SetFooter sets an optional footer row for the table.
+//
+// The footer must have the same number of columns as the headers, or validation will fail.
+func (t *Table) SetFooter(footer []string) {
+ t.footer = footer
+}
+
+// AppendBulk sets all data rows for the table at once, replacing any existing rows.
+//
+// Each row must have the same number of columns as the headers, or validation
+// will fail during Render().
+func (t *Table) AppendBulk(rows [][]string) {
+ t.rows = rows
+}
+
+// Render outputs the complete table to the configured writer. The table is rendered
+// with headers, data rows, and optional footer.
+//
+// If validation fails, an error message is written to the output and rendering stops.
+func (t *Table) Render() {
+ if err := t.render(); err != nil {
+ fmt.Fprintf(t.out, "Error: %v\n", err)
+ return
+ }
+}
+
+func (t *Table) render() error {
+ if err := t.validateColumnCount(); err != nil {
+ return err
+ }
+
+ widths := t.calculateColumnWidths()
+ rowSeparator := t.buildRowSeparator(widths)
+
+ if len(t.headers) > 0 {
+ t.printRow(t.headers, widths)
+ fmt.Fprintln(t.out, rowSeparator)
+ }
+
+ for _, row := range t.rows {
+ t.printRow(row, widths)
+ }
+
+ if len(t.footer) > 0 {
+ fmt.Fprintln(t.out, rowSeparator)
+ t.printRow(t.footer, widths)
+ }
+
+ return nil
+}
+
+// validateColumnCount checks that all rows and footer match the header column count
+func (t *Table) validateColumnCount() error {
+ if len(t.headers) == 0 {
+ return errors.New("table must have headers")
+ }
+
+ expectedCols := len(t.headers)
+
+ // Check all rows have same column count as headers
+ for i, row := range t.rows {
+ if len(row) != expectedCols {
+ return fmt.Errorf("row %d has %d columns, expected %d", i, len(row), expectedCols)
+ }
+ }
+
+ // Check footer has same column count as headers (if present)
+ footerPresent := len(t.footer) > 0
+ if footerPresent && len(t.footer) != expectedCols {
+ return fmt.Errorf("footer has %d columns, expected %d", len(t.footer), expectedCols)
+ }
+
+ return nil
+}
+
+// calculateColumnWidths determines the minimum width needed for each column.
+//
+// This is done by finding the longest content in each column across headers, rows, and footer.
+//
+// Returns an int slice where widths[i] is the display width for column i (including padding).
+func (t *Table) calculateColumnWidths() []int {
+ // Headers define the number of columns
+ cols := len(t.headers)
+ if cols == 0 {
+ return nil
+ }
+
+ // Track maximum content width for each column (before padding)
+ widths := make([]int, cols)
+
+ // Start with header widths
+ for i, h := range t.headers {
+ widths[i] = len(h)
+ }
+
+ // Find max width needed for data cells in each column
+ for _, row := range t.rows {
+ for i, cell := range row {
+ widths[i] = max(widths[i], len(cell))
+ }
+ }
+
+ // Find max width needed for footer in each column
+ for i, f := range t.footer {
+ widths[i] = max(widths[i], len(f))
+ }
+
+ for i := range widths {
+ widths[i] += totalPadding
+ }
+
+ return widths
+}
+
+// buildRowSeparator creates a horizontal line to separate table rows.
+//
+// It generates a string with dashes (-) for each column width, joined by plus signs (+).
+//
+// Example output: "----------+--------+-----------"
+func (t *Table) buildRowSeparator(widths []int) string {
+ parts := make([]string, len(widths))
+ for i, w := range widths {
+ parts[i] = strings.Repeat("-", w)
+ }
+ return strings.Join(parts, "+")
+}
+
+// printRow outputs a single row to the table writer.
+//
+// Each cell is padded with spaces and separated by pipe characters (|).
+//
+// Example output: " Database | Size | Items "
+func (t *Table) printRow(row []string, widths []int) {
+ for i, cell := range row {
+ if i > 0 {
+ fmt.Fprint(t.out, "|")
+ }
+
+ // Calculate centering pad without padding
+ contentWidth := widths[i] - totalPadding
+ cellLen := len(cell)
+ leftPadCentering := (contentWidth - cellLen) / 2
+ rightPadCentering := contentWidth - cellLen - leftPadCentering
+
+ // Build padded cell with centering
+ leftPadding := strings.Repeat(" ", cellPadding+leftPadCentering)
+ rightPadding := strings.Repeat(" ", cellPadding+rightPadCentering)
+
+ fmt.Fprintf(t.out, "%s%s%s", leftPadding, cell, rightPadding)
+ }
+ fmt.Fprintln(t.out)
+}
diff --git a/core/rawdb/database_tablewriter_tinygo_test.go b/core/rawdb/database_tablewriter_tinygo_test.go
new file mode 100644
index 0000000000..3bcf93832b
--- /dev/null
+++ b/core/rawdb/database_tablewriter_tinygo_test.go
@@ -0,0 +1,124 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+//go:build tinygo
+// +build tinygo
+
+package rawdb
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+func TestTableWriterTinyGo(t *testing.T) {
+ var buf bytes.Buffer
+ table := newTableWriter(&buf)
+
+ headers := []string{"Database", "Size", "Items", "Status"}
+ rows := [][]string{
+ {"chaindata", "2.5 GB", "1,234,567", "Active"},
+ {"state", "890 MB", "456,789", "Active"},
+ {"ancient", "15.2 GB", "2,345,678", "Readonly"},
+ {"logs", "120 MB", "89,012", "Active"},
+ }
+ footer := []string{"Total", "18.71 GB", "4,125,046", "-"}
+
+ table.SetHeader(headers)
+ table.AppendBulk(rows)
+ table.SetFooter(footer)
+ table.Render()
+
+ output := buf.String()
+ t.Logf("Table output using custom stub implementation:\n%s", output)
+}
+
+func TestTableWriterValidationErrors(t *testing.T) {
+ // Test missing headers
+ t.Run("MissingHeaders", func(t *testing.T) {
+ var buf bytes.Buffer
+ table := newTableWriter(&buf)
+
+ rows := [][]string{{"x", "y", "z"}}
+
+ table.AppendBulk(rows)
+ table.Render()
+
+ output := buf.String()
+ if !strings.Contains(output, "table must have headers") {
+ t.Errorf("Expected error for missing headers, got: %s", output)
+ }
+ })
+
+ t.Run("NotEnoughRowColumns", func(t *testing.T) {
+ var buf bytes.Buffer
+ table := newTableWriter(&buf)
+
+ headers := []string{"A", "B", "C"}
+ badRows := [][]string{
+ {"x", "y"}, // Missing column
+ }
+
+ table.SetHeader(headers)
+ table.AppendBulk(badRows)
+ table.Render()
+
+ output := buf.String()
+ if !strings.Contains(output, "row 0 has 2 columns, expected 3") {
+ t.Errorf("Expected validation error for row 0, got: %s", output)
+ }
+ })
+
+ t.Run("TooManyRowColumns", func(t *testing.T) {
+ var buf bytes.Buffer
+ table := newTableWriter(&buf)
+
+ headers := []string{"A", "B", "C"}
+ badRows := [][]string{
+ {"p", "q", "r", "s"}, // Extra column
+ }
+
+ table.SetHeader(headers)
+ table.AppendBulk(badRows)
+ table.Render()
+
+ output := buf.String()
+ if !strings.Contains(output, "row 0 has 4 columns, expected 3") {
+ t.Errorf("Expected validation error for row 0, got: %s", output)
+ }
+ })
+
+ // Test mismatched footer columns
+ t.Run("MismatchedFooterColumns", func(t *testing.T) {
+ var buf bytes.Buffer
+ table := newTableWriter(&buf)
+
+ headers := []string{"A", "B", "C"}
+ rows := [][]string{{"x", "y", "z"}}
+ footer := []string{"total", "sum"} // Missing column
+
+ table.SetHeader(headers)
+ table.AppendBulk(rows)
+ table.SetFooter(footer)
+ table.Render()
+
+ output := buf.String()
+ if !strings.Contains(output, "footer has 2 columns, expected 3") {
+ t.Errorf("Expected validation error for footer, got: %s", output)
+ }
+ })
+}
diff --git a/core/rawdb/database_tablewriter_unix.go b/core/rawdb/database_tablewriter_unix.go
new file mode 100644
index 0000000000..8bec5396e8
--- /dev/null
+++ b/core/rawdb/database_tablewriter_unix.go
@@ -0,0 +1,33 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+//go:build !tinygo
+// +build !tinygo
+
+package rawdb
+
+import (
+ "io"
+
+ "github.com/olekukonko/tablewriter"
+)
+
+// Re-export the real tablewriter types and functions
+type Table = tablewriter.Table
+
+func newTableWriter(w io.Writer) *Table {
+ return tablewriter.NewWriter(w)
+}
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index 72f9bd34ec..9a17e1c173 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -95,7 +95,7 @@ var (
uncleanShutdownKey = []byte("unclean-shutdown") // config prefix for the db
// transitionStatusKey tracks the eth2 transition status.
- transitionStatusKey = []byte("eth2-transition")
+ transitionStatusKey = []byte("eth2-transition") // deprecated
// snapSyncStatusFlagKey flags that status of snap sync.
snapSyncStatusFlagKey = []byte("SnapSyncStatus")
@@ -384,21 +384,48 @@ func accountHistoryIndexKey(addressHash common.Hash) []byte {
// storageHistoryIndexKey = StateHistoryStorageMetadataPrefix + addressHash + storageHash
func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []byte {
- return append(append(StateHistoryStorageMetadataPrefix, addressHash.Bytes()...), storageHash.Bytes()...)
+ totalLen := len(StateHistoryStorageMetadataPrefix) + 2*common.HashLength
+ out := make([]byte, totalLen)
+
+ off := 0
+ off += copy(out[off:], StateHistoryStorageMetadataPrefix)
+ off += copy(out[off:], addressHash.Bytes())
+ copy(out[off:], storageHash.Bytes())
+
+ return out
}
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
- var buf [4]byte
- binary.BigEndian.PutUint32(buf[:], blockID)
- return append(append(StateHistoryAccountBlockPrefix, addressHash.Bytes()...), buf[:]...)
+ var buf4 [4]byte
+ binary.BigEndian.PutUint32(buf4[:], blockID)
+
+ totalLen := len(StateHistoryAccountBlockPrefix) + common.HashLength + 4
+ out := make([]byte, totalLen)
+
+ off := 0
+ off += copy(out[off:], StateHistoryAccountBlockPrefix)
+ off += copy(out[off:], addressHash.Bytes())
+ copy(out[off:], buf4[:])
+
+ return out
}
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
- var buf [4]byte
- binary.BigEndian.PutUint32(buf[:], blockID)
- return append(append(append(StateHistoryStorageBlockPrefix, addressHash.Bytes()...), storageHash.Bytes()...), buf[:]...)
+ var buf4 [4]byte
+ binary.BigEndian.PutUint32(buf4[:], blockID)
+
+ totalLen := len(StateHistoryStorageBlockPrefix) + 2*common.HashLength + 4
+ out := make([]byte, totalLen)
+
+ off := 0
+ off += copy(out[off:], StateHistoryStorageBlockPrefix)
+ off += copy(out[off:], addressHash.Bytes())
+ off += copy(out[off:], storageHash.Bytes())
+ copy(out[off:], buf4[:])
+
+ return out
}
// transitionStateKey = transitionStatusKey + hash
diff --git a/core/state/database.go b/core/state/database.go
index b46e5d500d..3a0ac422ee 100644
--- a/core/state/database.go
+++ b/core/state/database.go
@@ -81,11 +81,19 @@ type Trie interface {
// be returned.
GetAccount(address common.Address) (*types.StateAccount, error)
+ // PrefetchAccount attempts to resolve specific accounts from the database
+ // to accelerate subsequent trie operations.
+ PrefetchAccount([]common.Address) error
+
// GetStorage returns the value for key stored in the trie. The value bytes
// must not be modified by the caller. If a node was not found in the database,
// a trie.MissingNodeError is returned.
GetStorage(addr common.Address, key []byte) ([]byte, error)
+ // PrefetchStorage attempts to resolve specific storage slots from the database
+ // to accelerate subsequent trie operations.
+ PrefetchStorage(addr common.Address, keys [][]byte) error
+
// UpdateAccount abstracts an account write to the trie. It encodes the
// provided account object with associated algorithm and then updates it
// in the trie with provided address.
@@ -122,7 +130,7 @@ type Trie interface {
// Witness returns a set containing all trie nodes that have been accessed.
// The returned map could be nil if the witness is empty.
- Witness() map[string]struct{}
+ Witness() map[string][]byte
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
// starts at the key after the given start key. And error will be returned
diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go
index 46558a6fce..11f3963a3e 100644
--- a/core/state/pruner/pruner.go
+++ b/core/state/pruner/pruner.go
@@ -160,11 +160,8 @@ func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, sta
var eta time.Duration // Realistically will never remain uninited
if done := binary.BigEndian.Uint64(key[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
- speed = done/uint64(time.Since(pstart)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
- eta = time.Duration(left/speed) * time.Millisecond
+ left := math.MaxUint64 - binary.BigEndian.Uint64(key[:8])
+ eta = common.CalculateETA(done, left, time.Since(pstart))
}
if time.Since(logged) > 8*time.Second {
log.Info("Pruning state data", "nodes", count, "skipped", skipped, "size", size,
diff --git a/core/state/reader.go b/core/state/reader.go
index 4fc67ebd60..3e8b31b6be 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -232,7 +232,7 @@ type trieReader struct {
lock sync.Mutex // Lock for protecting concurrent read
}
-// trieReader constructs a trie reader of the specific state. An error will be
+// newTrieReader constructs a trie reader of the specific state. An error will be
// returned if the associated trie specified by root is not existent.
func newTrieReader(root common.Hash, db *triedb.Database, cache *utils.PointCache) (*trieReader, error) {
var (
@@ -253,7 +253,7 @@ func newTrieReader(root common.Hash, db *triedb.Database, cache *utils.PointCach
if err != nil {
return nil, err
}
- tr = trie.NewTransitionTree(mpt, tr.(*trie.VerkleTrie), false)
+ tr = trie.NewTransitionTrie(mpt, tr.(*trie.VerkleTrie), false)
}
}
if err != nil {
diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go
index 4b0774f2ae..0d39687be4 100644
--- a/core/state/snapshot/conversion.go
+++ b/core/state/snapshot/conversion.go
@@ -171,20 +171,16 @@ func (stat *generateStats) report() {
// If there's progress on the account trie, estimate the time to finish crawling it
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
- left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
- speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- eta = time.Duration(left/speed) * time.Millisecond
+ left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
+ eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
+ left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
// Override the ETA if larger than the largest until now
- if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
+ if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}
diff --git a/core/state/state_sizer.go b/core/state/state_sizer.go
new file mode 100644
index 0000000000..2066c94845
--- /dev/null
+++ b/core/state/state_sizer.go
@@ -0,0 +1,638 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "container/heap"
+ "errors"
+ "fmt"
+ "maps"
+ "runtime"
+ "slices"
+ "sync"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/triedb"
+ "golang.org/x/sync/errgroup"
+)
+
+const (
+ statEvictThreshold = 128 // the depth of statistic to be preserved
+)
+
+// Database key scheme for states.
+var (
+ accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength)
+ storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2)
+ accountTrienodePrefixSize = int64(len(rawdb.TrieNodeAccountPrefix))
+ storageTrienodePrefixSize = int64(len(rawdb.TrieNodeStoragePrefix) + common.HashLength)
+ codeKeySize = int64(len(rawdb.CodePrefix) + common.HashLength)
+)
+
+// SizeStats represents either the current state size statistics or the size
+// differences resulting from a state transition.
+type SizeStats struct {
+ StateRoot common.Hash // State root hash at the time of measurement
+ BlockNumber uint64 // Associated block number at the time of measurement
+
+ Accounts int64 // Total number of accounts in the state
+ AccountBytes int64 // Total storage size used by all account data (in bytes)
+ Storages int64 // Total number of storage slots across all accounts
+ StorageBytes int64 // Total storage size used by all storage slot data (in bytes)
+ AccountTrienodes int64 // Total number of account trie nodes in the state
+ AccountTrienodeBytes int64 // Total storage size occupied by account trie nodes (in bytes)
+ StorageTrienodes int64 // Total number of storage trie nodes in the state
+ StorageTrienodeBytes int64 // Total storage size occupied by storage trie nodes (in bytes)
+ ContractCodes int64 // Total number of contract codes in the state
+ ContractCodeBytes int64 // Total size of all contract code (in bytes)
+}
+
+func (s SizeStats) String() string {
+ return fmt.Sprintf("Accounts: %d(%s), Storages: %d(%s), AccountTrienodes: %d(%s), StorageTrienodes: %d(%s), Codes: %d(%s)",
+ s.Accounts, common.StorageSize(s.AccountBytes),
+ s.Storages, common.StorageSize(s.StorageBytes),
+ s.AccountTrienodes, common.StorageSize(s.AccountTrienodeBytes),
+ s.StorageTrienodes, common.StorageSize(s.StorageTrienodeBytes),
+ s.ContractCodes, common.StorageSize(s.ContractCodeBytes),
+ )
+}
+
+// add applies the given state diffs and produces a new version of the statistics.
+func (s SizeStats) add(diff SizeStats) SizeStats {
+ s.StateRoot = diff.StateRoot
+ s.BlockNumber = diff.BlockNumber
+
+ s.Accounts += diff.Accounts
+ s.AccountBytes += diff.AccountBytes
+ s.Storages += diff.Storages
+ s.StorageBytes += diff.StorageBytes
+ s.AccountTrienodes += diff.AccountTrienodes
+ s.AccountTrienodeBytes += diff.AccountTrienodeBytes
+ s.StorageTrienodes += diff.StorageTrienodes
+ s.StorageTrienodeBytes += diff.StorageTrienodeBytes
+ s.ContractCodes += diff.ContractCodes
+ s.ContractCodeBytes += diff.ContractCodeBytes
+ return s
+}
+
+// calSizeStats measures the state size changes of the provided state update.
+func calSizeStats(update *stateUpdate) (SizeStats, error) {
+ stats := SizeStats{
+ BlockNumber: update.blockNumber,
+ StateRoot: update.root,
+ }
+
+ // Measure the account changes
+ for addr, oldValue := range update.accountsOrigin {
+ addrHash := crypto.Keccak256Hash(addr.Bytes())
+ newValue, exists := update.accounts[addrHash]
+ if !exists {
+ return SizeStats{}, fmt.Errorf("account %x not found", addr)
+ }
+ oldLen, newLen := len(oldValue), len(newValue)
+
+ switch {
+ case oldLen > 0 && newLen == 0:
+ // Account deletion
+ stats.Accounts -= 1
+ stats.AccountBytes -= accountKeySize + int64(oldLen)
+ case oldLen == 0 && newLen > 0:
+ // Account creation
+ stats.Accounts += 1
+ stats.AccountBytes += accountKeySize + int64(newLen)
+ default:
+ // Account update
+ stats.AccountBytes += int64(newLen - oldLen)
+ }
+ }
+
+ // Measure storage changes
+ for addr, slots := range update.storagesOrigin {
+ addrHash := crypto.Keccak256Hash(addr.Bytes())
+ subset, exists := update.storages[addrHash]
+ if !exists {
+ return SizeStats{}, fmt.Errorf("storage %x not found", addr)
+ }
+ for key, oldValue := range slots {
+ var (
+ exists bool
+ newValue []byte
+ )
+ if update.rawStorageKey {
+ newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())]
+ } else {
+ newValue, exists = subset[key]
+ }
+ if !exists {
+ return SizeStats{}, fmt.Errorf("storage slot %x-%x not found", addr, key)
+ }
+ oldLen, newLen := len(oldValue), len(newValue)
+
+ switch {
+ case oldLen > 0 && newLen == 0:
+ // Storage deletion
+ stats.Storages -= 1
+ stats.StorageBytes -= storageKeySize + int64(oldLen)
+ case oldLen == 0 && newLen > 0:
+ // Storage creation
+ stats.Storages += 1
+ stats.StorageBytes += storageKeySize + int64(newLen)
+ default:
+ // Storage update
+ stats.StorageBytes += int64(newLen - oldLen)
+ }
+ }
+ }
+
+ // Measure trienode changes
+ for owner, subset := range update.nodes.Sets {
+ var (
+ keyPrefix int64
+ isAccount = owner == (common.Hash{})
+ )
+ if isAccount {
+ keyPrefix = accountTrienodePrefixSize
+ } else {
+ keyPrefix = storageTrienodePrefixSize
+ }
+
+ // Iterate over Origins since every modified node has an origin entry
+ for path, oldNode := range subset.Origins {
+ newNode, exists := subset.Nodes[path]
+ if !exists {
+ return SizeStats{}, fmt.Errorf("node %x-%v not found", owner, path)
+ }
+ keySize := keyPrefix + int64(len(path))
+
+ switch {
+ case len(oldNode) > 0 && len(newNode.Blob) == 0:
+ // Node deletion
+ if isAccount {
+ stats.AccountTrienodes -= 1
+ stats.AccountTrienodeBytes -= keySize + int64(len(oldNode))
+ } else {
+ stats.StorageTrienodes -= 1
+ stats.StorageTrienodeBytes -= keySize + int64(len(oldNode))
+ }
+ case len(oldNode) == 0 && len(newNode.Blob) > 0:
+ // Node creation
+ if isAccount {
+ stats.AccountTrienodes += 1
+ stats.AccountTrienodeBytes += keySize + int64(len(newNode.Blob))
+ } else {
+ stats.StorageTrienodes += 1
+ stats.StorageTrienodeBytes += keySize + int64(len(newNode.Blob))
+ }
+ default:
+ // Node update
+ if isAccount {
+ stats.AccountTrienodeBytes += int64(len(newNode.Blob) - len(oldNode))
+ } else {
+ stats.StorageTrienodeBytes += int64(len(newNode.Blob) - len(oldNode))
+ }
+ }
+ }
+ }
+
+ // Measure code changes. Note that the reported contract code size may be slightly
+ // inaccurate due to database deduplication (code is stored by its hash). However,
+ // this deviation is negligible and acceptable for measurement purposes.
+ for _, code := range update.codes {
+ stats.ContractCodes += 1
+ stats.ContractCodeBytes += codeKeySize + int64(len(code.blob))
+ }
+ return stats, nil
+}
+
+type stateSizeQuery struct {
+ root *common.Hash // nil means latest
+ err error // non-nil if the state size is not yet initialized
+ result chan *SizeStats // nil means the state is unknown
+}
+
+// SizeTracker handles the state size initialization and tracks of state size metrics.
+type SizeTracker struct {
+ db ethdb.KeyValueStore
+ triedb *triedb.Database
+ abort chan struct{}
+ aborted chan struct{}
+ updateCh chan *stateUpdate
+ queryCh chan *stateSizeQuery
+}
+
+// NewSizeTracker creates a new state size tracker and starts it automatically
+func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTracker, error) {
+ if triedb.Scheme() != rawdb.PathScheme {
+ return nil, errors.New("state size tracker is not compatible with hash mode")
+ }
+ t := &SizeTracker{
+ db: db,
+ triedb: triedb,
+ abort: make(chan struct{}),
+ aborted: make(chan struct{}),
+ updateCh: make(chan *stateUpdate),
+ queryCh: make(chan *stateSizeQuery),
+ }
+ go t.run()
+ return t, nil
+}
+
+func (t *SizeTracker) Stop() {
+ close(t.abort)
+ <-t.aborted
+}
+
+// sizeStatsHeap is a heap.Interface implementation over statesize statistics for
+// retrieving the oldest statistics for eviction.
+type sizeStatsHeap []SizeStats
+
+func (h sizeStatsHeap) Len() int { return len(h) }
+func (h sizeStatsHeap) Less(i, j int) bool { return h[i].BlockNumber < h[j].BlockNumber }
+func (h sizeStatsHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
+
+func (h *sizeStatsHeap) Push(x any) {
+ *h = append(*h, x.(SizeStats))
+}
+
+func (h *sizeStatsHeap) Pop() any {
+ old := *h
+ n := len(old)
+ x := old[n-1]
+ *h = old[0 : n-1]
+ return x
+}
+
+// run performs the state size initialization and handles updates
+func (t *SizeTracker) run() {
+ defer close(t.aborted)
+
+ var last common.Hash
+ stats, err := t.init() // launch background thread for state size init
+ if err != nil {
+ return
+ }
+ h := sizeStatsHeap(slices.Collect(maps.Values(stats)))
+ heap.Init(&h)
+
+ for {
+ select {
+ case u := <-t.updateCh:
+ base, found := stats[u.originRoot]
+ if !found {
+ log.Debug("Ignored the state size without parent", "parent", u.originRoot, "root", u.root, "number", u.blockNumber)
+ continue
+ }
+ diff, err := calSizeStats(u)
+ if err != nil {
+ continue
+ }
+ stat := base.add(diff)
+ stats[u.root] = stat
+ last = u.root
+
+ heap.Push(&h, stats[u.root])
+ for u.blockNumber-h[0].BlockNumber > statEvictThreshold {
+ delete(stats, h[0].StateRoot)
+ heap.Pop(&h)
+ }
+ log.Debug("Update state size", "number", stat.BlockNumber, "root", stat.StateRoot, "stat", stat)
+
+ case r := <-t.queryCh:
+ var root common.Hash
+ if r.root != nil {
+ root = *r.root
+ } else {
+ root = last
+ }
+ if s, ok := stats[root]; ok {
+ r.result <- &s
+ } else {
+ r.result <- nil
+ }
+
+ case <-t.abort:
+ return
+ }
+ }
+}
+
+type buildResult struct {
+ stat SizeStats
+ root common.Hash
+ blockNumber uint64
+ elapsed time.Duration
+ err error
+}
+
+func (t *SizeTracker) init() (map[common.Hash]SizeStats, error) {
+ // Wait for snapshot completion and then init
+ ticker := time.NewTicker(10 * time.Second)
+ defer ticker.Stop()
+
+wait:
+ for {
+ select {
+ case <-ticker.C:
+ if t.triedb.SnapshotCompleted() {
+ break wait
+ }
+ case <-t.updateCh:
+ continue
+ case r := <-t.queryCh:
+ r.err = errors.New("state size is not initialized yet")
+ r.result <- nil
+ case <-t.abort:
+ return nil, errors.New("size tracker closed")
+ }
+ }
+
+ var (
+ updates = make(map[common.Hash]*stateUpdate)
+ children = make(map[common.Hash][]common.Hash)
+ done chan buildResult
+ )
+
+ for {
+ select {
+ case u := <-t.updateCh:
+ updates[u.root] = u
+ children[u.originRoot] = append(children[u.originRoot], u.root)
+ log.Debug("Received state update", "root", u.root, "blockNumber", u.blockNumber)
+
+ case r := <-t.queryCh:
+ r.err = errors.New("state size is not initialized yet")
+ r.result <- nil
+
+ case <-ticker.C:
+ // Only check timer if build hasn't started yet
+ if done != nil {
+ continue
+ }
+ root := rawdb.ReadSnapshotRoot(t.db)
+ if root == (common.Hash{}) {
+ continue
+ }
+ entry, exists := updates[root]
+ if !exists {
+ continue
+ }
+ done = make(chan buildResult)
+ go t.build(entry.root, entry.blockNumber, done)
+ log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.blockNumber)
+
+ case result := <-done:
+ if result.err != nil {
+ return nil, result.err
+ }
+ var (
+ stats = make(map[common.Hash]SizeStats)
+ apply func(root common.Hash, stat SizeStats) error
+ )
+ apply = func(root common.Hash, base SizeStats) error {
+ for _, child := range children[root] {
+ entry, ok := updates[child]
+ if !ok {
+ return fmt.Errorf("the state update is not found, %x", child)
+ }
+ diff, err := calSizeStats(entry)
+ if err != nil {
+ return err
+ }
+ stats[child] = base.add(diff)
+ if err := apply(child, stats[child]); err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ if err := apply(result.root, result.stat); err != nil {
+ return nil, err
+ }
+
+ // Set initial latest stats
+ stats[result.root] = result.stat
+ log.Info("Measured persistent state size", "root", result.root, "number", result.blockNumber, "stat", result.stat, "elapsed", common.PrettyDuration(result.elapsed))
+ return stats, nil
+
+ case <-t.abort:
+ return nil, errors.New("size tracker closed")
+ }
+ }
+}
+
+func (t *SizeTracker) build(root common.Hash, blockNumber uint64, done chan buildResult) {
+ // Metrics will be directly updated by each goroutine
+ var (
+ accounts, accountBytes int64
+ storages, storageBytes int64
+ codes, codeBytes int64
+
+ accountTrienodes, accountTrienodeBytes int64
+ storageTrienodes, storageTrienodeBytes int64
+
+ group errgroup.Group
+ start = time.Now()
+ )
+
+ // Start all table iterations concurrently with direct metric updates
+ group.Go(func() error {
+ count, bytes, err := t.iterateTableParallel(t.abort, rawdb.SnapshotAccountPrefix, "account")
+ if err != nil {
+ return err
+ }
+ accounts, accountBytes = count, bytes
+ return nil
+ })
+
+ group.Go(func() error {
+ count, bytes, err := t.iterateTableParallel(t.abort, rawdb.SnapshotStoragePrefix, "storage")
+ if err != nil {
+ return err
+ }
+ storages, storageBytes = count, bytes
+ return nil
+ })
+
+ group.Go(func() error {
+ count, bytes, err := t.iterateTableParallel(t.abort, rawdb.TrieNodeAccountPrefix, "accountnode")
+ if err != nil {
+ return err
+ }
+ accountTrienodes, accountTrienodeBytes = count, bytes
+ return nil
+ })
+
+ group.Go(func() error {
+ count, bytes, err := t.iterateTableParallel(t.abort, rawdb.TrieNodeStoragePrefix, "storagenode")
+ if err != nil {
+ return err
+ }
+ storageTrienodes, storageTrienodeBytes = count, bytes
+ return nil
+ })
+
+ group.Go(func() error {
+ count, bytes, err := t.iterateTable(t.abort, rawdb.CodePrefix, "contractcode")
+ if err != nil {
+ return err
+ }
+ codes, codeBytes = count, bytes
+ return nil
+ })
+
+ // Wait for all goroutines to complete
+ if err := group.Wait(); err != nil {
+ done <- buildResult{err: err}
+ } else {
+ stat := SizeStats{
+ StateRoot: root,
+ BlockNumber: blockNumber,
+ Accounts: accounts,
+ AccountBytes: accountBytes,
+ Storages: storages,
+ StorageBytes: storageBytes,
+ AccountTrienodes: accountTrienodes,
+ AccountTrienodeBytes: accountTrienodeBytes,
+ StorageTrienodes: storageTrienodes,
+ StorageTrienodeBytes: storageTrienodeBytes,
+ ContractCodes: codes,
+ ContractCodeBytes: codeBytes,
+ }
+ done <- buildResult{
+ root: root,
+ blockNumber: blockNumber,
+ stat: stat,
+ elapsed: time.Since(start),
+ }
+ }
+}
+
+// iterateTable performs iteration over a specific table and returns the results.
+func (t *SizeTracker) iterateTable(closed chan struct{}, prefix []byte, name string) (int64, int64, error) {
+ var (
+ start = time.Now()
+ logged = time.Now()
+ count, bytes int64
+ )
+
+ iter := t.db.NewIterator(prefix, nil)
+ defer iter.Release()
+
+ log.Debug("Iterating state", "category", name)
+ for iter.Next() {
+ count++
+ bytes += int64(len(iter.Key()) + len(iter.Value()))
+
+ if time.Since(logged) > time.Second*8 {
+ logged = time.Now()
+
+ select {
+ case <-closed:
+ log.Debug("State iteration cancelled", "category", name)
+ return 0, 0, errors.New("size tracker closed")
+ default:
+ log.Debug("Iterating state", "category", name, "count", count, "size", common.StorageSize(bytes))
+ }
+ }
+ }
+ // Check for iterator errors
+ if err := iter.Error(); err != nil {
+ log.Error("Iterator error", "category", name, "err", err)
+ return 0, 0, err
+ }
+ log.Debug("Finished state iteration", "category", name, "count", count, "size", common.StorageSize(bytes), "elapsed", common.PrettyDuration(time.Since(start)))
+ return count, bytes, nil
+}
+
+// iterateTableParallel performs parallel iteration over a table by splitting into
+// hex ranges. For storage tables, it splits on the first byte of the account hash
+// (after the prefix).
+func (t *SizeTracker) iterateTableParallel(closed chan struct{}, prefix []byte, name string) (int64, int64, error) {
+ var (
+ totalCount int64
+ totalBytes int64
+
+ start = time.Now()
+ workers = runtime.NumCPU()
+ group errgroup.Group
+ mu sync.Mutex
+ )
+ group.SetLimit(workers)
+ log.Debug("Starting parallel state iteration", "category", name, "workers", workers)
+
+ if len(prefix) > 0 {
+ if blob, err := t.db.Get(prefix); err == nil && len(blob) > 0 {
+ // If there's a direct hit on the prefix, include it in the stats
+ totalCount = 1
+ totalBytes = int64(len(prefix) + len(blob))
+ }
+ }
+ for i := 0; i < 256; i++ {
+ h := byte(i)
+ group.Go(func() error {
+ count, bytes, err := t.iterateTable(closed, slices.Concat(prefix, []byte{h}), fmt.Sprintf("%s-%02x", name, h))
+ if err != nil {
+ return err
+ }
+ mu.Lock()
+ totalCount += count
+ totalBytes += bytes
+ mu.Unlock()
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return 0, 0, err
+ }
+ log.Debug("Finished parallel state iteration", "category", name, "count", totalCount, "size", common.StorageSize(totalBytes), "elapsed", common.PrettyDuration(time.Since(start)))
+ return totalCount, totalBytes, nil
+}
+
+// Notify is an async method used to send the state update to the size tracker.
+// It ignores empty updates (where no state changes occurred).
+// If the channel is full, it drops the update to avoid blocking.
+func (t *SizeTracker) Notify(update *stateUpdate) {
+ if update == nil || update.empty() {
+ return
+ }
+ select {
+ case t.updateCh <- update:
+ case <-t.abort:
+ return
+ }
+}
+
+// Query returns the state size specified by the root, or nil if not available.
+// If the root is nil, query the size of latest chain head;
+// If the root is non-nil, query the size of the specified state;
+func (t *SizeTracker) Query(root *common.Hash) (*SizeStats, error) {
+ r := &stateSizeQuery{
+ root: root,
+ result: make(chan *SizeStats, 1),
+ }
+ select {
+ case <-t.aborted:
+ return nil, errors.New("state sizer has been closed")
+ case t.queryCh <- r:
+ return <-r.result, r.err
+ }
+}
diff --git a/core/state/state_sizer_test.go b/core/state/state_sizer_test.go
new file mode 100644
index 0000000000..cab0c38163
--- /dev/null
+++ b/core/state/state_sizer_test.go
@@ -0,0 +1,231 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package state
+
+import (
+ "testing"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/triedb"
+ "github.com/ethereum/go-ethereum/triedb/pathdb"
+ "github.com/holiman/uint256"
+)
+
+func TestSizeTracker(t *testing.T) {
+ db := rawdb.NewMemoryDatabase()
+ defer db.Close()
+
+ tdb := triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults})
+ sdb := NewDatabase(tdb, nil)
+
+ // Generate 50 blocks to establish a baseline
+ baselineBlockNum := uint64(50)
+ currentRoot := types.EmptyRootHash
+
+ addr1 := common.BytesToAddress([]byte{1, 0, 0, 1})
+ addr2 := common.BytesToAddress([]byte{1, 0, 0, 2})
+ addr3 := common.BytesToAddress([]byte{1, 0, 0, 3})
+
+ // Create initial state with fixed accounts
+ state, _ := New(currentRoot, sdb)
+ state.AddBalance(addr1, uint256.NewInt(1000), tracing.BalanceChangeUnspecified)
+ state.SetNonce(addr1, 1, tracing.NonceChangeUnspecified)
+ state.SetState(addr1, common.HexToHash("0x1111"), common.HexToHash("0xaaaa"))
+ state.SetState(addr1, common.HexToHash("0x2222"), common.HexToHash("0xbbbb"))
+
+ state.AddBalance(addr2, uint256.NewInt(2000), tracing.BalanceChangeUnspecified)
+ state.SetNonce(addr2, 2, tracing.NonceChangeUnspecified)
+ state.SetCode(addr2, []byte{0x60, 0x80, 0x60, 0x40, 0x52}, tracing.CodeChangeUnspecified)
+
+ state.AddBalance(addr3, uint256.NewInt(3000), tracing.BalanceChangeUnspecified)
+ state.SetNonce(addr3, 3, tracing.NonceChangeUnspecified)
+
+ currentRoot, _, err := state.CommitWithUpdate(1, true, false)
+ if err != nil {
+ t.Fatalf("Failed to commit initial state: %v", err)
+ }
+ if err := tdb.Commit(currentRoot, false); err != nil {
+ t.Fatalf("Failed to commit initial trie: %v", err)
+ }
+
+ for i := 1; i < 50; i++ { // blocks 2-50
+ blockNum := uint64(i + 1)
+
+ newState, err := New(currentRoot, sdb)
+ if err != nil {
+ t.Fatalf("Failed to create new state at block %d: %v", blockNum, err)
+ }
+ testAddr := common.BigToAddress(uint256.NewInt(uint64(i + 100)).ToBig())
+ newState.AddBalance(testAddr, uint256.NewInt(uint64((i+1)*1000)), tracing.BalanceChangeUnspecified)
+ newState.SetNonce(testAddr, uint64(i+10), tracing.NonceChangeUnspecified)
+
+ if i%2 == 0 {
+ newState.SetState(addr1, common.BigToHash(uint256.NewInt(uint64(i+0x1000)).ToBig()), common.BigToHash(uint256.NewInt(uint64(i+0x2000)).ToBig()))
+ }
+ if i%3 == 0 {
+ newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
+ }
+ root, _, err := newState.CommitWithUpdate(blockNum, true, false)
+ if err != nil {
+ t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
+ }
+ if err := tdb.Commit(root, false); err != nil {
+ t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
+ }
+ currentRoot = root
+ }
+ baselineRoot := currentRoot
+
+ // Wait for snapshot completion
+ for !tdb.SnapshotCompleted() {
+ time.Sleep(100 * time.Millisecond)
+ }
+
+ // Calculate baseline from the intermediate persisted state
+ baselineTracker := &SizeTracker{
+ db: db,
+ triedb: tdb,
+ abort: make(chan struct{}),
+ }
+ done := make(chan buildResult)
+
+ go baselineTracker.build(baselineRoot, baselineBlockNum, done)
+ var baselineResult buildResult
+ select {
+ case baselineResult = <-done:
+ if baselineResult.err != nil {
+ t.Fatalf("Failed to get baseline stats: %v", baselineResult.err)
+ }
+ case <-time.After(30 * time.Second):
+ t.Fatal("Timeout waiting for baseline stats")
+ }
+ baseline := baselineResult.stat
+
+ // Now start the tracker and notify it of updates that happen AFTER the baseline
+ tracker, err := NewSizeTracker(db, tdb)
+ if err != nil {
+ t.Fatalf("Failed to create size tracker: %v", err)
+ }
+ defer tracker.Stop()
+
+ var trackedUpdates []SizeStats
+ currentRoot = baselineRoot
+
+ // Generate additional blocks beyond the baseline and track them
+ for i := 49; i < 130; i++ { // blocks 51-132
+ blockNum := uint64(i + 2)
+ newState, err := New(currentRoot, sdb)
+ if err != nil {
+ t.Fatalf("Failed to create new state at block %d: %v", blockNum, err)
+ }
+ testAddr := common.BigToAddress(uint256.NewInt(uint64(i + 100)).ToBig())
+ newState.AddBalance(testAddr, uint256.NewInt(uint64((i+1)*1000)), tracing.BalanceChangeUnspecified)
+ newState.SetNonce(testAddr, uint64(i+10), tracing.NonceChangeUnspecified)
+
+ if i%2 == 0 {
+ newState.SetState(addr1, common.BigToHash(uint256.NewInt(uint64(i+0x1000)).ToBig()), common.BigToHash(uint256.NewInt(uint64(i+0x2000)).ToBig()))
+ }
+ if i%3 == 0 {
+ newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
+ }
+ root, update, err := newState.CommitWithUpdate(blockNum, true, false)
+ if err != nil {
+ t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
+ }
+ if err := tdb.Commit(root, false); err != nil {
+ t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
+ }
+
+ diff, err := calSizeStats(update)
+ if err != nil {
+ t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
+ }
+ trackedUpdates = append(trackedUpdates, diff)
+ tracker.Notify(update)
+ currentRoot = root
+ }
+ finalRoot := rawdb.ReadSnapshotRoot(db)
+
+ // Ensure all commits are flushed to disk
+ if err := tdb.Close(); err != nil {
+ t.Fatalf("Failed to close triedb: %v", err)
+ }
+ // Reopen the database to simulate a restart
+ tdb = triedb.NewDatabase(db, &triedb.Config{PathDB: pathdb.Defaults})
+ defer tdb.Close()
+
+ finalTracker := &SizeTracker{
+ db: db,
+ triedb: tdb,
+ abort: make(chan struct{}),
+ }
+ finalDone := make(chan buildResult)
+
+ go finalTracker.build(finalRoot, uint64(132), finalDone)
+ var result buildResult
+ select {
+ case result = <-finalDone:
+ if result.err != nil {
+ t.Fatalf("Failed to build final stats: %v", result.err)
+ }
+ case <-time.After(30 * time.Second):
+ t.Fatal("Timeout waiting for final stats")
+ }
+ actualStats := result.stat
+
+ expectedStats := baseline
+ for _, diff := range trackedUpdates {
+ expectedStats = expectedStats.add(diff)
+ }
+
+ // The final measured stats should match our calculated expected stats exactly
+ if actualStats.Accounts != expectedStats.Accounts {
+ t.Errorf("Account count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.Accounts, expectedStats.Accounts, actualStats.Accounts)
+ }
+ if actualStats.AccountBytes != expectedStats.AccountBytes {
+ t.Errorf("Account bytes mismatch: expected %d, got %d", expectedStats.AccountBytes, actualStats.AccountBytes)
+ }
+ if actualStats.Storages != expectedStats.Storages {
+ t.Errorf("Storage count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.Storages, expectedStats.Storages, actualStats.Storages)
+ }
+ if actualStats.StorageBytes != expectedStats.StorageBytes {
+ t.Errorf("Storage bytes mismatch: expected %d, got %d", expectedStats.StorageBytes, actualStats.StorageBytes)
+ }
+ if actualStats.ContractCodes != expectedStats.ContractCodes {
+ t.Errorf("Contract code count mismatch: baseline(%d) + tracked_changes = %d, but final_measurement = %d", baseline.ContractCodes, expectedStats.ContractCodes, actualStats.ContractCodes)
+ }
+ if actualStats.ContractCodeBytes != expectedStats.ContractCodeBytes {
+ t.Errorf("Contract code bytes mismatch: expected %d, got %d", expectedStats.ContractCodeBytes, actualStats.ContractCodeBytes)
+ }
+ // TODO: failed on github actions, need to investigate
+ // if actualStats.AccountTrienodes != expectedStats.AccountTrienodes {
+ // t.Errorf("Account trie nodes mismatch: expected %d, got %d", expectedStats.AccountTrienodes, actualStats.AccountTrienodes)
+ // }
+ // if actualStats.AccountTrienodeBytes != expectedStats.AccountTrienodeBytes {
+ // t.Errorf("Account trie node bytes mismatch: expected %d, got %d", expectedStats.AccountTrienodeBytes, actualStats.AccountTrienodeBytes)
+ // }
+ if actualStats.StorageTrienodes != expectedStats.StorageTrienodes {
+ t.Errorf("Storage trie nodes mismatch: expected %d, got %d", expectedStats.StorageTrienodes, actualStats.StorageTrienodes)
+ }
+ if actualStats.StorageTrienodeBytes != expectedStats.StorageTrienodeBytes {
+ t.Errorf("Storage trie node bytes mismatch: expected %d, got %d", expectedStats.StorageTrienodeBytes, actualStats.StorageTrienodeBytes)
+ }
+}
diff --git a/core/state/statedb.go b/core/state/statedb.go
index efb09a08a0..b770698255 100644
--- a/core/state/statedb.go
+++ b/core/state/statedb.go
@@ -136,7 +136,8 @@ type StateDB struct {
journal *journal
// State witness if cross validation is needed
- witness *stateless.Witness
+ witness *stateless.Witness
+ witnessStats *stateless.WitnessStats
// Measurements gathered during execution for debugging purposes
AccountReads time.Duration
@@ -191,12 +192,13 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
// state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot.
-func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
+func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness, witnessStats *stateless.WitnessStats) {
// Terminate any previously running prefetcher
s.StopPrefetcher()
// Enable witness collection if requested
s.witness = witness
+ s.witnessStats = witnessStats
// With the switch to the Proof-of-Stake consensus algorithm, block production
// rewards are now handled at the consensus layer. Consequently, a block may
@@ -456,7 +458,7 @@ func (s *StateDB) SetNonce(addr common.Address, nonce uint64, reason tracing.Non
}
}
-func (s *StateDB) SetCode(addr common.Address, code []byte) (prev []byte) {
+func (s *StateDB) SetCode(addr common.Address, code []byte, reason tracing.CodeChangeReason) (prev []byte) {
stateObject := s.getOrNewStateObject(addr)
if stateObject != nil {
return stateObject.SetCode(crypto.Keccak256Hash(code), code)
@@ -858,9 +860,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
- s.witness.AddState(trie.Witness())
+ witness := trie.Witness()
+ s.witness.AddState(witness)
+ if s.witnessStats != nil {
+ s.witnessStats.Add(witness, obj.addrHash)
+ }
} else if obj.trie != nil {
- s.witness.AddState(obj.trie.Witness())
+ witness := obj.trie.Witness()
+ s.witness.AddState(witness)
+ if s.witnessStats != nil {
+ s.witnessStats.Add(witness, obj.addrHash)
+ }
}
}
// Pull in only-read and non-destructed trie witnesses
@@ -874,9 +884,17 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
continue
}
if trie := obj.getPrefetchedTrie(); trie != nil {
- s.witness.AddState(trie.Witness())
+ witness := trie.Witness()
+ s.witness.AddState(witness)
+ if s.witnessStats != nil {
+ s.witnessStats.Add(witness, obj.addrHash)
+ }
} else if obj.trie != nil {
- s.witness.AddState(obj.trie.Witness())
+ witness := obj.trie.Witness()
+ s.witness.AddState(witness)
+ if s.witnessStats != nil {
+ s.witnessStats.Add(witness, obj.addrHash)
+ }
}
}
}
@@ -942,7 +960,11 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
// If witness building is enabled, gather the account trie witness
if s.witness != nil {
- s.witness.AddState(s.trie.Witness())
+ witness := s.trie.Witness()
+ s.witness.AddState(witness)
+ if s.witnessStats != nil {
+ s.witnessStats.Add(witness, common.Hash{})
+ }
}
return hash
}
@@ -1133,7 +1155,7 @@ func (s *StateDB) GetTrie() Trie {
// commit gathers the state mutations accumulated along with the associated
// trie changes, resetting all internal flags with the new state as the base.
-func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool) (*stateUpdate, error) {
+func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) {
// Short circuit in case any database failure occurred earlier.
if s.dbErr != nil {
return nil, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr)
@@ -1285,13 +1307,13 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool) (*stateU
origin := s.originalRoot
s.originalRoot = root
- return newStateUpdate(noStorageWiping, origin, root, deletes, updates, nodes), nil
+ return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes), nil
}
// commitAndFlush is a wrapper of commit which also commits the state mutations
// to the configured data stores.
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (*stateUpdate, error) {
- ret, err := s.commit(deleteEmptyObjects, noStorageWiping)
+ ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
if err != nil {
return nil, err
}
@@ -1356,6 +1378,16 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping
return ret.root, nil
}
+// CommitWithUpdate writes the state mutations and returns both the root hash and the state update.
+// This is useful for tracking state changes at the blockchain level.
+func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *stateUpdate, error) {
+ ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping)
+ if err != nil {
+ return common.Hash{}, nil, err
+ }
+ return ret.root, ret, nil
+}
+
// Prepare handles the preparatory steps for executing a state transition with.
// This method must be invoked before state transition.
//
diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go
index 7dada63d45..f4761bd10c 100644
--- a/core/state/statedb_fuzz_test.go
+++ b/core/state/statedb_fuzz_test.go
@@ -89,7 +89,7 @@ func newStateTestAction(addr common.Address, r *rand.Rand, index int) testAction
code := make([]byte, 16)
binary.BigEndian.PutUint64(code, uint64(a.args[0]))
binary.BigEndian.PutUint64(code[8:], uint64(a.args[1]))
- s.SetCode(addr, code)
+ s.SetCode(addr, code, tracing.CodeChangeUnspecified)
},
args: make([]int64, 2),
},
diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go
index 3d1ef15031..d2595bcefe 100644
--- a/core/state/statedb_hooked.go
+++ b/core/state/statedb_hooked.go
@@ -189,14 +189,20 @@ func (s *hookedStateDB) SetNonce(address common.Address, nonce uint64, reason tr
}
}
-func (s *hookedStateDB) SetCode(address common.Address, code []byte) []byte {
- prev := s.inner.SetCode(address, code)
- if s.hooks.OnCodeChange != nil {
+func (s *hookedStateDB) SetCode(address common.Address, code []byte, reason tracing.CodeChangeReason) []byte {
+ prev := s.inner.SetCode(address, code, reason)
+ if s.hooks.OnCodeChangeV2 != nil || s.hooks.OnCodeChange != nil {
prevHash := types.EmptyCodeHash
if len(prev) != 0 {
prevHash = crypto.Keccak256Hash(prev)
}
- s.hooks.OnCodeChange(address, prevHash, prev, crypto.Keccak256Hash(code), code)
+ codeHash := crypto.Keccak256Hash(code)
+
+ if s.hooks.OnCodeChangeV2 != nil {
+ s.hooks.OnCodeChangeV2(address, prevHash, prev, codeHash, code, reason)
+ } else if s.hooks.OnCodeChange != nil {
+ s.hooks.OnCodeChange(address, prevHash, prev, codeHash, code)
+ }
}
return prev
}
@@ -224,8 +230,12 @@ func (s *hookedStateDB) SelfDestruct(address common.Address) uint256.Int {
s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
}
- if s.hooks.OnCodeChange != nil && len(prevCode) > 0 {
- s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
+ if len(prevCode) > 0 {
+ if s.hooks.OnCodeChangeV2 != nil {
+ s.hooks.OnCodeChangeV2(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil, tracing.CodeChangeSelfDestruct)
+ } else if s.hooks.OnCodeChange != nil {
+ s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
+ }
}
return prev
@@ -242,12 +252,16 @@ func (s *hookedStateDB) SelfDestruct6780(address common.Address) (uint256.Int, b
prev, changed := s.inner.SelfDestruct6780(address)
- if s.hooks.OnBalanceChange != nil && changed && !prev.IsZero() {
+ if s.hooks.OnBalanceChange != nil && !prev.IsZero() {
s.hooks.OnBalanceChange(address, prev.ToBig(), new(big.Int), tracing.BalanceDecreaseSelfdestruct)
}
- if s.hooks.OnCodeChange != nil && changed && len(prevCode) > 0 {
- s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
+ if changed && len(prevCode) > 0 {
+ if s.hooks.OnCodeChangeV2 != nil {
+ s.hooks.OnCodeChangeV2(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil, tracing.CodeChangeSelfDestruct)
+ } else if s.hooks.OnCodeChange != nil {
+ s.hooks.OnCodeChange(address, prevCodeHash, prevCode, types.EmptyCodeHash, nil)
+ }
}
return prev, changed
diff --git a/core/state/statedb_hooked_test.go b/core/state/statedb_hooked_test.go
index f319b0e63c..bacb7baee1 100644
--- a/core/state/statedb_hooked_test.go
+++ b/core/state/statedb_hooked_test.go
@@ -114,7 +114,7 @@ func TestHooks(t *testing.T) {
sdb.AddBalance(common.Address{0xaa}, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
sdb.SubBalance(common.Address{0xaa}, uint256.NewInt(50), tracing.BalanceChangeTransfer)
sdb.SetNonce(common.Address{0xaa}, 1337, tracing.NonceChangeGenesis)
- sdb.SetCode(common.Address{0xaa}, []byte{0x13, 37})
+ sdb.SetCode(common.Address{0xaa}, []byte{0x13, 37}, tracing.CodeChangeUnspecified)
sdb.SetState(common.Address{0xaa}, common.HexToHash("0x01"), common.HexToHash("0x11"))
sdb.SetState(common.Address{0xaa}, common.HexToHash("0x01"), common.HexToHash("0x22"))
sdb.SetTransientState(common.Address{0xaa}, common.HexToHash("0x02"), common.HexToHash("0x01"))
diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go
index 67e27cc832..147546a3c7 100644
--- a/core/state/statedb_test.go
+++ b/core/state/statedb_test.go
@@ -65,7 +65,7 @@ func TestUpdateLeaks(t *testing.T) {
state.SetState(addr, common.BytesToHash([]byte{i, i, i}), common.BytesToHash([]byte{i, i, i, i}))
}
if i%3 == 0 {
- state.SetCode(addr, []byte{i, i, i, i, i})
+ state.SetCode(addr, []byte{i, i, i, i, i}, tracing.CodeChangeUnspecified)
}
}
@@ -101,7 +101,7 @@ func TestIntermediateLeaks(t *testing.T) {
state.SetState(addr, common.Hash{i, i, i, tweak}, common.Hash{i, i, i, i, tweak})
}
if i%3 == 0 {
- state.SetCode(addr, []byte{i, i, i, i, i, tweak})
+ state.SetCode(addr, []byte{i, i, i, i, i, tweak}, tracing.CodeChangeUnspecified)
}
}
@@ -374,7 +374,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
code := make([]byte, 16)
binary.BigEndian.PutUint64(code, uint64(a.args[0]))
binary.BigEndian.PutUint64(code[8:], uint64(a.args[1]))
- s.SetCode(addr, code)
+ s.SetCode(addr, code, tracing.CodeChangeUnspecified)
},
args: make([]int64, 2),
},
@@ -403,7 +403,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
// which would cause a difference in state when unrolling
// the journal. (CreateContact assumes created was false prior to
// invocation, and the journal rollback sets it to false).
- s.SetCode(addr, []byte{1})
+ s.SetCode(addr, []byte{1}, tracing.CodeChangeUnspecified)
}
},
},
@@ -731,7 +731,7 @@ func TestCopyCommitCopy(t *testing.T) {
sval := common.HexToHash("bbb")
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
- state.SetCode(addr, []byte("hello")) // Change an external metadata
+ state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
state.SetState(addr, skey, sval) // Change the storage trie
if balance := state.GetBalance(addr); balance.Cmp(uint256.NewInt(42)) != 0 {
@@ -804,7 +804,7 @@ func TestCopyCopyCommitCopy(t *testing.T) {
sval := common.HexToHash("bbb")
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
- state.SetCode(addr, []byte("hello")) // Change an external metadata
+ state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
state.SetState(addr, skey, sval) // Change the storage trie
if balance := state.GetBalance(addr); balance.Cmp(uint256.NewInt(42)) != 0 {
@@ -874,7 +874,7 @@ func TestCommitCopy(t *testing.T) {
sval1, sval2 := common.HexToHash("b1"), common.HexToHash("b2")
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
- state.SetCode(addr, []byte("hello")) // Change an external metadata
+ state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
state.SetState(addr, skey1, sval1) // Change the storage trie
if balance := state.GetBalance(addr); balance.Cmp(uint256.NewInt(42)) != 0 {
@@ -987,10 +987,10 @@ func testMissingTrieNodes(t *testing.T, scheme string) {
addr := common.BytesToAddress([]byte("so"))
{
state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
- state.SetCode(addr, []byte{1, 2, 3})
+ state.SetCode(addr, []byte{1, 2, 3}, tracing.CodeChangeUnspecified)
a2 := common.BytesToAddress([]byte("another"))
state.SetBalance(a2, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
- state.SetCode(a2, []byte{1, 2, 4})
+ state.SetCode(a2, []byte{1, 2, 4}, tracing.CodeChangeUnspecified)
root, _ = state.Commit(0, false, false)
t.Logf("root: %x", root)
// force-flush
diff --git a/core/state/stateupdate.go b/core/state/stateupdate.go
index 75c4ca028c..a62e2b2d2d 100644
--- a/core/state/stateupdate.go
+++ b/core/state/stateupdate.go
@@ -64,8 +64,10 @@ type accountUpdate struct {
// execution. It contains information about mutated contract codes, accounts,
// and storage slots, along with their original values.
type stateUpdate struct {
- originRoot common.Hash // hash of the state before applying mutation
- root common.Hash // hash of the state after applying mutation
+ originRoot common.Hash // hash of the state before applying mutation
+ root common.Hash // hash of the state after applying mutation
+ blockNumber uint64 // Associated block number
+
accounts map[common.Hash][]byte // accounts stores mutated accounts in 'slim RLP' encoding
accountsOrigin map[common.Address][]byte // accountsOrigin stores the original values of mutated accounts in 'slim RLP' encoding
@@ -95,7 +97,7 @@ func (sc *stateUpdate) empty() bool {
//
// rawStorageKey is a flag indicating whether to use the raw storage slot key or
// the hash of the slot key for constructing state update object.
-func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate {
+func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate {
var (
accounts = make(map[common.Hash][]byte)
accountsOrigin = make(map[common.Address][]byte)
@@ -164,6 +166,7 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash
return &stateUpdate{
originRoot: originRoot,
root: root,
+ blockNumber: blockNumber,
accounts: accounts,
accountsOrigin: accountsOrigin,
storages: storages,
diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go
index 6f492cf9f2..a9faddcdff 100644
--- a/core/state/trie_prefetcher.go
+++ b/core/state/trie_prefetcher.go
@@ -388,6 +388,10 @@ func (sf *subfetcher) loop() {
sf.tasks = nil
sf.lock.Unlock()
+ var (
+ addresses []common.Address
+ slots [][]byte
+ )
for _, task := range tasks {
if task.addr != nil {
key := *task.addr
@@ -400,6 +404,7 @@ func (sf *subfetcher) loop() {
sf.dupsCross++
continue
}
+ sf.seenReadAddr[key] = struct{}{}
} else {
if _, ok := sf.seenReadAddr[key]; ok {
sf.dupsCross++
@@ -409,7 +414,9 @@ func (sf *subfetcher) loop() {
sf.dupsWrite++
continue
}
+ sf.seenWriteAddr[key] = struct{}{}
}
+ addresses = append(addresses, *task.addr)
} else {
key := *task.slot
if task.read {
@@ -421,6 +428,7 @@ func (sf *subfetcher) loop() {
sf.dupsCross++
continue
}
+ sf.seenReadSlot[key] = struct{}{}
} else {
if _, ok := sf.seenReadSlot[key]; ok {
sf.dupsCross++
@@ -430,25 +438,19 @@ func (sf *subfetcher) loop() {
sf.dupsWrite++
continue
}
+ sf.seenWriteSlot[key] = struct{}{}
}
+ slots = append(slots, key.Bytes())
}
- if task.addr != nil {
- sf.trie.GetAccount(*task.addr)
- } else {
- sf.trie.GetStorage(sf.addr, (*task.slot)[:])
+ }
+ if len(addresses) != 0 {
+ if err := sf.trie.PrefetchAccount(addresses); err != nil {
+ log.Error("Failed to prefetch accounts", "err", err)
}
- if task.read {
- if task.addr != nil {
- sf.seenReadAddr[*task.addr] = struct{}{}
- } else {
- sf.seenReadSlot[*task.slot] = struct{}{}
- }
- } else {
- if task.addr != nil {
- sf.seenWriteAddr[*task.addr] = struct{}{}
- } else {
- sf.seenWriteSlot[*task.slot] = struct{}{}
- }
+ }
+ if len(slots) != 0 {
+ if err := sf.trie.PrefetchStorage(sf.addr, slots); err != nil {
+ log.Error("Failed to prefetch storage", "err", err)
}
}
diff --git a/core/state/trie_prefetcher_test.go b/core/state/trie_prefetcher_test.go
index 4d1b627c4d..41349c0c0e 100644
--- a/core/state/trie_prefetcher_test.go
+++ b/core/state/trie_prefetcher_test.go
@@ -39,7 +39,7 @@ func filledStateDB() *StateDB {
sval := common.HexToHash("bbb")
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
- state.SetCode(addr, []byte("hello")) // Change an external metadata
+ state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
state.SetState(addr, skey, sval) // Change the storage trie
for i := 0; i < 100; i++ {
sk := common.BigToHash(big.NewInt(int64(i)))
@@ -81,7 +81,7 @@ func TestVerklePrefetcher(t *testing.T) {
sval := testrand.Hash()
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
- state.SetCode(addr, []byte("hello")) // Change an external metadata
+ state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
state.SetState(addr, skey, sval) // Change the storage trie
root, _ := state.Commit(0, true, false)
diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go
index c0ce705c77..1c738c1e38 100644
--- a/core/state_prefetcher.go
+++ b/core/state_prefetcher.go
@@ -111,12 +111,6 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
fails.Add(1)
return nil // Ugh, something went horribly wrong, bail out
}
- // Pre-load trie nodes for the intermediate root.
- //
- // This operation incurs significant memory allocations due to
- // trie hashing and node decoding. TODO(rjl493456442): investigate
- // ways to mitigate this overhead.
- stateCpy.IntermediateRoot(true)
return nil
})
}
diff --git a/core/state_processor.go b/core/state_processor.go
index ee98326467..4a5e69ca6e 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -110,15 +110,15 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
requests = [][]byte{}
// EIP-6110
if err := ParseDepositLogs(&requests, allLogs, p.config); err != nil {
- return nil, err
+ return nil, fmt.Errorf("failed to parse deposit logs: %w", err)
}
// EIP-7002
if err := ProcessWithdrawalQueue(&requests, evm); err != nil {
- return nil, err
+ return nil, fmt.Errorf("failed to process withdrawal queue: %w", err)
}
// EIP-7251
if err := ProcessConsolidationQueue(&requests, evm); err != nil {
- return nil, err
+ return nil, fmt.Errorf("failed to process consolidation queue: %w", err)
}
}
diff --git a/core/state_transition.go b/core/state_transition.go
index 8853e13ed1..7739e3b001 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -642,12 +642,12 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization)
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
if auth.Address == (common.Address{}) {
// Delegation to zero address means clear.
- st.state.SetCode(authority, nil)
+ st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
return nil
}
// Otherwise install delegation to auth.Address.
- st.state.SetCode(authority, types.AddressToDelegation(auth.Address))
+ st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization)
return nil
}
diff --git a/core/stateless/stats.go b/core/stateless/stats.go
new file mode 100644
index 0000000000..1a6389284c
--- /dev/null
+++ b/core/stateless/stats.go
@@ -0,0 +1,78 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package stateless
+
+import (
+ "maps"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/metrics"
+)
+
+var accountTrieLeavesAtDepth [16]*metrics.Counter
+var storageTrieLeavesAtDepth [16]*metrics.Counter
+
+func init() {
+ for i := 0; i < 16; i++ {
+ accountTrieLeavesAtDepth[i] = metrics.NewRegisteredCounter("witness/trie/account/leaves/depth_"+strconv.Itoa(i), nil)
+ storageTrieLeavesAtDepth[i] = metrics.NewRegisteredCounter("witness/trie/storage/leaves/depth_"+strconv.Itoa(i), nil)
+ }
+}
+
+// WitnessStats aggregates statistics for account and storage trie accesses.
+type WitnessStats struct {
+ accountTrieLeaves [16]int64
+ storageTrieLeaves [16]int64
+}
+
+// NewWitnessStats creates a new WitnessStats collector.
+func NewWitnessStats() *WitnessStats {
+ return &WitnessStats{}
+}
+
+// Add records trie access depths from the given node paths.
+// If `owner` is the zero hash, accesses are attributed to the account trie;
+// otherwise, they are attributed to the storage trie of that account.
+func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
+ // Extract paths from the nodes map
+ paths := slices.Collect(maps.Keys(nodes))
+ sort.Strings(paths)
+
+ for i, path := range paths {
+ // If current path is a prefix of the next path, it's not a leaf.
+ // The last path is always a leaf.
+ if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) {
+ if owner == (common.Hash{}) {
+ s.accountTrieLeaves[len(path)] += 1
+ } else {
+ s.storageTrieLeaves[len(path)] += 1
+ }
+ }
+ }
+}
+
+// ReportMetrics reports the collected statistics to the global metrics registry.
+func (s *WitnessStats) ReportMetrics() {
+ for i := 0; i < 16; i++ {
+ accountTrieLeavesAtDepth[i].Inc(s.accountTrieLeaves[i])
+ storageTrieLeavesAtDepth[i].Inc(s.storageTrieLeaves[i])
+ }
+}
diff --git a/core/stateless/stats_test.go b/core/stateless/stats_test.go
new file mode 100644
index 0000000000..6219e622f5
--- /dev/null
+++ b/core/stateless/stats_test.go
@@ -0,0 +1,161 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package stateless
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+func TestWitnessStatsAdd(t *testing.T) {
+ tests := []struct {
+ name string
+ nodes map[string][]byte
+ owner common.Hash
+ expectedAccountLeaves map[int64]int64
+ expectedStorageLeaves map[int64]int64
+ }{
+ {
+ name: "empty nodes",
+ nodes: map[string][]byte{},
+ owner: common.Hash{},
+ },
+ {
+ name: "single account trie leaf at depth 0",
+ nodes: map[string][]byte{
+ "": []byte("data"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{0: 1},
+ },
+ {
+ name: "single account trie leaf",
+ nodes: map[string][]byte{
+ "abc": []byte("data"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{3: 1},
+ },
+ {
+ name: "account trie with internal nodes",
+ nodes: map[string][]byte{
+ "a": []byte("data1"),
+ "ab": []byte("data2"),
+ "abc": []byte("data3"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{3: 1}, // Only "abc" is a leaf
+ },
+ {
+ name: "multiple account trie branches",
+ nodes: map[string][]byte{
+ "a": []byte("data1"),
+ "ab": []byte("data2"),
+ "abc": []byte("data3"),
+ "b": []byte("data4"),
+ "bc": []byte("data5"),
+ "bcd": []byte("data6"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{3: 2}, // "abc" (3) + "bcd" (3)
+ },
+ {
+ name: "siblings are all leaves",
+ nodes: map[string][]byte{
+ "aa": []byte("data1"),
+ "ab": []byte("data2"),
+ "ac": []byte("data3"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{2: 3},
+ },
+ {
+ name: "storage trie leaves",
+ nodes: map[string][]byte{
+ "1": []byte("data1"),
+ "12": []byte("data2"),
+ "123": []byte("data3"),
+ "124": []byte("data4"),
+ },
+ owner: common.HexToHash("0x1234"),
+ expectedStorageLeaves: map[int64]int64{3: 2}, // "123" (3) + "124" (3)
+ },
+ {
+ name: "complex trie structure",
+ nodes: map[string][]byte{
+ "1": []byte("data1"),
+ "12": []byte("data2"),
+ "123": []byte("data3"),
+ "124": []byte("data4"),
+ "2": []byte("data5"),
+ "23": []byte("data6"),
+ "234": []byte("data7"),
+ "235": []byte("data8"),
+ "3": []byte("data9"),
+ },
+ owner: common.Hash{},
+ expectedAccountLeaves: map[int64]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1)
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ stats := NewWitnessStats()
+ stats.Add(tt.nodes, tt.owner)
+
+ var expectedAccountTrieLeaves [16]int64
+ for depth, count := range tt.expectedAccountLeaves {
+ expectedAccountTrieLeaves[depth] = count
+ }
+ var expectedStorageTrieLeaves [16]int64
+ for depth, count := range tt.expectedStorageLeaves {
+ expectedStorageTrieLeaves[depth] = count
+ }
+
+ // Check account trie depth
+ if stats.accountTrieLeaves != expectedAccountTrieLeaves {
+ t.Errorf("Account trie total depth = %v, want %v", stats.accountTrieLeaves, expectedAccountTrieLeaves)
+ }
+
+ // Check storage trie depth
+ if stats.storageTrieLeaves != expectedStorageTrieLeaves {
+ t.Errorf("Storage trie total depth = %v, want %v", stats.storageTrieLeaves, expectedStorageTrieLeaves)
+ }
+ })
+ }
+}
+
+func BenchmarkWitnessStatsAdd(b *testing.B) {
+ // Create a realistic trie node structure
+ nodes := make(map[string][]byte)
+ for i := 0; i < 100; i++ {
+ base := string(rune('a' + i%26))
+ nodes[base] = []byte("data")
+ for j := 0; j < 9; j++ {
+ key := base + string(rune('0'+j))
+ nodes[key] = []byte("data")
+ }
+ }
+
+ stats := NewWitnessStats()
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ stats.Add(nodes, common.Hash{})
+ }
+}
diff --git a/core/stateless/witness.go b/core/stateless/witness.go
index aecfad1d52..371a128f48 100644
--- a/core/stateless/witness.go
+++ b/core/stateless/witness.go
@@ -58,7 +58,7 @@ func NewWitness(context *types.Header, chain HeaderReader) (*Witness, error) {
}
headers = append(headers, parent)
}
- // Create the wtness with a reconstructed gutted out block
+ // Create the witness with a reconstructed gutted out block
return &Witness{
context: context,
Headers: headers,
@@ -88,14 +88,16 @@ func (w *Witness) AddCode(code []byte) {
}
// AddState inserts a batch of MPT trie nodes into the witness.
-func (w *Witness) AddState(nodes map[string]struct{}) {
+func (w *Witness) AddState(nodes map[string][]byte) {
if len(nodes) == 0 {
return
}
w.lock.Lock()
defer w.lock.Unlock()
- maps.Copy(w.State, nodes)
+ for _, value := range nodes {
+ w.State[string(value)] = struct{}{}
+ }
}
// Copy deep-copies the witness object. Witness.Block isn't deep-copied as it
diff --git a/core/tracing/CHANGELOG.md b/core/tracing/CHANGELOG.md
index a14e123d99..a94fa81b55 100644
--- a/core/tracing/CHANGELOG.md
+++ b/core/tracing/CHANGELOG.md
@@ -4,6 +4,27 @@ All notable changes to the tracing interface will be documented in this file.
## [Unreleased]
+### Deprecated methods
+
+- `OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte)`: This hook is deprecated in favor of `OnCodeChangeV2` which includes a reason parameter ([#32525](https://github.com/ethereum/go-ethereum/pull/32525)).
+
+### New methods
+
+- `OnCodeChangeV2(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason CodeChangeReason)`: This hook is called when a code change occurs. It is a successor to `OnCodeChange` with an additional reason parameter ([#32525](https://github.com/ethereum/go-ethereum/pull/32525)).
+
+### New types
+
+- `CodeChangeReason` is a new type used to provide a reason for code changes. It includes various reasons such as contract creation, genesis initialization, EIP-7702 authorization, self-destruct, and revert operations ([#32525](https://github.com/ethereum/go-ethereum/pull/32525)).
+
+## [v1.15.4](https://github.com/ethereum/go-ethereum/releases/tag/v1.15.4)
+
+### Modified types
+
+- `GasChangeReason` has been extended with auto-generated String() methods for better debugging and logging ([#31234](https://github.com/ethereum/go-ethereum/pull/31234)).
+- `NonceChangeReason` has been extended with auto-generated String() methods for better debugging and logging ([#31234](https://github.com/ethereum/go-ethereum/pull/31234)).
+
+## [v1.15.0](https://github.com/ethereum/go-ethereum/releases/tag/v1.15.0)
+
The tracing interface has been extended with backwards-compatible changes to support more use-cases and simplify tracer code. The most notable change is a state journaling library which emits reverse events when a call is reverted.
### Deprecated methods
@@ -23,8 +44,13 @@ The tracing interface has been extended with backwards-compatible changes to sup
### Modified types
-- `VMContext.StateDB` has been extended with `GetCodeHash(addr common.Address) common.Hash` method used to retrieve the code hash an account.
+- `VMContext.StateDB` has been extended with the following method:
+ - `GetCodeHash(addr common.Address) common.Hash` method used to retrieve the code hash of an account.
+- `BlockEvent` has been modified:
+ - The `TD` (Total Difficulty) field has been removed ([#30744](https://github.com/ethereum/go-ethereum/pull/30744)).
- `BalanceChangeReason` has been extended with the `BalanceChangeRevert` reason. More on that below.
+- `GasChangeReason` has been extended with the following reason:
+ - `GasChangeTxDataFloor` is the amount of extra gas the transaction has to pay to reach the minimum gas requirement for the transaction data. This change will always be a negative change.
### State journaling
@@ -49,6 +75,26 @@ The state changes that are covered by the journaling library are:
- `OnCodeChange`
- `OnStorageChange`
+## [v1.14.12](https://github.com/ethereum/go-ethereum/releases/tag/v1.14.12)
+
+This release contains a change in behavior for `OnCodeChange` hook and an extension to the StateDB interface.
+
+### Modified types
+
+- `VMContext.StateDB` has been extended with the following method:
+ - `GetTransientState(addr common.Address, slot common.Hash) common.Hash` method used to access contract transient storage ([#30531](https://github.com/ethereum/go-ethereum/pull/30531)).
+
+### `OnCodeChange` change
+
+The `OnCodeChange` hook is now called when the code of a contract is removed due to a selfdestruct. Previously, no code change was emitted on such occasions.
+
+## [v1.14.10](https://github.com/ethereum/go-ethereum/releases/tag/v1.14.10)
+
+### Modified types
+
+- `OpContext` has been extended with the following method:
+ - `ContractCode() []byte` provides access to the contract bytecode within the OpContext interface ([#30466](https://github.com/ethereum/go-ethereum/pull/30466)).
+
## [v1.14.9](https://github.com/ethereum/go-ethereum/releases/tag/v1.14.9)
### Modified types
@@ -56,13 +102,6 @@ The state changes that are covered by the journaling library are:
- `GasChangeReason` has been extended with the following reasons which will be enabled only post-Verkle. There shouldn't be any gas changes with those reasons prior to the fork.
- `GasChangeWitnessContractCollisionCheck` flags the event of adding to the witness when checking for contract address collision.
-## [v1.14.12]
-
-This release contains a change in behavior for `OnCodeChange` hook.
-
-### `OnCodeChange` change
-
-The `OnCodeChange` hook is now called when the code of a contract is removed due to a selfdestruct. Previously, no code change was emitted on such occasions.
## [v1.14.4]
@@ -148,7 +187,12 @@ The hooks `CaptureStart` and `CaptureEnd` have been removed. These hooks signale
- `CaptureState` -> `OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error)`. `op` is of type `byte` which can be cast to `vm.OpCode` when necessary. A `*vm.ScopeContext` is not passed anymore. It is replaced by `tracing.OpContext` which offers access to the memory, stack and current contract.
- `CaptureFault` -> `OnFault(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, depth int, err error)`. Similar to above.
-[unreleased]: https://github.com/ethereum/go-ethereum/compare/v1.14.8...master
-[v1.14.0]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.0
-[v1.14.3]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.3
+[unreleased]: https://github.com/ethereum/go-ethereum/compare/v1.16.3...master
+[v1.15.4]: https://github.com/ethereum/go-ethereum/releases/tag/v1.15.4
+[v1.15.0]: https://github.com/ethereum/go-ethereum/releases/tag/v1.15.0
+[v1.14.12]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.12
+[v1.14.10]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.10
+[v1.14.9]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.9
[v1.14.4]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.4
+[v1.14.3]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.3
+[v1.14.0]: https://github.com/ethereum/go-ethereum/releases/tag/v1.14.0
diff --git a/core/tracing/gen_code_change_reason_stringer.go b/core/tracing/gen_code_change_reason_stringer.go
new file mode 100644
index 0000000000..9372954063
--- /dev/null
+++ b/core/tracing/gen_code_change_reason_stringer.go
@@ -0,0 +1,29 @@
+// Code generated by "stringer -type=CodeChangeReason -trimprefix=CodeChange -output gen_code_change_reason_stringer.go"; DO NOT EDIT.
+
+package tracing
+
+import "strconv"
+
+func _() {
+ // An "invalid array index" compiler error signifies that the constant values have changed.
+ // Re-run the stringer command to generate them again.
+ var x [1]struct{}
+ _ = x[CodeChangeUnspecified-0]
+ _ = x[CodeChangeContractCreation-1]
+ _ = x[CodeChangeGenesis-2]
+ _ = x[CodeChangeAuthorization-3]
+ _ = x[CodeChangeAuthorizationClear-4]
+ _ = x[CodeChangeSelfDestruct-5]
+ _ = x[CodeChangeRevert-6]
+}
+
+const _CodeChangeReason_name = "UnspecifiedContractCreationGenesisAuthorizationAuthorizationClearSelfDestructRevert"
+
+var _CodeChangeReason_index = [...]uint8{0, 11, 27, 34, 47, 65, 77, 83}
+
+func (i CodeChangeReason) String() string {
+ if i >= CodeChangeReason(len(_CodeChangeReason_index)-1) {
+ return "CodeChangeReason(" + strconv.FormatInt(int64(i), 10) + ")"
+ }
+ return _CodeChangeReason_name[_CodeChangeReason_index[i]:_CodeChangeReason_index[i+1]]
+}
diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go
index 0485f7a3eb..8e50dc3d8f 100644
--- a/core/tracing/hooks.go
+++ b/core/tracing/hooks.go
@@ -177,6 +177,9 @@ type (
// CodeChangeHook is called when the code of an account changes.
CodeChangeHook = func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte)
+ // CodeChangeHookV2 is called when the code of an account changes.
+ CodeChangeHookV2 = func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason CodeChangeReason)
+
// StorageChangeHook is called when the storage of an account changes.
StorageChangeHook = func(addr common.Address, slot common.Hash, prev, new common.Hash)
@@ -211,6 +214,7 @@ type Hooks struct {
OnNonceChange NonceChangeHook
OnNonceChangeV2 NonceChangeHookV2
OnCodeChange CodeChangeHook
+ OnCodeChangeV2 CodeChangeHookV2
OnStorageChange StorageChangeHook
OnLog LogHook
// Block hash read
@@ -372,3 +376,31 @@ const (
// It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal).
NonceChangeRevert NonceChangeReason = 6
)
+
+// CodeChangeReason is used to indicate the reason for a code change.
+type CodeChangeReason byte
+
+//go:generate go run golang.org/x/tools/cmd/stringer -type=CodeChangeReason -trimprefix=CodeChange -output gen_code_change_reason_stringer.go
+
+const (
+ CodeChangeUnspecified CodeChangeReason = 0
+
+ // CodeChangeContractCreation is when a new contract is deployed via CREATE/CREATE2 operations.
+ CodeChangeContractCreation CodeChangeReason = 1
+
+ // CodeChangeGenesis is when contract code is set during blockchain genesis or initial setup.
+ CodeChangeGenesis CodeChangeReason = 2
+
+ // CodeChangeAuthorization is when code is set via EIP-7702 Set Code Authorization.
+ CodeChangeAuthorization CodeChangeReason = 3
+
+ // CodeChangeAuthorizationClear is when EIP-7702 delegation is cleared by setting to zero address.
+ CodeChangeAuthorizationClear CodeChangeReason = 4
+
+ // CodeChangeSelfDestruct is when contract code is cleared due to self-destruct.
+ CodeChangeSelfDestruct CodeChangeReason = 5
+
+ // CodeChangeRevert is emitted when the code is reverted back to a previous value due to call failure.
+ // It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal).
+ CodeChangeRevert CodeChangeReason = 6
+)
diff --git a/core/tracing/journal.go b/core/tracing/journal.go
index a402f1ac09..62a70d6c27 100644
--- a/core/tracing/journal.go
+++ b/core/tracing/journal.go
@@ -42,12 +42,15 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
return nil, errors.New("wrapping nil tracer")
}
// No state change to journal, return the wrapped hooks as is
- if hooks.OnBalanceChange == nil && hooks.OnNonceChange == nil && hooks.OnNonceChangeV2 == nil && hooks.OnCodeChange == nil && hooks.OnStorageChange == nil {
+ if hooks.OnBalanceChange == nil && hooks.OnNonceChange == nil && hooks.OnNonceChangeV2 == nil && hooks.OnCodeChange == nil && hooks.OnCodeChangeV2 == nil && hooks.OnStorageChange == nil {
return hooks, nil
}
if hooks.OnNonceChange != nil && hooks.OnNonceChangeV2 != nil {
return nil, errors.New("cannot have both OnNonceChange and OnNonceChangeV2")
}
+ if hooks.OnCodeChange != nil && hooks.OnCodeChangeV2 != nil {
+ return nil, errors.New("cannot have both OnCodeChange and OnCodeChangeV2")
+ }
// Create a new Hooks instance and copy all hooks
wrapped := *hooks
@@ -72,6 +75,9 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
if hooks.OnCodeChange != nil {
wrapped.OnCodeChange = j.OnCodeChange
}
+ if hooks.OnCodeChangeV2 != nil {
+ wrapped.OnCodeChangeV2 = j.OnCodeChangeV2
+ }
if hooks.OnStorageChange != nil {
wrapped.OnStorageChange = j.OnStorageChange
}
@@ -174,6 +180,19 @@ func (j *journal) OnCodeChange(addr common.Address, prevCodeHash common.Hash, pr
}
}
+func (j *journal) OnCodeChangeV2(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason CodeChangeReason) {
+ j.entries = append(j.entries, codeChange{
+ addr: addr,
+ prevCodeHash: prevCodeHash,
+ prevCode: prevCode,
+ newCodeHash: codeHash,
+ newCode: code,
+ })
+ if j.hooks.OnCodeChangeV2 != nil {
+ j.hooks.OnCodeChangeV2(addr, prevCodeHash, prevCode, codeHash, code, reason)
+ }
+}
+
func (j *journal) OnStorageChange(addr common.Address, slot common.Hash, prev, new common.Hash) {
j.entries = append(j.entries, storageChange{addr: addr, slot: slot, prev: prev, new: new})
if j.hooks.OnStorageChange != nil {
@@ -225,7 +244,9 @@ func (n nonceChange) revert(hooks *Hooks) {
}
func (c codeChange) revert(hooks *Hooks) {
- if hooks.OnCodeChange != nil {
+ if hooks.OnCodeChangeV2 != nil {
+ hooks.OnCodeChangeV2(c.addr, c.newCodeHash, c.newCode, c.prevCodeHash, c.prevCode, CodeChangeRevert)
+ } else if hooks.OnCodeChange != nil {
hooks.OnCodeChange(c.addr, c.newCodeHash, c.newCode, c.prevCodeHash, c.prevCode)
}
}
diff --git a/core/tracing/journal_test.go b/core/tracing/journal_test.go
index 99447e1e1d..cf74d83483 100644
--- a/core/tracing/journal_test.go
+++ b/core/tracing/journal_test.go
@@ -23,6 +23,7 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
)
type testTracer struct {
@@ -56,6 +57,11 @@ func (t *testTracer) OnCodeChange(addr common.Address, prevCodeHash common.Hash,
t.code = code
}
+func (t *testTracer) OnCodeChangeV2(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte, reason CodeChangeReason) {
+ t.t.Logf("OnCodeChangeV2(%v, %v -> %v, %v)", addr, prevCodeHash, codeHash, reason)
+ t.code = code
+}
+
func (t *testTracer) OnStorageChange(addr common.Address, slot common.Hash, prev common.Hash, new common.Hash) {
t.t.Logf("OnStorageCodeChange(%v, %v, %v -> %v)", addr, slot, prev, new)
if t.storage == nil {
@@ -232,6 +238,27 @@ func TestOnNonceChangeV2(t *testing.T) {
}
}
+func TestOnCodeChangeV2(t *testing.T) {
+ tr := &testTracer{t: t}
+ wr, err := WrapWithJournal(&Hooks{OnCodeChangeV2: tr.OnCodeChangeV2})
+ if err != nil {
+ t.Fatalf("failed to wrap test tracer: %v", err)
+ }
+
+ addr := common.HexToAddress("0x1234")
+ code := []byte{1, 2, 3}
+ {
+ wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
+ wr.OnCodeChangeV2(addr, common.Hash{}, nil, crypto.Keccak256Hash(code), code, CodeChangeContractCreation)
+ wr.OnExit(2, nil, 100, nil, true)
+ }
+
+ // After revert, code should be nil
+ if tr.code != nil {
+ t.Fatalf("unexpected code after revert: %v", tr.code)
+ }
+}
+
func TestAllHooksCalled(t *testing.T) {
tracer := newTracerAllHooks()
hooks := tracer.hooks()
@@ -298,6 +325,7 @@ func newTracerAllHooks() *tracerAllHooks {
t.hooksCalled[hooksType.Field(i).Name] = false
}
delete(t.hooksCalled, "OnNonceChange")
+ delete(t.hooksCalled, "OnCodeChange")
return t
}
@@ -322,7 +350,7 @@ func (t *tracerAllHooks) hooks() *Hooks {
hooksValue := reflect.ValueOf(h).Elem()
for i := 0; i < hooksValue.NumField(); i++ {
field := hooksValue.Type().Field(i)
- if field.Name == "OnNonceChange" {
+ if field.Name == "OnNonceChange" || field.Name == "OnCodeChange" {
continue
}
hookMethod := reflect.MakeFunc(field.Type, func(args []reflect.Value) []reflect.Value {
diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go
index 948ecd14c3..edc8eb3e55 100644
--- a/core/txpool/blobpool/blobpool.go
+++ b/core/txpool/blobpool/blobpool.go
@@ -1298,6 +1298,13 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
}
// GetBlobs returns a number of blobs and proofs for the given versioned hashes.
+// Blobpool must place responses in the order given in the request, using null
+// for any missing blobs.
+//
+// For instance, if the request is [A_versioned_hash, B_versioned_hash,
+// C_versioned_hash] and blobpool has data for blobs A and C, but doesn't have
+// data for B, the response MUST be [A, null, C].
+//
// This is a utility method for the engine API, enabling consensus clients to
// retrieve blobs from the pools directly instead of the network.
func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blob, []kzg4844.Commitment, [][]kzg4844.Proof, error) {
@@ -1317,26 +1324,30 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo
if _, ok := filled[vhash]; ok {
continue
}
- // Retrieve the corresponding blob tx with the vhash
+ // Retrieve the corresponding blob tx with the vhash, skip blob resolution
+ // if it's not found locally and place the null instead.
p.lock.RLock()
txID, exists := p.lookup.storeidOfBlob(vhash)
p.lock.RUnlock()
if !exists {
- return nil, nil, nil, fmt.Errorf("blob with vhash %x is not found", vhash)
+ continue
}
data, err := p.store.Get(txID)
if err != nil {
- return nil, nil, nil, err
+ log.Error("Tracked blob transaction missing from store", "id", txID, "err", err)
+ continue
}
// Decode the blob transaction
tx := new(types.Transaction)
if err := rlp.DecodeBytes(data, tx); err != nil {
- return nil, nil, nil, err
+ log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err)
+ continue
}
sidecar := tx.BlobTxSidecar()
if sidecar == nil {
- return nil, nil, nil, fmt.Errorf("blob tx without sidecar %x", tx.Hash())
+ log.Error("Blob tx without sidecar", "hash", tx.Hash(), "id", txID)
+ continue
}
// Traverse the blobs in the transaction
for i, hash := range tx.BlobHashes() {
@@ -1397,6 +1408,31 @@ func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int {
return available
}
+// convertSidecar converts the legacy sidecar in the submitted transactions
+// if Osaka fork has been activated.
+func (p *BlobPool) convertSidecar(txs []*types.Transaction) ([]*types.Transaction, []error) {
+ head := p.chain.CurrentBlock()
+ if !p.chain.Config().IsOsaka(head.Number, head.Time) {
+ return txs, make([]error, len(txs))
+ }
+ var errs []error
+ for _, tx := range txs {
+ sidecar := tx.BlobTxSidecar()
+ if sidecar == nil {
+ errs = append(errs, errors.New("missing sidecar in blob transaction"))
+ continue
+ }
+ if sidecar.Version == types.BlobSidecarVersion0 {
+ if err := sidecar.ToV1(); err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ }
+ errs = append(errs, nil)
+ }
+ return txs, errs
+}
+
// Add inserts a set of blob transactions into the pool if they pass validation (both
// consensus validity and pool restrictions).
//
@@ -1404,10 +1440,14 @@ func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int {
// related to the add is finished. Only use this during tests for determinism.
func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error {
var (
+ errs []error
adds = make([]*types.Transaction, 0, len(txs))
- errs = make([]error, len(txs))
)
+ txs, errs = p.convertSidecar(txs)
for i, tx := range txs {
+ if errs[i] != nil {
+ continue
+ }
errs[i] = p.add(tx)
if errs[i] == nil {
adds = append(adds, tx.WithoutBlobTxSidecar())
diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go
index 55eed86cff..8171ae294a 100644
--- a/core/txpool/blobpool/blobpool_test.go
+++ b/core/txpool/blobpool/blobpool_test.go
@@ -24,9 +24,11 @@ import (
"fmt"
"math"
"math/big"
+ "math/rand"
"os"
"path/filepath"
"reflect"
+ "slices"
"sync"
"testing"
@@ -40,6 +42,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
+ "github.com/ethereum/go-ethereum/internal/testrand"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/billy"
@@ -47,11 +50,12 @@ import (
)
var (
- testBlobs []*kzg4844.Blob
- testBlobCommits []kzg4844.Commitment
- testBlobProofs []kzg4844.Proof
- testBlobVHashes [][32]byte
- testBlobIndices = make(map[[32]byte]int)
+ testBlobs []*kzg4844.Blob
+ testBlobCommits []kzg4844.Commitment
+ testBlobProofs []kzg4844.Proof
+ testBlobCellProofs [][]kzg4844.Proof
+ testBlobVHashes [][32]byte
+ testBlobIndices = make(map[[32]byte]int)
)
const testMaxBlobsPerBlock = 6
@@ -67,6 +71,9 @@ func init() {
testBlobProof, _ := kzg4844.ComputeBlobProof(testBlob, testBlobCommit)
testBlobProofs = append(testBlobProofs, testBlobProof)
+ testBlobCellProof, _ := kzg4844.ComputeCellProofs(testBlob)
+ testBlobCellProofs = append(testBlobCellProofs, testBlobCellProof)
+
testBlobVHash := kzg4844.CalcBlobHashV1(sha256.New(), &testBlobCommit)
testBlobIndices[testBlobVHash] = len(testBlobVHashes)
testBlobVHashes = append(testBlobVHashes, testBlobVHash)
@@ -257,8 +264,8 @@ func makeUnsignedTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap
return makeUnsignedTxWithTestBlob(nonce, gasTipCap, gasFeeCap, blobFeeCap, rnd.Intn(len(testBlobs)))
}
-// makeUnsignedTx is a utility method to construct a random blob transaction
-// without signing it.
+// makeUnsignedTxWithTestBlob is a utility method to construct a random blob transaction
+// with a specific test blob without signing it.
func makeUnsignedTxWithTestBlob(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, blobIdx int) *types.BlobTx {
return &types.BlobTx{
ChainID: uint256.MustFromBig(params.MainnetChainConfig.ChainID),
@@ -416,24 +423,40 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
hashes = append(hashes, tx.vhashes...)
}
}
- blobs, _, proofs, err := pool.GetBlobs(hashes, types.BlobSidecarVersion0)
+ blobs1, _, proofs1, err := pool.GetBlobs(hashes, types.BlobSidecarVersion0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ blobs2, _, proofs2, err := pool.GetBlobs(hashes, types.BlobSidecarVersion1)
if err != nil {
t.Fatal(err)
}
// Cross validate what we received vs what we wanted
- if len(blobs) != len(hashes) || len(proofs) != len(hashes) {
- t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), len(hashes))
+ if len(blobs1) != len(hashes) || len(proofs1) != len(hashes) {
+ t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs1), len(proofs1), len(hashes))
+ return
+ }
+ if len(blobs2) != len(hashes) || len(proofs2) != len(hashes) {
+ t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want blobs %d, want proofs: %d", len(blobs2), len(proofs2), len(hashes), len(hashes))
return
}
for i, hash := range hashes {
// If an item is missing, but shouldn't, error
- if blobs[i] == nil || proofs[i] == nil {
+ if blobs1[i] == nil || proofs1[i] == nil {
+ t.Errorf("tracked blob retrieval failed: item %d, hash %x", i, hash)
+ continue
+ }
+ if blobs2[i] == nil || proofs2[i] == nil {
t.Errorf("tracked blob retrieval failed: item %d, hash %x", i, hash)
continue
}
// Item retrieved, make sure it matches the expectation
index := testBlobIndices[hash]
- if *blobs[i] != *testBlobs[index] || proofs[i][0] != testBlobProofs[index] {
+ if *blobs1[i] != *testBlobs[index] || proofs1[i][0] != testBlobProofs[index] {
+ t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
+ continue
+ }
+ if *blobs2[i] != *testBlobs[index] || !slices.Equal(proofs2[i], testBlobCellProofs[index]) {
t.Errorf("retrieved blob or proof mismatch: item %d, hash %x", i, hash)
continue
}
@@ -1668,6 +1691,49 @@ func TestAdd(t *testing.T) {
}
}
+// Tests that adding the transactions with legacy sidecar and expect them to
+// be converted to new format correctly.
+func TestAddLegacyBlobTx(t *testing.T) {
+ var (
+ key1, _ = crypto.GenerateKey()
+ key2, _ = crypto.GenerateKey()
+
+ addr1 = crypto.PubkeyToAddress(key1.PublicKey)
+ addr2 = crypto.PubkeyToAddress(key2.PublicKey)
+ )
+
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
+ statedb.AddBalance(addr2, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
+ statedb.Commit(0, true, false)
+
+ chain := &testBlockChain{
+ config: params.MergedTestChainConfig,
+ basefee: uint256.NewInt(1050),
+ blobfee: uint256.NewInt(105),
+ statedb: statedb,
+ }
+ pool := New(Config{Datadir: t.TempDir()}, chain, nil)
+ if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
+ t.Fatalf("failed to create blob pool: %v", err)
+ }
+
+ // Attempt to add legacy blob transactions.
+ var (
+ tx1 = makeMultiBlobTx(0, 1, 1000, 100, 6, 0, key1, types.BlobSidecarVersion0)
+ tx2 = makeMultiBlobTx(0, 1, 800, 70, 6, 6, key2, types.BlobSidecarVersion0)
+ tx3 = makeMultiBlobTx(1, 1, 800, 70, 6, 12, key2, types.BlobSidecarVersion1)
+ )
+ errs := pool.Add([]*types.Transaction{tx1, tx2, tx3}, true)
+ for _, err := range errs {
+ if err != nil {
+ t.Fatalf("failed to add tx: %v", err)
+ }
+ }
+ verifyPoolInternals(t, pool)
+ pool.Close()
+}
+
func TestGetBlobs(t *testing.T) {
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true)))
@@ -1750,10 +1816,10 @@ func TestGetBlobs(t *testing.T) {
}
cases := []struct {
- start int
- limit int
- version byte
- expErr bool
+ start int
+ limit int
+ fillRandom bool
+ version byte
}{
{
start: 0, limit: 6,
@@ -1763,6 +1829,14 @@ func TestGetBlobs(t *testing.T) {
start: 0, limit: 6,
version: types.BlobSidecarVersion1,
},
+ {
+ start: 0, limit: 6, fillRandom: true,
+ version: types.BlobSidecarVersion0,
+ },
+ {
+ start: 0, limit: 6, fillRandom: true,
+ version: types.BlobSidecarVersion1,
+ },
{
start: 3, limit: 9,
version: types.BlobSidecarVersion0,
@@ -1771,6 +1845,14 @@ func TestGetBlobs(t *testing.T) {
start: 3, limit: 9,
version: types.BlobSidecarVersion1,
},
+ {
+ start: 3, limit: 9, fillRandom: true,
+ version: types.BlobSidecarVersion0,
+ },
+ {
+ start: 3, limit: 9, fillRandom: true,
+ version: types.BlobSidecarVersion1,
+ },
{
start: 3, limit: 15,
version: types.BlobSidecarVersion0,
@@ -1779,6 +1861,14 @@ func TestGetBlobs(t *testing.T) {
start: 3, limit: 15,
version: types.BlobSidecarVersion1,
},
+ {
+ start: 3, limit: 15, fillRandom: true,
+ version: types.BlobSidecarVersion0,
+ },
+ {
+ start: 3, limit: 15, fillRandom: true,
+ version: types.BlobSidecarVersion1,
+ },
{
start: 0, limit: 18,
version: types.BlobSidecarVersion0,
@@ -1788,58 +1878,79 @@ func TestGetBlobs(t *testing.T) {
version: types.BlobSidecarVersion1,
},
{
- start: 18, limit: 20,
+ start: 0, limit: 18, fillRandom: true,
version: types.BlobSidecarVersion0,
- expErr: true,
+ },
+ {
+ start: 0, limit: 18, fillRandom: true,
+ version: types.BlobSidecarVersion1,
},
}
for i, c := range cases {
- var vhashes []common.Hash
+ var (
+ vhashes []common.Hash
+ filled = make(map[int]struct{})
+ )
+ if c.fillRandom {
+ filled[len(vhashes)] = struct{}{}
+ vhashes = append(vhashes, testrand.Hash())
+ }
for j := c.start; j < c.limit; j++ {
vhashes = append(vhashes, testBlobVHashes[j])
+ if c.fillRandom && rand.Intn(2) == 0 {
+ filled[len(vhashes)] = struct{}{}
+ vhashes = append(vhashes, testrand.Hash())
+ }
+ }
+ if c.fillRandom {
+ filled[len(vhashes)] = struct{}{}
+ vhashes = append(vhashes, testrand.Hash())
}
blobs, _, proofs, err := pool.GetBlobs(vhashes, c.version)
+ if err != nil {
+ t.Errorf("Unexpected error for case %d, %v", i, err)
+ }
- if c.expErr {
- if err == nil {
- t.Errorf("Unexpected return, want error for case %d", i)
- }
- } else {
- if err != nil {
- t.Errorf("Unexpected error for case %d, %v", i, err)
- }
- // Cross validate what we received vs what we wanted
- length := c.limit - c.start
- if len(blobs) != length || len(proofs) != length {
- t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), length)
+ // Cross validate what we received vs what we wanted
+ length := c.limit - c.start
+ wantLen := length + len(filled)
+ if len(blobs) != wantLen || len(proofs) != wantLen {
+ t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), wantLen)
+ continue
+ }
+
+ var unknown int
+ for j := 0; j < len(blobs); j++ {
+ if _, exist := filled[j]; exist {
+ if blobs[j] != nil || proofs[j] != nil {
+ t.Errorf("Unexpected blob and proof, item %d", j)
+ }
+ unknown++
continue
}
- for j := 0; j < len(blobs); j++ {
- // If an item is missing, but shouldn't, error
- if blobs[j] == nil || proofs[j] == nil {
- t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
- continue
+ // If an item is missing, but shouldn't, error
+ if blobs[j] == nil || proofs[j] == nil {
+ t.Errorf("tracked blob retrieval failed: item %d, hash %x", j, vhashes[j])
+ continue
+ }
+ // Item retrieved, make sure the blob matches the expectation
+ if *blobs[j] != *testBlobs[c.start+j-unknown] {
+ t.Errorf("retrieved blob mismatch: item %d, hash %x", j, vhashes[j])
+ continue
+ }
+ // Item retrieved, make sure the proof matches the expectation
+ if c.version == types.BlobSidecarVersion0 {
+ if proofs[j][0] != testBlobProofs[c.start+j-unknown] {
+ t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
}
- // Item retrieved, make sure the blob matches the expectation
- if *blobs[j] != *testBlobs[c.start+j] {
- t.Errorf("retrieved blob mismatch: item %d, hash %x", j, vhashes[j])
- continue
- }
- // Item retrieved, make sure the proof matches the expectation
- if c.version == types.BlobSidecarVersion0 {
- if proofs[j][0] != testBlobProofs[c.start+j] {
- t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
- }
- } else {
- want, _ := kzg4844.ComputeCellProofs(blobs[j])
- if !reflect.DeepEqual(want, proofs[j]) {
- t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
- }
+ } else {
+ want, _ := kzg4844.ComputeCellProofs(blobs[j])
+ if !reflect.DeepEqual(want, proofs[j]) {
+ t.Errorf("retrieved proof mismatch: item %d, hash %x", j, vhashes[j])
}
}
}
}
-
pool.Close()
}
diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go
index 93a003c172..425def170b 100644
--- a/core/txpool/legacypool/legacypool.go
+++ b/core/txpool/legacypool/legacypool.go
@@ -514,26 +514,15 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address]
pool.mu.Lock()
defer pool.mu.Unlock()
- // Convert the new uint256.Int types to the old big.Int ones used by the legacy pool
- var (
- minTipBig *big.Int
- baseFeeBig *big.Int
- )
- if filter.MinTip != nil {
- minTipBig = filter.MinTip.ToBig()
- }
- if filter.BaseFee != nil {
- baseFeeBig = filter.BaseFee.ToBig()
- }
pending := make(map[common.Address][]*txpool.LazyTransaction, len(pool.pending))
for addr, list := range pool.pending {
txs := list.Flatten()
// If the miner requests tip enforcement, cap the lists now
- if minTipBig != nil || filter.GasLimitCap != 0 {
+ if filter.MinTip != nil || filter.GasLimitCap != 0 {
for i, tx := range txs {
- if minTipBig != nil {
- if tx.EffectiveGasTipIntCmp(minTipBig, baseFeeBig) < 0 {
+ if filter.MinTip != nil {
+ if tx.EffectiveGasTipIntCmp(filter.MinTip, filter.BaseFee) < 0 {
txs = txs[:i]
break
}
diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go
index 1ba080b749..0c8642659d 100644
--- a/core/txpool/legacypool/legacypool_test.go
+++ b/core/txpool/legacypool/legacypool_test.go
@@ -2292,8 +2292,8 @@ func TestSetCodeTransactions(t *testing.T) {
pending: 1,
run: func(name string) {
aa := common.Address{0xaa, 0xaa}
- statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...))
- statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)})
+ statedb.SetCode(addrA, append(types.DelegationPrefix, aa.Bytes()...), tracing.CodeChangeUnspecified)
+ statedb.SetCode(aa, []byte{byte(vm.ADDRESS), byte(vm.PUSH0), byte(vm.SSTORE)}, tracing.CodeChangeUnspecified)
// Send gapped transaction, it should be rejected.
if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1), keyA)); !errors.Is(err, ErrOutOfOrderTxFromDelegated) {
@@ -2317,7 +2317,7 @@ func TestSetCodeTransactions(t *testing.T) {
}
// Reset the delegation, avoid leaking state into the other tests
- statedb.SetCode(addrA, nil)
+ statedb.SetCode(addrA, nil, tracing.CodeChangeUnspecified)
},
},
{
@@ -2583,7 +2583,7 @@ func TestSetCodeTransactionsReorg(t *testing.T) {
}
// Simulate the chain moving
blockchain.statedb.SetNonce(addrA, 1, tracing.NonceChangeAuthorization)
- blockchain.statedb.SetCode(addrA, types.AddressToDelegation(auth.Address))
+ blockchain.statedb.SetCode(addrA, types.AddressToDelegation(auth.Address), tracing.CodeChangeUnspecified)
<-pool.requestReset(nil, nil)
// Set an authorization for 0x00
auth, _ = types.SignSetCode(keyA, types.SetCodeAuthorization{
@@ -2601,7 +2601,7 @@ func TestSetCodeTransactionsReorg(t *testing.T) {
}
// Simulate the chain moving
blockchain.statedb.SetNonce(addrA, 2, tracing.NonceChangeAuthorization)
- blockchain.statedb.SetCode(addrA, nil)
+ blockchain.statedb.SetCode(addrA, nil, tracing.CodeChangeUnspecified)
<-pool.requestReset(nil, nil)
// Now send two transactions from addrA
if err := pool.addRemoteSync(pricedTransaction(2, 100000, big.NewInt(1000), keyA)); err != nil {
diff --git a/core/txpool/legacypool/list.go b/core/txpool/legacypool/list.go
index 736c28ec4a..507c0b429a 100644
--- a/core/txpool/legacypool/list.go
+++ b/core/txpool/legacypool/list.go
@@ -475,7 +475,7 @@ func (l *list) subTotalCost(txs []*types.Transaction) {
// then the heap is sorted based on the effective tip based on the given base fee.
// If baseFee is nil then the sorting is based on gasFeeCap.
type priceHeap struct {
- baseFee *big.Int // heap should always be re-sorted after baseFee is changed
+ baseFee *uint256.Int // heap should always be re-sorted after baseFee is changed
list []*types.Transaction
}
@@ -677,6 +677,10 @@ func (l *pricedList) Reheap() {
// SetBaseFee updates the base fee and triggers a re-heap. Note that Removed is not
// necessary to call right before SetBaseFee when processing a new block.
func (l *pricedList) SetBaseFee(baseFee *big.Int) {
- l.urgent.baseFee = baseFee
+ base := new(uint256.Int)
+ if baseFee != nil {
+ base.SetFromBig(baseFee)
+ }
+ l.urgent.baseFee = base
l.Reheap()
}
diff --git a/core/txpool/validation.go b/core/txpool/validation.go
index d4f3401086..46974fad3c 100644
--- a/core/txpool/validation.go
+++ b/core/txpool/validation.go
@@ -22,7 +22,6 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/consensus/misc/eip4844"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
@@ -167,9 +166,8 @@ func validateBlobTx(tx *types.Transaction, head *types.Header, opts *ValidationO
if len(hashes) == 0 {
return errors.New("blobless blob transaction")
}
- maxBlobs := eip4844.MaxBlobsPerBlock(opts.Config, head.Time)
- if len(hashes) > maxBlobs {
- return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), maxBlobs)
+ if len(hashes) > params.BlobTxMaxBlobs {
+ return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), params.BlobTxMaxBlobs)
}
if len(sidecar.Blobs) != len(hashes) {
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
diff --git a/core/types/hashes.go b/core/types/hashes.go
index 05cfaeed74..22f1f946dc 100644
--- a/core/types/hashes.go
+++ b/core/types/hashes.go
@@ -45,4 +45,7 @@ var (
// EmptyVerkleHash is the known hash of an empty verkle trie.
EmptyVerkleHash = common.Hash{}
+
+ // EmptyBinaryHash is the known hash of an empty binary trie.
+ EmptyBinaryHash = common.Hash{}
)
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 735442f49b..558c9fc8f1 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/holiman/uint256"
)
var (
@@ -36,6 +37,7 @@ var (
ErrInvalidTxType = errors.New("transaction type not valid in this context")
ErrTxTypeNotSupported = errors.New("transaction type not supported")
ErrGasFeeCapTooLow = errors.New("fee cap less than base fee")
+ ErrUint256Overflow = errors.New("bigint overflow, too large for uint256")
errShortTypedTx = errors.New("typed transaction too short")
errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
errVYParityMismatch = errors.New("'v' and 'yParity' fields do not match")
@@ -357,54 +359,66 @@ func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int {
}
// EffectiveGasTip returns the effective miner gasTipCap for the given base fee.
-// Note: if the effective gasTipCap is negative, this method returns both error
-// the actual negative value, _and_ ErrGasFeeCapTooLow
+// Note: if the effective gasTipCap would be negative, this method
+// returns ErrGasFeeCapTooLow, and value is undefined.
func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) {
- dst := new(big.Int)
- err := tx.calcEffectiveGasTip(dst, baseFee)
- return dst, err
+ dst := new(uint256.Int)
+ base := new(uint256.Int)
+ if baseFee != nil {
+ if base.SetFromBig(baseFee) {
+ return nil, ErrUint256Overflow
+ }
+ }
+ err := tx.calcEffectiveGasTip(dst, base)
+ return dst.ToBig(), err
}
// calcEffectiveGasTip calculates the effective gas tip of the transaction and
// saves the result to dst.
-func (tx *Transaction) calcEffectiveGasTip(dst *big.Int, baseFee *big.Int) error {
+func (tx *Transaction) calcEffectiveGasTip(dst *uint256.Int, baseFee *uint256.Int) error {
if baseFee == nil {
- dst.Set(tx.inner.gasTipCap())
+ if dst.SetFromBig(tx.inner.gasTipCap()) {
+ return ErrUint256Overflow
+ }
return nil
}
var err error
- gasFeeCap := tx.inner.gasFeeCap()
- if gasFeeCap.Cmp(baseFee) < 0 {
+ if dst.SetFromBig(tx.inner.gasFeeCap()) {
+ return ErrUint256Overflow
+ }
+ if dst.Cmp(baseFee) < 0 {
err = ErrGasFeeCapTooLow
}
- dst.Sub(gasFeeCap, baseFee)
- gasTipCap := tx.inner.gasTipCap()
+ dst.Sub(dst, baseFee)
+ gasTipCap := new(uint256.Int)
+ if gasTipCap.SetFromBig(tx.inner.gasTipCap()) {
+ return ErrUint256Overflow
+ }
if gasTipCap.Cmp(dst) < 0 {
dst.Set(gasTipCap)
}
return err
}
-// EffectiveGasTipCmp compares the effective gasTipCap of two transactions assuming the given base fee.
-func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *big.Int) int {
+func (tx *Transaction) EffectiveGasTipCmp(other *Transaction, baseFee *uint256.Int) int {
if baseFee == nil {
return tx.GasTipCapCmp(other)
}
// Use more efficient internal method.
- txTip, otherTip := new(big.Int), new(big.Int)
+ txTip, otherTip := new(uint256.Int), new(uint256.Int)
tx.calcEffectiveGasTip(txTip, baseFee)
other.calcEffectiveGasTip(otherTip, baseFee)
return txTip.Cmp(otherTip)
}
// EffectiveGasTipIntCmp compares the effective gasTipCap of a transaction to the given gasTipCap.
-func (tx *Transaction) EffectiveGasTipIntCmp(other *big.Int, baseFee *big.Int) int {
+func (tx *Transaction) EffectiveGasTipIntCmp(other *uint256.Int, baseFee *uint256.Int) int {
if baseFee == nil {
- return tx.GasTipCapIntCmp(other)
+ return tx.GasTipCapIntCmp(other.ToBig())
}
- txTip := new(big.Int)
+ txTip := new(uint256.Int)
tx.calcEffectiveGasTip(txTip, baseFee)
return txTip.Cmp(other)
}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 7d5e2f058a..cc41674dfd 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
+ "github.com/holiman/uint256"
)
// The values in those tests are from the Transaction Tests
@@ -609,12 +610,12 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
Data: nil,
}
tx, _ := SignNewTx(key, signer, txdata)
- baseFee := big.NewInt(1000000000) // 1 gwei
+ baseFee := uint256.NewInt(1000000000) // 1 gwei
b.Run("Original", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
- _, err := tx.EffectiveGasTip(baseFee)
+ _, err := tx.EffectiveGasTip(baseFee.ToBig())
if err != nil {
b.Fatal(err)
}
@@ -623,7 +624,7 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
b.Run("IntoMethod", func(b *testing.B) {
b.ReportAllocs()
- dst := new(big.Int)
+ dst := new(uint256.Int)
for i := 0; i < b.N; i++ {
err := tx.calcEffectiveGasTip(dst, baseFee)
if err != nil {
@@ -634,9 +635,6 @@ func BenchmarkEffectiveGasTip(b *testing.B) {
}
func TestEffectiveGasTipInto(t *testing.T) {
- signer := LatestSigner(params.TestChainConfig)
- key, _ := crypto.GenerateKey()
-
testCases := []struct {
tipCap int64
feeCap int64
@@ -652,8 +650,26 @@ func TestEffectiveGasTipInto(t *testing.T) {
{tipCap: 50, feeCap: 100, baseFee: nil}, // nil base fee
}
+ // original, non-allocation golfed version
+ orig := func(tx *Transaction, baseFee *big.Int) (*big.Int, error) {
+ if baseFee == nil {
+ return tx.GasTipCap(), nil
+ }
+ var err error
+ gasFeeCap := tx.GasFeeCap()
+ if gasFeeCap.Cmp(baseFee) < 0 {
+ err = ErrGasFeeCapTooLow
+ }
+ gasFeeCap = gasFeeCap.Sub(gasFeeCap, baseFee)
+ gasTipCap := tx.GasTipCap()
+ if gasTipCap.Cmp(gasFeeCap) < 0 {
+ return gasTipCap, err
+ }
+ return gasFeeCap, err
+ }
+
for i, tc := range testCases {
- txdata := &DynamicFeeTx{
+ tx := NewTx(&DynamicFeeTx{
ChainID: big.NewInt(1),
Nonce: 0,
GasTipCap: big.NewInt(tc.tipCap),
@@ -662,27 +678,28 @@ func TestEffectiveGasTipInto(t *testing.T) {
To: &common.Address{},
Value: big.NewInt(0),
Data: nil,
- }
- tx, _ := SignNewTx(key, signer, txdata)
+ })
var baseFee *big.Int
+ var baseFee2 *uint256.Int
if tc.baseFee != nil {
baseFee = big.NewInt(*tc.baseFee)
+ baseFee2 = uint256.NewInt(uint64(*tc.baseFee))
}
// Get result from original method
- orig, origErr := tx.EffectiveGasTip(baseFee)
+ orig, origErr := orig(tx, baseFee)
// Get result from new method
- dst := new(big.Int)
- newErr := tx.calcEffectiveGasTip(dst, baseFee)
+ dst := new(uint256.Int)
+ newErr := tx.calcEffectiveGasTip(dst, baseFee2)
// Compare results
if (origErr != nil) != (newErr != nil) {
t.Fatalf("case %d: error mismatch: orig %v, new %v", i, origErr, newErr)
}
- if orig.Cmp(dst) != 0 {
+ if origErr == nil && orig.Cmp(dst.ToBig()) != 0 {
t.Fatalf("case %d: result mismatch: orig %v, new %v", i, orig, dst)
}
}
@@ -692,3 +709,28 @@ func TestEffectiveGasTipInto(t *testing.T) {
func intPtr(i int64) *int64 {
return &i
}
+
+func BenchmarkEffectiveGasTipCmp(b *testing.B) {
+ signer := LatestSigner(params.TestChainConfig)
+ key, _ := crypto.GenerateKey()
+ txdata := &DynamicFeeTx{
+ ChainID: big.NewInt(1),
+ Nonce: 0,
+ GasTipCap: big.NewInt(2000000000),
+ GasFeeCap: big.NewInt(3000000000),
+ Gas: 21000,
+ To: &common.Address{},
+ Value: big.NewInt(0),
+ Data: nil,
+ }
+ tx, _ := SignNewTx(key, signer, txdata)
+ other, _ := SignNewTx(key, signer, txdata)
+ baseFee := uint256.NewInt(1000000000) // 1 gwei
+
+ b.Run("Original", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ tx.EffectiveGasTipCmp(other, baseFee)
+ }
+ })
+}
diff --git a/core/verkle_witness_test.go b/core/verkle_witness_test.go
index e200bf7f50..ca0c928c3c 100644
--- a/core/verkle_witness_test.go
+++ b/core/verkle_witness_test.go
@@ -232,7 +232,7 @@ func TestProcessParentBlockHash(t *testing.T) {
// etc
checkBlockHashes := func(statedb *state.StateDB, isVerkle bool) {
statedb.SetNonce(params.HistoryStorageAddress, 1, tracing.NonceChangeUnspecified)
- statedb.SetCode(params.HistoryStorageAddress, params.HistoryStorageCode)
+ statedb.SetCode(params.HistoryStorageAddress, params.HistoryStorageCode, tracing.CodeChangeUnspecified)
// Process n blocks, from 1 .. num
var num = 2
for i := 1; i <= num; i++ {
@@ -787,7 +787,7 @@ func TestProcessVerkleSelfDestructInSeparateTx(t *testing.T) {
}
}
-// TestProcessVerkleSelfDestructInSeparateTx controls the contents of the witness after
+// TestProcessVerkleSelfDestructInSameTx controls the contents of the witness after
// a eip6780-compliant selfdestruct occurs.
func TestProcessVerkleSelfDestructInSameTx(t *testing.T) {
// The test txs were taken from a secondary testnet with chain id 69421
diff --git a/core/vm/contracts.go b/core/vm/contracts.go
index e9b80280bf..a06b0d09ca 100644
--- a/core/vm/contracts.go
+++ b/core/vm/contracts.go
@@ -24,6 +24,7 @@ import (
"maps"
"math"
"math/big"
+ "math/bits"
"github.com/consensys/gnark-crypto/ecc"
bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381"
@@ -38,6 +39,7 @@ import (
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/crypto/secp256r1"
"github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
"golang.org/x/crypto/ripemd160"
)
@@ -47,6 +49,7 @@ import (
type PrecompiledContract interface {
RequiredGas(input []byte) uint64 // RequiredPrice calculates the contract gas use
Run(input []byte) ([]byte, error) // Run runs the precompiled contract
+ Name() string
}
// PrecompiledContracts contains the precompiled contracts supported at the given fork.
@@ -309,6 +312,10 @@ func (c *ecrecover) Run(input []byte) ([]byte, error) {
return common.LeftPadBytes(crypto.Keccak256(pubKey[1:])[12:], 32), nil
}
+func (c *ecrecover) Name() string {
+ return "ECREC"
+}
+
// SHA256 implemented as a native contract.
type sha256hash struct{}
@@ -324,6 +331,10 @@ func (c *sha256hash) Run(input []byte) ([]byte, error) {
return h[:], nil
}
+func (c *sha256hash) Name() string {
+ return "SHA256"
+}
+
// RIPEMD160 implemented as a native contract.
type ripemd160hash struct{}
@@ -340,6 +351,10 @@ func (c *ripemd160hash) Run(input []byte) ([]byte, error) {
return common.LeftPadBytes(ripemd.Sum(nil), 32), nil
}
+func (c *ripemd160hash) Name() string {
+ return "RIPEMD160"
+}
+
// data copy implemented as a native contract.
type dataCopy struct{}
@@ -354,6 +369,10 @@ func (c *dataCopy) Run(in []byte) ([]byte, error) {
return common.CopyBytes(in), nil
}
+func (c *dataCopy) Name() string {
+ return "ID"
+}
+
// bigModExp implements a native big integer exponential modular operation.
type bigModExp struct {
eip2565 bool
@@ -361,21 +380,7 @@ type bigModExp struct {
eip7883 bool
}
-var (
- big1 = big.NewInt(1)
- big3 = big.NewInt(3)
- big7 = big.NewInt(7)
- big20 = big.NewInt(20)
- big32 = big.NewInt(32)
- big64 = big.NewInt(64)
- big96 = big.NewInt(96)
- big480 = big.NewInt(480)
- big1024 = big.NewInt(1024)
- big3072 = big.NewInt(3072)
- big199680 = big.NewInt(199680)
-)
-
-// modexpMultComplexity implements bigModexp multComplexity formula, as defined in EIP-198
+// byzantiumMultComplexity implements the bigModexp multComplexity formula, as defined in EIP-198.
//
// def mult_complexity(x):
// if x <= 64: return x ** 2
@@ -383,120 +388,215 @@ var (
// else: return x ** 2 // 16 + 480 * x - 199680
//
// where is x is max(length_of_MODULUS, length_of_BASE)
-func modexpMultComplexity(x *big.Int) *big.Int {
+// returns MaxUint64 if an overflow occurred.
+func byzantiumMultComplexity(x uint64) uint64 {
switch {
- case x.Cmp(big64) <= 0:
- x.Mul(x, x) // x ** 2
- case x.Cmp(big1024) <= 0:
- // (x ** 2 // 4 ) + ( 96 * x - 3072)
- x = new(big.Int).Add(
- new(big.Int).Rsh(new(big.Int).Mul(x, x), 2),
- new(big.Int).Sub(new(big.Int).Mul(big96, x), big3072),
- )
+ case x <= 64:
+ return x * x
+ case x <= 1024:
+ // x^2 / 4 + 96*x - 3072
+ return x*x/4 + 96*x - 3072
+
default:
- // (x ** 2 // 16) + (480 * x - 199680)
- x = new(big.Int).Add(
- new(big.Int).Rsh(new(big.Int).Mul(x, x), 4),
- new(big.Int).Sub(new(big.Int).Mul(big480, x), big199680),
- )
+ // For large x, use uint256 arithmetic to avoid overflow
+ // x^2 / 16 + 480*x - 199680
+
+ // xSqr = x^2 / 16
+ carry, xSqr := bits.Mul64(x, x)
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ xSqr = xSqr >> 4
+
+ // Calculate 480 * x (can't overflow if x^2 didn't overflow)
+ x480 := x * 480
+ // Calculate 480 * x - 199680 (will not underflow, since x > 1024)
+ x480 = x480 - 199680
+
+ // xSqr + x480
+ sum, carry := bits.Add64(xSqr, x480, 0)
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ return sum
+ }
+}
+
+// berlinMultComplexity implements the multiplication complexity formula for Berlin.
+//
+// def mult_complexity(x):
+//
+// ceiling(x/8)^2
+//
+// where is x is max(length_of_MODULUS, length_of_BASE)
+func berlinMultComplexity(x uint64) uint64 {
+ // x = (x + 7) / 8
+ x, carry := bits.Add64(x, 7, 0)
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ x /= 8
+
+ // x^2
+ carry, x = bits.Mul64(x, x)
+ if carry != 0 {
+ return math.MaxUint64
}
return x
}
+// osakaMultComplexity implements the multiplication complexity formula for Osaka.
+//
+// For x <= 32: returns 16
+// For x > 32: returns 2 * ceiling(x/8)^2
+func osakaMultComplexity(x uint64) uint64 {
+ if x <= 32 {
+ return 16
+ }
+ // For x > 32, return 2 * berlinMultComplexity(x)
+ result := berlinMultComplexity(x)
+ carry, result := bits.Mul64(result, 2)
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ return result
+}
+
+// modexpIterationCount calculates the number of iterations for the modexp precompile.
+// This is the adjusted exponent length used in gas calculation.
+func modexpIterationCount(expLen uint64, expHead uint256.Int, multiplier uint64) uint64 {
+ var iterationCount uint64
+
+ // For large exponents (expLen > 32), add (expLen - 32) * multiplier
+ if expLen > 32 {
+ iterationCount = (expLen - 32) * multiplier
+ }
+
+ // Add the MSB position - 1 if expHead is non-zero
+ if bitLen := expHead.BitLen(); bitLen > 0 {
+ iterationCount += uint64(bitLen - 1)
+ }
+
+ return max(iterationCount, 1)
+}
+
+// byzantiumModexpGas calculates the gas cost for the modexp precompile using Byzantium rules.
+func byzantiumModexpGas(baseLen, expLen, modLen uint64, expHead uint256.Int) uint64 {
+ const (
+ multiplier = 8
+ divisor = 20
+ )
+
+ maxLen := max(baseLen, modLen)
+ multComplexity := byzantiumMultComplexity(maxLen)
+ if multComplexity == math.MaxUint64 {
+ return math.MaxUint64
+ }
+ iterationCount := modexpIterationCount(expLen, expHead, multiplier)
+
+ // Calculate gas: (multComplexity * iterationCount) / divisor
+ carry, gas := bits.Mul64(iterationCount, multComplexity)
+ gas /= divisor
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ return gas
+}
+
+// berlinModexpGas calculates the gas cost for the modexp precompile using Berlin rules.
+func berlinModexpGas(baseLen, expLen, modLen uint64, expHead uint256.Int) uint64 {
+ const (
+ multiplier = 8
+ divisor = 3
+ minGas = 200
+ )
+
+ maxLen := max(baseLen, modLen)
+ multComplexity := berlinMultComplexity(maxLen)
+ if multComplexity == math.MaxUint64 {
+ return math.MaxUint64
+ }
+ iterationCount := modexpIterationCount(expLen, expHead, multiplier)
+
+ // Calculate gas: (multComplexity * iterationCount) / divisor
+ carry, gas := bits.Mul64(iterationCount, multComplexity)
+ gas /= divisor
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ return max(gas, minGas)
+}
+
+// osakaModexpGas calculates the gas cost for the modexp precompile using Osaka rules.
+func osakaModexpGas(baseLen, expLen, modLen uint64, expHead uint256.Int) uint64 {
+ const (
+ multiplier = 16
+ divisor = 3
+ minGas = 500
+ )
+
+ maxLen := max(baseLen, modLen)
+ multComplexity := osakaMultComplexity(maxLen)
+ if multComplexity == math.MaxUint64 {
+ return math.MaxUint64
+ }
+ iterationCount := modexpIterationCount(expLen, expHead, multiplier)
+
+ // Calculate gas: (multComplexity * iterationCount) / osakaDivisor
+ carry, gas := bits.Mul64(iterationCount, multComplexity)
+ if carry != 0 {
+ return math.MaxUint64
+ }
+ return max(gas, minGas)
+}
+
// RequiredGas returns the gas required to execute the pre-compiled contract.
func (c *bigModExp) RequiredGas(input []byte) uint64 {
- var (
- baseLen = new(big.Int).SetBytes(getData(input, 0, 32))
- expLen = new(big.Int).SetBytes(getData(input, 32, 32))
- modLen = new(big.Int).SetBytes(getData(input, 64, 32))
- )
+ // Parse input lengths
+ baseLenBig := new(uint256.Int).SetBytes(getData(input, 0, 32))
+ expLenBig := new(uint256.Int).SetBytes(getData(input, 32, 32))
+ modLenBig := new(uint256.Int).SetBytes(getData(input, 64, 32))
+
+ // Convert to uint64, capping at max value
+ baseLen := baseLenBig.Uint64()
+ if !baseLenBig.IsUint64() {
+ baseLen = math.MaxUint64
+ }
+ expLen := expLenBig.Uint64()
+ if !expLenBig.IsUint64() {
+ expLen = math.MaxUint64
+ }
+ modLen := modLenBig.Uint64()
+ if !modLenBig.IsUint64() {
+ modLen = math.MaxUint64
+ }
+
+ // Skip the header
if len(input) > 96 {
input = input[96:]
} else {
input = input[:0]
}
+
// Retrieve the head 32 bytes of exp for the adjusted exponent length
- var expHead *big.Int
- if big.NewInt(int64(len(input))).Cmp(baseLen) <= 0 {
- expHead = new(big.Int)
- } else {
- if expLen.Cmp(big32) > 0 {
- expHead = new(big.Int).SetBytes(getData(input, baseLen.Uint64(), 32))
+ var expHead uint256.Int
+ if uint64(len(input)) > baseLen {
+ if expLen > 32 {
+ expHead.SetBytes(getData(input, baseLen, 32))
} else {
- expHead = new(big.Int).SetBytes(getData(input, baseLen.Uint64(), expLen.Uint64()))
+ // TODO: Check that if expLen < baseLen, then getData will return an empty slice
+ expHead.SetBytes(getData(input, baseLen, expLen))
}
}
- // Calculate the adjusted exponent length
- var msb int
- if bitlen := expHead.BitLen(); bitlen > 0 {
- msb = bitlen - 1
- }
- adjExpLen := new(big.Int)
- if expLen.Cmp(big32) > 0 {
- adjExpLen.Sub(expLen, big32)
- if c.eip7883 {
- adjExpLen.Lsh(adjExpLen, 4)
- } else {
- adjExpLen.Lsh(adjExpLen, 3)
- }
- }
- adjExpLen.Add(adjExpLen, big.NewInt(int64(msb)))
- // Calculate the gas cost of the operation
- gas := new(big.Int)
- if modLen.Cmp(baseLen) < 0 {
- gas.Set(baseLen)
+
+ // Choose the appropriate gas calculation based on the EIP flags
+ if c.eip7883 {
+ return osakaModexpGas(baseLen, expLen, modLen, expHead)
+ } else if c.eip2565 {
+ return berlinModexpGas(baseLen, expLen, modLen, expHead)
} else {
- gas.Set(modLen)
+ return byzantiumModexpGas(baseLen, expLen, modLen, expHead)
}
-
- maxLenOver32 := gas.Cmp(big32) > 0
- if c.eip2565 {
- // EIP-2565 (Berlin fork) has three changes:
- //
- // 1. Different multComplexity (inlined here)
- // in EIP-2565 (https://eips.ethereum.org/EIPS/eip-2565):
- //
- // def mult_complexity(x):
- // ceiling(x/8)^2
- //
- // where is x is max(length_of_MODULUS, length_of_BASE)
- gas.Add(gas, big7)
- gas.Rsh(gas, 3)
- gas.Mul(gas, gas)
-
- var minPrice uint64 = 200
- if c.eip7883 {
- minPrice = 500
- if maxLenOver32 {
- gas.Add(gas, gas)
- } else {
- gas = big.NewInt(16)
- }
- }
-
- if adjExpLen.Cmp(big1) > 0 {
- gas.Mul(gas, adjExpLen)
- }
- // 2. Different divisor (`GQUADDIVISOR`) (3)
- if !c.eip7883 {
- gas.Div(gas, big3)
- }
- if gas.BitLen() > 64 {
- return math.MaxUint64
- }
- return max(minPrice, gas.Uint64())
- }
-
- // Pre-Berlin logic.
- gas = modexpMultComplexity(gas)
- if adjExpLen.Cmp(big1) > 0 {
- gas.Mul(gas, adjExpLen)
- }
- gas.Div(gas, big20)
- if gas.BitLen() > 64 {
- return math.MaxUint64
- }
- return gas.Uint64()
}
func (c *bigModExp) Run(input []byte) ([]byte, error) {
@@ -543,6 +643,10 @@ func (c *bigModExp) Run(input []byte) ([]byte, error) {
return common.LeftPadBytes(v, int(modLen)), nil
}
+func (c *bigModExp) Name() string {
+ return "MODEXP"
+}
+
// newCurvePoint unmarshals a binary blob into a bn256 elliptic curve point,
// returning it, or an error if the point is invalid.
func newCurvePoint(blob []byte) (*bn256.G1, error) {
@@ -592,6 +696,10 @@ func (c *bn256AddIstanbul) Run(input []byte) ([]byte, error) {
return runBn256Add(input)
}
+func (c *bn256AddIstanbul) Name() string {
+ return "BN254_ADD"
+}
+
// bn256AddByzantium implements a native elliptic curve point addition
// conforming to Byzantium consensus rules.
type bn256AddByzantium struct{}
@@ -605,6 +713,10 @@ func (c *bn256AddByzantium) Run(input []byte) ([]byte, error) {
return runBn256Add(input)
}
+func (c *bn256AddByzantium) Name() string {
+ return "BN254_ADD"
+}
+
// runBn256ScalarMul implements the Bn256ScalarMul precompile, referenced by
// both Byzantium and Istanbul operations.
func runBn256ScalarMul(input []byte) ([]byte, error) {
@@ -630,6 +742,10 @@ func (c *bn256ScalarMulIstanbul) Run(input []byte) ([]byte, error) {
return runBn256ScalarMul(input)
}
+func (c *bn256ScalarMulIstanbul) Name() string {
+ return "BN254_MUL"
+}
+
// bn256ScalarMulByzantium implements a native elliptic curve scalar
// multiplication conforming to Byzantium consensus rules.
type bn256ScalarMulByzantium struct{}
@@ -643,6 +759,10 @@ func (c *bn256ScalarMulByzantium) Run(input []byte) ([]byte, error) {
return runBn256ScalarMul(input)
}
+func (c *bn256ScalarMulByzantium) Name() string {
+ return "BN254_MUL"
+}
+
var (
// true32Byte is returned if the bn256 pairing check succeeds.
true32Byte = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
@@ -698,6 +818,10 @@ func (c *bn256PairingIstanbul) Run(input []byte) ([]byte, error) {
return runBn256Pairing(input)
}
+func (c *bn256PairingIstanbul) Name() string {
+ return "BN254_PAIRING"
+}
+
// bn256PairingByzantium implements a pairing pre-compile for the bn256 curve
// conforming to Byzantium consensus rules.
type bn256PairingByzantium struct{}
@@ -711,6 +835,10 @@ func (c *bn256PairingByzantium) Run(input []byte) ([]byte, error) {
return runBn256Pairing(input)
}
+func (c *bn256PairingByzantium) Name() string {
+ return "BN254_PAIRING"
+}
+
type blake2F struct{}
func (c *blake2F) RequiredGas(input []byte) uint64 {
@@ -772,6 +900,10 @@ func (c *blake2F) Run(input []byte) ([]byte, error) {
return output, nil
}
+func (c *blake2F) Name() string {
+ return "BLAKE2F"
+}
+
var (
errBLS12381InvalidInputLength = errors.New("invalid input length")
errBLS12381InvalidFieldElementTopBytes = errors.New("invalid field element top bytes")
@@ -815,6 +947,10 @@ func (c *bls12381G1Add) Run(input []byte) ([]byte, error) {
return encodePointG1(p0), nil
}
+func (c *bls12381G1Add) Name() string {
+ return "BLS12_G1ADD"
+}
+
// bls12381G1MultiExp implements EIP-2537 G1MultiExp precompile.
type bls12381G1MultiExp struct{}
@@ -875,6 +1011,10 @@ func (c *bls12381G1MultiExp) Run(input []byte) ([]byte, error) {
return encodePointG1(r), nil
}
+func (c *bls12381G1MultiExp) Name() string {
+ return "BLS12_G1MSM"
+}
+
// bls12381G2Add implements EIP-2537 G2Add precompile.
type bls12381G2Add struct{}
@@ -912,6 +1052,10 @@ func (c *bls12381G2Add) Run(input []byte) ([]byte, error) {
return encodePointG2(r), nil
}
+func (c *bls12381G2Add) Name() string {
+ return "BLS12_G2ADD"
+}
+
// bls12381G2MultiExp implements EIP-2537 G2MultiExp precompile.
type bls12381G2MultiExp struct{}
@@ -972,6 +1116,10 @@ func (c *bls12381G2MultiExp) Run(input []byte) ([]byte, error) {
return encodePointG2(r), nil
}
+func (c *bls12381G2MultiExp) Name() string {
+ return "BLS12_G2MSM"
+}
+
// bls12381Pairing implements EIP-2537 Pairing precompile.
type bls12381Pairing struct{}
@@ -1035,6 +1183,10 @@ func (c *bls12381Pairing) Run(input []byte) ([]byte, error) {
return out, nil
}
+func (c *bls12381Pairing) Name() string {
+ return "BLS12_PAIRING_CHECK"
+}
+
func decodePointG1(in []byte) (*bls12381.G1Affine, error) {
if len(in) != 128 {
return nil, errors.New("invalid g1 point length")
@@ -1153,6 +1305,10 @@ func (c *bls12381MapG1) Run(input []byte) ([]byte, error) {
return encodePointG1(&r), nil
}
+func (c *bls12381MapG1) Name() string {
+ return "BLS12_MAP_FP_TO_G1"
+}
+
// bls12381MapG2 implements EIP-2537 MapG2 precompile.
type bls12381MapG2 struct{}
@@ -1186,6 +1342,10 @@ func (c *bls12381MapG2) Run(input []byte) ([]byte, error) {
return encodePointG2(&r), nil
}
+func (c *bls12381MapG2) Name() string {
+ return "BLS12_MAP_FP2_TO_G2"
+}
+
// kzgPointEvaluation implements the EIP-4844 point evaluation precompile.
type kzgPointEvaluation struct{}
@@ -1242,6 +1402,10 @@ func (b *kzgPointEvaluation) Run(input []byte) ([]byte, error) {
return common.Hex2Bytes(blobPrecompileReturnValue), nil
}
+func (b *kzgPointEvaluation) Name() string {
+ return "KZG_POINT_EVALUATION"
+}
+
// kZGToVersionedHash implements kzg_to_versioned_hash from EIP-4844
func kZGToVersionedHash(kzg kzg4844.Commitment) common.Hash {
h := sha256.Sum256(kzg[:])
@@ -1277,3 +1441,7 @@ func (c *p256Verify) Run(input []byte) ([]byte, error) {
}
return nil, nil
}
+
+func (c *p256Verify) Name() string {
+ return "P256VERIFY"
+}
diff --git a/core/vm/evm.go b/core/vm/evm.go
index e360187f7b..88ef1cf121 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -601,7 +601,7 @@ func (evm *EVM) initNewContract(contract *Contract, address common.Address) ([]b
}
}
- evm.StateDB.SetCode(address, ret)
+ evm.StateDB.SetCode(address, ret, tracing.CodeChangeContractCreation)
return ret, nil
}
diff --git a/core/vm/gas_table_test.go b/core/vm/gas_table_test.go
index cb6143c0b5..7fe76b0a63 100644
--- a/core/vm/gas_table_test.go
+++ b/core/vm/gas_table_test.go
@@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
@@ -87,7 +88,7 @@ func TestEIP2200(t *testing.T) {
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.CreateAccount(address)
- statedb.SetCode(address, hexutil.MustDecode(tt.input))
+ statedb.SetCode(address, hexutil.MustDecode(tt.input), tracing.CodeChangeUnspecified)
statedb.SetState(address, common.Hash{}, common.BytesToHash([]byte{tt.original}))
statedb.Finalise(true) // Push the state into the "original" slot
@@ -139,7 +140,7 @@ func TestCreateGas(t *testing.T) {
address := common.BytesToAddress([]byte("contract"))
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.CreateAccount(address)
- statedb.SetCode(address, hexutil.MustDecode(tt.code))
+ statedb.SetCode(address, hexutil.MustDecode(tt.code), tracing.CodeChangeUnspecified)
statedb.Finalise(true)
vmctx := BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool { return true },
diff --git a/core/vm/interface.go b/core/vm/interface.go
index 42a72db482..d7f4c10e1f 100644
--- a/core/vm/interface.go
+++ b/core/vm/interface.go
@@ -43,7 +43,7 @@ type StateDB interface {
GetCode(common.Address) []byte
// SetCode sets the new code for the address, and returns the previous code, if any.
- SetCode(common.Address, []byte) []byte
+ SetCode(common.Address, []byte, tracing.CodeChangeReason) []byte
GetCodeSize(common.Address) int
AddRefund(uint64)
diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go
index a0637a6800..52dbe83d86 100644
--- a/core/vm/interpreter.go
+++ b/core/vm/interpreter.go
@@ -33,6 +33,7 @@ type Config struct {
ExtraEips []int // Additional EIPS that are to be enabled
StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose)
+ EnableWitnessStats bool // Whether trie access statistics collection is enabled
}
// ScopeContext contains the things that are per-call, such as stack and memory,
diff --git a/core/vm/interpreter_test.go b/core/vm/interpreter_test.go
index 8ed512316b..90eeda34e6 100644
--- a/core/vm/interpreter_test.go
+++ b/core/vm/interpreter_test.go
@@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
@@ -45,7 +46,7 @@ func TestLoopInterrupt(t *testing.T) {
for i, tt := range loopInterruptTests {
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.CreateAccount(address)
- statedb.SetCode(address, common.Hex2Bytes(tt))
+ statedb.SetCode(address, common.Hex2Bytes(tt), tracing.CodeChangeUnspecified)
statedb.Finalise(true)
evm := NewEVM(vmctx, statedb, params.AllEthashProtocolChanges, Config{})
diff --git a/core/vm/program/program.go b/core/vm/program/program.go
index 5b9cfdcc5f..72cf6ff845 100644
--- a/core/vm/program/program.go
+++ b/core/vm/program/program.go
@@ -53,7 +53,7 @@ func (p *Program) add(op byte) *Program {
return p
}
-// pushBig creates a PUSHX instruction and pushes the given val.
+// doPush creates a PUSHX instruction and pushes the given val.
// - If the val is nil, it pushes zero
// - If the val is bigger than 32 bytes, it panics
func (p *Program) doPush(val *uint256.Int) {
diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go
index 9d984291f2..b40e99d047 100644
--- a/core/vm/runtime/runtime.go
+++ b/core/vm/runtime/runtime.go
@@ -22,6 +22,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
@@ -139,7 +140,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) {
cfg.State.Prepare(rules, cfg.Origin, cfg.Coinbase, &address, vm.ActivePrecompiles(rules), nil)
cfg.State.CreateAccount(address)
// set the receiver's (the executing contract) code for execution.
- cfg.State.SetCode(address, code)
+ cfg.State.SetCode(address, code, tracing.CodeChangeUnspecified)
// Call the code with the given configuration.
ret, leftOverGas, err := vmenv.Call(
cfg.Origin,
diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go
index d75a5b0459..cabc57d1fb 100644
--- a/core/vm/runtime/runtime_test.go
+++ b/core/vm/runtime/runtime_test.go
@@ -114,7 +114,7 @@ func TestCall(t *testing.T) {
byte(vm.PUSH1), 32,
byte(vm.PUSH1), 0,
byte(vm.RETURN),
- })
+ }, tracing.CodeChangeUnspecified)
ret, _, err := Call(address, nil, &Config{State: state})
if err != nil {
@@ -167,7 +167,7 @@ func benchmarkEVM_Create(bench *testing.B, code string) {
)
statedb.CreateAccount(sender)
- statedb.SetCode(receiver, common.FromHex(code))
+ statedb.SetCode(receiver, common.FromHex(code), tracing.CodeChangeUnspecified)
runtimeConfig := Config{
Origin: sender,
State: statedb,
@@ -232,7 +232,7 @@ func BenchmarkEVM_SWAP1(b *testing.B) {
b.Run("10k", func(b *testing.B) {
contractCode := swapContract(10_000)
- state.SetCode(contractAddr, contractCode)
+ state.SetCode(contractAddr, contractCode, tracing.CodeChangeUnspecified)
for i := 0; i < b.N; i++ {
_, _, err := Call(contractAddr, []byte{}, &Config{State: state})
@@ -263,7 +263,7 @@ func BenchmarkEVM_RETURN(b *testing.B) {
b.ReportAllocs()
contractCode := returnContract(n)
- state.SetCode(contractAddr, contractCode)
+ state.SetCode(contractAddr, contractCode, tracing.CodeChangeUnspecified)
for i := 0; i < b.N; i++ {
ret, _, err := Call(contractAddr, []byte{}, &Config{State: state})
@@ -422,12 +422,12 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode
byte(vm.PUSH1), 0x00,
byte(vm.PUSH1), 0x00,
byte(vm.REVERT),
- })
+ }, tracing.CodeChangeUnspecified)
}
//cfg.State.CreateAccount(cfg.Origin)
// set the receiver's (the executing contract) code for execution.
- cfg.State.SetCode(destination, code)
+ cfg.State.SetCode(destination, code, tracing.CodeChangeUnspecified)
Call(destination, nil, cfg)
b.Run(name, func(b *testing.B) {
@@ -772,12 +772,12 @@ func TestRuntimeJSTracer(t *testing.T) {
for i, jsTracer := range jsTracers {
for j, tc := range tests {
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
- statedb.SetCode(main, tc.code)
- statedb.SetCode(common.HexToAddress("0xbb"), calleeCode)
- statedb.SetCode(common.HexToAddress("0xcc"), calleeCode)
- statedb.SetCode(common.HexToAddress("0xdd"), calleeCode)
- statedb.SetCode(common.HexToAddress("0xee"), calleeCode)
- statedb.SetCode(common.HexToAddress("0xff"), suicideCode)
+ statedb.SetCode(main, tc.code, tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xbb"), calleeCode, tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xcc"), calleeCode, tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xdd"), calleeCode, tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xee"), calleeCode, tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xff"), suicideCode, tracing.CodeChangeUnspecified)
tracer, err := tracers.DefaultDirectory.New(jsTracer, new(tracers.Context), nil, params.MergedTestChainConfig)
if err != nil {
@@ -862,8 +862,8 @@ func BenchmarkTracerStepVsCallFrame(b *testing.B) {
// delegation designator incurs the correct amount of gas based on the tracer.
func TestDelegatedAccountAccessCost(t *testing.T) {
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
- statedb.SetCode(common.HexToAddress("0xff"), types.AddressToDelegation(common.HexToAddress("0xaa")))
- statedb.SetCode(common.HexToAddress("0xaa"), program.New().Return(0, 0).Bytes())
+ statedb.SetCode(common.HexToAddress("0xff"), types.AddressToDelegation(common.HexToAddress("0xaa")), tracing.CodeChangeUnspecified)
+ statedb.SetCode(common.HexToAddress("0xaa"), program.New().Return(0, 0).Bytes(), tracing.CodeChangeUnspecified)
for i, tc := range []struct {
code []byte
diff --git a/core/vm/testdata/precompiles/modexp_eip2565.json b/core/vm/testdata/precompiles/modexp_eip2565.json
index c55441439e..24455eeca7 100644
--- a/core/vm/testdata/precompiles/modexp_eip2565.json
+++ b/core/vm/testdata/precompiles/modexp_eip2565.json
@@ -117,5 +117,215 @@
"Name": "nagydani-5-pow0x10001",
"Gas": 87381,
"NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c1000000000000000000000000000000000000000000000000000000000000000cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe000007d7d7d83828282348286877d7d827d407d797d7d7d7d7d7d7d7d7d7d7d5b00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000021000000000000000000000000000000000000000000000000000000000000000cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4000007d7d7d83828282348286877d7d82",
+ "Expected": "36a385a417859b5e178d3ab9",
+ "Name": "marius-1-even",
+ "Gas": 2057,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000d80000000000000000000000000000000000000000000000000000000000000010ffffffffffffffff76ffffffffffffff1cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c76ec7c7c7c7ffffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7ffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c76ec7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7ffffffffff3f000000000000000000000000",
+ "Expected": "c3745de81615f80088ffffffffffffff",
+ "Name": "guido-1-even",
+ "Gas": 2298,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000d80000000000000000000000000000000000000000000000000000000000000010e0060000a921212121212121ff0000212b212121ffff1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f00feffff212121212121ffffffff1fe1e0e0e01e1f1f169f1f1f1f490afcefffffffffffffffff82828282828282828282828282828282828282828200ffff28ff2b212121ffff1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1fffffffffff0afceffffff7ffffffffff7c8282828282a1828282828282828282828282828200ffff28ff2b212121ffff1f1f1f1f1f1fd11f1f1f1f1f1f1f1f1f1f1fffffffffffffffff21212121212121fb2121212121ffff1f1f1f1f1f1f1f1fffaf82828282828200ffff28ff2b21828200",
+ "Expected": "458ef0af2549d46d24c89079499479e1",
+ "Name": "guido-2-even",
+ "Gas": 2300,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001e7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002cb0193585a48e18aad777e9c1b54221a0f58140392e4f091cd5f42b2e8644a9384fbd58ae1edec2477ebf7edbf7c0a3f8bd21d1890ee87646feab3c47be716f842cc3da9b940af312dc54450a960e3fc0b86e56abddd154068e10571a96fff6259431632bc15695c6c8679057e66c2c25c127e97e64ee5de6ea1fc0a4a0e431343fed1daafa072c238a45841da86a9806680bc9f298411173210790359209cd454b5af7b4d5688b4403924e5f863d97e2c5349e1a04b54fcf385b1e9d7714bab8fbf5835f6ff9ed575e77dff7af5cbb641db5d537933bae1fa6555d6c12d6fb31ca27b57771f4aebfbe0bf95e8990c0108ffe7cbdaf370be52cf3ade594543af75ad9329d2d11a402270b5b9a6bf4b83307506e118fca4862749d04e916fc7a039f0d13f2a02e0eedb800199ec95df15b4ccd8669b52586879624d51219e72102fad810b5909b1e372ddf33888fb9beb09b416e4164966edbabd89e4a286be36277fc576ed519a15643dac602e92b63d0b9121f0491da5b16ef793a967f096d80b6c81ecaaffad7e3f06a4a5ac2796f1ed9f68e6a0fd5cf191f0c5c2eec338952ff8d31abc68bf760febeb57e088995ba1d7726a2fdd6d8ca28a181378b8b4ab699bfd4b696739bbf17a9eb2df6251143046137fdbbfacac312ebf67a67da9741b596000000000000419a2917c61722b0713d3b00a2f0e1dd5aebbbe09615de424700eea3c3020fe6e9ea5de9fa1ace781df28b21f746d2ab61d0da496e08473c90ff7dfe25b43bcde76f4bafb82e0975bea75f5a0591dba80ba2fff80a07d8853bea5be13ab326ba70c57b153acc646151948d1cf061ca31b02d4719fac710e7c723ca44f5b1737824b7ccc74ba5bff980aabdbf267621cafc3d6dcc29d0ca9c16839a92ed34de136da7900aa3ee43d21aa57498981124357cf0ca9b86f9a8d3f9c604ca00c726e48f7a9945021ea6dfff92d6b2d6514693169ca133e993541bfa4c4c191de806aa80c48109bcfc9901eccfdeb2395ab75fe63c67de900829d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "Expected": "1194e971c875bb45f030316793bc6916343335b18670dfad7a4646fba99749b30283b78b818836de7400ff1a68ddad1a2dd850ec0f227441e2d4d13f5ee4b5d7a856db0a9ad1f86987e1f117d70f9e6a1a2b8d083fa82653aa16f1773b6deb2ed8a1f9e7f3a5db121c4a0c91cb954e2ec53e63422efe86c7984d79cd0e7b5e3eb8ca4980551d63f302c7d72500a84baf12c82fc7bd9b5c2ab8b9c33baf1df28b2031c58a8b2928a42c9f456e98874e22fe13cf17aa5915b11bb108b6ae40842d434604ccddcb4f64324c67b2dde32e6cd759d964f17d9cdf0046cd0ed3588e1fc4b88f67a5d4f3a870aad1cba89ead265d6ad327c8ea7ff54fe4b5e7dbe87c5c59c468543eab3675751111bfa1d6c51daf789d41dc21fd8ba9e05490f881a973a3c1567ff3129a49aa6658cf06f0a79530a7256ce5a07c2a77b4306383d538866bba376d90621c4f82d1f5f32304ee2b7170805d42418fc6967642e5648d8c64fe9c0fdff2d7c114a47add7767c8fccb8808de8c3c6e1a8880c05e16fafc1513fded8eba222dcaaa809bdb36999cc27ab8d0055009e9690e8a35b859df865dc510d25c7812d8eebbb35607ad595573f0fabd1b57970a2bc113ac6f0ca01e985032b9c2c139316ac099ed1632d2bc0fcc341343d303db2a9c3cf2ad572c6c43084b08d458bf822e92da16079f39cdb0dd10ef47f87ecae404117fc72660372cee9ea42266e7f8d973e7f6b09930ca5f96e04976bf23b9d356bbd2429597b04b7663e0e1a1228f4dfda3b854233e4888dc60c6886c1e0e8aec1705f681027b1e0b3017337557f107ef5cd272df5fd31dfec2bdffe163a8369895ffe124c0aa0ee00ca0fe1db4d5cf37b4af0e49bd73a89d88ced3d88f8e6f00d8e61ad09946d0e72cd3e25bc688a021a83758b5023daae7c269a6cbbd447aba5da7629b75801e1654ce85b8e21ccb9865654f8662e538625d75fea31000000000000000000000000000000000000000000000",
+ "Name": "guido-3-even",
+ "Gas": 5400,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000181000000000000000000000000000000000000000000000000000000000000000801ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2cffffffffffffffffffffffffffffffffffffffffffffffffffffffff3b10000000006c01ffffffffffffffffffffffffffffffffffffffffffffffdffffb97ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3bffffffffffffffffffffffffffffffffffffffffffffffffffffffffebafd93b37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc5bb6affffffff3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2a",
+ "Expected": "0000000000000001",
+ "Name": "guido-4-even",
+ "Gas": 1026,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f2ee5f854d076701c8753d72779187e404f9b2fb705c495137d78551250314a463ef5a213fe22de1cea28d60f518364ff95fe0b73660793e3efcfbe31bda68aeccc21cc9477a6aea5df8cae73422b700c47e54d892691e099167e77befc94780a920ae4155769cd69c30626f054134b5f003772473f57f84837402df6d166e66303f01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c954b27ac388a17c8e9a7ba12a968f288f3308d6fe7bcdf28e685e9a2e00d8be1af19726b7662016d6404f9336493ad633777feb88c9d02a1d2428e566ac38f42c0bb66d2962ec349088ce0d03b35bf27f6114414ef558c87ad8e543754a352f7dffcaca429690688595ab1d1b349d9295b480a82f43ac5c9112fe40720545cc78501cd8b42f3605212fe06a835c9cbc0328e07e94aedb2ac11f6d6649e7fcd8c43",
+ "Expected": "120cf297dbe810911c7d060e109e03699ccefa00a257d296c5a14b4180776c5f7c0d7f1cd789c694807689729af267b53f00373f395dee264a3daba11fcac1fa8875aee0950acd8fa656f1fc58077a7549d794dd160506ecea1acc9c0cda13795749c94f9973b683ce2162866e8d6b5b1165a4c7fa4234964d394d6ec4e0113698b89d173e24a962dd7a41a1819b0fef188ef64e7ee264595dce0d76fbc3ba42d5de833b143c8744366effede8bc8197e8f747ff8cdbc0bf1a93560bec960ca9",
+ "Name": "marcin-1-base-heavy",
+ "Gas": 200,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000080001020304050607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0001020304050607",
+ "Expected": "0000000000000000",
+ "Name": "marcin-1-exp-heavy",
+ "Gas": 215,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000028e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c000102030405060701fffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c950001020304050607",
+ "Expected": "1abce71dc2205cce4eb6934397a88136f94641342e283cbcd30e929e85605c6718ed67f475192ffd",
+ "Name": "marcin-1-balanced",
+ "Gas": 200,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000198e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f2ee5f854d076701c8753d72779187e404f9b2fb705c495137d78551250314a463ef5a213fe22de1cea28d60f518364ff95fe0b73660793e3efcfbe31bda68aeccc21cc9477a6aea5df8cae73422b700c47e54d892691e099167e77befc94780a920ae4155769cd69c30626f054134b5f003772473f57f84837402df6d166e663c29021b0e084f7dc16f6ec88cc597f1aea9f8e0b9501e0f7a546805d2a20eeda0bf080aeb3ed7ea6f9174d804bd242f0b31ff1ea24800344abb580cd87f61ca75a013a87733553966400242399dee3760877fead2cd87287747155e47a854acb50fe07922f57ae3b4553201bfd7c11aca85e1541f91db8e62dca9c418dc5feae086c9487350539c884510044efce5e3f2aaffca4215c12b9044506375097fecd9b22e2ef46f01f1af8aff742aebf96bdcaf55a341600971dc62555376b9e98a8000102030405060708090a0b0c0d0e0f101112131415161703f01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c954b27ac388a17c8e9a7ba12a968f288f3308d6fe7bcdf28e685e9a2e00d8be1af19726b7662016d6404f9336493ad633777feb88c9d02a1d2428e566ac38f42c0bb66d2962ec349088ce0d03b35bf27f6114414ef558c87ad8e543754a352f7dffcaca429690688595ab1d1b349d9295b480a82f43ac5c9112fe40720545cc78501cd8b42f3605212fe06a835c9cbc0328e07e94aedb2ac11f6d6649e7fcd8c43ddce2bd0cdc6c22c4dcd345735040d5bfe3f09b7c61362089f728e2222db96cab2f2c2ccf43574f9e119f4860fd0f1b6036a43ad9db8a428ea09a4ee385112f3fe9c6656ea2cec604cbb5a9227526653bfa7035e4ae80010b1ba16a76608d5dde0a62bc019e9047b5ec05b1005fd017366130a4ba555e7be654561ee3f539c93cb2c9988fca71bf0ad9c4a426b924641a28e1e4adb93609bfa5b2bc81714cbba1110208b86d7b87be28bdf63a62e33ae81dbcc43de9192bd192c40e85faab539000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "817d05c4d540ace3f250fd082e2deb8c2097410fcfe4ce40862cfa015b7f62a7bb72ec1af2915cad66294447b45d177fe759eb80370b0dbd3c0e1c448d54db81eadc11e40c19e394d066a5c019c443798a98d4afd116fc220593d42fbf191b6af0ae75410badb641187ba24a0b968f742a75e2822853f137151d9ea972fd1f36b7c7cc4e71355ecf50648aec094b864cfae9316c0a7be3ddb8ab2d0050b2a029ee956c2366d49430c8f889f29ab514aea8e5b8dec40ba1b49432e30aee32ec45e96dd548205a79d8f8f918eed46acb2115c59086b1011b1d2b093cea723535c3d95efc8e51a7da43b80586d69eb7f213dfb06f7a8e789a9472392bd224411b50f8ca6f2862bcd63431912d1ff99c8d76408245da9e4bea649f0eb930b32922f2d0a345a206160be44d418f1a6c74bd49c4618392ef9350b264a461dfc684c7343211e27675b027054f1cb3d4a5b1a066d3a3ea2eed9caf13251d8be936818f15274e8e3d7539b7c5f216cef327a270fd2a886fbf679c163fb5806249f2c74da5ee0e3ffe9ad1fde2634b29b35da6da6d184ab6ae70199229",
+ "Name": "marcin-2-base-heavy",
+ "Gas": 867,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000010000102030405060708090a0b0c0d0e0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000102030405060708090a0b0c0d0e0f",
+ "Expected": "00000000000000000000000000000000",
+ "Name": "marcin-2-exp-heavy",
+ "Gas": 852,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000038e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c000102030405060708090a0b0c0d0e0f10111213141516172bfffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "86bef3367fc7117c8a6b825cadebe80f3e94c321dda73e9e240b98188a1d5c071c60a195097c8d1fb85ce03a2e1b6964846edee5aa2c3f46",
+ "Name": "marcin-2-balanced",
+ "Gas": 996,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244cfffffffffffffffffffffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95",
+ "Expected": "4004762c491606a5132134da6086284f74cc8e14b08f18b90fc09f31bca3d78f",
+ "Name": "marcin-3-base-heavy",
+ "Gas": 677,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000018000102030405060708090a0b0c0d0e0f1011121314151617ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "000000000000000000000000000000000000000000000000",
+ "Name": "marcin-3-exp-heavy",
+ "Gas": 765,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95",
+ "Expected": "2d3feee20d394af68dd6744b86a8aca6a4a0b7f01bbcd3c3eec768245ca6acee",
+ "Name": "marcin-3-balanced",
+ "Gas": 1360,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000000800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffff",
+ "Name": "mod-8-exp-648",
+ "Gas": 215,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffff",
+ "Name": "mod-8-exp-896",
+ "Gas": 298,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-32",
+ "Gas": 200,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-36",
+ "Gas": 200,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-40",
+ "Gas": 208,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-64",
+ "Gas": 336,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-65",
+ "Gas": 341,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-128",
+ "Gas": 677,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "02fd01000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03feffffffffff",
+ "Name": "mod-256-exp-2",
+ "Gas": 341,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffff00",
+ "Expected": "0100fefffffffffffff710f80000000006f108000000000000feffffffffffff0100fefffffffffffef90ff80000000105f008ffffffffff02fdffffffffffff0100feffffffffff00f80ff80000010101f407fffffffd06fc0000000000fd020000feffffffff00fff80ff8000101fa08f207fffffb0afa0000000001fb03000000feffffff00fffff80ff80102f80405f207fff90ef80000000002f90400000000feffff00fffffff80ff903f6050005f207f712f60000000003f7050000000000feff00fffffffff810fcf406000005f1fd16f40000000004f506000000000000fe00fffffffffff915ea0700000005e522f20000000005f306ffffffffffffffffffffffffff",
+ "Name": "mod-264-exp-2",
+ "Gas": 363,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000040000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02feffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-1024-exp-2",
+ "Gas": 5461,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000008ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "2a02d5f86c2375ff",
+ "Name": "pawel-1-exp-heavy",
+ "Gas": 298,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000010ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "823ef7dc60d6d9616756c48f69b7c4ff",
+ "Name": "pawel-2-exp-heavy",
+ "Gas": 425,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000000000000018ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "c817dd5aa60a41948eed409706c2aa97be3000d4da0261ff",
+ "Name": "pawel-3-exp-heavy",
+ "Gas": 501,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "2defaca0137d6edacbbd5d36d6ed70cbf8a998ffb19fc270d45a18d37e0f35ff",
+ "Name": "pawel-4-exp-heavy",
+ "Gas": 506,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000017bffffffffffffffffffffffffffffffffffffffffffffbffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffe",
+ "Expected": "200f14de1d474710c1c979920452e0ffc2ac6f618afba5",
+ "Name": "mod_vul_pawel_3_exp_8",
+ "Gas": 200,
+ "NoBenchmark": false
}
]
\ No newline at end of file
diff --git a/core/vm/testdata/precompiles/modexp_eip7883.json b/core/vm/testdata/precompiles/modexp_eip7883.json
index 85e9ad1849..fb3169d148 100644
--- a/core/vm/testdata/precompiles/modexp_eip7883.json
+++ b/core/vm/testdata/precompiles/modexp_eip7883.json
@@ -103,5 +103,215 @@
"Name": "nagydani-5-pow0x10001",
"Gas": 524288,
"NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000c1000000000000000000000000000000000000000000000000000000000000000cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe000007d7d7d83828282348286877d7d827d407d797d7d7d7d7d7d7d7d7d7d7d5b00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000021000000000000000000000000000000000000000000000000000000000000000cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff4000007d7d7d83828282348286877d7d82",
+ "Expected": "36a385a417859b5e178d3ab9",
+ "Name": "marius-1-even",
+ "Gas": 45296,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000d80000000000000000000000000000000000000000000000000000000000000010ffffffffffffffff76ffffffffffffff1cffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c76ec7c7c7c7ffffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7ffffffffffffc7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c76ec7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7c7ffffffffff3f000000000000000000000000",
+ "Expected": "c3745de81615f80088ffffffffffffff",
+ "Name": "guido-1-even",
+ "Gas": 51136,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000d80000000000000000000000000000000000000000000000000000000000000010e0060000a921212121212121ff0000212b212121ffff1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f00feffff212121212121ffffffff1fe1e0e0e01e1f1f169f1f1f1f490afcefffffffffffffffff82828282828282828282828282828282828282828200ffff28ff2b212121ffff1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1fffffffffff0afceffffff7ffffffffff7c8282828282a1828282828282828282828282828200ffff28ff2b212121ffff1f1f1f1f1f1fd11f1f1f1f1f1f1f1f1f1f1fffffffffffffffff21212121212121fb2121212121ffff1f1f1f1f1f1f1f1fffaf82828282828200ffff28ff2b21828200",
+ "Expected": "458ef0af2549d46d24c89079499479e1",
+ "Name": "guido-2-even",
+ "Gas": 51152,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001e7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000002cb0193585a48e18aad777e9c1b54221a0f58140392e4f091cd5f42b2e8644a9384fbd58ae1edec2477ebf7edbf7c0a3f8bd21d1890ee87646feab3c47be716f842cc3da9b940af312dc54450a960e3fc0b86e56abddd154068e10571a96fff6259431632bc15695c6c8679057e66c2c25c127e97e64ee5de6ea1fc0a4a0e431343fed1daafa072c238a45841da86a9806680bc9f298411173210790359209cd454b5af7b4d5688b4403924e5f863d97e2c5349e1a04b54fcf385b1e9d7714bab8fbf5835f6ff9ed575e77dff7af5cbb641db5d537933bae1fa6555d6c12d6fb31ca27b57771f4aebfbe0bf95e8990c0108ffe7cbdaf370be52cf3ade594543af75ad9329d2d11a402270b5b9a6bf4b83307506e118fca4862749d04e916fc7a039f0d13f2a02e0eedb800199ec95df15b4ccd8669b52586879624d51219e72102fad810b5909b1e372ddf33888fb9beb09b416e4164966edbabd89e4a286be36277fc576ed519a15643dac602e92b63d0b9121f0491da5b16ef793a967f096d80b6c81ecaaffad7e3f06a4a5ac2796f1ed9f68e6a0fd5cf191f0c5c2eec338952ff8d31abc68bf760febeb57e088995ba1d7726a2fdd6d8ca28a181378b8b4ab699bfd4b696739bbf17a9eb2df6251143046137fdbbfacac312ebf67a67da9741b596000000000000419a2917c61722b0713d3b00a2f0e1dd5aebbbe09615de424700eea3c3020fe6e9ea5de9fa1ace781df28b21f746d2ab61d0da496e08473c90ff7dfe25b43bcde76f4bafb82e0975bea75f5a0591dba80ba2fff80a07d8853bea5be13ab326ba70c57b153acc646151948d1cf061ca31b02d4719fac710e7c723ca44f5b1737824b7ccc74ba5bff980aabdbf267621cafc3d6dcc29d0ca9c16839a92ed34de136da7900aa3ee43d21aa57498981124357cf0ca9b86f9a8d3f9c604ca00c726e48f7a9945021ea6dfff92d6b2d6514693169ca133e993541bfa4c4c191de806aa80c48109bcfc9901eccfdeb2395ab75fe63c67de900829d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "Expected": "1194e971c875bb45f030316793bc6916343335b18670dfad7a4646fba99749b30283b78b818836de7400ff1a68ddad1a2dd850ec0f227441e2d4d13f5ee4b5d7a856db0a9ad1f86987e1f117d70f9e6a1a2b8d083fa82653aa16f1773b6deb2ed8a1f9e7f3a5db121c4a0c91cb954e2ec53e63422efe86c7984d79cd0e7b5e3eb8ca4980551d63f302c7d72500a84baf12c82fc7bd9b5c2ab8b9c33baf1df28b2031c58a8b2928a42c9f456e98874e22fe13cf17aa5915b11bb108b6ae40842d434604ccddcb4f64324c67b2dde32e6cd759d964f17d9cdf0046cd0ed3588e1fc4b88f67a5d4f3a870aad1cba89ead265d6ad327c8ea7ff54fe4b5e7dbe87c5c59c468543eab3675751111bfa1d6c51daf789d41dc21fd8ba9e05490f881a973a3c1567ff3129a49aa6658cf06f0a79530a7256ce5a07c2a77b4306383d538866bba376d90621c4f82d1f5f32304ee2b7170805d42418fc6967642e5648d8c64fe9c0fdff2d7c114a47add7767c8fccb8808de8c3c6e1a8880c05e16fafc1513fded8eba222dcaaa809bdb36999cc27ab8d0055009e9690e8a35b859df865dc510d25c7812d8eebbb35607ad595573f0fabd1b57970a2bc113ac6f0ca01e985032b9c2c139316ac099ed1632d2bc0fcc341343d303db2a9c3cf2ad572c6c43084b08d458bf822e92da16079f39cdb0dd10ef47f87ecae404117fc72660372cee9ea42266e7f8d973e7f6b09930ca5f96e04976bf23b9d356bbd2429597b04b7663e0e1a1228f4dfda3b854233e4888dc60c6886c1e0e8aec1705f681027b1e0b3017337557f107ef5cd272df5fd31dfec2bdffe163a8369895ffe124c0aa0ee00ca0fe1db4d5cf37b4af0e49bd73a89d88ced3d88f8e6f00d8e61ad09946d0e72cd3e25bc688a021a83758b5023daae7c269a6cbbd447aba5da7629b75801e1654ce85b8e21ccb9865654f8662e538625d75fea31000000000000000000000000000000000000000000000",
+ "Name": "guido-3-even",
+ "Gas": 32400,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000181000000000000000000000000000000000000000000000000000000000000000801ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2cffffffffffffffffffffffffffffffffffffffffffffffffffffffff3b10000000006c01ffffffffffffffffffffffffffffffffffffffffffffffdffffb97ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3bffffffffffffffffffffffffffffffffffffffffffffffffffffffffebafd93b37ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc5bb6affffffff3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2a",
+ "Expected": "0000000000000001",
+ "Name": "guido-4-even",
+ "Gas": 94448,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f2ee5f854d076701c8753d72779187e404f9b2fb705c495137d78551250314a463ef5a213fe22de1cea28d60f518364ff95fe0b73660793e3efcfbe31bda68aeccc21cc9477a6aea5df8cae73422b700c47e54d892691e099167e77befc94780a920ae4155769cd69c30626f054134b5f003772473f57f84837402df6d166e66303f01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c954b27ac388a17c8e9a7ba12a968f288f3308d6fe7bcdf28e685e9a2e00d8be1af19726b7662016d6404f9336493ad633777feb88c9d02a1d2428e566ac38f42c0bb66d2962ec349088ce0d03b35bf27f6114414ef558c87ad8e543754a352f7dffcaca429690688595ab1d1b349d9295b480a82f43ac5c9112fe40720545cc78501cd8b42f3605212fe06a835c9cbc0328e07e94aedb2ac11f6d6649e7fcd8c43",
+ "Expected": "120cf297dbe810911c7d060e109e03699ccefa00a257d296c5a14b4180776c5f7c0d7f1cd789c694807689729af267b53f00373f395dee264a3daba11fcac1fa8875aee0950acd8fa656f1fc58077a7549d794dd160506ecea1acc9c0cda13795749c94f9973b683ce2162866e8d6b5b1165a4c7fa4234964d394d6ec4e0113698b89d173e24a962dd7a41a1819b0fef188ef64e7ee264595dce0d76fbc3ba42d5de833b143c8744366effede8bc8197e8f747ff8cdbc0bf1a93560bec960ca9",
+ "Name": "marcin-1-base-heavy",
+ "Gas": 1152,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000080001020304050607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0001020304050607",
+ "Expected": "0000000000000000",
+ "Name": "marcin-1-exp-heavy",
+ "Gas": 16624,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000028e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c000102030405060701fffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c950001020304050607",
+ "Expected": "1abce71dc2205cce4eb6934397a88136f94641342e283cbcd30e929e85605c6718ed67f475192ffd",
+ "Name": "marcin-1-balanced",
+ "Gas": 1200,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000198e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c51f81bcdfc324a0dff2b5bec9d92e21cbebc4d5e29d3a3d30de3e03fbeab8d7f2ee5f854d076701c8753d72779187e404f9b2fb705c495137d78551250314a463ef5a213fe22de1cea28d60f518364ff95fe0b73660793e3efcfbe31bda68aeccc21cc9477a6aea5df8cae73422b700c47e54d892691e099167e77befc94780a920ae4155769cd69c30626f054134b5f003772473f57f84837402df6d166e663c29021b0e084f7dc16f6ec88cc597f1aea9f8e0b9501e0f7a546805d2a20eeda0bf080aeb3ed7ea6f9174d804bd242f0b31ff1ea24800344abb580cd87f61ca75a013a87733553966400242399dee3760877fead2cd87287747155e47a854acb50fe07922f57ae3b4553201bfd7c11aca85e1541f91db8e62dca9c418dc5feae086c9487350539c884510044efce5e3f2aaffca4215c12b9044506375097fecd9b22e2ef46f01f1af8aff742aebf96bdcaf55a341600971dc62555376b9e98a8000102030405060708090a0b0c0d0e0f101112131415161703f01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c954b27ac388a17c8e9a7ba12a968f288f3308d6fe7bcdf28e685e9a2e00d8be1af19726b7662016d6404f9336493ad633777feb88c9d02a1d2428e566ac38f42c0bb66d2962ec349088ce0d03b35bf27f6114414ef558c87ad8e543754a352f7dffcaca429690688595ab1d1b349d9295b480a82f43ac5c9112fe40720545cc78501cd8b42f3605212fe06a835c9cbc0328e07e94aedb2ac11f6d6649e7fcd8c43ddce2bd0cdc6c22c4dcd345735040d5bfe3f09b7c61362089f728e2222db96cab2f2c2ccf43574f9e119f4860fd0f1b6036a43ad9db8a428ea09a4ee385112f3fe9c6656ea2cec604cbb5a9227526653bfa7035e4ae80010b1ba16a76608d5dde0a62bc019e9047b5ec05b1005fd017366130a4ba555e7be654561ee3f539c93cb2c9988fca71bf0ad9c4a426b924641a28e1e4adb93609bfa5b2bc81714cbba1110208b86d7b87be28bdf63a62e33ae81dbcc43de9192bd192c40e85faab539000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "817d05c4d540ace3f250fd082e2deb8c2097410fcfe4ce40862cfa015b7f62a7bb72ec1af2915cad66294447b45d177fe759eb80370b0dbd3c0e1c448d54db81eadc11e40c19e394d066a5c019c443798a98d4afd116fc220593d42fbf191b6af0ae75410badb641187ba24a0b968f742a75e2822853f137151d9ea972fd1f36b7c7cc4e71355ecf50648aec094b864cfae9316c0a7be3ddb8ab2d0050b2a029ee956c2366d49430c8f889f29ab514aea8e5b8dec40ba1b49432e30aee32ec45e96dd548205a79d8f8f918eed46acb2115c59086b1011b1d2b093cea723535c3d95efc8e51a7da43b80586d69eb7f213dfb06f7a8e789a9472392bd224411b50f8ca6f2862bcd63431912d1ff99c8d76408245da9e4bea649f0eb930b32922f2d0a345a206160be44d418f1a6c74bd49c4618392ef9350b264a461dfc684c7343211e27675b027054f1cb3d4a5b1a066d3a3ea2eed9caf13251d8be936818f15274e8e3d7539b7c5f216cef327a270fd2a886fbf679c163fb5806249f2c74da5ee0e3ffe9ad1fde2634b29b35da6da6d184ab6ae70199229",
+ "Name": "marcin-2-base-heavy",
+ "Gas": 5202,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000010000102030405060708090a0b0c0d0e0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000102030405060708090a0b0c0d0e0f",
+ "Expected": "00000000000000000000000000000000",
+ "Name": "marcin-2-exp-heavy",
+ "Gas": 16368,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000038e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244c000102030405060708090a0b0c0d0e0f10111213141516172bfffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "86bef3367fc7117c8a6b825cadebe80f3e94c321dda73e9e240b98188a1d5c071c60a195097c8d1fb85ce03a2e1b6964846edee5aa2c3f46",
+ "Name": "marcin-2-balanced",
+ "Gas": 5978,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244cfffffffffffffffffffffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95",
+ "Expected": "4004762c491606a5132134da6086284f74cc8e14b08f18b90fc09f31bca3d78f",
+ "Name": "marcin-3-base-heavy",
+ "Gas": 2032,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000018000102030405060708090a0b0c0d0e0f1011121314151617ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000102030405060708090a0b0c0d0e0f1011121314151617",
+ "Expected": "000000000000000000000000000000000000000000000000",
+ "Name": "marcin-3-exp-heavy",
+ "Gas": 4080,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020e8e77626586f73b955364c7b4bbf0bb7f7685ebd40e852b164633a4acbd3244cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01681d2220bfea4bb888a5543db8c0916274ddb1ea93b144c042c01d8164c95",
+ "Expected": "2d3feee20d394af68dd6744b86a8aca6a4a0b7f01bbcd3c3eec768245ca6acee",
+ "Name": "marcin-3-balanced",
+ "Gas": 4080,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000000800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffff",
+ "Name": "mod-8-exp-648",
+ "Gas": 16624,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffff",
+ "Name": "mod-8-exp-896",
+ "Gas": 24560,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-32",
+ "Gas": 500,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-36",
+ "Gas": 560,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-40",
+ "Gas": 624,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-64",
+ "Gas": 1008,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-65",
+ "Gas": 1024,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-32-exp-128",
+ "Gas": 2032,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "02fd01000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03ff0000000000000000000000000000000000000000000000fefffffffffffd03feffffffffff",
+ "Name": "mod-256-exp-2",
+ "Gas": 2048,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000001080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000010800ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffff00",
+ "Expected": "0100fefffffffffffff710f80000000006f108000000000000feffffffffffff0100fefffffffffffef90ff80000000105f008ffffffffff02fdffffffffffff0100feffffffffff00f80ff80000010101f407fffffffd06fc0000000000fd020000feffffffff00fff80ff8000101fa08f207fffffb0afa0000000001fb03000000feffffff00fffff80ff80102f80405f207fff90ef80000000002f90400000000feffff00fffffff80ff903f6050005f207f712f60000000003f7050000000000feff00fffffffff810fcf406000005f1fd16f40000000004f506000000000000fe00fffffffffff915ea0700000005e522f20000000005f306ffffffffffffffffffffffffff",
+ "Name": "mod-264-exp-2",
+ "Gas": 2178,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "00000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000040000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
+ "Expected": "00fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02fefffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe02feffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Name": "mod-1024-exp-2",
+ "Gas": 32768,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000008ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "2a02d5f86c2375ff",
+ "Name": "pawel-1-exp-heavy",
+ "Gas": 24560,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000010ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "823ef7dc60d6d9616756c48f69b7c4ff",
+ "Name": "pawel-2-exp-heavy",
+ "Gas": 6128,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000150000000000000000000000000000000000000000000000000000000000000018ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "c817dd5aa60a41948eed409706c2aa97be3000d4da0261ff",
+ "Name": "pawel-3-exp-heavy",
+ "Gas": 2672,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "Expected": "2defaca0137d6edacbbd5d36d6ed70cbf8a998ffb19fc270d45a18d37e0f35ff",
+ "Name": "pawel-4-exp-heavy",
+ "Gas": 1520,
+ "NoBenchmark": false
+ },
+ {
+ "Input": "000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000017bffffffffffffffffffffffffffffffffffffffffffffbffffffffffffffffff7ffffffffffffffffffffffffffffffffffffffffffe",
+ "Expected": "200f14de1d474710c1c979920452e0ffc2ac6f618afba5",
+ "Name": "mod_vul_pawel_3_exp_8",
+ "Gas": 1008,
+ "NoBenchmark": false
}
]
diff --git a/crypto/kzg4844/kzg4844_ckzg_cgo.go b/crypto/kzg4844/kzg4844_ckzg_cgo.go
index b215b19928..46509674b6 100644
--- a/crypto/kzg4844/kzg4844_ckzg_cgo.go
+++ b/crypto/kzg4844/kzg4844_ckzg_cgo.go
@@ -150,7 +150,7 @@ func ckzgComputeCellProofs(blob *Blob) ([]Proof, error) {
return p, nil
}
-// ckzgVerifyCellProofs verifies that the blob data corresponds to the provided commitment.
+// ckzgVerifyCellProofBatch verifies that the blob data corresponds to the provided commitment.
func ckzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProofs []Proof) error {
ckzgIniter.Do(ckzgInit)
var (
diff --git a/crypto/kzg4844/kzg4844_gokzg.go b/crypto/kzg4844/kzg4844_gokzg.go
index 82ec8379d4..e9676ff1b8 100644
--- a/crypto/kzg4844/kzg4844_gokzg.go
+++ b/crypto/kzg4844/kzg4844_gokzg.go
@@ -115,7 +115,7 @@ func gokzgComputeCellProofs(blob *Blob) ([]Proof, error) {
return p, nil
}
-// gokzgVerifyCellProofs verifies that the blob data corresponds to the provided commitment.
+// gokzgVerifyCellProofBatch verifies that the blob data corresponds to the provided commitment.
func gokzgVerifyCellProofBatch(blobs []Blob, commitments []Commitment, cellProofs []Proof) error {
gokzgIniter.Do(gokzgInit)
diff --git a/eth/api_debug.go b/eth/api_debug.go
index 188dee11aa..9cedbcbb2a 100644
--- a/eth/api_debug.go
+++ b/eth/api_debug.go
@@ -443,3 +443,51 @@ func (api *DebugAPI) GetTrieFlushInterval() (string, error) {
}
return api.eth.blockchain.GetTrieFlushInterval().String(), nil
}
+
+// StateSize returns the current state size statistics from the state size tracker.
+// Returns an error if the state size tracker is not initialized or if stats are not ready.
+func (api *DebugAPI) StateSize(blockHashOrNumber *rpc.BlockNumberOrHash) (interface{}, error) {
+ sizer := api.eth.blockchain.StateSizer()
+ if sizer == nil {
+ return nil, errors.New("state size tracker is not enabled")
+ }
+ var (
+ err error
+ stats *state.SizeStats
+ )
+ if blockHashOrNumber == nil {
+ stats, err = sizer.Query(nil)
+ } else {
+ header, herr := api.eth.APIBackend.HeaderByNumberOrHash(context.Background(), *blockHashOrNumber)
+ if herr != nil || header == nil {
+ return nil, fmt.Errorf("block %s is unknown", blockHashOrNumber)
+ }
+ stats, err = sizer.Query(&header.Root)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if stats == nil {
+ var s string
+ if blockHashOrNumber == nil {
+ s = "chain head"
+ } else {
+ s = blockHashOrNumber.String()
+ }
+ return nil, fmt.Errorf("state size of %s is not available", s)
+ }
+ return map[string]interface{}{
+ "stateRoot": stats.StateRoot,
+ "blockNumber": hexutil.Uint64(stats.BlockNumber),
+ "accounts": hexutil.Uint64(stats.Accounts),
+ "accountBytes": hexutil.Uint64(stats.AccountBytes),
+ "storages": hexutil.Uint64(stats.Storages),
+ "storageBytes": hexutil.Uint64(stats.StorageBytes),
+ "accountTrienodes": hexutil.Uint64(stats.AccountTrienodes),
+ "accountTrienodeBytes": hexutil.Uint64(stats.AccountTrienodeBytes),
+ "storageTrienodes": hexutil.Uint64(stats.StorageTrienodes),
+ "storageTrienodeBytes": hexutil.Uint64(stats.StorageTrienodeBytes),
+ "contractCodes": hexutil.Uint64(stats.ContractCodes),
+ "contractCodeBytes": hexutil.Uint64(stats.ContractCodeBytes),
+ }, nil
+}
diff --git a/eth/backend.go b/eth/backend.go
index 5bf1586029..0510e3e528 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -240,6 +240,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// - DATADIR/triedb/merkle.journal
// - DATADIR/triedb/verkle.journal
TrieJournalDirectory: stack.ResolvePath("triedb"),
+ StateSizeTracking: config.EnableStateSizeTracking,
}
)
if config.VMTrace != "" {
diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go
index 1507ce4d3f..f677ccd137 100644
--- a/eth/catalyst/api.go
+++ b/eth/catalyst/api.go
@@ -20,10 +20,12 @@ package catalyst
import (
"errors"
"fmt"
+ "reflect"
"strconv"
"sync"
"sync/atomic"
"time"
+ "unicode"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
@@ -80,47 +82,6 @@ const (
beaconUpdateWarnFrequency = 5 * time.Minute
)
-// All methods provided over the engine endpoint.
-var caps = []string{
- "engine_forkchoiceUpdatedV1",
- "engine_forkchoiceUpdatedV2",
- "engine_forkchoiceUpdatedV3",
- "engine_forkchoiceUpdatedV3P11",
- "engine_forkchoiceUpdatedWithWitnessV1",
- "engine_forkchoiceUpdatedWithWitnessV2",
- "engine_forkchoiceUpdatedWithWitnessV3",
- "engine_forkchoiceUpdatedWithWitnessV3P11",
- "engine_exchangeTransitionConfigurationV1",
- "engine_getPayloadV1",
- "engine_getPayloadV2",
- "engine_getPayloadV3",
- "engine_getPayloadV4",
- "engine_getPayloadV4P11",
- "engine_getPayloadV5",
- "engine_getBlobsV1",
- "engine_getBlobsV2",
- "engine_newPayloadV1",
- "engine_newPayloadV2",
- "engine_newPayloadV3",
- "engine_newPayloadV4",
- "engine_newPayloadV4P11",
- "engine_newPayloadWithWitnessV1",
- "engine_newPayloadWithWitnessV2",
- "engine_newPayloadWithWitnessV3",
- "engine_newPayloadWithWitnessV4",
- "engine_newPayloadWithWitnessV4P11",
- "engine_executeStatelessPayloadV1",
- "engine_executeStatelessPayloadV2",
- "engine_executeStatelessPayloadV3",
- "engine_executeStatelessPayloadV4",
- "engine_executeStatelessPayloadV4P11",
- "engine_getPayloadBodiesByHashV1",
- "engine_getPayloadBodiesByHashV2",
- "engine_getPayloadBodiesByRangeV1",
- "engine_getPayloadBodiesByRangeV2",
- "engine_getClientVersionV1",
-}
-
var (
// Number of blobs requested via getBlobsV2
getBlobsRequestedCounter = metrics.NewRegisteredCounter("engine/getblobs/requested", nil)
@@ -534,6 +495,26 @@ func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool) (*eng
}
// GetBlobsV1 returns a blob from the transaction pool.
+//
+// Specification:
+//
+// Given an array of blob versioned hashes client software MUST respond with an
+// array of BlobAndProofV1 objects with matching versioned hashes, respecting the
+// order of versioned hashes in the input array.
+//
+// Client software MUST place responses in the order given in the request, using
+// null for any missing blobs. For instance:
+//
+// if the request is [A_versioned_hash, B_versioned_hash, C_versioned_hash] and
+// client software has data for blobs A and C, but doesn't have data for B, the
+// response MUST be [A, null, C].
+//
+// Client software MUST support request sizes of at least 128 blob versioned hashes.
+// The client MUST return -38004: Too large request error if the number of requested
+// blobs is too large.
+//
+// Client software MAY return an array of all null entries if syncing or otherwise
+// unable to serve blob pool data.
func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProofV1, error) {
if len(hashes) > 128 {
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
@@ -544,6 +525,10 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
}
res := make([]*engine.BlobAndProofV1, len(hashes))
for i := 0; i < len(blobs); i++ {
+ // Skip the non-existing blob
+ if blobs[i] == nil {
+ continue
+ }
res[i] = &engine.BlobAndProofV1{
Blob: blobs[i][:],
Proof: proofs[i][0][:],
@@ -553,6 +538,33 @@ func (api *ConsensusAPI) GetBlobsV1(hashes []common.Hash) ([]*engine.BlobAndProo
}
// GetBlobsV2 returns a blob from the transaction pool.
+//
+// Specification:
+// Refer to the specification for engine_getBlobsV1 with changes of the following:
+//
+// Given an array of blob versioned hashes client software MUST respond with an
+// array of BlobAndProofV2 objects with matching versioned hashes, respecting
+// the order of versioned hashes in the input array.
+//
+// Client software MUST return null in case of any missing or older version blobs.
+// For instance,
+//
+// - if the request is [A_versioned_hash, B_versioned_hash, C_versioned_hash] and
+// client software has data for blobs A and C, but doesn't have data for B, the
+// response MUST be null.
+//
+// - if the request is [A_versioned_hash_for_blob_with_blob_proof], the response
+// MUST be null as well.
+//
+// Note, geth internally make the conversion from old version to new one, so the
+// data will be returned normally.
+//
+// Client software MUST support request sizes of at least 128 blob versioned
+// hashes. The client MUST return -38004: Too large request error if the number
+// of requested blobs is too large.
+//
+// Client software MUST return null if syncing or otherwise unable to serve
+// blob pool data.
func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProofV2, error) {
if len(hashes) > 128 {
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
@@ -566,12 +578,21 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
getBlobsV2RequestMiss.Inc(1)
return nil, nil
}
- getBlobsV2RequestHit.Inc(1)
blobs, _, proofs, err := api.eth.BlobTxPool().GetBlobs(hashes, types.BlobSidecarVersion1)
if err != nil {
return nil, engine.InvalidParams.With(err)
}
+
+ // To comply with API spec, check again that we really got all data needed
+ for _, blob := range blobs {
+ if blob == nil {
+ getBlobsV2RequestMiss.Inc(1)
+ return nil, nil
+ }
+ }
+ getBlobsV2RequestHit.Inc(1)
+
res := make([]*engine.BlobAndProofV2, len(hashes))
for i := 0; i < len(blobs); i++ {
var cellProofs []hexutil.Bytes
@@ -987,6 +1008,15 @@ func (api *ConsensusAPI) checkFork(timestamp uint64, forks ...forks.Fork) bool {
// ExchangeCapabilities returns the current methods provided by this node.
func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
+ valueT := reflect.TypeOf(api)
+ caps := make([]string, 0, valueT.NumMethod())
+ for i := 0; i < valueT.NumMethod(); i++ {
+ name := []rune(valueT.Method(i).Name)
+ if string(name) == "ExchangeCapabilities" {
+ continue
+ }
+ caps = append(caps, "engine_"+string(unicode.ToLower(name[0]))+string(name[1:]))
+ }
return caps
}
diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go
index d1fc66a94b..7e31b55cd6 100644
--- a/eth/catalyst/api_test.go
+++ b/eth/catalyst/api_test.go
@@ -19,7 +19,9 @@ package catalyst
import (
"bytes"
"context"
+ "crypto/ecdsa"
crand "crypto/rand"
+ "crypto/sha256"
"errors"
"fmt"
"math/big"
@@ -40,6 +42,7 @@ import (
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
+ "github.com/ethereum/go-ethereum/internal/testrand"
"github.com/ethereum/go-ethereum/internal/version"
"github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/node"
@@ -47,6 +50,7 @@ import (
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
+ "github.com/holiman/uint256"
)
var (
@@ -112,7 +116,7 @@ func TestEth2AssembleBlock(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
signer := types.NewEIP155Signer(ethservice.BlockChain().Config().ChainID)
tx, err := types.SignTx(types.NewTransaction(uint64(10), blocks[9].Coinbase(), big.NewInt(1000), params.TxGas, big.NewInt(params.InitialBaseFee), nil), signer, testKey)
if err != nil {
@@ -151,7 +155,7 @@ func TestEth2AssembleBlockWithAnotherBlocksTxs(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks[:9])
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// Put the 10th block's tx in the pool and produce a new block
txs := blocks[9].Transactions()
@@ -173,7 +177,7 @@ func TestEth2PrepareAndGetPayload(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks[:9])
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// Put the 10th block's tx in the pool and produce a new block
txs := blocks[9].Transactions()
@@ -238,8 +242,9 @@ func TestInvalidPayloadTimestamp(t *testing.T) {
genesis, preMergeBlocks := generateMergeChain(10, false)
n, ethservice := startEthService(t, genesis, preMergeBlocks)
defer n.Close()
+
var (
- api = NewConsensusAPI(ethservice)
+ api = newConsensusAPIWithoutHeartbeat(ethservice)
parent = ethservice.BlockChain().CurrentBlock()
)
tests := []struct {
@@ -281,7 +286,7 @@ func TestEth2NewBlock(t *testing.T) {
defer n.Close()
var (
- api = NewConsensusAPI(ethservice)
+ api = newConsensusAPIWithoutHeartbeat(ethservice)
parent = preMergeBlocks[len(preMergeBlocks)-1]
// This EVM code generates a log when the contract is created.
@@ -434,8 +439,14 @@ func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block)
t.Fatal("can't create node:", err)
}
- mcfg := miner.DefaultConfig
- ethcfg := ðconfig.Config{Genesis: genesis, SyncMode: ethconfig.FullSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256, Miner: mcfg}
+ ethcfg := ðconfig.Config{
+ Genesis: genesis,
+ SyncMode: ethconfig.FullSync,
+ TrieTimeout: time.Minute,
+ TrieDirtyCache: 256,
+ TrieCleanCache: 256,
+ Miner: miner.DefaultConfig,
+ }
ethservice, err := eth.New(n, ethcfg)
if err != nil {
t.Fatal("can't create eth service:", err)
@@ -459,6 +470,7 @@ func TestFullAPI(t *testing.T) {
genesis, preMergeBlocks := generateMergeChain(10, false)
n, ethservice := startEthService(t, genesis, preMergeBlocks)
defer n.Close()
+
var (
parent = ethservice.BlockChain().CurrentBlock()
// This EVM code generates a log when the contract is created.
@@ -476,7 +488,7 @@ func TestFullAPI(t *testing.T) {
}
func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal, beaconRoots []common.Hash, proposerPubkeys []common.Pubkey) []*types.Header {
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
var blocks []*types.Header
for i := 0; i < n; i++ {
callback(parent)
@@ -528,7 +540,7 @@ func TestExchangeTransitionConfig(t *testing.T) {
defer n.Close()
// invalid ttd
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
config := engine.TransitionConfigurationV1{
TerminalTotalDifficulty: (*hexutil.Big)(big.NewInt(0)),
TerminalBlockHash: common.Hash{},
@@ -589,7 +601,7 @@ func TestNewPayloadOnInvalidChain(t *testing.T) {
defer n.Close()
var (
- api = NewConsensusAPI(ethservice)
+ api = newConsensusAPIWithoutHeartbeat(ethservice)
parent = ethservice.BlockChain().CurrentBlock()
signer = types.LatestSigner(ethservice.BlockChain().Config())
// This EVM code generates a log when the contract is created.
@@ -693,7 +705,7 @@ func TestEmptyBlocks(t *testing.T) {
defer n.Close()
commonAncestor := ethservice.BlockChain().CurrentBlock()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// Setup 10 blocks on the canonical chain
setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil, nil)
@@ -820,8 +832,8 @@ func TestTrickRemoteBlockCache(t *testing.T) {
}
nodeA.Server().AddPeer(nodeB.Server().Self())
nodeB.Server().AddPeer(nodeA.Server().Self())
- apiA := NewConsensusAPI(ethserviceA)
- apiB := NewConsensusAPI(ethserviceB)
+ apiA := newConsensusAPIWithoutHeartbeat(ethserviceA)
+ apiB := newConsensusAPIWithoutHeartbeat(ethserviceB)
commonAncestor := ethserviceA.BlockChain().CurrentBlock()
@@ -878,7 +890,7 @@ func TestInvalidBloom(t *testing.T) {
defer n.Close()
commonAncestor := ethservice.BlockChain().CurrentBlock()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// Setup 10 blocks on the canonical chain
setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil, nil)
@@ -904,7 +916,7 @@ func TestSimultaneousNewBlock(t *testing.T) {
defer n.Close()
var (
- api = NewConsensusAPI(ethservice)
+ api = newConsensusAPIWithoutHeartbeat(ethservice)
parent = preMergeBlocks[len(preMergeBlocks)-1]
)
for i := 0; i < 10; i++ {
@@ -994,7 +1006,7 @@ func TestWithdrawals(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// 10: Build Shanghai block with no withdrawals.
parent := ethservice.BlockChain().CurrentHeader()
@@ -1111,7 +1123,7 @@ func TestNilWithdrawals(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
parent := ethservice.BlockChain().CurrentHeader()
aa := common.Address{0xaa}
@@ -1307,7 +1319,7 @@ func allBodies(blocks []*types.Block) []*types.Body {
func TestGetBlockBodiesByHash(t *testing.T) {
node, eth, blocks := setupBodies(t)
- api := NewConsensusAPI(eth)
+ api := newConsensusAPIWithoutHeartbeat(eth)
defer node.Close()
tests := []struct {
@@ -1363,7 +1375,7 @@ func TestGetBlockBodiesByHash(t *testing.T) {
func TestGetBlockBodiesByRange(t *testing.T) {
node, eth, blocks := setupBodies(t)
- api := NewConsensusAPI(eth)
+ api := newConsensusAPIWithoutHeartbeat(eth)
defer node.Close()
tests := []struct {
@@ -1444,7 +1456,7 @@ func TestGetBlockBodiesByRange(t *testing.T) {
func TestGetBlockBodiesByRangeInvalidParams(t *testing.T) {
node, eth, _ := setupBodies(t)
- api := NewConsensusAPI(eth)
+ api := newConsensusAPIWithoutHeartbeat(eth)
defer node.Close()
tests := []struct {
start hexutil.Uint64
@@ -1556,7 +1568,7 @@ func TestParentBeaconBlockRoot(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// 11: Build Shanghai block with no withdrawals.
parent := ethservice.BlockChain().CurrentHeader()
@@ -1639,7 +1651,7 @@ func TestWitnessCreationAndConsumption(t *testing.T) {
n, ethservice := startEthService(t, genesis, blocks[:9])
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
// Put the 10th block's tx in the pool and produce a new block
txs := blocks[9].Transactions()
@@ -1731,7 +1743,7 @@ func TestGetClientVersion(t *testing.T) {
n, ethservice := startEthService(t, genesis, preMergeBlocks)
defer n.Close()
- api := NewConsensusAPI(ethservice)
+ api := newConsensusAPIWithoutHeartbeat(ethservice)
info := engine.ClientVersionV1{
Code: "TT",
Name: "test",
@@ -1805,3 +1817,245 @@ func TestValidateRequests(t *testing.T) {
})
}
}
+
+var (
+ testBlobs []*kzg4844.Blob
+ testBlobCommits []kzg4844.Commitment
+ testBlobProofs []kzg4844.Proof
+ testBlobCellProofs [][]kzg4844.Proof
+ testBlobVHashes [][32]byte
+)
+
+func init() {
+ for i := 0; i < 6; i++ {
+ testBlob := &kzg4844.Blob{byte(i)}
+ testBlobs = append(testBlobs, testBlob)
+
+ testBlobCommit, _ := kzg4844.BlobToCommitment(testBlob)
+ testBlobCommits = append(testBlobCommits, testBlobCommit)
+
+ testBlobProof, _ := kzg4844.ComputeBlobProof(testBlob, testBlobCommit)
+ testBlobProofs = append(testBlobProofs, testBlobProof)
+
+ testBlobCellProof, _ := kzg4844.ComputeCellProofs(testBlob)
+ testBlobCellProofs = append(testBlobCellProofs, testBlobCellProof)
+
+ testBlobVHash := kzg4844.CalcBlobHashV1(sha256.New(), &testBlobCommit)
+ testBlobVHashes = append(testBlobVHashes, testBlobVHash)
+ }
+}
+
+// makeMultiBlobTx is a utility method to construct a random blob tx with
+// certain number of blobs in its sidecar.
+func makeMultiBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction {
+ var (
+ blobs []kzg4844.Blob
+ blobHashes []common.Hash
+ commitments []kzg4844.Commitment
+ proofs []kzg4844.Proof
+ )
+ for i := 0; i < blobCount; i++ {
+ blobs = append(blobs, *testBlobs[blobOffset+i])
+ commitments = append(commitments, testBlobCommits[blobOffset+i])
+ if version == types.BlobSidecarVersion0 {
+ proofs = append(proofs, testBlobProofs[blobOffset+i])
+ } else {
+ cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i])
+ proofs = append(proofs, cellProofs...)
+ }
+ blobHashes = append(blobHashes, testBlobVHashes[blobOffset+i])
+ }
+ blobtx := &types.BlobTx{
+ ChainID: uint256.MustFromBig(chainConfig.ChainID),
+ Nonce: nonce,
+ GasTipCap: uint256.NewInt(1),
+ GasFeeCap: uint256.NewInt(1000),
+ Gas: 21000,
+ BlobFeeCap: uint256.NewInt(1000),
+ BlobHashes: blobHashes,
+ Value: uint256.NewInt(100),
+ Sidecar: types.NewBlobTxSidecar(version, blobs, commitments, proofs),
+ }
+ return types.MustSignNewTx(key, types.LatestSigner(chainConfig), blobtx)
+}
+
+func newGetBlobEnv(t *testing.T, version byte) (*node.Node, *ConsensusAPI) {
+ var (
+ // Create a database pre-initialize with a genesis block
+ config = *params.MergedTestChainConfig
+
+ key1, _ = crypto.GenerateKey()
+ key2, _ = crypto.GenerateKey()
+ key3, _ = crypto.GenerateKey()
+
+ addr1 = crypto.PubkeyToAddress(key1.PublicKey)
+ addr2 = crypto.PubkeyToAddress(key2.PublicKey)
+ addr3 = crypto.PubkeyToAddress(key3.PublicKey)
+ )
+ // Disable Osaka fork for GetBlobsV1
+ if version == 0 {
+ config.OsakaTime = nil
+ }
+ gspec := &core.Genesis{
+ Config: &config,
+ Alloc: types.GenesisAlloc{
+ testAddr: {Balance: testBalance},
+ addr1: {Balance: testBalance},
+ addr2: {Balance: testBalance},
+ addr3: {Balance: testBalance},
+ },
+ Difficulty: common.Big0,
+ }
+ n, ethServ := startEthService(t, gspec, nil)
+
+ // fill blob txs into the pool
+ tx1 := makeMultiBlobTx(&config, 0, 2, 0, key1, version) // blob[0, 2)
+ tx2 := makeMultiBlobTx(&config, 0, 2, 2, key2, version) // blob[2, 4)
+ tx3 := makeMultiBlobTx(&config, 0, 2, 4, key3, version) // blob[4, 6)
+ ethServ.TxPool().Add([]*types.Transaction{tx1, tx2, tx3}, true)
+
+ api := newConsensusAPIWithoutHeartbeat(ethServ)
+ return n, api
+}
+
+func TestGetBlobsV1(t *testing.T) {
+ n, api := newGetBlobEnv(t, 0)
+ defer n.Close()
+
+ suites := []struct {
+ start int
+ limit int
+ fillRandom bool
+ }{
+ {
+ start: 0, limit: 1,
+ },
+ {
+ start: 0, limit: 1, fillRandom: true,
+ },
+ {
+ start: 0, limit: 2,
+ },
+ {
+ start: 0, limit: 2, fillRandom: true,
+ },
+ {
+ start: 1, limit: 3,
+ },
+ {
+ start: 1, limit: 3, fillRandom: true,
+ },
+ {
+ start: 0, limit: 6,
+ },
+ {
+ start: 0, limit: 6, fillRandom: true,
+ },
+ {
+ start: 1, limit: 5,
+ },
+ {
+ start: 1, limit: 5, fillRandom: true,
+ },
+ }
+ for i, suite := range suites {
+ // Fill the request for retrieving blobs
+ var (
+ vhashes []common.Hash
+ expect []*engine.BlobAndProofV1
+ )
+ // fill missing blob at the beginning
+ if suite.fillRandom {
+ vhashes = append(vhashes, testrand.Hash())
+ expect = append(expect, nil)
+ }
+ for j := suite.start; j < suite.limit; j++ {
+ vhashes = append(vhashes, testBlobVHashes[j])
+ expect = append(expect, &engine.BlobAndProofV1{
+ Blob: testBlobs[j][:],
+ Proof: testBlobProofs[j][:],
+ })
+
+ // fill missing blobs in the middle
+ if suite.fillRandom && rand.Intn(2) == 0 {
+ vhashes = append(vhashes, testrand.Hash())
+ expect = append(expect, nil)
+ }
+ }
+ // fill missing blobs at the end
+ if suite.fillRandom {
+ vhashes = append(vhashes, testrand.Hash())
+ expect = append(expect, nil)
+ }
+ result, err := api.GetBlobsV1(vhashes)
+ if err != nil {
+ t.Errorf("Unexpected error for case %d, %v", i, err)
+ }
+ if !reflect.DeepEqual(result, expect) {
+ t.Fatalf("Unexpected result for case %d", i)
+ }
+ }
+}
+
+func TestGetBlobsV2(t *testing.T) {
+ n, api := newGetBlobEnv(t, 1)
+ defer n.Close()
+
+ suites := []struct {
+ start int
+ limit int
+ fillRandom bool
+ }{
+ {
+ start: 0, limit: 1,
+ },
+ {
+ start: 0, limit: 2,
+ },
+ {
+ start: 1, limit: 3,
+ },
+ {
+ start: 0, limit: 6,
+ },
+ {
+ start: 1, limit: 5,
+ },
+ {
+ start: 0, limit: 6, fillRandom: true,
+ },
+ }
+ for i, suite := range suites {
+ // Fill the request for retrieving blobs
+ var (
+ vhashes []common.Hash
+ expect []*engine.BlobAndProofV2
+ )
+ // fill missing blob
+ if suite.fillRandom {
+ vhashes = append(vhashes, testrand.Hash())
+ }
+ for j := suite.start; j < suite.limit; j++ {
+ vhashes = append(vhashes, testBlobVHashes[j])
+ var cellProofs []hexutil.Bytes
+ for _, proof := range testBlobCellProofs[j] {
+ cellProofs = append(cellProofs, proof[:])
+ }
+ expect = append(expect, &engine.BlobAndProofV2{
+ Blob: testBlobs[j][:],
+ CellProofs: cellProofs,
+ })
+ }
+ result, err := api.GetBlobsV2(vhashes)
+ if err != nil {
+ t.Errorf("Unexpected error for case %d, %v", i, err)
+ }
+ // null is responded if any blob is missing
+ if suite.fillRandom {
+ expect = nil
+ }
+ if !reflect.DeepEqual(result, expect) {
+ t.Fatalf("Unexpected result for case %d", i)
+ }
+ }
+}
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index 0579286861..cf7cfb7c94 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -144,6 +144,9 @@ type Config struct {
// Enables tracking of SHA3 preimages in the VM
EnablePreimageRecording bool
+ // Enables tracking of state size
+ EnableStateSizeTracking bool
+
// Enables VM tracing
VMTrace string
VMTraceJsonConfig string
diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go
index 0a188ba23c..2fdd219dee 100644
--- a/eth/ethconfig/gen_config.go
+++ b/eth/ethconfig/gen_config.go
@@ -49,6 +49,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
BlobPool blobpool.Config
GPO gasprice.Config
EnablePreimageRecording bool
+ EnableStateSizeTracking bool
VMTrace string
VMTraceJsonConfig string
RPCGasCap uint64
@@ -90,6 +91,7 @@ func (c Config) MarshalTOML() (interface{}, error) {
enc.BlobPool = c.BlobPool
enc.GPO = c.GPO
enc.EnablePreimageRecording = c.EnablePreimageRecording
+ enc.EnableStateSizeTracking = c.EnableStateSizeTracking
enc.VMTrace = c.VMTrace
enc.VMTraceJsonConfig = c.VMTraceJsonConfig
enc.RPCGasCap = c.RPCGasCap
@@ -135,6 +137,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
BlobPool *blobpool.Config
GPO *gasprice.Config
EnablePreimageRecording *bool
+ EnableStateSizeTracking *bool
VMTrace *string
VMTraceJsonConfig *string
RPCGasCap *uint64
@@ -243,6 +246,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error {
if dec.EnablePreimageRecording != nil {
c.EnablePreimageRecording = *dec.EnablePreimageRecording
}
+ if dec.EnableStateSizeTracking != nil {
+ c.EnableStateSizeTracking = *dec.EnableStateSizeTracking
+ }
if dec.VMTrace != nil {
c.VMTrace = *dec.VMTrace
}
diff --git a/eth/handler.go b/eth/handler.go
index aaea00e037..304560a158 100644
--- a/eth/handler.go
+++ b/eth/handler.go
@@ -17,21 +17,22 @@
package eth
import (
+ "cmp"
+ crand "crypto/rand"
"errors"
"maps"
"math"
- "math/big"
"slices"
"sync"
"sync/atomic"
"time"
+ "github.com/dchest/siphash"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
- "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/fetcher"
@@ -119,9 +120,10 @@ type handler struct {
chain *core.BlockChain
maxPeers int
- downloader *downloader.Downloader
- txFetcher *fetcher.TxFetcher
- peers *peerSet
+ downloader *downloader.Downloader
+ txFetcher *fetcher.TxFetcher
+ peers *peerSet
+ txBroadcastKey [16]byte
eventMux *event.TypeMux
txsCh chan core.NewTxsEvent
@@ -153,6 +155,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
txpool: config.TxPool,
chain: config.Chain,
peers: newPeerSet(),
+ txBroadcastKey: newBroadcastChoiceKey(),
requiredBlocks: config.RequiredBlocks,
quitSync: make(chan struct{}),
handlerDoneCh: make(chan struct{}),
@@ -480,58 +483,40 @@ func (h *handler) BroadcastTransactions(txs types.Transactions) {
txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly
annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce
- )
- // Broadcast transactions to a batch of peers not knowing about it
- direct := big.NewInt(int64(math.Sqrt(float64(h.peers.len())))) // Approximate number of peers to broadcast to
- if direct.BitLen() == 0 {
- direct = big.NewInt(1)
- }
- total := new(big.Int).Exp(direct, big.NewInt(2), nil) // Stabilise total peer count a bit based on sqrt peers
- var (
- signer = types.LatestSigner(h.chain.Config()) // Don't care about chain status, we just need *a* sender
- hasher = crypto.NewKeccakState()
- hash = make([]byte, 32)
+ signer = types.LatestSigner(h.chain.Config())
+ choice = newBroadcastChoice(h.nodeID, h.txBroadcastKey)
+ peers = h.peers.all()
)
+
for _, tx := range txs {
- var maybeDirect bool
+ var directSet map[*ethPeer]struct{}
switch {
case tx.Type() == types.BlobTxType:
blobTxs++
case tx.Size() > txMaxBroadcastSize:
largeTxs++
default:
- maybeDirect = true
+ // Get transaction sender address. Here we can ignore any error
+ // since we're just interested in any value.
+ txSender, _ := types.Sender(signer, tx)
+ directSet = choice.choosePeers(peers, txSender)
}
- // Send the transaction (if it's small enough) directly to a subset of
- // the peers that have not received it yet, ensuring that the flow of
- // transactions is grouped by account to (try and) avoid nonce gaps.
- //
- // To do this, we hash the local enode IW with together with a peer's
- // enode ID together with the transaction sender and broadcast if
- // `sha(self, peer, sender) mod peers < sqrt(peers)`.
- for _, peer := range h.peers.peersWithoutTransaction(tx.Hash()) {
- var broadcast bool
- if maybeDirect {
- hasher.Reset()
- hasher.Write(h.nodeID.Bytes())
- hasher.Write(peer.Node().ID().Bytes())
- from, _ := types.Sender(signer, tx) // Ignore error, we only use the addr as a propagation target splitter
- hasher.Write(from.Bytes())
-
- hasher.Read(hash)
- if new(big.Int).Mod(new(big.Int).SetBytes(hash), total).Cmp(direct) < 0 {
- broadcast = true
- }
+ for _, peer := range peers {
+ if peer.KnownTransaction(tx.Hash()) {
+ continue
}
- if broadcast {
+ if _, ok := directSet[peer]; ok {
+ // Send direct.
txset[peer] = append(txset[peer], tx.Hash())
} else {
+ // Send announcement.
annos[peer] = append(annos[peer], tx.Hash())
}
}
}
+
for peer, hashes := range txset {
directCount += len(hashes)
peer.AsyncSendTransactions(hashes)
@@ -594,7 +579,7 @@ func newBlockRangeState(chain *core.BlockChain, typeMux *event.TypeMux) *blockRa
return st
}
-// blockRangeBroadcastLoop announces changes in locally-available block range to peers.
+// blockRangeLoop announces changes in locally-available block range to peers.
// The range to announce is the range that is available in the store, so it's not just
// about imported blocks.
func (h *handler) blockRangeLoop(st *blockRangeState) {
@@ -696,3 +681,62 @@ func (st *blockRangeState) stop() {
func (st *blockRangeState) currentRange() eth.BlockRangeUpdatePacket {
return *st.next.Load()
}
+
+// broadcastChoice implements a deterministic random choice of peers. This is designed
+// specifically for choosing which peer receives a direct broadcast of a transaction.
+//
+// The choice is made based on the involved p2p node IDs and the transaction sender,
+// ensuring that the flow of transactions is grouped by account to (try and) avoid nonce
+// gaps.
+type broadcastChoice struct {
+ self enode.ID
+ key [16]byte
+ buffer map[*ethPeer]struct{}
+ tmp []broadcastPeer
+}
+
+type broadcastPeer struct {
+ p *ethPeer
+ score uint64
+}
+
+func newBroadcastChoiceKey() (k [16]byte) {
+ crand.Read(k[:])
+ return k
+}
+
+func newBroadcastChoice(self enode.ID, key [16]byte) *broadcastChoice {
+ return &broadcastChoice{
+ self: self,
+ key: key,
+ buffer: make(map[*ethPeer]struct{}),
+ }
+}
+
+// choosePeers selects the peers that will receive a direct transaction broadcast message.
+// Note the return value will only stay valid until the next call to choosePeers.
+func (bc *broadcastChoice) choosePeers(peers []*ethPeer, txSender common.Address) map[*ethPeer]struct{} {
+ // Compute randomized scores.
+ bc.tmp = slices.Grow(bc.tmp[:0], len(peers))[:len(peers)]
+ hash := siphash.New(bc.key[:])
+ for i, peer := range peers {
+ hash.Reset()
+ hash.Write(bc.self[:])
+ hash.Write(peer.Peer.Peer.ID().Bytes())
+ hash.Write(txSender[:])
+ bc.tmp[i] = broadcastPeer{peer, hash.Sum64()}
+ }
+
+ // Sort by score.
+ slices.SortFunc(bc.tmp, func(a, b broadcastPeer) int {
+ return cmp.Compare(a.score, b.score)
+ })
+
+ // Take top n.
+ clear(bc.buffer)
+ n := int(math.Ceil(math.Sqrt(float64(len(bc.tmp)))))
+ for i := range n {
+ bc.buffer[bc.tmp[i].p] = struct{}{}
+ }
+ return bc.buffer
+}
diff --git a/eth/handler_test.go b/eth/handler_test.go
index d0da098430..b37e6227f4 100644
--- a/eth/handler_test.go
+++ b/eth/handler_test.go
@@ -17,9 +17,12 @@
package eth
import (
+ "maps"
"math/big"
+ "math/rand"
"sort"
"sync"
+ "testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
@@ -29,8 +32,11 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/ethconfig"
+ "github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
+ "github.com/ethereum/go-ethereum/p2p"
+ "github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
@@ -212,3 +218,102 @@ func (b *testHandler) close() {
b.handler.Stop()
b.chain.Stop()
}
+
+func TestBroadcastChoice(t *testing.T) {
+ self := enode.HexID("1111111111111111111111111111111111111111111111111111111111111111")
+ choice49 := newBroadcastChoice(self, [16]byte{1})
+ choice50 := newBroadcastChoice(self, [16]byte{1})
+
+ // Create test peers and random tx sender addresses.
+ rand := rand.New(rand.NewSource(33))
+ txsenders := make([]common.Address, 400)
+ for i := range txsenders {
+ rand.Read(txsenders[i][:])
+ }
+ peers := createTestPeers(rand, 50)
+ defer closePeers(peers)
+
+ // Evaluate choice49 first.
+ expectedCount := 7 // sqrt(49)
+ var chosen49 = make([]map[*ethPeer]struct{}, len(txsenders))
+ for i, txSender := range txsenders {
+ set := choice49.choosePeers(peers[:49], txSender)
+ chosen49[i] = maps.Clone(set)
+
+ // Sanity check choices. Here we check that the function selects different peers
+ // for different transaction senders.
+ if len(set) != expectedCount {
+ t.Fatalf("choice49 produced wrong count %d, want %d", len(set), expectedCount)
+ }
+ if i > 0 && maps.Equal(set, chosen49[i-1]) {
+ t.Errorf("choice49 for tx %d is equal to tx %d", i, i-1)
+ }
+ }
+
+ // Evaluate choice50 for the same peers and transactions. It should always yield more
+ // peers than choice49, and the chosen set should be a superset of choice49's.
+ for i, txSender := range txsenders {
+ set := choice50.choosePeers(peers[:50], txSender)
+ if len(set) < len(chosen49[i]) {
+ t.Errorf("for tx %d, choice50 has less peers than choice49", i)
+ }
+ for p := range chosen49[i] {
+ if _, ok := set[p]; !ok {
+ t.Errorf("for tx %d, choice50 did not choose peer %v, but choice49 did", i, p.ID())
+ }
+ }
+ }
+}
+
+func BenchmarkBroadcastChoice(b *testing.B) {
+ b.Run("50", func(b *testing.B) {
+ benchmarkBroadcastChoice(b, 50)
+ })
+ b.Run("200", func(b *testing.B) {
+ benchmarkBroadcastChoice(b, 200)
+ })
+ b.Run("500", func(b *testing.B) {
+ benchmarkBroadcastChoice(b, 500)
+ })
+}
+
+// This measures the overhead of sending one transaction to N peers.
+func benchmarkBroadcastChoice(b *testing.B, npeers int) {
+ rand := rand.New(rand.NewSource(33))
+ peers := createTestPeers(rand, npeers)
+ defer closePeers(peers)
+
+ txsenders := make([]common.Address, b.N)
+ for i := range txsenders {
+ rand.Read(txsenders[i][:])
+ }
+
+ self := enode.HexID("1111111111111111111111111111111111111111111111111111111111111111")
+ choice := newBroadcastChoice(self, [16]byte{1})
+
+ b.ResetTimer()
+ for i := range b.N {
+ set := choice.choosePeers(peers, txsenders[i])
+ if len(set) == 0 {
+ b.Fatal("empty result")
+ }
+ }
+}
+
+func createTestPeers(rand *rand.Rand, n int) []*ethPeer {
+ peers := make([]*ethPeer, n)
+ for i := range peers {
+ var id enode.ID
+ rand.Read(id[:])
+ p2pPeer := p2p.NewPeer(id, "test", nil)
+ ep := eth.NewPeer(eth.ETH69, p2pPeer, nil, nil)
+ peers[i] = ðPeer{Peer: ep}
+ }
+ return peers
+}
+
+func closePeers(peers []*ethPeer) {
+ for _, p := range peers {
+ p.Close()
+ }
+}
diff --git a/eth/peerset.go b/eth/peerset.go
index 6b0aff226c..e6f623f90c 100644
--- a/eth/peerset.go
+++ b/eth/peerset.go
@@ -19,9 +19,10 @@ package eth
import (
"errors"
"fmt"
+ "maps"
+ "slices"
"sync"
- "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/eth/protocols/snap"
"github.com/ethereum/go-ethereum/p2p"
@@ -191,19 +192,12 @@ func (ps *peerSet) peer(id string) *ethPeer {
return ps.peers[id]
}
-// peersWithoutTransaction retrieves a list of peers that do not have a given
-// transaction in their set of known hashes.
-func (ps *peerSet) peersWithoutTransaction(hash common.Hash) []*ethPeer {
+// all returns all current peers.
+func (ps *peerSet) all() []*ethPeer {
ps.lock.RLock()
defer ps.lock.RUnlock()
- list := make([]*ethPeer, 0, len(ps.peers))
- for _, p := range ps.peers {
- if !p.KnownTransaction(hash) {
- list = append(list, p)
- }
- }
- return list
+ return slices.Collect(maps.Values(ps.peers))
}
// len returns if the current number of `eth` peers in the set. Since the `snap`
diff --git a/eth/sync_test.go b/eth/sync_test.go
index cad3a4732e..dc295f2790 100644
--- a/eth/sync_test.go
+++ b/eth/sync_test.go
@@ -88,9 +88,17 @@ func testSnapSyncDisabling(t *testing.T, ethVer uint, snapVer uint) {
if err := empty.handler.downloader.BeaconSync(ethconfig.SnapSync, full.chain.CurrentBlock(), nil); err != nil {
t.Fatal("sync failed:", err)
}
- time.Sleep(time.Second * 5) // Downloader internally has to wait a timer (3s) to be expired before exiting
-
- if empty.handler.snapSync.Load() {
- t.Fatalf("snap sync not disabled after successful synchronisation")
+ // Downloader internally has to wait for a timer (3s) to be expired before
+ // exiting. Poll after to determine if sync is disabled.
+ time.Sleep(time.Second * 3)
+ for timeout := time.After(time.Second); ; {
+ select {
+ case <-timeout:
+ t.Fatalf("snap sync not disabled after successful synchronisation")
+ case <-time.After(100 * time.Millisecond):
+ if !empty.handler.snapSync.Load() {
+ return
+ }
+ }
}
}
diff --git a/eth/syncer/syncer.go b/eth/syncer/syncer.go
index 6e3918ce88..6b33ec54ba 100644
--- a/eth/syncer/syncer.go
+++ b/eth/syncer/syncer.go
@@ -89,6 +89,7 @@ func (s *Syncer) run() {
target *types.Header
ticker = time.NewTicker(time.Second * 5)
)
+ defer ticker.Stop()
for {
select {
case req := <-s.request:
diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go
index 7ed2a5936e..39c39ff05d 100644
--- a/eth/tracers/api_test.go
+++ b/eth/tracers/api_test.go
@@ -527,7 +527,7 @@ func TestTraceTransaction(t *testing.T) {
b.AddTx(tx)
target = tx.Hash()
})
- defer backend.chain.Stop()
+ defer backend.teardown()
api := NewAPI(backend)
result, err := api.TraceTransaction(context.Background(), target, nil)
if err != nil {
@@ -584,7 +584,7 @@ func TestTraceBlock(t *testing.T) {
b.AddTx(tx)
txHash = tx.Hash()
})
- defer backend.chain.Stop()
+ defer backend.teardown()
api := NewAPI(backend)
var testSuite = []struct {
@@ -681,7 +681,7 @@ func TestTracingWithOverrides(t *testing.T) {
signer, accounts[0].key)
b.AddTx(tx)
})
- defer backend.chain.Stop()
+ defer backend.teardown()
api := NewAPI(backend)
randomAccounts := newAccounts(3)
type res struct {
@@ -1105,6 +1105,7 @@ func TestTraceChain(t *testing.T) {
nonce += 1
}
})
+ defer backend.teardown()
backend.refHook = func() { ref.Add(1) }
backend.relHook = func() { rel.Add(1) }
api := NewAPI(backend)
@@ -1212,7 +1213,7 @@ func TestTraceBlockWithBasefee(t *testing.T) {
txHash = tx.Hash()
baseFee.Set(b.BaseFee())
})
- defer backend.chain.Stop()
+ defer backend.teardown()
api := NewAPI(backend)
var testSuite = []struct {
@@ -1298,7 +1299,7 @@ func TestStandardTraceBlockToFile(t *testing.T) {
b.AddTx(tx)
txHashs = append(txHashs, tx.Hash())
})
- defer backend.chain.Stop()
+ defer backend.teardown()
var testSuite = []struct {
blockNumber rpc.BlockNumber
diff --git a/eth/tracers/internal/tracetest/prestate_test.go b/eth/tracers/internal/tracetest/prestate_test.go
index 29c2834ba2..456d962c69 100644
--- a/eth/tracers/internal/tracetest/prestate_test.go
+++ b/eth/tracers/internal/tracetest/prestate_test.go
@@ -109,6 +109,9 @@ func testPrestateTracer(tracerName string, dirPath string, t *testing.T) {
if err != nil {
t.Fatalf("failed to execute transaction: %v", err)
}
+ if vmRet.Failed() {
+ t.Logf("(warn) transaction failed: %v", vmRet.Err)
+ }
tracer.OnTxEnd(&types.Receipt{GasUsed: vmRet.UsedGas}, nil)
// Retrieve the trace result and compare against the expected
res, err := tracer.GetResult()
diff --git a/eth/tracers/internal/tracetest/supply_test.go b/eth/tracers/internal/tracetest/supply_test.go
index 8aedc9d564..2b5a8212aa 100644
--- a/eth/tracers/internal/tracetest/supply_test.go
+++ b/eth/tracers/internal/tracetest/supply_test.go
@@ -77,7 +77,7 @@ func TestSupplyOmittedFields(t *testing.T) {
out, _, err := testSupplyTracer(t, gspec, func(b *core.BlockGen) {
b.SetPoS()
- })
+ }, 1)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -120,7 +120,7 @@ func TestSupplyGenesisAlloc(t *testing.T) {
ParentHash: common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"),
}
- out, _, err := testSupplyTracer(t, gspec, emptyBlockGenerationFunc)
+ out, _, err := testSupplyTracer(t, gspec, emptyBlockGenerationFunc, 1)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -148,7 +148,55 @@ func TestSupplyRewards(t *testing.T) {
ParentHash: common.HexToHash("0xadeda0a83e337b6c073e3f0e9a17531a04009b397a9588c093b628f21b8bc5a3"),
}
- out, _, err := testSupplyTracer(t, gspec, emptyBlockGenerationFunc)
+ out, _, err := testSupplyTracer(t, gspec, emptyBlockGenerationFunc, 1)
+ if err != nil {
+ t.Fatalf("failed to test supply tracer: %v", err)
+ }
+
+ actual := out[expected.Number]
+
+ compareAsJSON(t, expected, actual)
+}
+
+func TestSupplyRewardsWithUncle(t *testing.T) {
+ var (
+ config = *params.AllEthashProtocolChanges
+
+ gspec = &core.Genesis{
+ Config: &config,
+ }
+ )
+
+ // Base reward for the miner
+ baseReward := ethash.ConstantinopleBlockReward.ToBig()
+ // Miner reward for uncle inclusion is 1/32 of the base reward
+ uncleInclusionReward := new(big.Int).Rsh(baseReward, 5)
+ // Uncle miner reward for an uncle that is 1 block behind is 7/8 of the base reward
+ uncleReward := big.NewInt(7)
+ uncleReward.Mul(uncleReward, baseReward).Rsh(uncleReward, 3)
+
+ totalReward := baseReward.Add(baseReward, uncleInclusionReward).Add(baseReward, uncleReward)
+
+ expected := supplyInfo{
+ Issuance: &supplyInfoIssuance{
+ Reward: (*hexutil.Big)(totalReward),
+ },
+ Number: 3,
+ Hash: common.HexToHash("0x0737d31f8671c18d32b5143833cfa600e4264df62324c9de569668c6de9eed6d"),
+ ParentHash: common.HexToHash("0x45af6557df87719cb3c7e6f8a98b61508ea74a797733191aececb4c2ec802447"),
+ }
+
+ // Generate a new chain where block 3 includes an uncle
+ uncleGenerationFunc := func(b *core.BlockGen) {
+ if b.Number().Uint64() == 3 {
+ prevBlock := b.PrevBlock(1) // Block 2
+ uncle := types.CopyHeader(prevBlock.Header())
+ uncle.Extra = []byte("uncle!")
+ b.AddUncle(uncle)
+ }
+ }
+
+ out, _, err := testSupplyTracer(t, gspec, uncleGenerationFunc, 3)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -195,7 +243,7 @@ func TestSupplyEip1559Burn(t *testing.T) {
b.AddTx(tx)
}
- out, chain, err := testSupplyTracer(t, gspec, eip1559BlockGenerationFunc)
+ out, chain, err := testSupplyTracer(t, gspec, eip1559BlockGenerationFunc, 1)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -238,7 +286,7 @@ func TestSupplyWithdrawals(t *testing.T) {
})
}
- out, chain, err := testSupplyTracer(t, gspec, withdrawalsBlockGenerationFunc)
+ out, chain, err := testSupplyTracer(t, gspec, withdrawalsBlockGenerationFunc, 1)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -318,7 +366,7 @@ func TestSupplySelfdestruct(t *testing.T) {
}
// 1. Test pre Cancun
- preCancunOutput, preCancunChain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc)
+ preCancunOutput, preCancunChain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc, 1)
if err != nil {
t.Fatalf("Pre-cancun failed to test supply tracer: %v", err)
}
@@ -360,7 +408,7 @@ func TestSupplySelfdestruct(t *testing.T) {
gspec.Config.CancunTime = &cancunTime
gspec.Config.BlobScheduleConfig = params.DefaultBlobSchedule
- postCancunOutput, postCancunChain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc)
+ postCancunOutput, postCancunChain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc, 1)
if err != nil {
t.Fatalf("Post-cancun failed to test supply tracer: %v", err)
}
@@ -500,7 +548,7 @@ func TestSupplySelfdestructItselfAndRevert(t *testing.T) {
b.AddTx(tx)
}
- output, chain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc)
+ output, chain, err := testSupplyTracer(t, gspec, testBlockGenerationFunc, 1)
if err != nil {
t.Fatalf("failed to test supply tracer: %v", err)
}
@@ -542,7 +590,7 @@ func TestSupplySelfdestructItselfAndRevert(t *testing.T) {
compareAsJSON(t, expected, actual)
}
-func testSupplyTracer(t *testing.T, genesis *core.Genesis, gen func(*core.BlockGen)) ([]supplyInfo, *core.BlockChain, error) {
+func testSupplyTracer(t *testing.T, genesis *core.Genesis, gen func(b *core.BlockGen), numBlocks int) ([]supplyInfo, *core.BlockChain, error) {
engine := beacon.New(ethash.NewFaker())
traceOutputPath := filepath.ToSlash(t.TempDir())
@@ -562,7 +610,7 @@ func testSupplyTracer(t *testing.T, genesis *core.Genesis, gen func(*core.BlockG
}
defer chain.Stop()
- _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 1, func(i int, b *core.BlockGen) {
+ _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, numBlocks, func(i int, b *core.BlockGen) {
b.SetCoinbase(common.Address{1})
gen(b)
})
diff --git a/eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json b/eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json
index ca7e1f35c7..a86c289c3f 100644
--- a/eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json
+++ b/eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json
@@ -67,26 +67,23 @@
},
"config": {
"chainId": 1,
- "homesteadBlock": 1150000,
- "daoForkBlock": 1920000,
- "daoForkSupport": true,
- "eip150Block": 2463000,
- "eip155Block": 2675000,
- "eip158Block": 2675000,
- "byzantiumBlock": 4370000,
- "constantinopleBlock": 7280000,
- "petersburgBlock": 7280000,
- "istanbulBlock": 9069000,
- "muirGlacierBlock": 9200000,
- "berlinBlock": 12244000,
- "londonBlock": 12965000,
- "arrowGlacierBlock": 13773000,
- "grayGlacierBlock": 15050000,
- "shanghaiTime": 1681338455,
- "cancunTime": 1710338135,
- "pragueTime": 1746612311,
- "terminalTotalDifficulty": 58750000000000000000000,
- "depositContractAddress": "0x00000000219ab540356cbb839cbe05303d7705fa",
+ "homesteadBlock": 0,
+ "eip150Block": 0,
+ "eip155Block": 0,
+ "eip158Block": 0,
+ "byzantiumBlock": 0,
+ "constantinopleBlock": 0,
+ "petersburgBlock": 0,
+ "istanbulBlock": 0,
+ "muirGlacierBlock": 0,
+ "berlinBlock": 0,
+ "londonBlock": 0,
+ "arrowGlacierBlock": 0,
+ "grayGlacierBlock": 0,
+ "shanghaiTime": 0,
+ "cancunTime": 0,
+ "pragueTime": 0,
+ "terminalTotalDifficulty": 0,
"ethash": {},
"blobSchedule": {
"cancun": {
@@ -113,26 +110,58 @@
"input": "0x04f8ec0182075f830f424084714d24d7830493e09417816e9a858b161c3e37016d139cf618056cacd480a000000000000000000000000000000000000000000000000316580c3ab7e66cc4c0f85ef85c0194b684710e6d5914ad6e64493de2a3c424cc43e970823dc101a02f15ba55009fcd3682cd0f9c9645dd94e616f9a969ba3f1a5a2d871f9fe0f2b4a053c332a83312d0b17dd4c16eeb15b1ff5223398b14e0a55c70762e8f3972b7a580a02aceec9737d2a211c79aff3dbd4bf44a5cdabbdd6bbe19ff346a89d94d61914aa062e92842bfe7d2f3ff785c594c70fafafcb180fb32a774de1b92c588be8cd87b",
"result": {
"0x17816e9a858b161c3e37016d139cf618056cacd4": {
- "balance": "0x0",
- "code": "0xef0100b684710e6d5914ad6e64493de2a3c424cc43e970",
- "codeHash":"0xca4cab497827c53a640924e1f7ebb69c3280f8ce8cef2d1d2f9a3707def2a856",
- "nonce": 15809
+ "balance": "0x0",
+ "code": "0xef0100b684710e6d5914ad6e64493de2a3c424cc43e970",
+ "codeHash":"0xca4cab497827c53a640924e1f7ebb69c3280f8ce8cef2d1d2f9a3707def2a856",
+ "nonce": 15809
+ },
+ "0x236501327e701692a281934230af0b6be8df3353": {
+ "balance": "0x0",
+ "code": "0x6080604052600a600c565b005b60186014601a565b605e565b565b600060597f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5473ffffffffffffffffffffffffffffffffffffffff1690565b905090565b3660008037600080366000845af43d6000803e808015607c573d6000f35b3d6000fdfea26469706673582212200b737106e31d6abde738d261a4c4f12fcdfac5141ebc6ab5ffe4cf6e1630aaed64736f6c63430008140033",
+ "codeHash": "0x297bbcbd2b9ae035f750536c62603b5b7240c69b04fe22eec21cf6fcbb61179f",
+ "nonce": 1,
+ "storage": {
+ "0x078d9cc432fb3eab476f678ef9a73d8ca570f23897c68eb99b2721ebf46e5a9e": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc": "0x000000000000000000000000bdb50eff425fb2b1b67fea21b8420eeb6d99ccc0",
+ "0x5555c0547520ec9521cc3134a71677625cdeb6accbb330321dcaf2cbc22c1fe9": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0x84fdd52031be5dc8bcfa0ffd090a0bf85ef922e1fa9d026be0cf5716edafb4db": "0x0000000000000000000000000000000000000000007b74591c97f086c1057bee",
+ "0x8c854b3845c254f768d5435bc89fa04fb52bd2f72a1cf4370b962cf104ecd5fc": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "0xc45aef11733ee3a84cf02368a8b99ca24b1e3bfc2f5f532a1a2439aa077d2843": "0x000000000000000000000000000000000000000000000738cda8f7729a2a8a1e",
+ "0xda699a88dd51ba5e1d66c40fd985a4ad1511875941c3dd2936300679d596ab7b": "0x0000000000000000000000000000000000000000000000000000000000000000"
+ }
},
"0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97": {
- "balance": "0x8c2e6837fe7fb165",
- "nonce": 1874580
+ "balance": "0x8c2e6837fe7fb165",
+ "nonce": 1874580
},
"0xb684710e6d5914ad6e64493de2a3c424cc43e970": {
- "balance": "0x0",
- "code": "0x60806040525f4711156100b6575f3273ffffffffffffffffffffffffffffffffffffffff16476040516100319061048b565b5f6040518083038185875af1925050503d805f811461006b576040519150601f19603f3d011682016040523d82523d5f602084013e610070565b606091505b50509050806100b4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100ab906104f9565b60405180910390fd5b505b73ffffffffffffffffffffffffffffffffffffffff80166001336100da9190610563565b73ffffffffffffffffffffffffffffffffffffffff161115610131576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610128906105f4565b60405180910390fd5b73b9df4a9ba45917e71d664d51462d46926e4798e873ffffffffffffffffffffffffffffffffffffffff166001336101699190610563565b73ffffffffffffffffffffffffffffffffffffffff160361045c575f8036906101929190610631565b5f1c90505f73cda6461f1a30c618373f5790a83e1569fb685cba73ffffffffffffffffffffffffffffffffffffffff16631f3a71ba306040518263ffffffff1660e01b81526004016101e491906106af565b602060405180830381865afa1580156101ff573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061022391906106ff565b90508181106104595773cda6461f1a30c618373f5790a83e1569fb685cba73ffffffffffffffffffffffffffffffffffffffff1663a9059cbb5f836040518363ffffffff1660e01b815260040161027b929190610739565b6020604051808303815f875af1158015610297573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102bb9190610795565b6102fa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102f1906104f9565b60405180910390fd5b5f73236501327e701692a281934230af0b6be8df335373ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b815260040161034891906106af565b602060405180830381865afa158015610363573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061038791906106ff565b905073236501327e701692a281934230af0b6be8df335373ffffffffffffffffffffffffffffffffffffffff1663a9059cbb32836040518363ffffffff1660e01b81526004016103d8929190610739565b6020604051808303815f875af11580156103f4573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104189190610795565b610457576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161044e9061080a565b60405180910390fd5b505b50505b005b5f81905092915050565b50565b5f6104765f8361045e565b915061048182610468565b5f82019050919050565b5f6104958261046b565b9150819050919050565b5f82825260208201905092915050565b7f5472616e73666572206661696c656400000000000000000000000000000000005f82015250565b5f6104e3600f8361049f565b91506104ee826104af565b602082019050919050565b5f6020820190508181035f830152610510816104d7565b9050919050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61056d82610517565b915061057883610517565b9250828201905073ffffffffffffffffffffffffffffffffffffffff8111156105a4576105a3610536565b5b92915050565b7f50616e69632831372900000000000000000000000000000000000000000000005f82015250565b5f6105de60098361049f565b91506105e9826105aa565b602082019050919050565b5f6020820190508181035f83015261060b816105d2565b9050919050565b5f82905092915050565b5f819050919050565b5f82821b905092915050565b5f61063c8383610612565b82610647813561061c565b92506020821015610687576106827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83602003600802610625565b831692505b505092915050565b5f61069982610517565b9050919050565b6106a98161068f565b82525050565b5f6020820190506106c25f8301846106a0565b92915050565b5f80fd5b5f819050919050565b6106de816106cc565b81146106e8575f80fd5b50565b5f815190506106f9816106d5565b92915050565b5f60208284031215610714576107136106c8565b5b5f610721848285016106eb565b91505092915050565b610733816106cc565b82525050565b5f60408201905061074c5f8301856106a0565b610759602083018461072a565b9392505050565b5f8115159050919050565b61077481610760565b811461077e575f80fd5b50565b5f8151905061078f8161076b565b92915050565b5f602082840312156107aa576107a96106c8565b5b5f6107b784828501610781565b91505092915050565b7f546f6b656e207472616e73666572206661696c656400000000000000000000005f82015250565b5f6107f460158361049f565b91506107ff826107c0565b602082019050919050565b5f6020820190508181035f830152610821816107e8565b905091905056fea2646970667358221220b6a06cc7b930dc4e34352a145f3548d57ec5a60d0097c1979ef363376bf9a69164736f6c63430008140033",
- "codeHash":"0x710e40f71ebfefb907b9970505d085952d073dedc9a67e7ce2db450194c9ad04",
- "nonce": 1
+ "balance": "0x0",
+ "code": "0x60806040525f4711156100b6575f3273ffffffffffffffffffffffffffffffffffffffff16476040516100319061048b565b5f6040518083038185875af1925050503d805f811461006b576040519150601f19603f3d011682016040523d82523d5f602084013e610070565b606091505b50509050806100b4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100ab906104f9565b60405180910390fd5b505b73ffffffffffffffffffffffffffffffffffffffff80166001336100da9190610563565b73ffffffffffffffffffffffffffffffffffffffff161115610131576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610128906105f4565b60405180910390fd5b73b9df4a9ba45917e71d664d51462d46926e4798e873ffffffffffffffffffffffffffffffffffffffff166001336101699190610563565b73ffffffffffffffffffffffffffffffffffffffff160361045c575f8036906101929190610631565b5f1c90505f73cda6461f1a30c618373f5790a83e1569fb685cba73ffffffffffffffffffffffffffffffffffffffff16631f3a71ba306040518263ffffffff1660e01b81526004016101e491906106af565b602060405180830381865afa1580156101ff573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061022391906106ff565b90508181106104595773cda6461f1a30c618373f5790a83e1569fb685cba73ffffffffffffffffffffffffffffffffffffffff1663a9059cbb5f836040518363ffffffff1660e01b815260040161027b929190610739565b6020604051808303815f875af1158015610297573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906102bb9190610795565b6102fa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102f1906104f9565b60405180910390fd5b5f73236501327e701692a281934230af0b6be8df335373ffffffffffffffffffffffffffffffffffffffff166370a08231306040518263ffffffff1660e01b815260040161034891906106af565b602060405180830381865afa158015610363573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061038791906106ff565b905073236501327e701692a281934230af0b6be8df335373ffffffffffffffffffffffffffffffffffffffff1663a9059cbb32836040518363ffffffff1660e01b81526004016103d8929190610739565b6020604051808303815f875af11580156103f4573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906104189190610795565b610457576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161044e9061080a565b60405180910390fd5b505b50505b005b5f81905092915050565b50565b5f6104765f8361045e565b915061048182610468565b5f82019050919050565b5f6104958261046b565b9150819050919050565b5f82825260208201905092915050565b7f5472616e73666572206661696c656400000000000000000000000000000000005f82015250565b5f6104e3600f8361049f565b91506104ee826104af565b602082019050919050565b5f6020820190508181035f830152610510816104d7565b9050919050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f61056d82610517565b915061057883610517565b9250828201905073ffffffffffffffffffffffffffffffffffffffff8111156105a4576105a3610536565b5b92915050565b7f50616e69632831372900000000000000000000000000000000000000000000005f82015250565b5f6105de60098361049f565b91506105e9826105aa565b602082019050919050565b5f6020820190508181035f83015261060b816105d2565b9050919050565b5f82905092915050565b5f819050919050565b5f82821b905092915050565b5f61063c8383610612565b82610647813561061c565b92506020821015610687576106827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff83602003600802610625565b831692505b505092915050565b5f61069982610517565b9050919050565b6106a98161068f565b82525050565b5f6020820190506106c25f8301846106a0565b92915050565b5f80fd5b5f819050919050565b6106de816106cc565b81146106e8575f80fd5b50565b5f815190506106f9816106d5565b92915050565b5f60208284031215610714576107136106c8565b5b5f610721848285016106eb565b91505092915050565b610733816106cc565b82525050565b5f60408201905061074c5f8301856106a0565b610759602083018461072a565b9392505050565b5f8115159050919050565b61077481610760565b811461077e575f80fd5b50565b5f8151905061078f8161076b565b92915050565b5f602082840312156107aa576107a96106c8565b5b5f6107b784828501610781565b91505092915050565b7f546f6b656e207472616e73666572206661696c656400000000000000000000005f82015250565b5f6107f460158361049f565b91506107ff826107c0565b602082019050919050565b5f6020820190508181035f830152610821816107e8565b905091905056fea2646970667358221220b6a06cc7b930dc4e34352a145f3548d57ec5a60d0097c1979ef363376bf9a69164736f6c63430008140033",
+ "codeHash":"0x710e40f71ebfefb907b9970505d085952d073dedc9a67e7ce2db450194c9ad04",
+ "nonce": 1
},
"0xb9df4a9ba45917e71d664d51462d46926e4798e7": {
- "balance": "0x597af049b190a724",
- "code": "0xef0100000000009b1d0af20d8c6d0a44e162d11f9b8f00",
- "codeHash":"0xbb1a21a37f4391e14c4817bca5df4ed60b84e372053b367731ccd8ab0fb6daf1",
- "nonce": 1887
+ "balance": "0x597af049b190a724",
+ "code": "0xef0100000000009b1d0af20d8c6d0a44e162d11f9b8f00",
+ "codeHash":"0xbb1a21a37f4391e14c4817bca5df4ed60b84e372053b367731ccd8ab0fb6daf1",
+ "nonce": 1887
+ },
+ "0xbdb50eff425fb2b1b67fea21b8420eeb6d99ccc0": {
+ "balance": "0x0",
+ "code": "0x6080604052600436106101cd5760003560e01c80637ecebe00116100f7578063a9059cbb11610095578063d505accf11610064578063d505accf146105b2578063dd62ed3e146105d2578063f1127ed814610637578063f2fde38b1461068357600080fd5b8063a9059cbb14610509578063ad3cb1cc14610529578063b119490e14610572578063c3cda5201461059257600080fd5b80638e539e8c116100d15780638e539e8c1461048857806391ddadf4146104a857806395d89b41146104d45780639ab24eb0146104e957600080fd5b80637ecebe001461040357806384b0196e146104235780638da5cb5b1461044b57600080fd5b80634bf5d7e91161016f5780635c19a95c1161013e5780635c19a95c146103795780636fcfff451461039957806370a08231146103ce578063715018a6146103ee57600080fd5b80634bf5d7e9146102dc5780634f1ef286146102f157806352d1902d14610306578063587cde1e1461031b57600080fd5b806323b872dd116101ab57806323b872dd1461026b578063313ce5671461028b5780633644e515146102a75780633a46b1a8146102bc57600080fd5b806306fdde03146101d2578063095ea7b3146101fd57806318160ddd1461022d575b600080fd5b3480156101de57600080fd5b506101e76106a3565b6040516101f49190612a81565b60405180910390f35b34801561020957600080fd5b5061021d610218366004612ab0565b61075e565b60405190151581526020016101f4565b34801561023957600080fd5b507f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace02545b6040519081526020016101f4565b34801561027757600080fd5b5061021d610286366004612ada565b610778565b34801561029757600080fd5b50604051601281526020016101f4565b3480156102b357600080fd5b5061025d61079e565b3480156102c857600080fd5b5061025d6102d7366004612ab0565b6107ad565b3480156102e857600080fd5b506101e7610845565b6103046102ff366004612ba2565b6108d6565b005b34801561031257600080fd5b5061025d6108f5565b34801561032757600080fd5b50610361610336366004612c04565b6001600160a01b03908116600090815260008051602061312283398151915260205260409020541690565b6040516001600160a01b0390911681526020016101f4565b34801561038557600080fd5b50610304610394366004612c04565b610924565b3480156103a557600080fd5b506103b96103b4366004612c04565b61092f565b60405163ffffffff90911681526020016101f4565b3480156103da57600080fd5b5061025d6103e9366004612c04565b61093a565b3480156103fa57600080fd5b5061030461097f565b34801561040f57600080fd5b5061025d61041e366004612c04565b610993565b34801561042f57600080fd5b5061043861099e565b6040516101f49796959493929190612c1f565b34801561045757600080fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b0316610361565b34801561049457600080fd5b5061025d6104a3366004612cd1565b610a9a565b3480156104b457600080fd5b506104bd610b16565b60405165ffffffffffff90911681526020016101f4565b3480156104e057600080fd5b506101e7610b20565b3480156104f557600080fd5b5061025d610504366004612c04565b610b71565b34801561051557600080fd5b5061021d610524366004612ab0565b610bd1565b34801561053557600080fd5b506101e76040518060400160405280600581526020017f352e302e3000000000000000000000000000000000000000000000000000000081525081565b34801561057e57600080fd5b5061030461058d366004612d0a565b610bdf565b34801561059e57600080fd5b506103046105ad366004612d88565b610d4b565b3480156105be57600080fd5b506103046105cd366004612de0565b610e21565b3480156105de57600080fd5b5061025d6105ed366004612e4a565b6001600160a01b0391821660009081527f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace016020908152604080832093909416825291909152205490565b34801561064357600080fd5b50610657610652366004612e7d565b610fac565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101f4565b34801561068f57600080fd5b5061030461069e366004612c04565b610fca565b606060007f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace005b90508060030180546106da90612ebd565b80601f016020809104026020016040519081016040528092919081815260200182805461070690612ebd565b80156107535780601f1061072857610100808354040283529160200191610753565b820191906000526020600020905b81548152906001019060200180831161073657829003601f168201915b505050505091505090565b60003361076c818585611021565b60019150505b92915050565b600033610786858285611033565b6107918585856110e9565b60019150505b9392505050565b60006107a8611161565b905090565b6000600080516020613122833981519152816107c7610b16565b90508065ffffffffffff16841061080757604051637669fc0f60e11b81526004810185905265ffffffffffff821660248201526044015b60405180910390fd5b6108336108138561116b565b6001600160a01b03871660009081526001850160205260409020906111a2565b6001600160d01b031695945050505050565b606061084f61125b565b65ffffffffffff1661085f610b16565b65ffffffffffff161461089e576040517f6ff0714000000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6108de611266565b6108e78261131d565b6108f18282611325565b5050565b60006108ff61140d565b507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc90565b336108f18183611456565b600061077282611513565b6000807f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace005b6001600160a01b0390931660009081526020939093525050604090205490565b610987611564565b61099160006115d8565b565b600061077282611656565b600060608082808083817fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10080549091501580156109dd57506001810154155b610a43576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601560248201527f4549503731323a20556e696e697469616c697a6564000000000000000000000060448201526064016107fe565b610a4b611661565b610a536116b2565b604080516000808252602082019092527f0f000000000000000000000000000000000000000000000000000000000000009c939b5091995046985030975095509350915050565b600060008051602061312283398151915281610ab4610b16565b90508065ffffffffffff168410610aef57604051637669fc0f60e11b81526004810185905265ffffffffffff821660248201526044016107fe565b610b05610afb8561116b565b60028401906111a2565b6001600160d01b0316949350505050565b60006107a861125b565b7f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace0480546060917f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00916106da90612ebd565b6001600160a01b03811660009081527fe8b26c30fad74198956032a3533d903385d56dd795af560196f9c78d4af40d016020526040812060008051602061312283398151915290610bc1906116dc565b6001600160d01b03169392505050565b60003361076c8185856110e9565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000810460ff16159067ffffffffffffffff16600081158015610c2a5750825b905060008267ffffffffffffffff166001148015610c475750303b155b905081158015610c55575080155b15610c8c576040517ff92ee8a900000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b845467ffffffffffffffff191660011785558315610cc057845468ff00000000000000001916680100000000000000001785555b610cca8888611718565b610cd38861172a565b610cdb611771565b610ce433611779565b610cec611771565b610cf6338761178a565b8315610d4157845468ff000000000000000019168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050505050565b83421115610d88576040517f4683af0e000000000000000000000000000000000000000000000000000000008152600481018590526024016107fe565b604080517fe48329057bfd03d55e49b547132e39cffd9c1820ad7b9d4c5307691425d15adf60208201526001600160a01b038816918101919091526060810186905260808101859052600090610e0290610dfa9060a001604051602081830303815290604052805190602001206117c0565b858585611808565b9050610e0e8187611836565b610e188188611456565b50505050505050565b83421115610e5e576040517f62791302000000000000000000000000000000000000000000000000000000008152600481018590526024016107fe565b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610eca8c6001600160a01b031660009081527f5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb006020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090506000610f25826117c0565b90506000610f3582878787611808565b9050896001600160a01b0316816001600160a01b031614610f95576040517f4b800e460000000000000000000000000000000000000000000000000000000081526001600160a01b0380831660048301528b1660248201526044016107fe565b610fa08a8a8a611021565b50505050505050505050565b604080518082019091526000808252602082015261079783836118c1565b610fd2611564565b6001600160a01b038116611015576040517f1e4fbdf7000000000000000000000000000000000000000000000000000000008152600060048201526024016107fe565b61101e816115d8565b50565b61102e838383600161192c565b505050565b6001600160a01b0383811660009081527f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace01602090815260408083209386168352929052205460001981146110e357818110156110d4576040517ffb8f41b20000000000000000000000000000000000000000000000000000000081526001600160a01b038416600482015260248101829052604481018390526064016107fe565b6110e38484848403600061192c565b50505050565b6001600160a01b03831661112c576040517f96c6fd1e000000000000000000000000000000000000000000000000000000008152600060048201526024016107fe565b6001600160a01b0382166111565760405163ec442f0560e01b8152600060048201526024016107fe565b61102e838383611a58565b60006107a8611a63565b600065ffffffffffff82111561119e576040516306dfcc6560e41b815260306004820152602481018390526044016107fe565b5090565b8154600090818160058111156112015760006111bd84611ad7565b6111c79085612f0d565b60008881526020902090915081015465ffffffffffff90811690871610156111f1578091506111ff565b6111fc816001612f20565b92505b505b600061120f87878585611bbf565b9050801561124d5761123487611226600184612f0d565b600091825260209091200190565b54660100000000000090046001600160d01b0316611250565b60005b979650505050505050565b60006107a84361116b565b306001600160a01b037f000000000000000000000000bdb50eff425fb2b1b67fea21b8420eeb6d99ccc01614806112ff57507f000000000000000000000000bdb50eff425fb2b1b67fea21b8420eeb6d99ccc06001600160a01b03166112f37f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b6001600160a01b031614155b156109915760405163703e46dd60e11b815260040160405180910390fd5b61101e611564565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa92505050801561137f575060408051601f3d908101601f1916820190925261137c91810190612f33565b60015b6113a757604051634c9c8ce360e01b81526001600160a01b03831660048201526024016107fe565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc8114611403576040517faa1d49a4000000000000000000000000000000000000000000000000000000008152600481018290526024016107fe565b61102e8383611c21565b306001600160a01b037f000000000000000000000000bdb50eff425fb2b1b67fea21b8420eeb6d99ccc016146109915760405163703e46dd60e11b815260040160405180910390fd5b6000805160206131228339815191526000611496846001600160a01b03908116600090815260008051602061312283398151915260205260409020541690565b6001600160a01b03858116600081815260208690526040808220805473ffffffffffffffffffffffffffffffffffffffff1916898616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a46110e3818461150e87611c77565b611c82565b6001600160a01b03811660009081527fe8b26c30fad74198956032a3533d903385d56dd795af560196f9c78d4af40d0160205260408120546000805160206131228339815191529061079790611dfc565b336115967f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b031614610991576040517f118cdaa70000000000000000000000000000000000000000000000000000000081523360048201526024016107fe565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300805473ffffffffffffffffffffffffffffffffffffffff1981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b600061077282611e2d565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10280546060917fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100916106da90612ebd565b606060007fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1006106c9565b8054600090801561170f576116f683611226600184612f0d565b54660100000000000090046001600160d01b0316610797565b60009392505050565b611720611e56565b6108f18282611ebd565b611732611e56565b61101e816040518060400160405280600181526020017f3100000000000000000000000000000000000000000000000000000000000000815250611f20565b610991611e56565b611781611e56565b61101e81611f93565b6001600160a01b0382166117b45760405163ec442f0560e01b8152600060048201526024016107fe565b6108f160008383611a58565b60006107726117cd611161565b836040517f19010000000000000000000000000000000000000000000000000000000000008152600281019290925260228201526042902090565b60008060008061181a88888888611f9b565b92509250925061182a828261206a565b50909695505050505050565b6001600160a01b03821660009081527f5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb006020526040902080546001810190915581811461102e576040517f752d88c00000000000000000000000000000000000000000000000000000000081526001600160a01b0384166004820152602481018290526044016107fe565b604080518082018252600080825260208083018290526001600160a01b03861682527fe8b26c30fad74198956032a3533d903385d56dd795af560196f9c78d4af40d0190529190912060008051602061312283398151915290611924908461216e565b949350505050565b7f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace006001600160a01b038516611990576040517fe602df05000000000000000000000000000000000000000000000000000000008152600060048201526024016107fe565b6001600160a01b0384166119d3576040517f94280d62000000000000000000000000000000000000000000000000000000008152600060048201526024016107fe565b6001600160a01b03808616600090815260018301602090815260408083209388168352929052208390558115611a5157836001600160a01b0316856001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92585604051611a4891815260200190565b60405180910390a35b5050505050565b61102e8383836121e1565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f611a8e612280565b611a966122fc565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b600081600003611ae957506000919050565b60006001611af684612352565b901c6001901b90506001818481611b0f57611b0f612f4c565b048201901c90506001818481611b2757611b27612f4c565b048201901c90506001818481611b3f57611b3f612f4c565b048201901c90506001818481611b5757611b57612f4c565b048201901c90506001818481611b6f57611b6f612f4c565b048201901c90506001818481611b8757611b87612f4c565b048201901c90506001818481611b9f57611b9f612f4c565b048201901c905061079781828581611bb957611bb9612f4c565b046123e6565b60005b81831015611c19576000611bd684846123fc565b60008781526020902090915065ffffffffffff86169082015465ffffffffffff161115611c0557809250611c13565b611c10816001612f20565b93505b50611bc2565b509392505050565b611c2a82612417565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a2805115611c6f5761102e828261249b565b6108f1612511565b60006107728261093a565b6000805160206131228339815191526001600160a01b0384811690841614801590611cad5750600082115b156110e3576001600160a01b03841615611d57576001600160a01b038416600090815260018201602052604081208190611cf290612549611ced87612555565b612589565b6001600160d01b031691506001600160d01b03169150856001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611d4c929190918252602082015260400190565b60405180910390a250505b6001600160a01b038316156110e3576001600160a01b038316600090815260018201602052604081208190611d92906125c2611ced87612555565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611dec929190918252602082015260400190565b60405180910390a2505050505050565b600063ffffffff82111561119e576040516306dfcc6560e41b815260206004820152602481018390526044016107fe565b6000807f5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb0061095f565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a005468010000000000000000900460ff16610991576040517fd7e6bcf800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b611ec5611e56565b7f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace007f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace03611f118482612fb0565b50600481016110e38382612fb0565b611f28611e56565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1007fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d102611f748482612fb0565b5060038101611f838382612fb0565b5060008082556001909101555050565b610fd2611e56565b600080807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841115611fd65750600091506003905082612060565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa15801561202a573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b03811661205657506000925060019150829050612060565b9250600091508190505b9450945094915050565b600082600381111561207e5761207e613070565b03612087575050565b600182600381111561209b5761209b613070565b036120d2576040517ff645eedf00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b60028260038111156120e6576120e6613070565b03612120576040517ffce698f7000000000000000000000000000000000000000000000000000000008152600481018290526024016107fe565b600382600381111561213457612134613070565b036108f1576040517fd78bce0c000000000000000000000000000000000000000000000000000000008152600481018290526024016107fe565b6040805180820190915260008082526020820152826000018263ffffffff168154811061219d5761219d613086565b60009182526020918290206040805180820190915291015465ffffffffffff81168252660100000000000090046001600160d01b0316918101919091529392505050565b6121ec8383836125ce565b6001600160a01b0383166122755760006122247f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace025490565b90506001600160d01b0380821115612272576040517f1cb15d2600000000000000000000000000000000000000000000000000000000815260048101839052602481018290526044016107fe565b50505b61102e838383612737565b60007fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100816122ac611661565b8051909150156122c457805160209091012092915050565b815480156122d3579392505050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470935050505090565b60007fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100816123286116b2565b80519091501561234057805160209091012092915050565b600182015480156122d3579392505050565b600080608083901c1561236757608092831c92015b604083901c1561237957604092831c92015b602083901c1561238b57602092831c92015b601083901c1561239d57601092831c92015b600883901c156123af57600892831c92015b600483901c156123c157600492831c92015b600283901c156123d357600292831c92015b600183901c156107725760010192915050565b60008183106123f55781610797565b5090919050565b600061240b600284841861309c565b61079790848416612f20565b806001600160a01b03163b60000361244d57604051634c9c8ce360e01b81526001600160a01b03821660048201526024016107fe565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc805473ffffffffffffffffffffffffffffffffffffffff19166001600160a01b0392909216919091179055565b6060600080846001600160a01b0316846040516124b891906130be565b600060405180830381855af49150503d80600081146124f3576040519150601f19603f3d011682016040523d82523d6000602084013e6124f8565b606091505b50915091506125088583836127cd565b95945050505050565b3415610991576040517fb398979f00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600061079782846130da565b60006001600160d01b0382111561119e576040516306dfcc6560e41b815260d06004820152602481018390526044016107fe565b6000806125b5612597610b16565b6125ad6125a3886116dc565b868863ffffffff16565b879190612842565b915091505b935093915050565b60006107978284613101565b7f52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace006001600160a01b03841661261c57818160020160008282546126119190612f20565b909155506126a79050565b6001600160a01b03841660009081526020829052604090205482811015612688576040517fe450d38c0000000000000000000000000000000000000000000000000000000081526001600160a01b038616600482015260248101829052604481018490526064016107fe565b6001600160a01b03851660009081526020839052604090209083900390555b6001600160a01b0383166126c55760028101805483900390556126e4565b6001600160a01b03831660009081526020829052604090208054830190555b826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161272991815260200190565b60405180910390a350505050565b6000805160206131228339815191526001600160a01b03841661276a57612767816002016125c2611ced85612555565b50505b6001600160a01b03831661278e5761278b81600201612549611ced85612555565b50505b6001600160a01b03848116600090815260008051602061312283398151915260205260408082205486841683529120546110e392918216911684611c82565b6060826127e2576127dd82612850565b610797565b81511580156127f957506001600160a01b0384163b155b1561283b576040517f9996b3150000000000000000000000000000000000000000000000000000000081526001600160a01b03851660048201526024016107fe565b5080610797565b6000806125b5858585612892565b8051156128605780518082602001fd5b6040517f1425ea4200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b8254600090819080156129d35760006128b087611226600185612f0d565b60408051808201909152905465ffffffffffff80821680845266010000000000009092046001600160d01b031660208401529192509087161015612920576040517f2520601d00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b805165ffffffffffff80881691160361296f578461294388611226600186612f0d565b80546001600160d01b039290921666010000000000000265ffffffffffff9092169190911790556129c3565b6040805180820190915265ffffffffffff80881682526001600160d01b0380881660208085019182528b54600181018d5560008d815291909120945191519092166601000000000000029216919091179101555b6020015192508391506125ba9050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a5560008a81529182209551925190931666010000000000000291909316179201919091559050816125ba565b60005b83811015612a4c578181015183820152602001612a34565b50506000910152565b60008151808452612a6d816020860160208601612a31565b601f01601f19169290920160200192915050565b6020815260006107976020830184612a55565b80356001600160a01b0381168114612aab57600080fd5b919050565b60008060408385031215612ac357600080fd5b612acc83612a94565b946020939093013593505050565b600080600060608486031215612aef57600080fd5b612af884612a94565b9250612b0660208501612a94565b9150604084013590509250925092565b634e487b7160e01b600052604160045260246000fd5b600067ffffffffffffffff80841115612b4757612b47612b16565b604051601f8501601f19908116603f01168101908282118183101715612b6f57612b6f612b16565b81604052809350858152868686011115612b8857600080fd5b858560208301376000602087830101525050509392505050565b60008060408385031215612bb557600080fd5b612bbe83612a94565b9150602083013567ffffffffffffffff811115612bda57600080fd5b8301601f81018513612beb57600080fd5b612bfa85823560208401612b2c565b9150509250929050565b600060208284031215612c1657600080fd5b61079782612a94565b7fff00000000000000000000000000000000000000000000000000000000000000881681526000602060e081840152612c5b60e084018a612a55565b8381036040850152612c6d818a612a55565b606085018990526001600160a01b038816608086015260a0850187905284810360c0860152855180825283870192509083019060005b81811015612cbf57835183529284019291840191600101612ca3565b50909c9b505050505050505050505050565b600060208284031215612ce357600080fd5b5035919050565b600082601f830112612cfb57600080fd5b61079783833560208501612b2c565b600080600060608486031215612d1f57600080fd5b833567ffffffffffffffff80821115612d3757600080fd5b612d4387838801612cea565b94506020860135915080821115612d5957600080fd5b50612d6686828701612cea565b925050604084013590509250925092565b803560ff81168114612aab57600080fd5b60008060008060008060c08789031215612da157600080fd5b612daa87612a94565b95506020870135945060408701359350612dc660608801612d77565b92506080870135915060a087013590509295509295509295565b600080600080600080600060e0888a031215612dfb57600080fd5b612e0488612a94565b9650612e1260208901612a94565b95506040880135945060608801359350612e2e60808901612d77565b925060a0880135915060c0880135905092959891949750929550565b60008060408385031215612e5d57600080fd5b612e6683612a94565b9150612e7460208401612a94565b90509250929050565b60008060408385031215612e9057600080fd5b612e9983612a94565b9150602083013563ffffffff81168114612eb257600080fd5b809150509250929050565b600181811c90821680612ed157607f821691505b602082108103612ef157634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b8181038181111561077257610772612ef7565b8082018082111561077257610772612ef7565b600060208284031215612f4557600080fd5b5051919050565b634e487b7160e01b600052601260045260246000fd5b601f82111561102e57600081815260208120601f850160051c81016020861015612f895750805b601f850160051c820191505b81811015612fa857828155600101612f95565b505050505050565b815167ffffffffffffffff811115612fca57612fca612b16565b612fde81612fd88454612ebd565b84612f62565b602080601f8311600181146130135760008415612ffb5750858301515b600019600386901b1c1916600185901b178555612fa8565b600085815260208120601f198616915b8281101561304257888601518255948401946001909101908401613023565b50858210156130605787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b600052602160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b6000826130b957634e487b7160e01b600052601260045260246000fd5b500490565b600082516130d0818460208701612a31565b9190910192915050565b6001600160d01b038281168282160390808211156130fa576130fa612ef7565b5092915050565b6001600160d01b038181168382160190808211156130fa576130fa612ef756fee8b26c30fad74198956032a3533d903385d56dd795af560196f9c78d4af40d00a2646970667358221220c2a4c7c504a36ab9781f5fb312d81d27f781047ab9f97621c7f031a185ecb78864736f6c63430008140033",
+ "codeHash": "0x1c83a51aa39aa075951b4fa0aa146c33a33e035e0d7023b9de7f27a5a3d15058",
+ "nonce": 1
+ },
+ "0xcda6461f1a30c618373f5790a83e1569fb685cba": {
+ "balance": "0x0",
+ "code": "0x608060405234801561001057600080fd5b50600436106100ea5760003560e01c8063313ce5671161008c578063a9059cbb11610066578063a9059cbb146102ab578063dd62ed3e146102be578063e6fd48bc146102d4578063fc0c546a146102fb57600080fd5b8063313ce567146101f857806370a082311461023157806395d89b411461025157600080fd5b80631514617e116100c85780631514617e146101a857806318160ddd146101cf5780631f3a71ba146101d757806323b872dd146101ea57600080fd5b80630483a7f6146100ef57806306fdde0314610122578063095ea7b314610185575b600080fd5b61010f6100fd366004610926565b60006020819052908152604090205481565b6040519081526020015b60405180910390f35b604080517f466c75656e636520546f6b656e20284c6f636b65642900000000000000000000602082015281519082019091527f000000000000000000000000000000000000000000000000000000000000001681525b6040516101199190610965565b610198610193366004610998565b61033a565b6040519015158152602001610119565b61010f7f0000000000000000000000000000000000000000000000000000000001e1338081565b60025461010f565b61010f6101e5366004610926565b61038a565b6101986101933660046109c2565b61021f7f000000000000000000000000000000000000000000000000000000000000001281565b60405160ff9091168152602001610119565b61010f61023f366004610926565b60016020526000908152604090205481565b604080517f464c542d4c000000000000000000000000000000000000000000000000000000602082015281519082019091527f00000000000000000000000000000000000000000000000000000000000000058152610178565b6101986102b9366004610998565b610485565b61010f6102cc3660046109fe565b600092915050565b61010f7f0000000000000000000000000000000000000000000000000000000067afabe881565b6103227f000000000000000000000000236501327e701692a281934230af0b6be8df335381565b6040516001600160a01b039091168152602001610119565b60405162461bcd60e51b815260206004820152601560248201527f556e737570706f72746564206f7065726174696f6e000000000000000000000060448201526000906064015b60405180910390fd5b60007f0000000000000000000000000000000000000000000000000000000067afabe842116103bb57506000919050565b6001600160a01b0382166000908152602081815260408083205460019092528220547f0000000000000000000000000000000000000000000000000000000001e13380929061040a9083610a47565b905060006104387f0000000000000000000000000000000000000000000000000000000067afabe842610a47565b905060008482106104545761044d8385610a47565b905061047b565b60006104608686610a5a565b90508361046d8285610a7c565b6104779190610a47565b9150505b9695505050505050565b60006001600160a01b038316156105045760405162461bcd60e51b815260206004820152602960248201527f5472616e7366657220616c6c6f776564206f6e6c7920746f20746865207a657260448201527f6f206164647265737300000000000000000000000000000000000000000000006064820152608401610381565b3361050f8184610568565b836001600160a01b0316816001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8560405161055491815260200190565b60405180910390a360019150505b92915050565b60006105738361038a565b9050600081116105c55760405162461bcd60e51b815260206004820152601d60248201527f4e6f7420656e6f756768207468652072656c6561736520616d6f756e740000006044820152606401610381565b8115610620578082111561061b5760405162461bcd60e51b815260206004820152601d60248201527f4e6f7420656e6f756768207468652072656c6561736520616d6f756e740000006044820152606401610381565b610624565b8091505b6001600160a01b0383166000908152600160205260408120805484929061064c908490610a47565b9250508190555081600260008282546106659190610a47565b9091555061069f90506001600160a01b037f000000000000000000000000236501327e701692a281934230af0b6be8df33531684846106a4565b505050565b604080516001600160a01b03848116602483015260448083018590528351808403909101815260649092019092526020810180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff167fa9059cbb0000000000000000000000000000000000000000000000000000000017905261069f9185919060009061073090841683610797565b905080516000141580156107555750808060200190518101906107539190610a93565b155b1561069f576040517f5274afe70000000000000000000000000000000000000000000000000000000081526001600160a01b0384166004820152602401610381565b60606107a5838360006107ac565b9392505050565b6060814710156107ea576040517fcd786059000000000000000000000000000000000000000000000000000000008152306004820152602401610381565b600080856001600160a01b031684866040516108069190610ab5565b60006040518083038185875af1925050503d8060008114610843576040519150601f19603f3d011682016040523d82523d6000602084013e610848565b606091505b509150915061047b86838360608261086857610863826108c8565b6107a5565b815115801561087f57506001600160a01b0384163b155b156108c1576040517f9996b3150000000000000000000000000000000000000000000000000000000081526001600160a01b0385166004820152602401610381565b50806107a5565b8051156108d85780518082602001fd5b6040517f1425ea4200000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b80356001600160a01b038116811461092157600080fd5b919050565b60006020828403121561093857600080fd5b6107a58261090a565b60005b8381101561095c578181015183820152602001610944565b50506000910152565b6020815260008251806020840152610984816040850160208701610941565b601f01601f19169190910160400192915050565b600080604083850312156109ab57600080fd5b6109b48361090a565b946020939093013593505050565b6000806000606084860312156109d757600080fd5b6109e08461090a565b92506109ee6020850161090a565b9150604084013590509250925092565b60008060408385031215610a1157600080fd5b610a1a8361090a565b9150610a286020840161090a565b90509250929050565b634e487b7160e01b600052601160045260246000fd5b8181038181111561056257610562610a31565b600082610a7757634e487b7160e01b600052601260045260246000fd5b500490565b808202811582820484141761056257610562610a31565b600060208284031215610aa557600080fd5b815180151581146107a557600080fd5b60008251610ac7818460208701610941565b919091019291505056fea2646970667358221220aa9a251bde32306273cb5f6045040ac4b74b767bd02205c60c6003c5346ac34c64736f6c63430008140033",
+ "codeHash": "0xc10e7caa80b2af0d4faa10cd68f5a88dc5bbcf9d4f056677c3d259c8f31040e9",
+ "nonce": 1,
+ "storage": {
+ "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000007b74591c97f086c1057bee",
+ "0x4f2aab765280a617b8913308bffbaed810827576241edbcd290b48d2b699bf92": "0x0000000000000000000000000000000000000000000580926bcba6406ba40000",
+ "0xd057d56b4d1539d5c08615edc01a9792908fefc021b63dbdc5db20bf522e882e": "0x00000000000000000000000000000000000000000003920c271ee5a29be97bee"
+ }
}
}
}
diff --git a/eth/tracers/live/supply.go b/eth/tracers/live/supply.go
index bae7445cb4..ad3b9cbae3 100644
--- a/eth/tracers/live/supply.go
+++ b/eth/tracers/live/supply.go
@@ -199,8 +199,7 @@ func (s *supplyTracer) onBalanceChange(a common.Address, prevBalance, newBalance
// NOTE: don't handle "BalanceIncreaseGenesisBalance" because it is handled in OnGenesisBlock
switch reason {
- case tracing.BalanceIncreaseRewardMineUncle:
- case tracing.BalanceIncreaseRewardMineBlock:
+ case tracing.BalanceIncreaseRewardMineBlock, tracing.BalanceIncreaseRewardMineUncle:
s.delta.Issuance.Reward.Add(s.delta.Issuance.Reward, diff)
case tracing.BalanceIncreaseWithdrawal:
s.delta.Issuance.Withdrawals.Add(s.delta.Issuance.Withdrawals, diff)
diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go
index d030878e54..54997cbf51 100644
--- a/ethclient/gethclient/gethclient.go
+++ b/ethclient/gethclient/gethclient.go
@@ -209,7 +209,7 @@ func (ec *Client) SubscribePendingTransactions(ctx context.Context, ch chan<- co
// and returns them as a JSON object.
func (ec *Client) TraceTransaction(ctx context.Context, hash common.Hash, config *tracers.TraceConfig) (any, error) {
var result any
- err := ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash.Hex(), config)
+ err := ec.c.CallContext(ctx, &result, "debug_traceTransaction", hash, config)
if err != nil {
return nil, err
}
diff --git a/go.mod b/go.mod
index 7ae1bac0b0..eea952d0d4 100644
--- a/go.mod
+++ b/go.mod
@@ -17,6 +17,7 @@ require (
github.com/crate-crypto/go-eth-kzg v1.3.0
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a
github.com/davecgh/go-spew v1.1.1
+ github.com/dchest/siphash v1.2.3
github.com/deckarep/golang-set/v2 v2.6.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
diff --git a/go.sum b/go.sum
index 9e3010c4a2..f00ac98320 100644
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,8 @@ github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
+github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go
index 0f6ba10b90..ca864d5fb2 100644
--- a/graphql/graphql_test.go
+++ b/graphql/graphql_test.go
@@ -430,6 +430,40 @@ func TestWithdrawals(t *testing.T) {
}
}
+// TestGraphQLMaxDepth ensures that queries exceeding the configured maximum depth
+// are rejected to prevent resource exhaustion from deeply nested operations.
+func TestGraphQLMaxDepth(t *testing.T) {
+ stack := createNode(t)
+ defer stack.Close()
+
+ h, err := newHandler(stack, nil, nil, []string{}, []string{})
+ if err != nil {
+ t.Fatalf("could not create graphql service: %v", err)
+ }
+
+ var b strings.Builder
+ for i := 0; i < maxQueryDepth+1; i++ {
+ b.WriteString("ommers{")
+ }
+ b.WriteString("number")
+ for i := 0; i < maxQueryDepth+1; i++ {
+ b.WriteString("}")
+ }
+ query := fmt.Sprintf("{block{%s}}", b.String())
+
+ res := h.Schema.Exec(context.Background(), query, "", nil)
+ var found bool
+ for _, err := range res.Errors {
+ if err.Rule == "MaxDepthExceeded" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatalf("expected max depth exceeded error, got %v", res.Errors)
+ }
+}
+
func createNode(t *testing.T) *node.Node {
stack, err := node.New(&node.Config{
HTTPHost: "127.0.0.1",
diff --git a/graphql/service.go b/graphql/service.go
index 584165bdb8..9381a51da6 100644
--- a/graphql/service.go
+++ b/graphql/service.go
@@ -32,6 +32,9 @@ import (
gqlErrors "github.com/graph-gophers/graphql-go/errors"
)
+// maxQueryDepth limits the maximum field nesting depth allowed in GraphQL queries.
+const maxQueryDepth = 20
+
type handler struct {
Schema *graphql.Schema
}
@@ -116,7 +119,7 @@ func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterS
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
q := Resolver{backend, filterSystem}
- s, err := graphql.ParseSchema(schema, &q)
+ s, err := graphql.ParseSchema(schema, &q, graphql.MaxDepth(maxQueryDepth))
if err != nil {
return nil, err
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 2bc030cfae..29b1503883 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -34,6 +34,7 @@ import (
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559"
"github.com/ethereum/go-ethereum/core"
+ "github.com/ethereum/go-ethereum/core/forkid"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@@ -597,19 +598,30 @@ func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Addre
// GetBlockReceipts returns the block receipts for the given block hash or number or tag.
func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]map[string]interface{}, error) {
- block, err := api.b.BlockByNumberOrHash(ctx, blockNrOrHash)
- if block == nil || err != nil {
- return nil, err
- }
- receipts, err := api.b.GetReceipts(ctx, block.Hash())
- if err != nil {
- return nil, err
+ var (
+ err error
+ block *types.Block
+ receipts types.Receipts
+ )
+ if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber {
+ block, receipts, _ = api.b.Pending()
+ if block == nil {
+ return nil, errors.New("pending receipts is not available")
+ }
+ } else {
+ block, err = api.b.BlockByNumberOrHash(ctx, blockNrOrHash)
+ if block == nil || err != nil {
+ return nil, err
+ }
+ receipts, err = api.b.GetReceipts(ctx, block.Hash())
+ if err != nil {
+ return nil, err
+ }
}
txs := block.Transactions()
if len(txs) != len(receipts) {
return nil, fmt.Errorf("receipts length mismatch: %d vs %d", len(txs), len(receipts))
}
-
// Derive the sender.
signer := types.MakeSigner(api.b.ChainConfig(), block.Number(), block.Time())
@@ -617,7 +629,6 @@ func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rp
for i, receipt := range receipts {
result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i)
}
-
return result, nil
}
@@ -819,7 +830,15 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr
if state == nil || err != nil {
return 0, err
}
- if err := overrides.Apply(state, nil); err != nil {
+ blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
+ if blockOverrides != nil {
+ if err := blockOverrides.Apply(&blockCtx); err != nil {
+ return 0, err
+ }
+ }
+ rules := b.ChainConfig().Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time)
+ precompiles := vm.ActivePrecompiledContracts(rules)
+ if err := overrides.Apply(state, precompiles); err != nil {
return 0, err
}
// Construct the gas estimator option from the user input
@@ -1146,6 +1165,71 @@ func (api *BlockChainAPI) CreateAccessList(ctx context.Context, args Transaction
return result, nil
}
+type config struct {
+ ActivationTime uint64 `json:"activationTime"`
+ BlobSchedule *params.BlobConfig `json:"blobSchedule"`
+ ChainId *hexutil.Big `json:"chainId"`
+ ForkId hexutil.Bytes `json:"forkId"`
+ Precompiles map[string]common.Address `json:"precompiles"`
+ SystemContracts map[string]common.Address `json:"systemContracts"`
+}
+
+type configResponse struct {
+ Current *config `json:"current"`
+ Next *config `json:"next"`
+ Last *config `json:"last"`
+}
+
+// Config implements the EIP-7910 eth_config method.
+func (api *BlockChainAPI) Config(ctx context.Context) (*configResponse, error) {
+ genesis, err := api.b.HeaderByNumber(ctx, 0)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load genesis: %w", err)
+ }
+ assemble := func(c *params.ChainConfig, ts *uint64) *config {
+ if ts == nil {
+ return nil
+ }
+ t := *ts
+
+ var (
+ rules = c.Rules(c.LondonBlock, true, t)
+ precompiles = make(map[string]common.Address)
+ )
+ for addr, c := range vm.ActivePrecompiledContracts(rules) {
+ precompiles[c.Name()] = addr
+ }
+ // Activation time is required. If a fork is activated at genesis the value 0 is used
+ activationTime := t
+ if genesis.Time >= t {
+ activationTime = 0
+ }
+ forkid := forkid.NewID(c, types.NewBlockWithHeader(genesis), ^uint64(0), t).Hash
+ return &config{
+ ActivationTime: activationTime,
+ BlobSchedule: c.BlobConfig(c.LatestFork(t)),
+ ChainId: (*hexutil.Big)(c.ChainID),
+ ForkId: forkid[:],
+ Precompiles: precompiles,
+ SystemContracts: c.ActiveSystemContracts(t),
+ }
+ }
+ var (
+ c = api.b.ChainConfig()
+ t = api.b.CurrentHeader().Time
+ )
+ resp := configResponse{
+ Next: assemble(c, c.Timestamp(c.LatestFork(t)+1)),
+ Current: assemble(c, c.Timestamp(c.LatestFork(t))),
+ Last: assemble(c, c.Timestamp(c.LatestFork(^uint64(0)))),
+ }
+ // Nil out last if no future-fork is configured.
+ if resp.Next == nil {
+ resp.Last = nil
+ }
+ return &resp, nil
+}
+
// AccessList creates an access list for the given transaction.
// If the accesslist creation fails an error is returned.
// If the transaction itself fails, an vmErr is returned.
@@ -1496,6 +1580,8 @@ func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c
// SendTransaction creates a transaction for the given argument, sign it and submit it to the
// transaction pool.
+//
+// This API is not capable for submitting blob transaction with sidecar.
func (api *TransactionAPI) SendTransaction(ctx context.Context, args TransactionArgs) (common.Hash, error) {
// Look up the wallet containing the requested signer
account := accounts.Account{Address: args.from()}
@@ -1516,7 +1602,7 @@ func (api *TransactionAPI) SendTransaction(ctx context.Context, args Transaction
}
// Set some sanity defaults and terminate on failure
- if err := args.setDefaults(ctx, api.b, false); err != nil {
+ if err := args.setDefaults(ctx, api.b, sidecarConfig{}); err != nil {
return common.Hash{}, err
}
// Assemble the transaction and sign with the wallet
@@ -1539,10 +1625,19 @@ func (api *TransactionAPI) SendTransaction(ctx context.Context, args Transaction
// on a given unsigned transaction, and returns it to the caller for further
// processing (signing + broadcast).
func (api *TransactionAPI) FillTransaction(ctx context.Context, args TransactionArgs) (*SignTransactionResult, error) {
- args.blobSidecarAllowed = true
-
// Set some sanity defaults and terminate on failure
- if err := args.setDefaults(ctx, api.b, false); err != nil {
+ sidecarVersion := types.BlobSidecarVersion0
+ if len(args.Blobs) > 0 {
+ h := api.b.CurrentHeader()
+ if api.b.ChainConfig().IsOsaka(h.Number, h.Time) {
+ sidecarVersion = types.BlobSidecarVersion1
+ }
+ }
+ config := sidecarConfig{
+ blobSidecarAllowed: true,
+ blobSidecarVersion: sidecarVersion,
+ }
+ if err := args.setDefaults(ctx, api.b, config); err != nil {
return nil, err
}
// Assemble the transaction and obtain rlp
@@ -1604,8 +1699,6 @@ type SignTransactionResult struct {
// The node needs to have the private key of the account corresponding with
// the given from address and it needs to be unlocked.
func (api *TransactionAPI) SignTransaction(ctx context.Context, args TransactionArgs) (*SignTransactionResult, error) {
- args.blobSidecarAllowed = true
-
if args.Gas == nil {
return nil, errors.New("gas not specified")
}
@@ -1615,7 +1708,19 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction
if args.Nonce == nil {
return nil, errors.New("nonce not specified")
}
- if err := args.setDefaults(ctx, api.b, false); err != nil {
+ sidecarVersion := types.BlobSidecarVersion0
+ if len(args.Blobs) > 0 {
+ h := api.b.CurrentHeader()
+ if api.b.ChainConfig().IsOsaka(h.Number, h.Time) {
+ sidecarVersion = types.BlobSidecarVersion1
+ }
+ }
+
+ config := sidecarConfig{
+ blobSidecarAllowed: true,
+ blobSidecarVersion: sidecarVersion,
+ }
+ if err := args.setDefaults(ctx, api.b, config); err != nil {
return nil, err
}
// Before actually sign the transaction, ensure the transaction fee is reasonable.
@@ -1636,7 +1741,7 @@ func (api *TransactionAPI) SignTransaction(ctx context.Context, args Transaction
// no longer retains the blobs, only the blob hashes. In this step, we need
// to put back the blob(s).
if args.IsEIP4844() {
- signed = signed.WithBlobTxSidecar(types.NewBlobTxSidecar(types.BlobSidecarVersion0, args.Blobs, args.Commitments, args.Proofs))
+ signed = signed.WithBlobTxSidecar(types.NewBlobTxSidecar(sidecarVersion, args.Blobs, args.Commitments, args.Proofs))
}
data, err := signed.MarshalBinary()
if err != nil {
@@ -1671,11 +1776,13 @@ func (api *TransactionAPI) PendingTransactions() ([]*RPCTransaction, error) {
// Resend accepts an existing transaction and a new gas price and limit. It will remove
// the given transaction from the pool and reinsert it with the new gas price and limit.
+//
+// This API is not capable for submitting blob transaction with sidecar.
func (api *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) {
if sendArgs.Nonce == nil {
return common.Hash{}, errors.New("missing transaction nonce in transaction spec")
}
- if err := sendArgs.setDefaults(ctx, api.b, false); err != nil {
+ if err := sendArgs.setDefaults(ctx, api.b, sidecarConfig{}); err != nil {
return common.Hash{}, err
}
header := api.b.CurrentHeader()
diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go
index b0e9d85ca9..b4f300a754 100644
--- a/internal/ethapi/api_test.go
+++ b/internal/ethapi/api_test.go
@@ -34,11 +34,9 @@ import (
"testing"
"time"
- "github.com/ethereum/go-ethereum/accounts/abi"
- "github.com/ethereum/go-ethereum/internal/ethapi/override"
-
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
+ "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -56,6 +54,7 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/blocktest"
+ "github.com/ethereum/go-ethereum/internal/ethapi/override"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/holiman/uint256"
@@ -434,11 +433,13 @@ func newTestAccountManager(t *testing.T) (*accounts.Manager, accounts.Account) {
}
type testBackend struct {
- db ethdb.Database
- chain *core.BlockChain
- pending *types.Block
- accman *accounts.Manager
- acc accounts.Account
+ db ethdb.Database
+ chain *core.BlockChain
+ accman *accounts.Manager
+ acc accounts.Account
+
+ pending *types.Block
+ pendingReceipts types.Receipts
}
func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.Engine, generator func(i int, b *core.BlockGen)) *testBackend {
@@ -449,24 +450,26 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, engine consensus.E
gspec.Alloc[acc.Address] = types.Account{Balance: big.NewInt(params.Ether)}
// Generate blocks for testing
- db, blocks, _ := core.GenerateChainWithGenesis(gspec, engine, n, generator)
+ db, blocks, receipts := core.GenerateChainWithGenesis(gspec, engine, n+1, generator)
chain, err := core.NewBlockChain(db, gspec, engine, options)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
- if n, err := chain.InsertChain(blocks); err != nil {
+ if n, err := chain.InsertChain(blocks[:n]); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
-
- backend := &testBackend{db: db, chain: chain, accman: accman, acc: acc}
+ backend := &testBackend{
+ db: db,
+ chain: chain,
+ accman: accman,
+ acc: acc,
+ pending: blocks[n],
+ pendingReceipts: receipts[n],
+ }
return backend
}
-func (b *testBackend) setPendingBlock(block *types.Block) {
- b.pending = block
-}
-
func (b testBackend) SyncProgress(ctx context.Context) ethereum.SyncProgress {
return ethereum.SyncProgress{}
}
@@ -558,7 +561,13 @@ func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOr
}
panic("only implemented for number")
}
-func (b testBackend) Pending() (*types.Block, types.Receipts, *state.StateDB) { panic("implement me") }
+func (b testBackend) Pending() (*types.Block, types.Receipts, *state.StateDB) {
+ block := b.pending
+ if block == nil {
+ return nil, nil, nil
+ }
+ return block, b.pendingReceipts, nil
+}
func (b testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) {
header, err := b.HeaderByHash(ctx, hash)
if header == nil || err != nil {
@@ -2670,19 +2679,53 @@ func TestSendBlobTransaction(t *testing.T) {
func TestFillBlobTransaction(t *testing.T) {
t.Parallel()
+
+ testFillBlobTransaction(t, false)
+ testFillBlobTransaction(t, true)
+}
+
+func testFillBlobTransaction(t *testing.T, osaka bool) {
// Initialize test accounts
+ config := *params.MergedTestChainConfig
+ if !osaka {
+ config.OsakaTime = nil
+ }
var (
key, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a")
to = crypto.PubkeyToAddress(key.PublicKey)
genesis = &core.Genesis{
- Config: params.MergedTestChainConfig,
+ Config: &config,
Alloc: types.GenesisAlloc{},
}
- emptyBlob = new(kzg4844.Blob)
- emptyBlobs = []kzg4844.Blob{*emptyBlob}
- emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob)
- emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit)
- emptyBlobHash common.Hash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit)
+ emptyBlob = new(kzg4844.Blob)
+ emptyBlobs = []kzg4844.Blob{*emptyBlob}
+ emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob)
+ emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit)
+ emptyBlobCellProofs, _ = kzg4844.ComputeCellProofs(emptyBlob)
+ emptyBlobHash common.Hash = kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit)
+
+ fillEmptyKZGProofs = func(blobs int) []kzg4844.Proof {
+ if osaka {
+ return make([]kzg4844.Proof, blobs*kzg4844.CellProofsPerBlob)
+ }
+ return make([]kzg4844.Proof, blobs)
+ }
+ expectSidecar = func() *types.BlobTxSidecar {
+ if osaka {
+ return types.NewBlobTxSidecar(
+ types.BlobSidecarVersion1,
+ emptyBlobs,
+ []kzg4844.Commitment{emptyBlobCommit},
+ emptyBlobCellProofs,
+ )
+ }
+ return types.NewBlobTxSidecar(
+ types.BlobSidecarVersion0,
+ emptyBlobs,
+ []kzg4844.Commitment{emptyBlobCommit},
+ []kzg4844.Proof{emptyBlobProof},
+ )
+ }
)
b := newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {
b.SetPoS()
@@ -2742,7 +2785,7 @@ func TestFillBlobTransaction(t *testing.T) {
Commitments: []kzg4844.Commitment{{}, {}},
Proofs: []kzg4844.Proof{{}},
},
- err: `number of blobs and proofs mismatch (have=1, want=2)`,
+ err: fmt.Sprintf(`number of blobs and proofs mismatch (have=1, want=%d)`, len(fillEmptyKZGProofs(2))),
},
{
name: "TestInvalidProofVerification",
@@ -2752,7 +2795,7 @@ func TestFillBlobTransaction(t *testing.T) {
Value: (*hexutil.Big)(big.NewInt(1)),
Blobs: []kzg4844.Blob{{}, {}},
Commitments: []kzg4844.Commitment{{}, {}},
- Proofs: []kzg4844.Proof{{}, {}},
+ Proofs: fillEmptyKZGProofs(2),
},
err: `failed to verify blob proof: short buffer`,
},
@@ -2768,7 +2811,7 @@ func TestFillBlobTransaction(t *testing.T) {
},
want: &result{
Hashes: []common.Hash{emptyBlobHash},
- Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion0, emptyBlobs, []kzg4844.Commitment{emptyBlobCommit}, []kzg4844.Proof{emptyBlobProof}),
+ Sidecar: expectSidecar(),
},
},
{
@@ -2784,7 +2827,7 @@ func TestFillBlobTransaction(t *testing.T) {
},
want: &result{
Hashes: []common.Hash{emptyBlobHash},
- Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion0, emptyBlobs, []kzg4844.Commitment{emptyBlobCommit}, []kzg4844.Proof{emptyBlobProof}),
+ Sidecar: expectSidecar(),
},
},
{
@@ -2810,7 +2853,7 @@ func TestFillBlobTransaction(t *testing.T) {
},
want: &result{
Hashes: []common.Hash{emptyBlobHash},
- Sidecar: types.NewBlobTxSidecar(types.BlobSidecarVersion0, emptyBlobs, []kzg4844.Commitment{emptyBlobCommit}, []kzg4844.Proof{emptyBlobProof}),
+ Sidecar: expectSidecar(),
},
},
}
@@ -3150,21 +3193,6 @@ func TestRPCGetBlockOrHeader(t *testing.T) {
}
genBlocks = 10
signer = types.HomesteadSigner{}
- tx = types.NewTx(&types.LegacyTx{
- Nonce: 11,
- GasPrice: big.NewInt(11111),
- Gas: 1111,
- To: &acc2Addr,
- Value: big.NewInt(111),
- Data: []byte{0x11, 0x11, 0x11},
- })
- withdrawal = &types.Withdrawal{
- Index: 0,
- Validator: 1,
- Address: common.Address{0x12, 0x34},
- Amount: 10,
- }
- pending = types.NewBlock(&types.Header{Number: big.NewInt(11), Time: 42}, &types.Body{Transactions: types.Transactions{tx}, Withdrawals: types.Withdrawals{withdrawal}}, nil, blocktest.NewHasher())
)
backend := newTestBackend(t, genBlocks, genesis, ethash.NewFaker(), func(i int, b *core.BlockGen) {
// Transfer from account[0] to account[1]
@@ -3173,7 +3201,6 @@ func TestRPCGetBlockOrHeader(t *testing.T) {
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &acc2Addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), signer, acc1Key)
b.AddTx(tx)
})
- backend.setPendingBlock(pending)
api := NewBlockChainAPI(backend)
blockHashes := make([]common.Hash, genBlocks+1)
ctx := context.Background()
@@ -3184,7 +3211,7 @@ func TestRPCGetBlockOrHeader(t *testing.T) {
}
blockHashes[i] = header.Hash()
}
- pendingHash := pending.Hash()
+ pendingHash := backend.pending.Hash()
var testSuite = []struct {
blockNumber rpc.BlockNumber
@@ -3415,7 +3442,7 @@ func setupReceiptBackend(t *testing.T, genBlocks int) (*testBackend, []common.Ha
},
}
signer = types.LatestSignerForChainID(params.TestChainConfig.ChainID)
- txHashes = make([]common.Hash, genBlocks)
+ txHashes = make([]common.Hash, 0, genBlocks)
)
backend := newTestBackend(t, genBlocks, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {
@@ -3425,9 +3452,6 @@ func setupReceiptBackend(t *testing.T, genBlocks int) (*testBackend, []common.Ha
)
b.SetPoS()
switch i {
- case 0:
- // transfer 1000wei
- tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &acc2Addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), types.HomesteadSigner{}, acc1Key)
case 1:
// create contract
tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: nil, Gas: 53100, GasPrice: b.BaseFee(), Data: common.FromHex("0x60806040")}), signer, acc1Key)
@@ -3464,13 +3488,16 @@ func setupReceiptBackend(t *testing.T, genBlocks int) (*testBackend, []common.Ha
BlobHashes: []common.Hash{{1}},
Value: new(uint256.Int),
}), signer, acc1Key)
+ default:
+ // transfer 1000wei
+ tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &acc2Addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), types.HomesteadSigner{}, acc1Key)
}
if err != nil {
t.Errorf("failed to sign tx: %v", err)
}
if tx != nil {
b.AddTx(tx)
- txHashes[i] = tx.Hash()
+ txHashes = append(txHashes, tx.Hash())
}
})
return backend, txHashes
@@ -3586,6 +3613,11 @@ func TestRPCGetBlockReceipts(t *testing.T) {
test: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber),
file: "tag-latest",
},
+ // 3. pending tag
+ {
+ test: rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber),
+ file: "tag-pending",
+ },
// 4. block with legacy transfer tx(hash)
{
test: rpc.BlockNumberOrHashWithHash(blockHashes[1], false),
@@ -3735,3 +3767,130 @@ func TestCreateAccessListWithStateOverrides(t *testing.T) {
}}
require.Equal(t, expected, result.Accesslist)
}
+
+func TestEstimateGasWithMovePrecompile(t *testing.T) {
+ t.Parallel()
+ // Initialize test accounts
+ var (
+ accounts = newAccounts(2)
+ genesis = &core.Genesis{
+ Config: params.MergedTestChainConfig,
+ Alloc: types.GenesisAlloc{
+ accounts[0].addr: {Balance: big.NewInt(params.Ether)},
+ },
+ }
+ )
+ backend := newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {
+ b.SetPoS()
+ })
+ api := NewBlockChainAPI(backend)
+ // Move SHA256 precompile (0x2) to a new address (0x100)
+ // and estimate gas for calling the moved precompile.
+ var (
+ sha256Addr = common.BytesToAddress([]byte{0x2})
+ newSha256Addr = common.BytesToAddress([]byte{0x10, 0})
+ sha256Input = hexutil.Bytes([]byte("hello"))
+ args = TransactionArgs{
+ From: &accounts[0].addr,
+ To: &newSha256Addr,
+ Data: &sha256Input,
+ }
+ overrides = &override.StateOverride{
+ sha256Addr: override.OverrideAccount{
+ MovePrecompileTo: &newSha256Addr,
+ },
+ }
+ )
+ gas, err := api.EstimateGas(context.Background(), args, nil, overrides, nil)
+ if err != nil {
+ t.Fatalf("EstimateGas failed: %v", err)
+ }
+ if gas != 21366 {
+ t.Fatalf("mismatched gas: %d, want 21366", gas)
+ }
+}
+
+func TestEIP7910Config(t *testing.T) {
+ var (
+ newUint64 = func(val uint64) *uint64 { return &val }
+ // Define a snapshot of the current Hoodi config (only Prague scheduled) so that future forks do not
+ // cause this test to fail.
+ config = ¶ms.ChainConfig{
+ ChainID: big.NewInt(560048),
+ HomesteadBlock: big.NewInt(0),
+ DAOForkBlock: nil,
+ DAOForkSupport: true,
+ EIP150Block: big.NewInt(0),
+ EIP155Block: big.NewInt(0),
+ EIP158Block: big.NewInt(0),
+ ByzantiumBlock: big.NewInt(0),
+ ConstantinopleBlock: big.NewInt(0),
+ PetersburgBlock: big.NewInt(0),
+ IstanbulBlock: big.NewInt(0),
+ MuirGlacierBlock: big.NewInt(0),
+ BerlinBlock: big.NewInt(0),
+ LondonBlock: big.NewInt(0),
+ ArrowGlacierBlock: nil,
+ GrayGlacierBlock: nil,
+ TerminalTotalDifficulty: big.NewInt(0),
+ MergeNetsplitBlock: big.NewInt(0),
+ ShanghaiTime: newUint64(0),
+ CancunTime: newUint64(0),
+ PragueTime: newUint64(1742999832),
+ DepositContractAddress: common.HexToAddress("0x00000000219ab540356cBB839Cbe05303d7705Fa"),
+ Ethash: new(params.EthashConfig),
+ BlobScheduleConfig: ¶ms.BlobScheduleConfig{
+ Cancun: params.DefaultCancunBlobConfig,
+ Prague: params.DefaultPragueBlobConfig,
+ },
+ }
+ )
+ gspec := core.DefaultHoodiGenesisBlock()
+ gspec.Config = config
+
+ var testSuite = []struct {
+ time uint64
+ file string
+ }{
+ {
+ time: 0,
+ file: "next-and-last",
+ },
+ {
+ time: *gspec.Config.PragueTime,
+ file: "current",
+ },
+ }
+
+ for i, tt := range testSuite {
+ backend := configTimeBackend{nil, gspec, tt.time}
+ api := NewBlockChainAPI(backend)
+ result, err := api.Config(context.Background())
+ if err != nil {
+ t.Errorf("test %d: want no error, have %v", i, err)
+ continue
+ }
+ testRPCResponseWithFile(t, i, result, "eth_config", tt.file)
+ }
+}
+
+type configTimeBackend struct {
+ *testBackend
+ genesis *core.Genesis
+ time uint64
+}
+
+func (b configTimeBackend) ChainConfig() *params.ChainConfig {
+ return b.genesis.Config
+}
+
+func (b configTimeBackend) HeaderByNumber(_ context.Context, n rpc.BlockNumber) (*types.Header, error) {
+ if n == 0 {
+ return b.genesis.ToBlock().Header(), nil
+ }
+ panic("not implemented")
+}
+
+func (b configTimeBackend) CurrentHeader() *types.Header {
+ return &types.Header{Time: b.time}
+}
diff --git a/internal/ethapi/override/override.go b/internal/ethapi/override/override.go
index 70449c1644..0bfbcd9ce0 100644
--- a/internal/ethapi/override/override.go
+++ b/internal/ethapi/override/override.go
@@ -91,7 +91,7 @@ func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.Precompi
}
// Override account(contract) code.
if account.Code != nil {
- statedb.SetCode(addr, *account.Code)
+ statedb.SetCode(addr, *account.Code, tracing.CodeChangeUnspecified)
}
// Override account balance.
if account.Balance != nil {
diff --git a/internal/ethapi/override/override_test.go b/internal/ethapi/override/override_test.go
index 41b4f2c253..6feafaac75 100644
--- a/internal/ethapi/override/override_test.go
+++ b/internal/ethapi/override/override_test.go
@@ -31,14 +31,14 @@ import (
type precompileContract struct{}
-func (p *precompileContract) Name() string {
- panic("implement me")
-}
-
func (p *precompileContract) RequiredGas(input []byte) uint64 { return 0 }
func (p *precompileContract) Run(input []byte) ([]byte, error) { return nil, nil }
+func (p *precompileContract) Name() string {
+ panic("implement me")
+}
+
func TestStateOverrideMovePrecompile(t *testing.T) {
db := state.NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
statedb, err := state.New(types.EmptyRootHash, db)
diff --git a/internal/ethapi/testdata/eth_config-current.json b/internal/ethapi/testdata/eth_config-current.json
new file mode 100644
index 0000000000..0597c23e39
--- /dev/null
+++ b/internal/ethapi/testdata/eth_config-current.json
@@ -0,0 +1,40 @@
+{
+ "current": {
+ "activationTime": 1742999832,
+ "blobSchedule": {
+ "target": 6,
+ "max": 9,
+ "baseFeeUpdateFraction": 5007716
+ },
+ "chainId": "0x88bb0",
+ "forkId": "0x0929e24e",
+ "precompiles": {
+ "BLAKE2F": "0x0000000000000000000000000000000000000009",
+ "BLS12_G1ADD": "0x000000000000000000000000000000000000000b",
+ "BLS12_G1MSM": "0x000000000000000000000000000000000000000c",
+ "BLS12_G2ADD": "0x000000000000000000000000000000000000000d",
+ "BLS12_G2MSM": "0x000000000000000000000000000000000000000e",
+ "BLS12_MAP_FP2_TO_G2": "0x0000000000000000000000000000000000000011",
+ "BLS12_MAP_FP_TO_G1": "0x0000000000000000000000000000000000000010",
+ "BLS12_PAIRING_CHECK": "0x000000000000000000000000000000000000000f",
+ "BN254_ADD": "0x0000000000000000000000000000000000000006",
+ "BN254_MUL": "0x0000000000000000000000000000000000000007",
+ "BN254_PAIRING": "0x0000000000000000000000000000000000000008",
+ "ECREC": "0x0000000000000000000000000000000000000001",
+ "ID": "0x0000000000000000000000000000000000000004",
+ "KZG_POINT_EVALUATION": "0x000000000000000000000000000000000000000a",
+ "MODEXP": "0x0000000000000000000000000000000000000005",
+ "RIPEMD160": "0x0000000000000000000000000000000000000003",
+ "SHA256": "0x0000000000000000000000000000000000000002"
+ },
+ "systemContracts": {
+ "BEACON_ROOTS_ADDRESS": "0x000f3df6d732807ef1319fb7b8bb8522d0beac02",
+ "CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS": "0x0000bbddc7ce488642fb579f8b00f3a590007251",
+ "DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa",
+ "HISTORY_STORAGE_ADDRESS": "0x0000f90827f1c53a10cb7a02335b175320002935",
+ "WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS": "0x00000961ef480eb55e80d19ad83579a64c007002"
+ }
+ },
+ "next": null,
+ "last": null
+}
diff --git a/internal/ethapi/testdata/eth_config-next-and-last.json b/internal/ethapi/testdata/eth_config-next-and-last.json
new file mode 100644
index 0000000000..81869ba174
--- /dev/null
+++ b/internal/ethapi/testdata/eth_config-next-and-last.json
@@ -0,0 +1,99 @@
+{
+ "current": {
+ "activationTime": 0,
+ "blobSchedule": {
+ "baseFeeUpdateFraction": 3338477,
+ "max": 6,
+ "target": 3
+ },
+ "chainId": "0x88bb0",
+ "forkId": "0xbef71d30",
+ "precompiles": {
+ "BLAKE2F": "0x0000000000000000000000000000000000000009",
+ "BN254_ADD": "0x0000000000000000000000000000000000000006",
+ "BN254_MUL": "0x0000000000000000000000000000000000000007",
+ "BN254_PAIRING": "0x0000000000000000000000000000000000000008",
+ "ECREC": "0x0000000000000000000000000000000000000001",
+ "ID": "0x0000000000000000000000000000000000000004",
+ "KZG_POINT_EVALUATION": "0x000000000000000000000000000000000000000a",
+ "MODEXP": "0x0000000000000000000000000000000000000005",
+ "RIPEMD160": "0x0000000000000000000000000000000000000003",
+ "SHA256": "0x0000000000000000000000000000000000000002"
+ },
+ "systemContracts": {
+ "BEACON_ROOTS_ADDRESS": "0x000f3df6d732807ef1319fb7b8bb8522d0beac02"
+ }
+ },
+ "next": {
+ "activationTime": 1742999832,
+ "blobSchedule": {
+ "baseFeeUpdateFraction": 5007716,
+ "max": 9,
+ "target": 6
+ },
+ "chainId": "0x88bb0",
+ "forkId": "0x0929e24e",
+ "precompiles": {
+ "BLAKE2F": "0x0000000000000000000000000000000000000009",
+ "BLS12_G1ADD": "0x000000000000000000000000000000000000000b",
+ "BLS12_G1MSM": "0x000000000000000000000000000000000000000c",
+ "BLS12_G2ADD": "0x000000000000000000000000000000000000000d",
+ "BLS12_G2MSM": "0x000000000000000000000000000000000000000e",
+ "BLS12_MAP_FP2_TO_G2": "0x0000000000000000000000000000000000000011",
+ "BLS12_MAP_FP_TO_G1": "0x0000000000000000000000000000000000000010",
+ "BLS12_PAIRING_CHECK": "0x000000000000000000000000000000000000000f",
+ "BN254_ADD": "0x0000000000000000000000000000000000000006",
+ "BN254_MUL": "0x0000000000000000000000000000000000000007",
+ "BN254_PAIRING": "0x0000000000000000000000000000000000000008",
+ "ECREC": "0x0000000000000000000000000000000000000001",
+ "ID": "0x0000000000000000000000000000000000000004",
+ "KZG_POINT_EVALUATION": "0x000000000000000000000000000000000000000a",
+ "MODEXP": "0x0000000000000000000000000000000000000005",
+ "RIPEMD160": "0x0000000000000000000000000000000000000003",
+ "SHA256": "0x0000000000000000000000000000000000000002"
+ },
+ "systemContracts": {
+ "BEACON_ROOTS_ADDRESS": "0x000f3df6d732807ef1319fb7b8bb8522d0beac02",
+ "CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS": "0x0000bbddc7ce488642fb579f8b00f3a590007251",
+ "DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa",
+ "HISTORY_STORAGE_ADDRESS": "0x0000f90827f1c53a10cb7a02335b175320002935",
+ "WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS": "0x00000961ef480eb55e80d19ad83579a64c007002"
+ }
+ },
+ "last": {
+ "activationTime": 1742999832,
+ "blobSchedule": {
+ "baseFeeUpdateFraction": 5007716,
+ "max": 9,
+ "target": 6
+ },
+ "chainId": "0x88bb0",
+ "forkId": "0x0929e24e",
+ "precompiles": {
+ "BLAKE2F": "0x0000000000000000000000000000000000000009",
+ "BLS12_G1ADD": "0x000000000000000000000000000000000000000b",
+ "BLS12_G1MSM": "0x000000000000000000000000000000000000000c",
+ "BLS12_G2ADD": "0x000000000000000000000000000000000000000d",
+ "BLS12_G2MSM": "0x000000000000000000000000000000000000000e",
+ "BLS12_MAP_FP2_TO_G2": "0x0000000000000000000000000000000000000011",
+ "BLS12_MAP_FP_TO_G1": "0x0000000000000000000000000000000000000010",
+ "BLS12_PAIRING_CHECK": "0x000000000000000000000000000000000000000f",
+ "BN254_ADD": "0x0000000000000000000000000000000000000006",
+ "BN254_MUL": "0x0000000000000000000000000000000000000007",
+ "BN254_PAIRING": "0x0000000000000000000000000000000000000008",
+ "ECREC": "0x0000000000000000000000000000000000000001",
+ "ID": "0x0000000000000000000000000000000000000004",
+ "KZG_POINT_EVALUATION": "0x000000000000000000000000000000000000000a",
+ "MODEXP": "0x0000000000000000000000000000000000000005",
+ "RIPEMD160": "0x0000000000000000000000000000000000000003",
+ "SHA256": "0x0000000000000000000000000000000000000002"
+ },
+ "systemContracts": {
+ "BEACON_ROOTS_ADDRESS": "0x000f3df6d732807ef1319fb7b8bb8522d0beac02",
+ "CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS": "0x0000bbddc7ce488642fb579f8b00f3a590007251",
+ "DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa",
+ "HISTORY_STORAGE_ADDRESS": "0x0000f90827f1c53a10cb7a02335b175320002935",
+ "WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS": "0x00000961ef480eb55e80d19ad83579a64c007002"
+ }
+ }
+}
diff --git a/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json b/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json
index 60f18bc114..2e323dcfe7 100644
--- a/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json
+++ b/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json
@@ -1,49 +1,40 @@
{
- "difficulty": "0x0",
+ "baseFeePerGas": "0xde56ab3",
+ "difficulty": "0x20000",
"extraData": "0x",
- "gasLimit": "0x0",
- "gasUsed": "0x0",
+ "gasLimit": "0x47e7c4",
+ "gasUsed": "0x5208",
"hash": null,
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner": null,
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": null,
"number": "0xb",
- "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
+ "parentHash": "0xa063415a5020f1569fae73ecb0d37bc5649ebe86d59e764a389eb37814bd42cb",
+ "receiptsRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
- "size": "0x256",
- "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "timestamp": "0x2a",
+ "size": "0x26a",
+ "stateRoot": "0xce0e05397e548614a5b93254662174329466f8f4b1b391eb36fec9a7a591e58e",
+ "timestamp": "0x6e",
"transactions": [
{
- "blockHash": "0x6cebd9f966ea686f44b981685e3f0eacea28591a7a86d7fbbe521a86e9f81165",
+ "blockHash": "0xfda6c7cb7a3a712e0c424909a7724cab0448e89e286617fa8d5fd27f63f28bd2",
"blockNumber": "0xb",
- "from": "0x0000000000000000000000000000000000000000",
- "gas": "0x457",
- "gasPrice": "0x2b67",
- "hash": "0x4afee081df5dff7a025964032871f7d4ba4d21baf5f6376a2f4a9f79fc506298",
- "input": "0x111111",
- "nonce": "0xb",
+ "from": "0x703c4b2bd70c169f5717101caee543299fc946c7",
+ "gas": "0x5208",
+ "gasPrice": "0xde56ab3",
+ "hash": "0xd773fbb47ec87b1a958ac16430943ddf2797ecae2b33fe7b16ddb334e30325ed",
+ "input": "0x",
+ "nonce": "0xa",
"to": "0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e",
"transactionIndex": "0x0",
- "value": "0x6f",
+ "value": "0x3e8",
"type": "0x0",
- "chainId": "0x7fffffffffffffee",
- "v": "0x0",
- "r": "0x0",
- "s": "0x0"
+ "v": "0x1c",
+ "r": "0xfa029dacd66238d20cd649fe3b323bb458d2cfa4af7db0ff4f6b3e1039bc320a",
+ "s": "0x52fb4d45c1d623f2f05508bae063a4728761d762ae45b8b0908ffea546f3d95e"
}
],
- "transactionsRoot": "0x98d9f6dd0aa479c0fb448f2627e9f1964aca699fccab8f6e95861547a4699e37",
- "uncles": [],
- "withdrawals": [
- {
- "index": "0x0",
- "validatorIndex": "0x1",
- "address": "0x1234000000000000000000000000000000000000",
- "amount": "0xa"
- }
- ],
- "withdrawalsRoot": "0x73d756269cdfc22e7e17a3548e36f42f750ca06d7e3cd98d1b6d0eb5add9dc84"
+ "transactionsRoot": "0x59abb8ec0655f66e66450d1502618bc64022ae2d2950fa471eec6e8da2846264",
+ "uncles": []
}
\ No newline at end of file
diff --git a/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending.json b/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending.json
index dda2d93213..3254482cd9 100644
--- a/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending.json
+++ b/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending.json
@@ -1,32 +1,24 @@
{
- "difficulty": "0x0",
+ "baseFeePerGas": "0xde56ab3",
+ "difficulty": "0x20000",
"extraData": "0x",
- "gasLimit": "0x0",
- "gasUsed": "0x0",
+ "gasLimit": "0x47e7c4",
+ "gasUsed": "0x5208",
"hash": null,
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner": null,
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": null,
"number": "0xb",
- "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
+ "parentHash": "0xa063415a5020f1569fae73ecb0d37bc5649ebe86d59e764a389eb37814bd42cb",
+ "receiptsRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
- "size": "0x256",
- "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "timestamp": "0x2a",
+ "size": "0x26a",
+ "stateRoot": "0xce0e05397e548614a5b93254662174329466f8f4b1b391eb36fec9a7a591e58e",
+ "timestamp": "0x6e",
"transactions": [
- "0x4afee081df5dff7a025964032871f7d4ba4d21baf5f6376a2f4a9f79fc506298"
+ "0xd773fbb47ec87b1a958ac16430943ddf2797ecae2b33fe7b16ddb334e30325ed"
],
- "transactionsRoot": "0x98d9f6dd0aa479c0fb448f2627e9f1964aca699fccab8f6e95861547a4699e37",
- "uncles": [],
- "withdrawals": [
- {
- "index": "0x0",
- "validatorIndex": "0x1",
- "address": "0x1234000000000000000000000000000000000000",
- "amount": "0xa"
- }
- ],
- "withdrawalsRoot": "0x73d756269cdfc22e7e17a3548e36f42f750ca06d7e3cd98d1b6d0eb5add9dc84"
+ "transactionsRoot": "0x59abb8ec0655f66e66450d1502618bc64022ae2d2950fa471eec6e8da2846264",
+ "uncles": []
}
\ No newline at end of file
diff --git a/internal/ethapi/testdata/eth_getBlockReceipts-tag-pending.json b/internal/ethapi/testdata/eth_getBlockReceipts-tag-pending.json
new file mode 100644
index 0000000000..75f9f3ad99
--- /dev/null
+++ b/internal/ethapi/testdata/eth_getBlockReceipts-tag-pending.json
@@ -0,0 +1,18 @@
+[
+ {
+ "blockHash": "0xc74cf882395ec92eec3673d93a57f9a3bf1a5e696fae3e52f252059af62756c8",
+ "blockNumber": "0x7",
+ "contractAddress": null,
+ "cumulativeGasUsed": "0x5208",
+ "effectiveGasPrice": "0x17b07ddf",
+ "from": "0x703c4b2bd70c169f5717101caee543299fc946c7",
+ "gasUsed": "0x5208",
+ "logs": [],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "status": "0x1",
+ "to": "0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e",
+ "transactionHash": "0xa7eeffe8111539a8f9725eb4d49e341efa1287d33190300adab220929daa5fac",
+ "transactionIndex": "0x0",
+ "type": "0x0"
+ }
+]
\ No newline at end of file
diff --git a/internal/ethapi/testdata/eth_getHeaderByNumber-tag-pending.json b/internal/ethapi/testdata/eth_getHeaderByNumber-tag-pending.json
index 289ff5fece..e4121824ef 100644
--- a/internal/ethapi/testdata/eth_getHeaderByNumber-tag-pending.json
+++ b/internal/ethapi/testdata/eth_getHeaderByNumber-tag-pending.json
@@ -1,19 +1,19 @@
{
- "difficulty": "0x0",
+ "baseFeePerGas": "0xde56ab3",
+ "difficulty": "0x20000",
"extraData": "0x",
- "gasLimit": "0x0",
- "gasUsed": "0x0",
+ "gasLimit": "0x47e7c4",
+ "gasUsed": "0x5208",
"hash": null,
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner": null,
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": null,
"number": "0xb",
- "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
+ "parentHash": "0xa063415a5020f1569fae73ecb0d37bc5649ebe86d59e764a389eb37814bd42cb",
+ "receiptsRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
- "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
- "timestamp": "0x2a",
- "transactionsRoot": "0x98d9f6dd0aa479c0fb448f2627e9f1964aca699fccab8f6e95861547a4699e37",
- "withdrawalsRoot": "0x73d756269cdfc22e7e17a3548e36f42f750ca06d7e3cd98d1b6d0eb5add9dc84"
+ "stateRoot": "0xce0e05397e548614a5b93254662174329466f8f4b1b391eb36fec9a7a591e58e",
+ "timestamp": "0x6e",
+ "transactionsRoot": "0x59abb8ec0655f66e66450d1502618bc64022ae2d2950fa471eec6e8da2846264"
}
\ No newline at end of file
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 3502f56244..5ab49ce203 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -70,9 +70,6 @@ type TransactionArgs struct {
// For SetCodeTxType
AuthorizationList []types.SetCodeAuthorization `json:"authorizationList"`
-
- // This configures whether blobs are allowed to be passed.
- blobSidecarAllowed bool
}
// from retrieves the transaction sender address.
@@ -94,9 +91,17 @@ func (args *TransactionArgs) data() []byte {
return nil
}
+// sidecarConfig defines the options for deriving missing fields of transactions.
+type sidecarConfig struct {
+ // This configures whether blobs are allowed to be passed and
+ // the associated sidecar version should be attached.
+ blobSidecarAllowed bool
+ blobSidecarVersion byte
+}
+
// setDefaults fills in default values for unspecified tx fields.
-func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGasEstimation bool) error {
- if err := args.setBlobTxSidecar(ctx); err != nil {
+func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, config sidecarConfig) error {
+ if err := args.setBlobTxSidecar(ctx, config); err != nil {
return err
}
if err := args.setFeeDefaults(ctx, b, b.CurrentHeader()); err != nil {
@@ -119,11 +124,10 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGas
// BlobTx fields
if args.BlobHashes != nil && len(args.BlobHashes) == 0 {
- return errors.New(`need at least 1 blob for a blob transaction`)
+ return errors.New("need at least 1 blob for a blob transaction")
}
- maxBlobs := eip4844.MaxBlobsPerBlock(b.ChainConfig(), b.CurrentHeader().Time)
- if args.BlobHashes != nil && len(args.BlobHashes) > maxBlobs {
- return fmt.Errorf(`too many blobs in transaction (have=%d, max=%d)`, len(args.BlobHashes), maxBlobs)
+ if args.BlobHashes != nil && len(args.BlobHashes) > params.BlobTxMaxBlobs {
+ return fmt.Errorf("too many blobs in transaction (have=%d, max=%d)", len(args.BlobHashes), params.BlobTxMaxBlobs)
}
// create check
@@ -137,36 +141,28 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend, skipGas
}
if args.Gas == nil {
- if skipGasEstimation { // Skip gas usage estimation if a precise gas limit is not critical, e.g., in non-transaction calls.
- gas := hexutil.Uint64(b.RPCGasCap())
- if gas == 0 {
- gas = hexutil.Uint64(math.MaxUint64 / 2)
- }
- args.Gas = &gas
- } else { // Estimate the gas usage otherwise.
- // These fields are immutable during the estimation, safe to
- // pass the pointer directly.
- data := args.data()
- callArgs := TransactionArgs{
- From: args.From,
- To: args.To,
- GasPrice: args.GasPrice,
- MaxFeePerGas: args.MaxFeePerGas,
- MaxPriorityFeePerGas: args.MaxPriorityFeePerGas,
- Value: args.Value,
- Data: (*hexutil.Bytes)(&data),
- AccessList: args.AccessList,
- BlobFeeCap: args.BlobFeeCap,
- BlobHashes: args.BlobHashes,
- }
- latestBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
- estimated, err := DoEstimateGas(ctx, b, callArgs, latestBlockNr, nil, nil, b.RPCGasCap())
- if err != nil {
- return err
- }
- args.Gas = &estimated
- log.Trace("Estimate gas usage automatically", "gas", args.Gas)
+ // These fields are immutable during the estimation, safe to
+ // pass the pointer directly.
+ data := args.data()
+ callArgs := TransactionArgs{
+ From: args.From,
+ To: args.To,
+ GasPrice: args.GasPrice,
+ MaxFeePerGas: args.MaxFeePerGas,
+ MaxPriorityFeePerGas: args.MaxPriorityFeePerGas,
+ Value: args.Value,
+ Data: (*hexutil.Bytes)(&data),
+ AccessList: args.AccessList,
+ BlobFeeCap: args.BlobFeeCap,
+ BlobHashes: args.BlobHashes,
}
+ latestBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
+ estimated, err := DoEstimateGas(ctx, b, callArgs, latestBlockNr, nil, nil, b.RPCGasCap())
+ if err != nil {
+ return err
+ }
+ args.Gas = &estimated
+ log.Trace("Estimated gas usage automatically", "gas", args.Gas)
}
// If chain id is provided, ensure it matches the local chain id. Otherwise, set the local
@@ -283,18 +279,17 @@ func (args *TransactionArgs) setLondonFeeDefaults(ctx context.Context, head *typ
}
// setBlobTxSidecar adds the blob tx
-func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context) error {
+func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context, config sidecarConfig) error {
// No blobs, we're done.
if args.Blobs == nil {
return nil
}
// Passing blobs is not allowed in all contexts, only in specific methods.
- if !args.blobSidecarAllowed {
+ if !config.blobSidecarAllowed {
return errors.New(`"blobs" is not supported for this RPC method`)
}
- n := len(args.Blobs)
// Assume user provides either only blobs (w/o hashes), or
// blobs together with commitments and proofs.
if args.Commitments == nil && args.Proofs != nil {
@@ -303,43 +298,77 @@ func (args *TransactionArgs) setBlobTxSidecar(ctx context.Context) error {
return errors.New(`blob commitments provided while proofs were not`)
}
- // len(blobs) == len(commitments) == len(proofs) == len(hashes)
- if args.Commitments != nil && len(args.Commitments) != n {
- return fmt.Errorf("number of blobs and commitments mismatch (have=%d, want=%d)", len(args.Commitments), n)
- }
- if args.Proofs != nil && len(args.Proofs) != n {
- return fmt.Errorf("number of blobs and proofs mismatch (have=%d, want=%d)", len(args.Proofs), n)
- }
+ // len(blobs) == len(commitments) == len(hashes)
+ n := len(args.Blobs)
if args.BlobHashes != nil && len(args.BlobHashes) != n {
return fmt.Errorf("number of blobs and hashes mismatch (have=%d, want=%d)", len(args.BlobHashes), n)
}
+ if args.Commitments != nil && len(args.Commitments) != n {
+ return fmt.Errorf("number of blobs and commitments mismatch (have=%d, want=%d)", len(args.Commitments), n)
+ }
+ // if V0: len(blobs) == len(proofs)
+ // if V1: len(blobs) == len(proofs) * 128
+ proofLen := n
+ if config.blobSidecarVersion == types.BlobSidecarVersion1 {
+ proofLen = n * kzg4844.CellProofsPerBlob
+ }
+ if args.Proofs != nil && len(args.Proofs) != proofLen {
+ if len(args.Proofs) != n {
+ return fmt.Errorf("number of blobs and proofs mismatch (have=%d, want=%d)", len(args.Proofs), proofLen)
+ }
+ // Unset the commitments and proofs, as they may be submitted in the legacy format
+ log.Debug("Unset legacy commitments and proofs", "blobs", n, "proofs", len(args.Proofs))
+ args.Commitments, args.Proofs = nil, nil
+ }
+
+ // Generate commitments and proofs if they are missing, or validate them if they
+ // are provided.
if args.Commitments == nil {
- // Generate commitment and proof.
- commitments := make([]kzg4844.Commitment, n)
- proofs := make([]kzg4844.Proof, n)
+ var (
+ commitments = make([]kzg4844.Commitment, n)
+ proofs = make([]kzg4844.Proof, 0, proofLen)
+ )
for i, b := range args.Blobs {
c, err := kzg4844.BlobToCommitment(&b)
if err != nil {
return fmt.Errorf("blobs[%d]: error computing commitment: %v", i, err)
}
commitments[i] = c
- p, err := kzg4844.ComputeBlobProof(&b, c)
- if err != nil {
- return fmt.Errorf("blobs[%d]: error computing proof: %v", i, err)
+
+ switch config.blobSidecarVersion {
+ case types.BlobSidecarVersion0:
+ p, err := kzg4844.ComputeBlobProof(&b, c)
+ if err != nil {
+ return fmt.Errorf("blobs[%d]: error computing proof: %v", i, err)
+ }
+ proofs = append(proofs, p)
+ case types.BlobSidecarVersion1:
+ ps, err := kzg4844.ComputeCellProofs(&b)
+ if err != nil {
+ return fmt.Errorf("blobs[%d]: error computing proof: %v", i, err)
+ }
+ proofs = append(proofs, ps...)
}
- proofs[i] = p
}
args.Commitments = commitments
args.Proofs = proofs
} else {
- for i, b := range args.Blobs {
- if err := kzg4844.VerifyBlobProof(&b, args.Commitments[i], args.Proofs[i]); err != nil {
+ switch config.blobSidecarVersion {
+ case types.BlobSidecarVersion0:
+ for i, b := range args.Blobs {
+ if err := kzg4844.VerifyBlobProof(&b, args.Commitments[i], args.Proofs[i]); err != nil {
+ return fmt.Errorf("failed to verify blob proof: %v", err)
+ }
+ }
+ case types.BlobSidecarVersion1:
+ if err := kzg4844.VerifyCellProofs(args.Blobs, args.Commitments, args.Proofs); err != nil {
return fmt.Errorf("failed to verify blob proof: %v", err)
}
}
}
+ // Generate blob hashes if they are missing, or validate them if they are provided.
hashes := make([]common.Hash, n)
hasher := sha256.New()
for i, c := range args.Commitments {
@@ -544,8 +573,11 @@ func (args *TransactionArgs) ToTransaction(defaultType int, isPrague1 bool, dist
BlobFeeCap: uint256.MustFromBig((*big.Int)(args.BlobFeeCap)),
}
if args.Blobs != nil {
- // TODO(rjl493456442, marius) support V1
- data.(*types.BlobTx).Sidecar = types.NewBlobTxSidecar(types.BlobSidecarVersion0, args.Blobs, args.Commitments, args.Proofs)
+ version := types.BlobSidecarVersion0
+ if len(args.Proofs) == len(args.Blobs)*kzg4844.CellProofsPerBlob {
+ version = types.BlobSidecarVersion1
+ }
+ data.(*types.BlobTx).Sidecar = types.NewBlobTxSidecar(version, args.Blobs, args.Commitments, args.Proofs)
}
case types.DynamicFeeTxType:
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index d7f37a79ee..0aedffe230 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -217,11 +217,6 @@ web3._extend({
call: 'debug_setHead',
params: 1
}),
- new web3._extend.Method({
- name: 'seedHash',
- call: 'debug_seedHash',
- params: 1
- }),
new web3._extend.Method({
name: 'dumpBlock',
call: 'debug_dumpBlock',
@@ -473,6 +468,12 @@ web3._extend({
call: 'debug_sync',
params: 1
}),
+ new web3._extend.Method({
+ name: 'stateSize',
+ call: 'debug_stateSize',
+ params: 1,
+ inputFormatter: [null],
+ }),
],
properties: []
});
@@ -600,6 +601,11 @@ web3._extend({
call: 'eth_getBlockReceipts',
params: 1,
}),
+ new web3._extend.Method({
+ name: 'config',
+ call: 'eth_config',
+ params: 0,
+ })
],
properties: [
new web3._extend.Property({
diff --git a/metrics/cputime_nop.go b/metrics/cputime_nop.go
index 465d88c4d2..a6285ec10a 100644
--- a/metrics/cputime_nop.go
+++ b/metrics/cputime_nop.go
@@ -14,8 +14,8 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-//go:build windows || js
-// +build windows js
+//go:build windows || js || tinygo
+// +build windows js tinygo
package metrics
diff --git a/metrics/cputime_unix.go b/metrics/cputime_unix.go
index a44bf80876..5db38b16a2 100644
--- a/metrics/cputime_unix.go
+++ b/metrics/cputime_unix.go
@@ -14,8 +14,8 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see .
-//go:build !windows && !js && !wasip1
-// +build !windows,!js,!wasip1
+//go:build !windows && !js && !wasip1 && !tinygo
+// +build !windows,!js,!wasip1,!tinygo
package metrics
diff --git a/miner/miner.go b/miner/miner.go
index 5ef44a8242..ca6ac91bab 100644
--- a/miner/miner.go
+++ b/miner/miner.go
@@ -149,10 +149,10 @@ func (miner *Miner) getPending() *newPayloadResult {
header := miner.chain.CurrentHeader()
miner.pendingMu.Lock()
defer miner.pendingMu.Unlock()
+
if cached := miner.pending.resolve(header.Hash()); cached != nil {
return cached
}
-
var (
timestamp = uint64(time.Now().Unix())
withdrawal types.Withdrawals
diff --git a/miner/worker.go b/miner/worker.go
index 1a65d59099..cdd4fe847f 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -276,7 +276,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
if err != nil {
return nil, err
}
- state.StartPrefetcher("miner", bundle)
+ state.StartPrefetcher("miner", bundle, nil)
}
// Note the passed coinbase may be different with header.Coinbase.
return &environment{
@@ -356,7 +356,6 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*
func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error {
var (
- isOsaka = miner.chainConfig.IsOsaka(env.header.Number, env.header.Time)
isCancun = miner.chainConfig.IsCancun(env.header.Number, env.header.Time)
)
for {
@@ -433,21 +432,6 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran
if !env.txFitsSize(tx) {
break
}
-
- // Make sure all transactions after osaka have cell proofs
- if isOsaka {
- if sidecar := tx.BlobTxSidecar(); sidecar != nil {
- if sidecar.Version == types.BlobSidecarVersion0 {
- log.Info("Including blob tx with v0 sidecar, recomputing proofs", "hash", ltx.Hash)
- if err := sidecar.ToV1(); err != nil {
- txs.Pop()
- log.Warn("Failed to recompute cell proofs", "hash", ltx.Hash, "err", err)
- continue
- }
- }
- }
- }
-
// Error may be ignored here. The error has already been checked
// during transaction acceptance in the transaction pool.
from, _ := types.Sender(env.signer, tx)
diff --git a/node/api.go b/node/api.go
index 33dfb3a1cc..e5dda5ac4d 100644
--- a/node/api.go
+++ b/node/api.go
@@ -191,7 +191,7 @@ func (api *adminAPI) StartHTTP(host *string, port *int, cors *string, apis *stri
}
if vhosts != nil {
config.Vhosts = nil
- for _, vhost := range strings.Split(*host, ",") {
+ for _, vhost := range strings.Split(*vhosts, ",") {
config.Vhosts = append(config.Vhosts, strings.TrimSpace(vhost))
}
}
diff --git a/node/node.go b/node/node.go
index f0c5db274b..b43cc69b3c 100644
--- a/node/node.go
+++ b/node/node.go
@@ -756,7 +756,7 @@ func (n *Node) EventMux() *event.TypeMux {
return n.eventmux
}
-// OpenDatabase opens an existing database with the given name (or creates one if no
+// OpenDatabaseWithOptions opens an existing database with the given name (or creates one if no
// previous can be found) from within the node's instance directory. If the node has no
// data directory, an in-memory database is returned.
func (n *Node) OpenDatabaseWithOptions(name string, opt DatabaseOptions) (ethdb.Database, error) {
diff --git a/p2p/config.go b/p2p/config.go
index 68a9c0bb5f..17607a1f88 100644
--- a/p2p/config.go
+++ b/p2p/config.go
@@ -35,8 +35,7 @@ type Config struct {
// This field must be set to a valid secp256k1 private key.
PrivateKey *ecdsa.PrivateKey `toml:"-"`
- // MaxPeers is the maximum number of peers that can be
- // connected. It must be greater than zero.
+ // MaxPeers is the maximum number of peers that can be connected.
MaxPeers int
// MaxPendingPeers is the maximum number of peers that can be pending in the
diff --git a/p2p/discover/v5_udp_test.go b/p2p/discover/v5_udp_test.go
index 3a384aab12..6abe20d7a4 100644
--- a/p2p/discover/v5_udp_test.go
+++ b/p2p/discover/v5_udp_test.go
@@ -378,9 +378,93 @@ func TestUDPv5_findnodeCall(t *testing.T) {
if !reflect.DeepEqual(response, nodes) {
t.Fatalf("wrong nodes in response")
}
+}
- // TODO: check invalid IPs
- // TODO: check invalid/unsigned record
+// BadIdentityScheme mocks an identity scheme not supported by the test node.
+type BadIdentityScheme struct{}
+
+func (s BadIdentityScheme) Verify(r *enr.Record, sig []byte) error { return nil }
+func (s BadIdentityScheme) NodeAddr(r *enr.Record) []byte {
+ var id enode.ID
+ r.Load(enr.WithEntry("badaddr", &id))
+ return id[:]
+}
+
+// This test covers invalid NODES responses for the FINDNODE call in a single table-driven test.
+func TestUDPv5_findnodeCall_InvalidNodes(t *testing.T) {
+ t.Parallel()
+ test := newUDPV5Test(t)
+ defer test.close()
+
+ for i, tt := range []struct {
+ name string
+ ip enr.Entry
+ port enr.Entry
+ sign func(r *enr.Record, id enode.ID) *enode.Node
+ }{
+ {
+ name: "invalid ip (unspecified 0.0.0.0)",
+ ip: enr.IP(net.IPv4zero),
+ },
+ {
+ name: "invalid udp port (<=1024)",
+ port: enr.UDP(1024),
+ },
+ {
+ name: "invalid record, no signature",
+ sign: func(r *enr.Record, id enode.ID) *enode.Node {
+ r.Set(enr.ID("bad"))
+ r.Set(enr.WithEntry("badaddr", id))
+ r.SetSig(BadIdentityScheme{}, []byte{})
+ n, _ := enode.New(BadIdentityScheme{}, r)
+ return n
+ },
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ // Build ENR node for test.
+ var (
+ distance = 230
+ remote = test.getNode(test.remotekey, test.remoteaddr).Node()
+ id = idAtDistance(remote.ID(), distance)
+ r enr.Record
+ )
+ r.Set(enr.IP(intIP(i)))
+ if tt.ip != nil {
+ r.Set(tt.ip)
+ }
+ r.Set(enr.UDP(30303))
+ if tt.port != nil {
+ r.Set(tt.port)
+ }
+ r = *enode.SignNull(&r, id).Record()
+ if tt.sign != nil {
+ r = *tt.sign(&r, id).Record()
+ }
+
+ // Launch findnode request.
+ var (
+ done = make(chan error, 1)
+ got []*enode.Node
+ )
+ go func() {
+ var err error
+ got, err = test.udp.Findnode(remote, []uint{uint(distance)})
+ done <- err
+ }()
+
+ // Handle request.
+ test.waitPacketOut(func(p *v5wire.Findnode, _ netip.AddrPort, _ v5wire.Nonce) {
+ test.packetIn(&v5wire.Nodes{ReqID: p.ReqID, RespCount: 1, Nodes: []*enr.Record{&r}})
+ })
+ if err := <-done; err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(got) != 0 {
+ t.Fatalf("expected 0 nodes, got %d", len(got))
+ }
+ })
+ }
}
// This test checks that pending calls are re-sent when a handshake happens.
diff --git a/p2p/discover/v5wire/encoding.go b/p2p/discover/v5wire/encoding.go
index ec5ef8a261..08292a70ba 100644
--- a/p2p/discover/v5wire/encoding.go
+++ b/p2p/discover/v5wire/encoding.go
@@ -27,6 +27,7 @@ import (
"errors"
"fmt"
"hash"
+ "slices"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/p2p/enode"
@@ -222,7 +223,7 @@ func (c *Codec) Encode(id enode.ID, addr string, packet Packet, challenge *Whoar
// Store sent WHOAREYOU challenges.
if challenge, ok := packet.(*Whoareyou); ok {
- challenge.ChallengeData = bytesCopy(&c.buf)
+ challenge.ChallengeData = slices.Clone(c.buf.Bytes())
enc, err := c.EncodeRaw(id, head, msgData)
if err != nil {
return nil, Nonce{}, err
@@ -325,7 +326,7 @@ func (c *Codec) encodeWhoareyou(toID enode.ID, packet *Whoareyou) (Header, error
// Create header.
head := c.makeHeader(toID, flagWhoareyou, 0)
- head.AuthData = bytesCopy(&c.buf)
+ head.AuthData = slices.Clone(c.buf.Bytes())
head.Nonce = packet.Nonce
// Encode auth data.
@@ -430,7 +431,7 @@ func (c *Codec) encodeMessageHeader(toID enode.ID, s *session) (Header, error) {
auth := messageAuthData{SrcID: c.localnode.ID()}
c.buf.Reset()
binary.Write(&c.buf, binary.BigEndian, &auth)
- head.AuthData = bytesCopy(&c.buf)
+ head.AuthData = slices.Clone(c.buf.Bytes())
head.Nonce = nonce
return head, err
}
@@ -686,9 +687,3 @@ func (h *Header) mask(destID enode.ID) cipher.Stream {
}
return cipher.NewCTR(block, h.IV[:])
}
-
-func bytesCopy(r *bytes.Buffer) []byte {
- b := make([]byte, r.Len())
- copy(b, r.Bytes())
- return b
-}
diff --git a/p2p/enode/iter.go b/p2p/enode/iter.go
index f8f79a9436..4890321f49 100644
--- a/p2p/enode/iter.go
+++ b/p2p/enode/iter.go
@@ -38,7 +38,7 @@ type SourceIterator interface {
NodeSource() string // source of current node
}
-// WithSource attaches a 'source name' to an iterator.
+// WithSourceName attaches a 'source name' to an iterator.
func WithSourceName(name string, it Iterator) SourceIterator {
return sourceIter{it, name}
}
diff --git a/p2p/msgrate/msgrate.go b/p2p/msgrate/msgrate.go
index de1a3177db..7702256ed4 100644
--- a/p2p/msgrate/msgrate.go
+++ b/p2p/msgrate/msgrate.go
@@ -171,8 +171,7 @@ func (t *Tracker) Capacity(kind uint64, targetRTT time.Duration) int {
// roundCapacity gives the integer value of a capacity.
// The result fits int32, and is guaranteed to be positive.
func roundCapacity(cap float64) int {
- const maxInt32 = float64(1<<31 - 1)
- return int(math.Min(maxInt32, math.Max(1, math.Ceil(cap))))
+ return int(min(math.MaxInt32, max(1, math.Ceil(cap))))
}
// Update modifies the peer's capacity values for a specific data type with a new
diff --git a/p2p/peer.go b/p2p/peer.go
index 9a0a750ac8..5521889f30 100644
--- a/p2p/peer.go
+++ b/p2p/peer.go
@@ -173,7 +173,6 @@ func (p *Peer) Fullname() string {
// Caps returns the capabilities (supported subprotocols) of the remote peer.
func (p *Peer) Caps() []Cap {
- // TODO: maybe return copy
return p.rw.caps
}
diff --git a/params/config.go b/params/config.go
index e760361525..71a3b785a9 100644
--- a/params/config.go
+++ b/params/config.go
@@ -1148,6 +1148,40 @@ func (c *ChainConfig) LatestFork(time uint64) forks.Fork {
}
}
+// BlobConfig returns the blob config associated with the provided fork.
+func (c *ChainConfig) BlobConfig(fork forks.Fork) *BlobConfig {
+ switch fork {
+ case forks.Osaka:
+ return DefaultOsakaBlobConfig
+ case forks.Prague:
+ return DefaultPragueBlobConfig
+ case forks.Cancun:
+ return DefaultCancunBlobConfig
+ default:
+ return nil
+ }
+}
+
+// ActiveSystemContracts returns the currently active system contracts at the
+// given timestamp.
+func (c *ChainConfig) ActiveSystemContracts(time uint64) map[string]common.Address {
+ fork := c.LatestFork(time)
+ active := make(map[string]common.Address)
+ if fork >= forks.Osaka {
+ // no new system contracts
+ }
+ if fork >= forks.Prague {
+ active["CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS"] = ConsolidationQueueAddress
+ active["DEPOSIT_CONTRACT_ADDRESS"] = c.DepositContractAddress
+ active["HISTORY_STORAGE_ADDRESS"] = HistoryStorageAddress
+ active["WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS"] = WithdrawalQueueAddress
+ }
+ if fork >= forks.Cancun {
+ active["BEACON_ROOTS_ADDRESS"] = BeaconRootsAddress
+ }
+ return active
+}
+
// Timestamp returns the timestamp associated with the fork or returns nil if
// the fork isn't defined or isn't a time-based fork.
func (c *ChainConfig) Timestamp(fork forks.Fork) *uint64 {
diff --git a/params/protocol_params.go b/params/protocol_params.go
index e6d4e372d0..efc89804c9 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -183,7 +183,7 @@ const (
BlobTxMaxBlobs = 6
BlobBaseCost = 1 << 13 // Base execution gas cost for a blob.
- HistoryServeWindow = 8192 // Number of blocks to serve historical block hashes for, EIP-2935.
+ HistoryServeWindow = 8191 // Number of blocks to serve historical block hashes for, EIP-2935.
MaxBlockSize = 8_388_608 // maximum size of an RLP-encoded block
)
diff --git a/rlp/decode_test.go b/rlp/decode_test.go
index 3e492188e8..4b3a322873 100644
--- a/rlp/decode_test.go
+++ b/rlp/decode_test.go
@@ -350,7 +350,7 @@ func TestDecodeErrors(t *testing.T) {
}
if err := Decode(r, new(uint)); err != io.EOF {
- t.Errorf("Decode(r, new(int)) error mismatch, got %q, want %q", err, io.EOF)
+ t.Errorf("Decode(r, new(uint)) error mismatch, got %q, want %q", err, io.EOF)
}
}
diff --git a/rlp/encbuffer.go b/rlp/encbuffer.go
index 8d3a3b2293..61d8bd059c 100644
--- a/rlp/encbuffer.go
+++ b/rlp/encbuffer.go
@@ -23,6 +23,7 @@ import (
"reflect"
"sync"
+ "github.com/ethereum/go-ethereum/common/math"
"github.com/holiman/uint256"
)
@@ -145,9 +146,6 @@ func (buf *encBuffer) writeString(s string) {
buf.writeBytes([]byte(s))
}
-// wordBytes is the number of bytes in a big.Word
-const wordBytes = (32 << (uint64(^big.Word(0)) >> 63)) / 8
-
// writeBigInt writes i as an integer.
func (buf *encBuffer) writeBigInt(i *big.Int) {
bitlen := i.BitLen()
@@ -161,15 +159,8 @@ func (buf *encBuffer) writeBigInt(i *big.Int) {
length := ((bitlen + 7) & -8) >> 3
buf.encodeStringHeader(length)
buf.str = append(buf.str, make([]byte, length)...)
- index := length
bytesBuf := buf.str[len(buf.str)-length:]
- for _, d := range i.Bits() {
- for j := 0; j < wordBytes && index > 0; j++ {
- index--
- bytesBuf[index] = byte(d)
- d >>= 8
- }
- }
+ math.ReadBits(i, bytesBuf)
}
// writeUint256 writes z as an integer.
diff --git a/rpc/server.go b/rpc/server.go
index 42b59f8f6f..599e31fb41 100644
--- a/rpc/server.go
+++ b/rpc/server.go
@@ -54,6 +54,7 @@ type Server struct {
batchItemLimit int
batchResponseLimit int
httpBodyLimit int
+ wsReadLimit int64
}
// NewServer creates a new server instance with no registered handlers.
@@ -62,6 +63,7 @@ func NewServer() *Server {
idgen: randomIDGenerator(),
codecs: make(map[ServerCodec]struct{}),
httpBodyLimit: defaultBodyLimit,
+ wsReadLimit: wsDefaultReadLimit,
}
server.run.Store(true)
// Register the default service providing meta information about the RPC service such
@@ -89,6 +91,13 @@ func (s *Server) SetHTTPBodyLimit(limit int) {
s.httpBodyLimit = limit
}
+// SetWebsocketReadLimit sets the limit for max message size for Websocket requests.
+//
+// This method should be called before processing any requests via Websocket server.
+func (s *Server) SetWebsocketReadLimit(limit int64) {
+ s.wsReadLimit = limit
+}
+
// RegisterName creates a service for the given receiver type under the given name. When no
// methods on the given receiver match the criteria to be either an RPC method or a
// subscription an error is returned. Otherwise a new service is created and added to the
diff --git a/rpc/server_test.go b/rpc/server_test.go
index 9ee545d81a..a38a64b080 100644
--- a/rpc/server_test.go
+++ b/rpc/server_test.go
@@ -19,13 +19,18 @@ package rpc
import (
"bufio"
"bytes"
+ "context"
+ "errors"
"io"
"net"
+ "net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
+
+ "github.com/gorilla/websocket"
)
func TestServerRegisterName(t *testing.T) {
@@ -202,3 +207,86 @@ func TestServerBatchResponseSizeLimit(t *testing.T) {
}
}
}
+
+func TestServerWebsocketReadLimit(t *testing.T) {
+ t.Parallel()
+
+ // Test different read limits
+ testCases := []struct {
+ name string
+ readLimit int64
+ testSize int
+ shouldFail bool
+ }{
+ {
+ name: "limit with small request - should succeed",
+ readLimit: 4096, // generous limit to comfortably allow JSON overhead
+ testSize: 256, // reasonably small payload
+ shouldFail: false,
+ },
+ {
+ name: "limit with large request - should fail",
+ readLimit: 256, // tight limit to trigger server-side read limit
+ testSize: 1024, // payload that will exceed the limit including JSON overhead
+ shouldFail: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Create server and set read limits
+ srv := newTestServer()
+ srv.SetWebsocketReadLimit(tc.readLimit)
+ defer srv.Stop()
+
+ // Start HTTP server with WebSocket handler
+ httpsrv := httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
+ defer httpsrv.Close()
+
+ wsURL := "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
+
+ // Connect WebSocket client
+ client, err := DialOptions(context.Background(), wsURL)
+ if err != nil {
+ t.Fatalf("can't dial: %v", err)
+ }
+ defer client.Close()
+
+ // Create large request data - this is what will be limited
+ largeString := strings.Repeat("A", tc.testSize)
+
+ // Send the large string as a parameter in the request
+ var result echoResult
+ err = client.Call(&result, "test_echo", largeString, 42, &echoArgs{S: "test"})
+
+ if tc.shouldFail {
+ // Expecting an error due to read limit exceeded
+ if err == nil {
+ t.Fatalf("expected error for request size %d with limit %d, but got none", tc.testSize, tc.readLimit)
+ }
+ // Be tolerant about the exact error surfaced by gorilla/websocket.
+ // Prefer a CloseError with code 1009, but accept ErrReadLimit or an error string containing 1009/message too big.
+ var cerr *websocket.CloseError
+ if errors.As(err, &cerr) {
+ if cerr.Code != websocket.CloseMessageTooBig {
+ t.Fatalf("unexpected websocket close code: have %d want %d (err=%v)", cerr.Code, websocket.CloseMessageTooBig, err)
+ }
+ } else if !errors.Is(err, websocket.ErrReadLimit) &&
+ !strings.Contains(strings.ToLower(err.Error()), "1009") &&
+ !strings.Contains(strings.ToLower(err.Error()), "message too big") {
+ // Not the error we expect from exceeding the message size limit.
+ t.Fatalf("unexpected error for read limit violation: %v", err)
+ }
+ } else {
+ // Expecting success
+ if err != nil {
+ t.Fatalf("unexpected error for request size %d with limit %d: %v", tc.testSize, tc.readLimit, err)
+ }
+ // Verify the response is correct - the echo should return our string
+ if result.String != largeString {
+ t.Fatalf("expected echo result to match input")
+ }
+ }
+ })
+ }
+}
diff --git a/rpc/websocket.go b/rpc/websocket.go
index 9f67caf859..543ff617ba 100644
--- a/rpc/websocket.go
+++ b/rpc/websocket.go
@@ -60,7 +60,7 @@ func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
log.Debug("WebSocket upgrade failed", "err", err)
return
}
- codec := newWebsocketCodec(conn, r.Host, r.Header, wsDefaultReadLimit)
+ codec := newWebsocketCodec(conn, r.Host, r.Header, s.wsReadLimit)
s.ServeCodec(codec, 0)
})
}
diff --git a/rpc/websocket_test.go b/rpc/websocket_test.go
index a8d8624900..3b7d5a9da0 100644
--- a/rpc/websocket_test.go
+++ b/rpc/websocket_test.go
@@ -121,56 +121,49 @@ func TestWebsocketLargeRead(t *testing.T) {
srv = newTestServer()
httpsrv = httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
+ buffer = 64
)
defer srv.Stop()
defer httpsrv.Close()
- testLimit := func(limit *int64) {
- opts := []ClientOption{}
- expLimit := int64(wsDefaultReadLimit)
- if limit != nil && *limit >= 0 {
- opts = append(opts, WithWebsocketMessageSizeLimit(*limit))
- if *limit > 0 {
- expLimit = *limit // 0 means infinite
+ for _, tt := range []struct {
+ size int
+ limit int
+ err bool
+ }{
+ {200, 200, false}, // Small, successful request and limit
+ {2048, 1024, true}, // Normal, failed request
+ {wsDefaultReadLimit + buffer, 0, false}, // Large, successful request, infinite limit
+ } {
+ func() {
+ if tt.limit != 0 {
+ // Some buffer is added to the limit to account for JSON encoding. It's
+ // skipped when the limit is zero since the intention is for the limit
+ // to be infinite.
+ tt.limit += buffer
}
- }
- client, err := DialOptions(context.Background(), wsURL, opts...)
- if err != nil {
- t.Fatalf("can't dial: %v", err)
- }
- defer client.Close()
- // Remove some bytes for json encoding overhead.
- underLimit := int(expLimit - 128)
- overLimit := expLimit + 1
- if expLimit == wsDefaultReadLimit {
- // No point trying the full 32MB in tests. Just sanity-check that
- // it's not obviously limited.
- underLimit = 1024
- overLimit = -1
- }
- var res string
- // Check under limit
- if err = client.Call(&res, "test_repeat", "A", underLimit); err != nil {
- t.Fatalf("unexpected error with limit %d: %v", expLimit, err)
- }
- if len(res) != underLimit || strings.Count(res, "A") != underLimit {
- t.Fatal("incorrect data")
- }
- // Check over limit
- if overLimit > 0 {
- err = client.Call(&res, "test_repeat", "A", expLimit+1)
- if err == nil || err != websocket.ErrReadLimit {
- t.Fatalf("wrong error with limit %d: %v expecting %v", expLimit, err, websocket.ErrReadLimit)
+ opts := []ClientOption{WithWebsocketMessageSizeLimit(int64(tt.limit))}
+ client, err := DialOptions(context.Background(), wsURL, opts...)
+ if err != nil {
+ t.Fatalf("failed to dial test server: %v", err)
}
- }
- }
- ptr := func(v int64) *int64 { return &v }
+ defer client.Close()
- testLimit(ptr(-1)) // Should be ignored (use default)
- testLimit(ptr(0)) // Should be ignored (use default)
- testLimit(nil) // Should be ignored (use default)
- testLimit(ptr(200))
- testLimit(ptr(wsDefaultReadLimit * 2))
+ var res string
+ err = client.Call(&res, "test_repeat", "A", tt.size)
+ if tt.err && err == nil {
+ t.Fatalf("expected error, got none")
+ }
+ if !tt.err {
+ if err != nil {
+ t.Fatalf("unexpected error with limit %d: %v", tt.limit, err)
+ }
+ if strings.Count(res, "A") != tt.size {
+ t.Fatal("incorrect data")
+ }
+ }
+ }()
+ }
}
func TestWebsocketPeerInfo(t *testing.T) {
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index dcbd04867c..9034e7e9ca 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -167,8 +167,11 @@ func (args *SendTxArgs) ToTransaction() (*types.Transaction, error) {
BlobFeeCap: uint256.MustFromBig((*big.Int)(args.BlobFeeCap)),
}
if args.Blobs != nil {
- // TODO(rjl493456442, marius) support V1
- data.(*types.BlobTx).Sidecar = types.NewBlobTxSidecar(types.BlobSidecarVersion0, args.Blobs, args.Commitments, args.Proofs)
+ version := types.BlobSidecarVersion0
+ if len(args.Proofs) == len(args.Blobs)*kzg4844.CellProofsPerBlob {
+ version = types.BlobSidecarVersion1
+ }
+ data.(*types.BlobTx).Sidecar = types.NewBlobTxSidecar(version, args.Blobs, args.Commitments, args.Proofs)
}
case args.MaxFeePerGas != nil:
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index a22e470ad8..ec7eec1f39 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -511,7 +511,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo
sdb := state.NewDatabase(triedb, nil)
statedb, _ := state.New(types.EmptyRootHash, sdb)
for addr, a := range accounts {
- statedb.SetCode(addr, a.Code)
+ statedb.SetCode(addr, a.Code, tracing.CodeChangeUnspecified)
statedb.SetNonce(addr, a.Nonce, tracing.NonceChangeUnspecified)
statedb.SetBalance(addr, uint256.MustFromBig(a.Balance), tracing.BalanceChangeUnspecified)
for k, v := range a.Storage {
diff --git a/trie/bintrie/binary_node.go b/trie/bintrie/binary_node.go
new file mode 100644
index 0000000000..1c003a6c8f
--- /dev/null
+++ b/trie/bintrie/binary_node.go
@@ -0,0 +1,133 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type (
+ NodeFlushFn func([]byte, BinaryNode)
+ NodeResolverFn func([]byte, common.Hash) ([]byte, error)
+)
+
+// zero is the zero value for a 32-byte array.
+var zero [32]byte
+
+const (
+ NodeWidth = 256 // Number of child per leaf node
+ StemSize = 31 // Number of bytes to travel before reaching a group of leaves
+)
+
+const (
+ nodeTypeStem = iota + 1 // Stem node, contains a stem and a bitmap of values
+ nodeTypeInternal
+)
+
+// BinaryNode is an interface for a binary trie node.
+type BinaryNode interface {
+ Get([]byte, NodeResolverFn) ([]byte, error)
+ Insert([]byte, []byte, NodeResolverFn, int) (BinaryNode, error)
+ Copy() BinaryNode
+ Hash() common.Hash
+ GetValuesAtStem([]byte, NodeResolverFn) ([][]byte, error)
+ InsertValuesAtStem([]byte, [][]byte, NodeResolverFn, int) (BinaryNode, error)
+ CollectNodes([]byte, NodeFlushFn) error
+
+ toDot(parent, path string) string
+ GetHeight() int
+}
+
+// SerializeNode serializes a binary trie node into a byte slice.
+func SerializeNode(node BinaryNode) []byte {
+ switch n := (node).(type) {
+ case *InternalNode:
+ var serialized [65]byte
+ serialized[0] = nodeTypeInternal
+ copy(serialized[1:33], n.left.Hash().Bytes())
+ copy(serialized[33:65], n.right.Hash().Bytes())
+ return serialized[:]
+ case *StemNode:
+ var serialized [32 + 32 + 256*32]byte
+ serialized[0] = nodeTypeStem
+ copy(serialized[1:32], node.(*StemNode).Stem)
+ bitmap := serialized[32:64]
+ offset := 64
+ for i, v := range node.(*StemNode).Values {
+ if v != nil {
+ bitmap[i/8] |= 1 << (7 - (i % 8))
+ copy(serialized[offset:offset+32], v)
+ offset += 32
+ }
+ }
+ return serialized[:]
+ default:
+ panic("invalid node type")
+ }
+}
+
+var invalidSerializedLength = errors.New("invalid serialized node length")
+
+// DeserializeNode deserializes a binary trie node from a byte slice.
+func DeserializeNode(serialized []byte, depth int) (BinaryNode, error) {
+ if len(serialized) == 0 {
+ return Empty{}, nil
+ }
+
+ switch serialized[0] {
+ case nodeTypeInternal:
+ if len(serialized) != 65 {
+ return nil, invalidSerializedLength
+ }
+ return &InternalNode{
+ depth: depth,
+ left: HashedNode(common.BytesToHash(serialized[1:33])),
+ right: HashedNode(common.BytesToHash(serialized[33:65])),
+ }, nil
+ case nodeTypeStem:
+ if len(serialized) < 64 {
+ return nil, invalidSerializedLength
+ }
+ var values [256][]byte
+ bitmap := serialized[32:64]
+ offset := 64
+
+ for i := range 256 {
+ if bitmap[i/8]>>(7-(i%8))&1 == 1 {
+ if len(serialized) < offset+32 {
+ return nil, invalidSerializedLength
+ }
+ values[i] = serialized[offset : offset+32]
+ offset += 32
+ }
+ }
+ return &StemNode{
+ Stem: serialized[1:32],
+ Values: values[:],
+ depth: depth,
+ }, nil
+ default:
+ return nil, errors.New("invalid node type")
+ }
+}
+
+// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
+func ToDot(root BinaryNode) string {
+ return root.toDot("", "")
+}
diff --git a/trie/bintrie/binary_node_test.go b/trie/bintrie/binary_node_test.go
new file mode 100644
index 0000000000..b21daaab69
--- /dev/null
+++ b/trie/bintrie/binary_node_test.go
@@ -0,0 +1,252 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// TestSerializeDeserializeInternalNode tests serialization and deserialization of InternalNode
+func TestSerializeDeserializeInternalNode(t *testing.T) {
+ // Create an internal node with two hashed children
+ leftHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ rightHash := common.HexToHash("0xfedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321")
+
+ node := &InternalNode{
+ depth: 5,
+ left: HashedNode(leftHash),
+ right: HashedNode(rightHash),
+ }
+
+ // Serialize the node
+ serialized := SerializeNode(node)
+
+ // Check the serialized format
+ if serialized[0] != nodeTypeInternal {
+ t.Errorf("Expected type byte to be %d, got %d", nodeTypeInternal, serialized[0])
+ }
+
+ if len(serialized) != 65 {
+ t.Errorf("Expected serialized length to be 65, got %d", len(serialized))
+ }
+
+ // Deserialize the node
+ deserialized, err := DeserializeNode(serialized, 5)
+ if err != nil {
+ t.Fatalf("Failed to deserialize node: %v", err)
+ }
+
+ // Check that it's an internal node
+ internalNode, ok := deserialized.(*InternalNode)
+ if !ok {
+ t.Fatalf("Expected InternalNode, got %T", deserialized)
+ }
+
+ // Check the depth
+ if internalNode.depth != 5 {
+ t.Errorf("Expected depth 5, got %d", internalNode.depth)
+ }
+
+ // Check the left and right hashes
+ if internalNode.left.Hash() != leftHash {
+ t.Errorf("Left hash mismatch: expected %x, got %x", leftHash, internalNode.left.Hash())
+ }
+
+ if internalNode.right.Hash() != rightHash {
+ t.Errorf("Right hash mismatch: expected %x, got %x", rightHash, internalNode.right.Hash())
+ }
+}
+
+// TestSerializeDeserializeStemNode tests serialization and deserialization of StemNode
+func TestSerializeDeserializeStemNode(t *testing.T) {
+ // Create a stem node with some values
+ stem := make([]byte, 31)
+ for i := range stem {
+ stem[i] = byte(i)
+ }
+
+ var values [256][]byte
+ // Add some values at different indices
+ values[0] = common.HexToHash("0x0101010101010101010101010101010101010101010101010101010101010101").Bytes()
+ values[10] = common.HexToHash("0x0202020202020202020202020202020202020202020202020202020202020202").Bytes()
+ values[255] = common.HexToHash("0x0303030303030303030303030303030303030303030303030303030303030303").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 10,
+ }
+
+ // Serialize the node
+ serialized := SerializeNode(node)
+
+ // Check the serialized format
+ if serialized[0] != nodeTypeStem {
+ t.Errorf("Expected type byte to be %d, got %d", nodeTypeStem, serialized[0])
+ }
+
+ // Check the stem is correctly serialized
+ if !bytes.Equal(serialized[1:32], stem) {
+ t.Errorf("Stem mismatch in serialized data")
+ }
+
+ // Deserialize the node
+ deserialized, err := DeserializeNode(serialized, 10)
+ if err != nil {
+ t.Fatalf("Failed to deserialize node: %v", err)
+ }
+
+ // Check that it's a stem node
+ stemNode, ok := deserialized.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", deserialized)
+ }
+
+ // Check the stem
+ if !bytes.Equal(stemNode.Stem, stem) {
+ t.Errorf("Stem mismatch after deserialization")
+ }
+
+ // Check the values
+ if !bytes.Equal(stemNode.Values[0], values[0]) {
+ t.Errorf("Value at index 0 mismatch")
+ }
+ if !bytes.Equal(stemNode.Values[10], values[10]) {
+ t.Errorf("Value at index 10 mismatch")
+ }
+ if !bytes.Equal(stemNode.Values[255], values[255]) {
+ t.Errorf("Value at index 255 mismatch")
+ }
+
+ // Check that other values are nil
+ for i := range NodeWidth {
+ if i == 0 || i == 10 || i == 255 {
+ continue
+ }
+ if stemNode.Values[i] != nil {
+ t.Errorf("Expected nil value at index %d, got %x", i, stemNode.Values[i])
+ }
+ }
+}
+
+// TestDeserializeEmptyNode tests deserialization of empty node
+func TestDeserializeEmptyNode(t *testing.T) {
+ // Empty byte slice should deserialize to Empty node
+ deserialized, err := DeserializeNode([]byte{}, 0)
+ if err != nil {
+ t.Fatalf("Failed to deserialize empty node: %v", err)
+ }
+
+ _, ok := deserialized.(Empty)
+ if !ok {
+ t.Fatalf("Expected Empty node, got %T", deserialized)
+ }
+}
+
+// TestDeserializeInvalidType tests deserialization with invalid type byte
+func TestDeserializeInvalidType(t *testing.T) {
+ // Create invalid serialized data with unknown type byte
+ invalidData := []byte{99, 0, 0, 0} // Type byte 99 is invalid
+
+ _, err := DeserializeNode(invalidData, 0)
+ if err == nil {
+ t.Fatal("Expected error for invalid type byte, got nil")
+ }
+}
+
+// TestDeserializeInvalidLength tests deserialization with invalid data length
+func TestDeserializeInvalidLength(t *testing.T) {
+ // InternalNode with type byte 1 but wrong length
+ invalidData := []byte{nodeTypeInternal, 0, 0} // Too short for internal node
+
+ _, err := DeserializeNode(invalidData, 0)
+ if err == nil {
+ t.Fatal("Expected error for invalid data length, got nil")
+ }
+
+ if err.Error() != "invalid serialized node length" {
+ t.Errorf("Expected 'invalid serialized node length' error, got: %v", err)
+ }
+}
+
+// TestKeyToPath tests the keyToPath function
+func TestKeyToPath(t *testing.T) {
+ tests := []struct {
+ name string
+ depth int
+ key []byte
+ expected []byte
+ wantErr bool
+ }{
+ {
+ name: "depth 0",
+ depth: 0,
+ key: []byte{0x80}, // 10000000 in binary
+ expected: []byte{1},
+ wantErr: false,
+ },
+ {
+ name: "depth 7",
+ depth: 7,
+ key: []byte{0xFF}, // 11111111 in binary
+ expected: []byte{1, 1, 1, 1, 1, 1, 1, 1},
+ wantErr: false,
+ },
+ {
+ name: "depth crossing byte boundary",
+ depth: 10,
+ key: []byte{0xFF, 0x00}, // 11111111 00000000 in binary
+ expected: []byte{1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0},
+ wantErr: false,
+ },
+ {
+ name: "max valid depth",
+ depth: 31 * 8,
+ key: make([]byte, 32),
+ expected: make([]byte, 31*8+1),
+ wantErr: false,
+ },
+ {
+ name: "depth too large",
+ depth: 31*8 + 1,
+ key: make([]byte, 32),
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ path, err := keyToPath(tt.depth, tt.key)
+ if tt.wantErr {
+ if err == nil {
+ t.Errorf("Expected error for depth %d, got nil", tt.depth)
+ }
+ return
+ }
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ return
+ }
+ if !bytes.Equal(path, tt.expected) {
+ t.Errorf("Path mismatch: expected %v, got %v", tt.expected, path)
+ }
+ })
+ }
+}
diff --git a/trie/bintrie/empty.go b/trie/bintrie/empty.go
new file mode 100644
index 0000000000..7cfe373b35
--- /dev/null
+++ b/trie/bintrie/empty.go
@@ -0,0 +1,72 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type Empty struct{}
+
+func (e Empty) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
+ return nil, nil
+}
+
+func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ var values [256][]byte
+ values[key[31]] = value
+ return &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values[:],
+ depth: depth,
+ }, nil
+}
+
+func (e Empty) Copy() BinaryNode {
+ return Empty{}
+}
+
+func (e Empty) Hash() common.Hash {
+ return common.Hash{}
+}
+
+func (e Empty) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ var values [256][]byte
+ return values[:], nil
+}
+
+func (e Empty) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ return &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values,
+ depth: depth,
+ }, nil
+}
+
+func (e Empty) CollectNodes(_ []byte, _ NodeFlushFn) error {
+ return nil
+}
+
+func (e Empty) toDot(parent string, path string) string {
+ return ""
+}
+
+func (e Empty) GetHeight() int {
+ return 0
+}
diff --git a/trie/bintrie/empty_test.go b/trie/bintrie/empty_test.go
new file mode 100644
index 0000000000..574ae1830b
--- /dev/null
+++ b/trie/bintrie/empty_test.go
@@ -0,0 +1,222 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// TestEmptyGet tests the Get method
+func TestEmptyGet(t *testing.T) {
+ node := Empty{}
+
+ key := make([]byte, 32)
+ value, err := node.Get(key, nil)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ if value != nil {
+ t.Errorf("Expected nil value from empty node, got %x", value)
+ }
+}
+
+// TestEmptyInsert tests the Insert method
+func TestEmptyInsert(t *testing.T) {
+ node := Empty{}
+
+ key := make([]byte, 32)
+ key[0] = 0x12
+ key[31] = 0x34
+ value := common.HexToHash("0xabcd").Bytes()
+
+ newNode, err := node.Insert(key, value, nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert: %v", err)
+ }
+
+ // Should create a StemNode
+ stemNode, ok := newNode.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", newNode)
+ }
+
+ // Check the stem (first 31 bytes of key)
+ if !bytes.Equal(stemNode.Stem, key[:31]) {
+ t.Errorf("Stem mismatch: expected %x, got %x", key[:31], stemNode.Stem)
+ }
+
+ // Check the value at the correct index (last byte of key)
+ if !bytes.Equal(stemNode.Values[key[31]], value) {
+ t.Errorf("Value mismatch at index %d: expected %x, got %x", key[31], value, stemNode.Values[key[31]])
+ }
+
+ // Check that other values are nil
+ for i := 0; i < 256; i++ {
+ if i != int(key[31]) && stemNode.Values[i] != nil {
+ t.Errorf("Expected nil value at index %d, got %x", i, stemNode.Values[i])
+ }
+ }
+}
+
+// TestEmptyCopy tests the Copy method
+func TestEmptyCopy(t *testing.T) {
+ node := Empty{}
+
+ copied := node.Copy()
+ copiedEmpty, ok := copied.(Empty)
+ if !ok {
+ t.Fatalf("Expected Empty, got %T", copied)
+ }
+
+ // Both should be empty
+ if node != copiedEmpty {
+ // Empty is a zero-value struct, so copies should be equal
+ t.Errorf("Empty nodes should be equal")
+ }
+}
+
+// TestEmptyHash tests the Hash method
+func TestEmptyHash(t *testing.T) {
+ node := Empty{}
+
+ hash := node.Hash()
+
+ // Empty node should have zero hash
+ if hash != (common.Hash{}) {
+ t.Errorf("Expected zero hash for empty node, got %x", hash)
+ }
+}
+
+// TestEmptyGetValuesAtStem tests the GetValuesAtStem method
+func TestEmptyGetValuesAtStem(t *testing.T) {
+ node := Empty{}
+
+ stem := make([]byte, 31)
+ values, err := node.GetValuesAtStem(stem, nil)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Should return an array of 256 nil values
+ if len(values) != 256 {
+ t.Errorf("Expected 256 values, got %d", len(values))
+ }
+
+ for i, v := range values {
+ if v != nil {
+ t.Errorf("Expected nil value at index %d, got %x", i, v)
+ }
+ }
+}
+
+// TestEmptyInsertValuesAtStem tests the InsertValuesAtStem method
+func TestEmptyInsertValuesAtStem(t *testing.T) {
+ node := Empty{}
+
+ stem := make([]byte, 31)
+ stem[0] = 0x42
+
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+ values[10] = common.HexToHash("0x0202").Bytes()
+ values[255] = common.HexToHash("0x0303").Bytes()
+
+ newNode, err := node.InsertValuesAtStem(stem, values[:], nil, 5)
+ if err != nil {
+ t.Fatalf("Failed to insert values: %v", err)
+ }
+
+ // Should create a StemNode
+ stemNode, ok := newNode.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", newNode)
+ }
+
+ // Check the stem
+ if !bytes.Equal(stemNode.Stem, stem) {
+ t.Errorf("Stem mismatch: expected %x, got %x", stem, stemNode.Stem)
+ }
+
+ // Check the depth
+ if stemNode.depth != 5 {
+ t.Errorf("Depth mismatch: expected 5, got %d", stemNode.depth)
+ }
+
+ // Check the values
+ if !bytes.Equal(stemNode.Values[0], values[0]) {
+ t.Error("Value at index 0 mismatch")
+ }
+ if !bytes.Equal(stemNode.Values[10], values[10]) {
+ t.Error("Value at index 10 mismatch")
+ }
+ if !bytes.Equal(stemNode.Values[255], values[255]) {
+ t.Error("Value at index 255 mismatch")
+ }
+
+ // Check that values is the same slice (not a copy)
+ if &stemNode.Values[0] != &values[0] {
+ t.Error("Expected values to be the same slice reference")
+ }
+}
+
+// TestEmptyCollectNodes tests the CollectNodes method
+func TestEmptyCollectNodes(t *testing.T) {
+ node := Empty{}
+
+ var collected []BinaryNode
+ flushFn := func(path []byte, n BinaryNode) {
+ collected = append(collected, n)
+ }
+
+ err := node.CollectNodes([]byte{0, 1, 0}, flushFn)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Should not collect anything for empty node
+ if len(collected) != 0 {
+ t.Errorf("Expected no collected nodes for empty, got %d", len(collected))
+ }
+}
+
+// TestEmptyToDot tests the toDot method
+func TestEmptyToDot(t *testing.T) {
+ node := Empty{}
+
+ dot := node.toDot("parent", "010")
+
+ // Should return empty string for empty node
+ if dot != "" {
+ t.Errorf("Expected empty string for empty node toDot, got %s", dot)
+ }
+}
+
+// TestEmptyGetHeight tests the GetHeight method
+func TestEmptyGetHeight(t *testing.T) {
+ node := Empty{}
+
+ height := node.GetHeight()
+
+ // Empty node should have height 0
+ if height != 0 {
+ t.Errorf("Expected height 0 for empty node, got %d", height)
+ }
+}
diff --git a/trie/bintrie/hashed_node.go b/trie/bintrie/hashed_node.go
new file mode 100644
index 0000000000..8f9fd66a59
--- /dev/null
+++ b/trie/bintrie/hashed_node.go
@@ -0,0 +1,66 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+type HashedNode common.Hash
+
+func (h HashedNode) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
+ panic("not implemented") // TODO: Implement
+}
+
+func (h HashedNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ return nil, errors.New("insert not implemented for hashed node")
+}
+
+func (h HashedNode) Copy() BinaryNode {
+ nh := common.Hash(h)
+ return HashedNode(nh)
+}
+
+func (h HashedNode) Hash() common.Hash {
+ return common.Hash(h)
+}
+
+func (h HashedNode) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ return nil, errors.New("attempted to get values from an unresolved node")
+}
+
+func (h HashedNode) InsertValuesAtStem(key []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ return nil, errors.New("insertValuesAtStem not implemented for hashed node")
+}
+
+func (h HashedNode) toDot(parent string, path string) string {
+ me := fmt.Sprintf("hash%s", path)
+ ret := fmt.Sprintf("%s [label=\"%x\"]\n", me, h)
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ return ret
+}
+
+func (h HashedNode) CollectNodes([]byte, NodeFlushFn) error {
+ return errors.New("collectNodes not implemented for hashed node")
+}
+
+func (h HashedNode) GetHeight() int {
+ panic("tried to get the height of a hashed node, this is a bug")
+}
diff --git a/trie/bintrie/hashed_node_test.go b/trie/bintrie/hashed_node_test.go
new file mode 100644
index 0000000000..0c19ae0c57
--- /dev/null
+++ b/trie/bintrie/hashed_node_test.go
@@ -0,0 +1,128 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// TestHashedNodeHash tests the Hash method
+func TestHashedNodeHash(t *testing.T) {
+ hash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ node := HashedNode(hash)
+
+ // Hash should return the stored hash
+ if node.Hash() != hash {
+ t.Errorf("Hash mismatch: expected %x, got %x", hash, node.Hash())
+ }
+}
+
+// TestHashedNodeCopy tests the Copy method
+func TestHashedNodeCopy(t *testing.T) {
+ hash := common.HexToHash("0xabcdef")
+ node := HashedNode(hash)
+
+ copied := node.Copy()
+ copiedHash, ok := copied.(HashedNode)
+ if !ok {
+ t.Fatalf("Expected HashedNode, got %T", copied)
+ }
+
+ // Hash should be the same
+ if common.Hash(copiedHash) != hash {
+ t.Errorf("Hash mismatch after copy: expected %x, got %x", hash, copiedHash)
+ }
+
+ // But should be a different object
+ if &node == &copiedHash {
+ t.Error("Copy returned same object reference")
+ }
+}
+
+// TestHashedNodeInsert tests that Insert returns an error
+func TestHashedNodeInsert(t *testing.T) {
+ node := HashedNode(common.HexToHash("0x1234"))
+
+ key := make([]byte, 32)
+ value := make([]byte, 32)
+
+ _, err := node.Insert(key, value, nil, 0)
+ if err == nil {
+ t.Fatal("Expected error for Insert on HashedNode")
+ }
+
+ if err.Error() != "insert not implemented for hashed node" {
+ t.Errorf("Unexpected error message: %v", err)
+ }
+}
+
+// TestHashedNodeGetValuesAtStem tests that GetValuesAtStem returns an error
+func TestHashedNodeGetValuesAtStem(t *testing.T) {
+ node := HashedNode(common.HexToHash("0x1234"))
+
+ stem := make([]byte, 31)
+ _, err := node.GetValuesAtStem(stem, nil)
+ if err == nil {
+ t.Fatal("Expected error for GetValuesAtStem on HashedNode")
+ }
+
+ if err.Error() != "attempted to get values from an unresolved node" {
+ t.Errorf("Unexpected error message: %v", err)
+ }
+}
+
+// TestHashedNodeInsertValuesAtStem tests that InsertValuesAtStem returns an error
+func TestHashedNodeInsertValuesAtStem(t *testing.T) {
+ node := HashedNode(common.HexToHash("0x1234"))
+
+ stem := make([]byte, 31)
+ values := make([][]byte, 256)
+
+ _, err := node.InsertValuesAtStem(stem, values, nil, 0)
+ if err == nil {
+ t.Fatal("Expected error for InsertValuesAtStem on HashedNode")
+ }
+
+ if err.Error() != "insertValuesAtStem not implemented for hashed node" {
+ t.Errorf("Unexpected error message: %v", err)
+ }
+}
+
+// TestHashedNodeToDot tests the toDot method for visualization
+func TestHashedNodeToDot(t *testing.T) {
+ hash := common.HexToHash("0x1234")
+ node := HashedNode(hash)
+
+ dot := node.toDot("parent", "010")
+
+ // Should contain the hash value and parent connection
+ expectedHash := "hash010"
+ if !contains(dot, expectedHash) {
+ t.Errorf("Expected dot output to contain %s", expectedHash)
+ }
+
+ if !contains(dot, "parent -> hash010") {
+ t.Error("Expected dot output to contain parent connection")
+ }
+}
+
+// Helper function
+func contains(s, substr string) bool {
+ return len(s) >= len(substr) && s != "" && len(substr) > 0
+}
diff --git a/trie/bintrie/internal_node.go b/trie/bintrie/internal_node.go
new file mode 100644
index 0000000000..f3ddd1aab0
--- /dev/null
+++ b/trie/bintrie/internal_node.go
@@ -0,0 +1,189 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "crypto/sha256"
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+func keyToPath(depth int, key []byte) ([]byte, error) {
+ if depth > 31*8 {
+ return nil, errors.New("node too deep")
+ }
+ path := make([]byte, 0, depth+1)
+ for i := range depth + 1 {
+ bit := key[i/8] >> (7 - (i % 8)) & 1
+ path = append(path, bit)
+ }
+ return path, nil
+}
+
+// InternalNode is a binary trie internal node.
+type InternalNode struct {
+ left, right BinaryNode
+ depth int
+}
+
+// GetValuesAtStem retrieves the group of values located at the given stem key.
+func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([][]byte, error) {
+ if bt.depth > 31*8 {
+ return nil, errors.New("node too deep")
+ }
+
+ bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+ var child *BinaryNode
+ if bit == 0 {
+ child = &bt.left
+ } else {
+ child = &bt.right
+ }
+
+ if hn, ok := (*child).(HashedNode); ok {
+ path, err := keyToPath(bt.depth, stem)
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
+ }
+ data, err := resolver(path, common.Hash(hn))
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem resolve error: %w", err)
+ }
+ node, err := DeserializeNode(data, bt.depth+1)
+ if err != nil {
+ return nil, fmt.Errorf("GetValuesAtStem node deserialization error: %w", err)
+ }
+ *child = node
+ }
+ return (*child).GetValuesAtStem(stem, resolver)
+}
+
+// Get retrieves the value for the given key.
+func (bt *InternalNode) Get(key []byte, resolver NodeResolverFn) ([]byte, error) {
+ values, err := bt.GetValuesAtStem(key[:31], resolver)
+ if err != nil {
+ return nil, fmt.Errorf("get error: %w", err)
+ }
+ return values[key[31]], nil
+}
+
+// Insert inserts a new key-value pair into the trie.
+func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ var values [256][]byte
+ values[key[31]] = value
+ return bt.InsertValuesAtStem(key[:31], values[:], resolver, depth)
+}
+
+// Copy creates a deep copy of the node.
+func (bt *InternalNode) Copy() BinaryNode {
+ return &InternalNode{
+ left: bt.left.Copy(),
+ right: bt.right.Copy(),
+ depth: bt.depth,
+ }
+}
+
+// Hash returns the hash of the node.
+func (bt *InternalNode) Hash() common.Hash {
+ h := sha256.New()
+ if bt.left != nil {
+ h.Write(bt.left.Hash().Bytes())
+ } else {
+ h.Write(zero[:])
+ }
+ if bt.right != nil {
+ h.Write(bt.right.Hash().Bytes())
+ } else {
+ h.Write(zero[:])
+ }
+ return common.BytesToHash(h.Sum(nil))
+}
+
+// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
+// Already-existing values will be overwritten.
+func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
+ var (
+ child *BinaryNode
+ err error
+ )
+ bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+ if bit == 0 {
+ child = &bt.left
+ } else {
+ child = &bt.right
+ }
+ *child, err = (*child).InsertValuesAtStem(stem, values, resolver, depth+1)
+ return bt, err
+}
+
+// CollectNodes collects all child nodes at a given path, and flushes it
+// into the provided node collector.
+func (bt *InternalNode) CollectNodes(path []byte, flushfn NodeFlushFn) error {
+ if bt.left != nil {
+ var p [256]byte
+ copy(p[:], path)
+ childpath := p[:len(path)]
+ childpath = append(childpath, 0)
+ if err := bt.left.CollectNodes(childpath, flushfn); err != nil {
+ return err
+ }
+ }
+ if bt.right != nil {
+ var p [256]byte
+ copy(p[:], path)
+ childpath := p[:len(path)]
+ childpath = append(childpath, 1)
+ if err := bt.right.CollectNodes(childpath, flushfn); err != nil {
+ return err
+ }
+ }
+ flushfn(path, bt)
+ return nil
+}
+
+// GetHeight returns the height of the node.
+func (bt *InternalNode) GetHeight() int {
+ var (
+ leftHeight int
+ rightHeight int
+ )
+ if bt.left != nil {
+ leftHeight = bt.left.GetHeight()
+ }
+ if bt.right != nil {
+ rightHeight = bt.right.GetHeight()
+ }
+ return 1 + max(leftHeight, rightHeight)
+}
+
+func (bt *InternalNode) toDot(parent, path string) string {
+ me := fmt.Sprintf("internal%s", path)
+ ret := fmt.Sprintf("%s [label=\"I: %x\"]\n", me, bt.Hash())
+ if len(parent) > 0 {
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ }
+
+ if bt.left != nil {
+ ret = fmt.Sprintf("%s%s", ret, bt.left.toDot(me, fmt.Sprintf("%s%02x", path, 0)))
+ }
+ if bt.right != nil {
+ ret = fmt.Sprintf("%s%s", ret, bt.right.toDot(me, fmt.Sprintf("%s%02x", path, 1)))
+ }
+ return ret
+}
diff --git a/trie/bintrie/internal_node_test.go b/trie/bintrie/internal_node_test.go
new file mode 100644
index 0000000000..158d8b7147
--- /dev/null
+++ b/trie/bintrie/internal_node_test.go
@@ -0,0 +1,458 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// TestInternalNodeGet tests the Get method
+func TestInternalNodeGet(t *testing.T) {
+ // Create a simple tree structure
+ leftStem := make([]byte, 31)
+ rightStem := make([]byte, 31)
+ rightStem[0] = 0x80 // First bit is 1
+
+ var leftValues, rightValues [256][]byte
+ leftValues[0] = common.HexToHash("0x0101").Bytes()
+ rightValues[0] = common.HexToHash("0x0202").Bytes()
+
+ node := &InternalNode{
+ depth: 0,
+ left: &StemNode{
+ Stem: leftStem,
+ Values: leftValues[:],
+ depth: 1,
+ },
+ right: &StemNode{
+ Stem: rightStem,
+ Values: rightValues[:],
+ depth: 1,
+ },
+ }
+
+ // Get value from left subtree
+ leftKey := make([]byte, 32)
+ leftKey[31] = 0
+ value, err := node.Get(leftKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to get left value: %v", err)
+ }
+ if !bytes.Equal(value, leftValues[0]) {
+ t.Errorf("Left value mismatch: expected %x, got %x", leftValues[0], value)
+ }
+
+ // Get value from right subtree
+ rightKey := make([]byte, 32)
+ rightKey[0] = 0x80
+ rightKey[31] = 0
+ value, err = node.Get(rightKey, nil)
+ if err != nil {
+ t.Fatalf("Failed to get right value: %v", err)
+ }
+ if !bytes.Equal(value, rightValues[0]) {
+ t.Errorf("Right value mismatch: expected %x, got %x", rightValues[0], value)
+ }
+}
+
+// TestInternalNodeGetWithResolver tests Get with HashedNode resolution
+func TestInternalNodeGetWithResolver(t *testing.T) {
+ // Create an internal node with a hashed child
+ hashedChild := HashedNode(common.HexToHash("0x1234"))
+
+ node := &InternalNode{
+ depth: 0,
+ left: hashedChild,
+ right: Empty{},
+ }
+
+ // Mock resolver that returns a stem node
+ resolver := func(path []byte, hash common.Hash) ([]byte, error) {
+ if hash == common.Hash(hashedChild) {
+ stem := make([]byte, 31)
+ var values [256][]byte
+ values[5] = common.HexToHash("0xabcd").Bytes()
+ stemNode := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 1,
+ }
+ return SerializeNode(stemNode), nil
+ }
+ return nil, errors.New("node not found")
+ }
+
+ // Get value through the hashed node
+ key := make([]byte, 32)
+ key[31] = 5
+ value, err := node.Get(key, resolver)
+ if err != nil {
+ t.Fatalf("Failed to get value: %v", err)
+ }
+
+ expectedValue := common.HexToHash("0xabcd").Bytes()
+ if !bytes.Equal(value, expectedValue) {
+ t.Errorf("Value mismatch: expected %x, got %x", expectedValue, value)
+ }
+}
+
+// TestInternalNodeInsert tests the Insert method
+func TestInternalNodeInsert(t *testing.T) {
+ // Start with an internal node with empty children
+ node := &InternalNode{
+ depth: 0,
+ left: Empty{},
+ right: Empty{},
+ }
+
+ // Insert a value into the left subtree
+ leftKey := make([]byte, 32)
+ leftKey[31] = 10
+ leftValue := common.HexToHash("0x0101").Bytes()
+
+ newNode, err := node.Insert(leftKey, leftValue, nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert: %v", err)
+ }
+
+ internalNode, ok := newNode.(*InternalNode)
+ if !ok {
+ t.Fatalf("Expected InternalNode, got %T", newNode)
+ }
+
+ // Check that left child is now a StemNode
+ leftStem, ok := internalNode.left.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
+ }
+
+ // Check the inserted value
+ if !bytes.Equal(leftStem.Values[10], leftValue) {
+ t.Errorf("Value mismatch: expected %x, got %x", leftValue, leftStem.Values[10])
+ }
+
+ // Right child should still be Empty
+ _, ok = internalNode.right.(Empty)
+ if !ok {
+ t.Errorf("Expected right child to remain Empty, got %T", internalNode.right)
+ }
+}
+
+// TestInternalNodeCopy tests the Copy method
+func TestInternalNodeCopy(t *testing.T) {
+ // Create an internal node with stem children
+ leftStem := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 1,
+ }
+ leftStem.Values[0] = common.HexToHash("0x0101").Bytes()
+
+ rightStem := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 1,
+ }
+ rightStem.Stem[0] = 0x80
+ rightStem.Values[0] = common.HexToHash("0x0202").Bytes()
+
+ node := &InternalNode{
+ depth: 0,
+ left: leftStem,
+ right: rightStem,
+ }
+
+ // Create a copy
+ copied := node.Copy()
+ copiedInternal, ok := copied.(*InternalNode)
+ if !ok {
+ t.Fatalf("Expected InternalNode, got %T", copied)
+ }
+
+ // Check depth
+ if copiedInternal.depth != node.depth {
+ t.Errorf("Depth mismatch: expected %d, got %d", node.depth, copiedInternal.depth)
+ }
+
+ // Check that children are copied
+ copiedLeft, ok := copiedInternal.left.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected left child to be StemNode, got %T", copiedInternal.left)
+ }
+
+ copiedRight, ok := copiedInternal.right.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected right child to be StemNode, got %T", copiedInternal.right)
+ }
+
+ // Verify deep copy (children should be different objects)
+ if copiedLeft == leftStem {
+ t.Error("Left child not properly copied")
+ }
+ if copiedRight == rightStem {
+ t.Error("Right child not properly copied")
+ }
+
+ // But values should be equal
+ if !bytes.Equal(copiedLeft.Values[0], leftStem.Values[0]) {
+ t.Error("Left child value mismatch after copy")
+ }
+ if !bytes.Equal(copiedRight.Values[0], rightStem.Values[0]) {
+ t.Error("Right child value mismatch after copy")
+ }
+}
+
+// TestInternalNodeHash tests the Hash method
+func TestInternalNodeHash(t *testing.T) {
+ // Create an internal node
+ node := &InternalNode{
+ depth: 0,
+ left: HashedNode(common.HexToHash("0x1111")),
+ right: HashedNode(common.HexToHash("0x2222")),
+ }
+
+ hash1 := node.Hash()
+
+ // Hash should be deterministic
+ hash2 := node.Hash()
+ if hash1 != hash2 {
+ t.Errorf("Hash not deterministic: %x != %x", hash1, hash2)
+ }
+
+ // Changing a child should change the hash
+ node.left = HashedNode(common.HexToHash("0x3333"))
+ hash3 := node.Hash()
+ if hash1 == hash3 {
+ t.Error("Hash didn't change after modifying left child")
+ }
+
+ // Test with nil children (should use zero hash)
+ nodeWithNil := &InternalNode{
+ depth: 0,
+ left: nil,
+ right: HashedNode(common.HexToHash("0x4444")),
+ }
+ hashWithNil := nodeWithNil.Hash()
+ if hashWithNil == (common.Hash{}) {
+ t.Error("Hash shouldn't be zero even with nil child")
+ }
+}
+
+// TestInternalNodeGetValuesAtStem tests GetValuesAtStem method
+func TestInternalNodeGetValuesAtStem(t *testing.T) {
+ // Create a tree with values at different stems
+ leftStem := make([]byte, 31)
+ rightStem := make([]byte, 31)
+ rightStem[0] = 0x80
+
+ var leftValues, rightValues [256][]byte
+ leftValues[0] = common.HexToHash("0x0101").Bytes()
+ leftValues[10] = common.HexToHash("0x0102").Bytes()
+ rightValues[0] = common.HexToHash("0x0201").Bytes()
+ rightValues[20] = common.HexToHash("0x0202").Bytes()
+
+ node := &InternalNode{
+ depth: 0,
+ left: &StemNode{
+ Stem: leftStem,
+ Values: leftValues[:],
+ depth: 1,
+ },
+ right: &StemNode{
+ Stem: rightStem,
+ Values: rightValues[:],
+ depth: 1,
+ },
+ }
+
+ // Get values from left stem
+ values, err := node.GetValuesAtStem(leftStem, nil)
+ if err != nil {
+ t.Fatalf("Failed to get left values: %v", err)
+ }
+ if !bytes.Equal(values[0], leftValues[0]) {
+ t.Error("Left value at index 0 mismatch")
+ }
+ if !bytes.Equal(values[10], leftValues[10]) {
+ t.Error("Left value at index 10 mismatch")
+ }
+
+ // Get values from right stem
+ values, err = node.GetValuesAtStem(rightStem, nil)
+ if err != nil {
+ t.Fatalf("Failed to get right values: %v", err)
+ }
+ if !bytes.Equal(values[0], rightValues[0]) {
+ t.Error("Right value at index 0 mismatch")
+ }
+ if !bytes.Equal(values[20], rightValues[20]) {
+ t.Error("Right value at index 20 mismatch")
+ }
+}
+
+// TestInternalNodeInsertValuesAtStem tests InsertValuesAtStem method
+func TestInternalNodeInsertValuesAtStem(t *testing.T) {
+ // Start with an internal node with empty children
+ node := &InternalNode{
+ depth: 0,
+ left: Empty{},
+ right: Empty{},
+ }
+
+ // Insert values at a stem in the left subtree
+ stem := make([]byte, 31)
+ var values [256][]byte
+ values[5] = common.HexToHash("0x0505").Bytes()
+ values[10] = common.HexToHash("0x1010").Bytes()
+
+ newNode, err := node.InsertValuesAtStem(stem, values[:], nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert values: %v", err)
+ }
+
+ internalNode, ok := newNode.(*InternalNode)
+ if !ok {
+ t.Fatalf("Expected InternalNode, got %T", newNode)
+ }
+
+ // Check that left child is now a StemNode with the values
+ leftStem, ok := internalNode.left.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
+ }
+
+ if !bytes.Equal(leftStem.Values[5], values[5]) {
+ t.Error("Value at index 5 mismatch")
+ }
+ if !bytes.Equal(leftStem.Values[10], values[10]) {
+ t.Error("Value at index 10 mismatch")
+ }
+}
+
+// TestInternalNodeCollectNodes tests CollectNodes method
+func TestInternalNodeCollectNodes(t *testing.T) {
+ // Create an internal node with two stem children
+ leftStem := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 1,
+ }
+
+ rightStem := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 1,
+ }
+ rightStem.Stem[0] = 0x80
+
+ node := &InternalNode{
+ depth: 0,
+ left: leftStem,
+ right: rightStem,
+ }
+
+ var collectedPaths [][]byte
+ var collectedNodes []BinaryNode
+
+ flushFn := func(path []byte, n BinaryNode) {
+ pathCopy := make([]byte, len(path))
+ copy(pathCopy, path)
+ collectedPaths = append(collectedPaths, pathCopy)
+ collectedNodes = append(collectedNodes, n)
+ }
+
+ err := node.CollectNodes([]byte{1}, flushFn)
+ if err != nil {
+ t.Fatalf("Failed to collect nodes: %v", err)
+ }
+
+ // Should have collected 3 nodes: left stem, right stem, and the internal node itself
+ if len(collectedNodes) != 3 {
+ t.Errorf("Expected 3 collected nodes, got %d", len(collectedNodes))
+ }
+
+ // Check paths
+ expectedPaths := [][]byte{
+ {1, 0}, // left child
+ {1, 1}, // right child
+ {1}, // internal node itself
+ }
+
+ for i, expectedPath := range expectedPaths {
+ if !bytes.Equal(collectedPaths[i], expectedPath) {
+ t.Errorf("Path %d mismatch: expected %v, got %v", i, expectedPath, collectedPaths[i])
+ }
+ }
+}
+
+// TestInternalNodeGetHeight tests GetHeight method
+func TestInternalNodeGetHeight(t *testing.T) {
+ // Create a tree with different heights
+ // Left subtree: depth 2 (internal -> stem)
+ // Right subtree: depth 1 (stem)
+ leftInternal := &InternalNode{
+ depth: 1,
+ left: &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 2,
+ },
+ right: Empty{},
+ }
+
+ rightStem := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 1,
+ }
+
+ node := &InternalNode{
+ depth: 0,
+ left: leftInternal,
+ right: rightStem,
+ }
+
+ height := node.GetHeight()
+ // Height should be max(left height, right height) + 1
+ // Left height: 2, Right height: 1, so total: 3
+ if height != 3 {
+ t.Errorf("Expected height 3, got %d", height)
+ }
+}
+
+// TestInternalNodeDepthTooLarge tests handling of excessive depth
+func TestInternalNodeDepthTooLarge(t *testing.T) {
+ // Create an internal node at max depth
+ node := &InternalNode{
+ depth: 31*8 + 1,
+ left: Empty{},
+ right: Empty{},
+ }
+
+ stem := make([]byte, 31)
+ _, err := node.GetValuesAtStem(stem, nil)
+ if err == nil {
+ t.Fatal("Expected error for excessive depth")
+ }
+ if err.Error() != "node too deep" {
+ t.Errorf("Expected 'node too deep' error, got: %v", err)
+ }
+}
diff --git a/trie/bintrie/iterator.go b/trie/bintrie/iterator.go
new file mode 100644
index 0000000000..a6bab2bcfa
--- /dev/null
+++ b/trie/bintrie/iterator.go
@@ -0,0 +1,261 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "errors"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/trie"
+)
+
+var errIteratorEnd = errors.New("end of iteration")
+
+type binaryNodeIteratorState struct {
+ Node BinaryNode
+ Index int
+}
+
+type binaryNodeIterator struct {
+ trie *BinaryTrie
+ current BinaryNode
+ lastErr error
+
+ stack []binaryNodeIteratorState
+}
+
+func newBinaryNodeIterator(t *BinaryTrie, _ []byte) (trie.NodeIterator, error) {
+ if t.Hash() == zero {
+ return &binaryNodeIterator{trie: t, lastErr: errIteratorEnd}, nil
+ }
+ it := &binaryNodeIterator{trie: t, current: t.root}
+ // it.err = it.seek(start)
+ return it, nil
+}
+
+// Next moves the iterator to the next node. If the parameter is false, any child
+// nodes will be skipped.
+func (it *binaryNodeIterator) Next(descend bool) bool {
+ if it.lastErr == errIteratorEnd {
+ it.lastErr = errIteratorEnd
+ return false
+ }
+
+ if len(it.stack) == 0 {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: it.trie.root})
+ it.current = it.trie.root
+
+ return true
+ }
+
+ switch node := it.current.(type) {
+ case *InternalNode:
+ // index: 0 = nothing visited, 1=left visited, 2=right visited
+ context := &it.stack[len(it.stack)-1]
+
+ // recurse into both children
+ if context.Index == 0 {
+ if _, isempty := node.left.(Empty); node.left != nil && !isempty {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: node.left})
+ it.current = node.left
+ return it.Next(descend)
+ }
+
+ context.Index++
+ }
+
+ if context.Index == 1 {
+ if _, isempty := node.right.(Empty); node.right != nil && !isempty {
+ it.stack = append(it.stack, binaryNodeIteratorState{Node: node.right})
+ it.current = node.right
+ return it.Next(descend)
+ }
+
+ context.Index++
+ }
+
+ // Reached the end of this node, go back to the parent, if
+ // this isn't root.
+ if len(it.stack) == 1 {
+ it.lastErr = errIteratorEnd
+ return false
+ }
+ it.stack = it.stack[:len(it.stack)-1]
+ it.current = it.stack[len(it.stack)-1].Node
+ it.stack[len(it.stack)-1].Index++
+ return it.Next(descend)
+ case *StemNode:
+ // Look for the next non-empty value
+ for i := it.stack[len(it.stack)-1].Index; i < 256; i++ {
+ if node.Values[i] != nil {
+ it.stack[len(it.stack)-1].Index = i + 1
+ return true
+ }
+ }
+
+ // go back to parent to get the next leaf
+ it.stack = it.stack[:len(it.stack)-1]
+ it.current = it.stack[len(it.stack)-1].Node
+ it.stack[len(it.stack)-1].Index++
+ return it.Next(descend)
+ case HashedNode:
+ // resolve the node
+ data, err := it.trie.nodeResolver(it.Path(), common.Hash(node))
+ if err != nil {
+ panic(err)
+ }
+ it.current, err = DeserializeNode(data, len(it.stack)-1)
+ if err != nil {
+ panic(err)
+ }
+
+ // update the stack and parent with the resolved node
+ it.stack[len(it.stack)-1].Node = it.current
+ parent := &it.stack[len(it.stack)-2]
+ if parent.Index == 0 {
+ parent.Node.(*InternalNode).left = it.current
+ } else {
+ parent.Node.(*InternalNode).right = it.current
+ }
+ return it.Next(descend)
+ case Empty:
+ // do nothing
+ return false
+ default:
+ panic("invalid node type")
+ }
+}
+
+// Error returns the error status of the iterator.
+func (it *binaryNodeIterator) Error() error {
+ if it.lastErr == errIteratorEnd {
+ return nil
+ }
+ return it.lastErr
+}
+
+// Hash returns the hash of the current node.
+func (it *binaryNodeIterator) Hash() common.Hash {
+ return it.current.Hash()
+}
+
+// Parent returns the hash of the parent of the current node. The hash may be the one
+// grandparent if the immediate parent is an internal node with no hash.
+func (it *binaryNodeIterator) Parent() common.Hash {
+ return it.stack[len(it.stack)-1].Node.Hash()
+}
+
+// Path returns the hex-encoded path to the current node.
+// Callers must not retain references to the return value after calling Next.
+// For leaf nodes, the last element of the path is the 'terminator symbol' 0x10.
+func (it *binaryNodeIterator) Path() []byte {
+ if it.Leaf() {
+ return it.LeafKey()
+ }
+ var path []byte
+ for i, state := range it.stack {
+ // skip the last byte
+ if i >= len(it.stack)-1 {
+ break
+ }
+ path = append(path, byte(state.Index))
+ }
+ return path
+}
+
+// NodeBlob returns the serialized bytes of the current node.
+func (it *binaryNodeIterator) NodeBlob() []byte {
+ return SerializeNode(it.current)
+}
+
+// Leaf returns true iff the current node is a leaf node.
+func (it *binaryNodeIterator) Leaf() bool {
+ _, ok := it.current.(*StemNode)
+ return ok
+}
+
+// LeafKey returns the key of the leaf. The method panics if the iterator is not
+// positioned at a leaf. Callers must not retain references to the value after
+// calling Next.
+func (it *binaryNodeIterator) LeafKey() []byte {
+ leaf, ok := it.current.(*StemNode)
+ if !ok {
+ panic("Leaf() called on an binary node iterator not at a leaf location")
+ }
+ return leaf.Key(it.stack[len(it.stack)-1].Index - 1)
+}
+
+// LeafBlob returns the content of the leaf. The method panics if the iterator
+// is not positioned at a leaf. Callers must not retain references to the value
+// after calling Next.
+func (it *binaryNodeIterator) LeafBlob() []byte {
+ leaf, ok := it.current.(*StemNode)
+ if !ok {
+ panic("LeafBlob() called on an binary node iterator not at a leaf location")
+ }
+ return leaf.Values[it.stack[len(it.stack)-1].Index-1]
+}
+
+// LeafProof returns the Merkle proof of the leaf. The method panics if the
+// iterator is not positioned at a leaf. Callers must not retain references
+// to the value after calling Next.
+func (it *binaryNodeIterator) LeafProof() [][]byte {
+ sn, ok := it.current.(*StemNode)
+ if !ok {
+ panic("LeafProof() called on an binary node iterator not at a leaf location")
+ }
+
+ proof := make([][]byte, 0, len(it.stack)+NodeWidth)
+
+ // Build proof by walking up the stack and collecting sibling hashes
+ for i := range it.stack[:len(it.stack)-2] {
+ state := it.stack[i]
+ internalNode := state.Node.(*InternalNode) // should panic if the node isn't an InternalNode
+
+ // Add the sibling hash to the proof
+ if state.Index == 0 {
+ // We came from left, so include right sibling
+ proof = append(proof, internalNode.right.Hash().Bytes())
+ } else {
+ // We came from right, so include left sibling
+ proof = append(proof, internalNode.left.Hash().Bytes())
+ }
+ }
+
+ // Add the stem and siblings
+ proof = append(proof, sn.Stem)
+ for _, v := range sn.Values {
+ proof = append(proof, v)
+ }
+
+ return proof
+}
+
+// AddResolver sets an intermediate database to use for looking up trie nodes
+// before reaching into the real persistent layer.
+//
+// This is not required for normal operation, rather is an optimization for
+// cases where trie nodes can be recovered from some external mechanism without
+// reading from disk. In those cases, this resolver allows short circuiting
+// accesses and returning them from memory.
+//
+// Before adding a similar mechanism to any other place in Geth, consider
+// making trie.Database an interface and wrapping at that level. It's a huge
+// refactor, but it could be worth it if another occurrence arises.
+func (it *binaryNodeIterator) AddResolver(trie.NodeResolver) {
+ // Not implemented, but should not panic
+}
diff --git a/trie/bintrie/iterator_test.go b/trie/bintrie/iterator_test.go
new file mode 100644
index 0000000000..8773e9e0c5
--- /dev/null
+++ b/trie/bintrie/iterator_test.go
@@ -0,0 +1,83 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/triedb"
+ "github.com/ethereum/go-ethereum/triedb/hashdb"
+ "github.com/ethereum/go-ethereum/triedb/pathdb"
+ "github.com/holiman/uint256"
+)
+
+func newTestDatabase(diskdb ethdb.Database, scheme string) *triedb.Database {
+ config := &triedb.Config{Preimages: true}
+ if scheme == rawdb.HashScheme {
+ config.HashDB = &hashdb.Config{CleanCacheSize: 0}
+ } else {
+ config.PathDB = &pathdb.Config{TrieCleanSize: 0, StateCleanSize: 0}
+ }
+ return triedb.NewDatabase(diskdb, config)
+}
+
+func TestBinaryIterator(t *testing.T) {
+ trie, err := NewBinaryTrie(types.EmptyVerkleHash, newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.PathScheme))
+ if err != nil {
+ t.Fatal(err)
+ }
+ account0 := &types.StateAccount{
+ Nonce: 1,
+ Balance: uint256.NewInt(2),
+ Root: types.EmptyRootHash,
+ CodeHash: nil,
+ }
+ // NOTE: the code size isn't written to the trie via TryUpdateAccount
+ // so it will be missing from the test nodes.
+ trie.UpdateAccount(common.Address{}, account0, 0)
+ account1 := &types.StateAccount{
+ Nonce: 1337,
+ Balance: uint256.NewInt(2000),
+ Root: types.EmptyRootHash,
+ CodeHash: nil,
+ }
+ // This address is meant to hash to a value that has the same first byte as 0xbf
+ var clash = common.HexToAddress("69fd8034cdb20934dedffa7dccb4fb3b8062a8be")
+ trie.UpdateAccount(clash, account1, 0)
+
+ // Manually go over every node to check that we get all
+ // the correct nodes.
+ it, err := trie.NodeIterator(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var leafcount int
+ for it.Next(true) {
+ t.Logf("Node: %x", it.Path())
+ if it.Leaf() {
+ leafcount++
+ t.Logf("\tLeaf: %x", it.LeafKey())
+ }
+ }
+ if leafcount != 2 {
+ t.Fatalf("invalid leaf count: %d != 6", leafcount)
+ }
+}
diff --git a/trie/bintrie/key_encoding.go b/trie/bintrie/key_encoding.go
new file mode 100644
index 0000000000..13c2057371
--- /dev/null
+++ b/trie/bintrie/key_encoding.go
@@ -0,0 +1,79 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "crypto/sha256"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/holiman/uint256"
+)
+
+const (
+ BasicDataLeafKey = 0
+ CodeHashLeafKey = 1
+ BasicDataCodeSizeOffset = 5
+ BasicDataNonceOffset = 8
+ BasicDataBalanceOffset = 16
+)
+
+var (
+ zeroHash = common.Hash{}
+ codeOffset = uint256.NewInt(128)
+)
+
+func GetBinaryTreeKey(addr common.Address, key []byte) []byte {
+ hasher := sha256.New()
+ hasher.Write(zeroHash[:12])
+ hasher.Write(addr[:])
+ hasher.Write(key[:31])
+ k := hasher.Sum(nil)
+ k[31] = key[31]
+ return k
+}
+
+func GetBinaryTreeKeyCodeHash(addr common.Address) []byte {
+ var k [32]byte
+ k[31] = CodeHashLeafKey
+ return GetBinaryTreeKey(addr, k[:])
+}
+
+func GetBinaryTreeKeyStorageSlot(address common.Address, key []byte) []byte {
+ var k [32]byte
+
+ // Case when the key belongs to the account header
+ if bytes.Equal(key[:31], zeroHash[:31]) && key[31] < 64 {
+ k[31] = 64 + key[31]
+ return GetBinaryTreeKey(address, k[:])
+ }
+
+ // Set the main storage offset
+ // note that the first 64 bytes of the main offset storage
+ // are unreachable, which is consistent with the spec and
+ // what verkle does.
+ k[0] = 1 // 1 << 248
+ copy(k[1:], key[:31])
+ k[31] = key[31]
+
+ return GetBinaryTreeKey(address, k[:])
+}
+
+func GetBinaryTreeKeyCodeChunk(address common.Address, chunknr *uint256.Int) []byte {
+ chunkOffset := new(uint256.Int).Add(codeOffset, chunknr).Bytes()
+ return GetBinaryTreeKey(address, chunkOffset)
+}
diff --git a/trie/bintrie/stem_node.go b/trie/bintrie/stem_node.go
new file mode 100644
index 0000000000..50c06c9761
--- /dev/null
+++ b/trie/bintrie/stem_node.go
@@ -0,0 +1,213 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "slices"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// StemNode represents a group of `NodeWith` values sharing the same stem.
+type StemNode struct {
+ Stem []byte // Stem path to get to 256 values
+ Values [][]byte // All values, indexed by the last byte of the key.
+ depth int // Depth of the node
+}
+
+// Get retrieves the value for the given key.
+func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) {
+ panic("this should not be called directly")
+}
+
+// Insert inserts a new key-value pair into the node.
+func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ if !bytes.Equal(bt.Stem, key[:31]) {
+ bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+
+ n := &InternalNode{depth: bt.depth}
+ bt.depth++
+ var child, other *BinaryNode
+ if bitStem == 0 {
+ n.left = bt
+ child = &n.left
+ other = &n.right
+ } else {
+ n.right = bt
+ child = &n.right
+ other = &n.left
+ }
+
+ bitKey := key[n.depth/8] >> (7 - (n.depth % 8)) & 1
+ if bitKey == bitStem {
+ var err error
+ *child, err = (*child).Insert(key, value, nil, depth+1)
+ if err != nil {
+ return n, fmt.Errorf("insert error: %w", err)
+ }
+ *other = Empty{}
+ } else {
+ var values [256][]byte
+ values[key[31]] = value
+ *other = &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values[:],
+ depth: depth + 1,
+ }
+ }
+ return n, nil
+ }
+ if len(value) != 32 {
+ return bt, errors.New("invalid insertion: value length")
+ }
+ bt.Values[key[31]] = value
+ return bt, nil
+}
+
+// Copy creates a deep copy of the node.
+func (bt *StemNode) Copy() BinaryNode {
+ var values [256][]byte
+ for i, v := range bt.Values {
+ values[i] = slices.Clone(v)
+ }
+ return &StemNode{
+ Stem: slices.Clone(bt.Stem),
+ Values: values[:],
+ depth: bt.depth,
+ }
+}
+
+// GetHeight returns the height of the node.
+func (bt *StemNode) GetHeight() int {
+ return 1
+}
+
+// Hash returns the hash of the node.
+func (bt *StemNode) Hash() common.Hash {
+ var data [NodeWidth]common.Hash
+ for i, v := range bt.Values {
+ if v != nil {
+ h := sha256.Sum256(v)
+ data[i] = common.BytesToHash(h[:])
+ }
+ }
+
+ h := sha256.New()
+ for level := 1; level <= 8; level++ {
+ for i := range NodeWidth / (1 << level) {
+ h.Reset()
+
+ if data[i*2] == (common.Hash{}) && data[i*2+1] == (common.Hash{}) {
+ data[i] = common.Hash{}
+ continue
+ }
+
+ h.Write(data[i*2][:])
+ h.Write(data[i*2+1][:])
+ data[i] = common.Hash(h.Sum(nil))
+ }
+ }
+
+ h.Reset()
+ h.Write(bt.Stem)
+ h.Write([]byte{0})
+ h.Write(data[0][:])
+ return common.BytesToHash(h.Sum(nil))
+}
+
+// CollectNodes collects all child nodes at a given path, and flushes it
+// into the provided node collector.
+func (bt *StemNode) CollectNodes(path []byte, flush NodeFlushFn) error {
+ flush(path, bt)
+ return nil
+}
+
+// GetValuesAtStem retrieves the group of values located at the given stem key.
+func (bt *StemNode) GetValuesAtStem(_ []byte, _ NodeResolverFn) ([][]byte, error) {
+ return bt.Values[:], nil
+}
+
+// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
+// Already-existing values will be overwritten.
+func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
+ if !bytes.Equal(bt.Stem, key[:31]) {
+ bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
+
+ n := &InternalNode{depth: bt.depth}
+ bt.depth++
+ var child, other *BinaryNode
+ if bitStem == 0 {
+ n.left = bt
+ child = &n.left
+ other = &n.right
+ } else {
+ n.right = bt
+ child = &n.right
+ other = &n.left
+ }
+
+ bitKey := key[n.depth/8] >> (7 - (n.depth % 8)) & 1
+ if bitKey == bitStem {
+ var err error
+ *child, err = (*child).InsertValuesAtStem(key, values, nil, depth+1)
+ if err != nil {
+ return n, fmt.Errorf("insert error: %w", err)
+ }
+ *other = Empty{}
+ } else {
+ *other = &StemNode{
+ Stem: slices.Clone(key[:31]),
+ Values: values,
+ depth: n.depth + 1,
+ }
+ }
+ return n, nil
+ }
+
+ // same stem, just merge the two value lists
+ for i, v := range values {
+ if v != nil {
+ bt.Values[i] = v
+ }
+ }
+ return bt, nil
+}
+
+func (bt *StemNode) toDot(parent, path string) string {
+ me := fmt.Sprintf("stem%s", path)
+ ret := fmt.Sprintf("%s [label=\"stem=%x c=%x\"]\n", me, bt.Stem, bt.Hash())
+ ret = fmt.Sprintf("%s %s -> %s\n", ret, parent, me)
+ for i, v := range bt.Values {
+ if v != nil {
+ ret = fmt.Sprintf("%s%s%x [label=\"%x\"]\n", ret, me, i, v)
+ ret = fmt.Sprintf("%s%s -> %s%x\n", ret, me, me, i)
+ }
+ }
+ return ret
+}
+
+// Key returns the full key for the given index.
+func (bt *StemNode) Key(i int) []byte {
+ var ret [32]byte
+ copy(ret[:], bt.Stem)
+ ret[StemSize] = byte(i)
+ return ret[:]
+}
diff --git a/trie/bintrie/stem_node_test.go b/trie/bintrie/stem_node_test.go
new file mode 100644
index 0000000000..e0ffd5c3c8
--- /dev/null
+++ b/trie/bintrie/stem_node_test.go
@@ -0,0 +1,373 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+// TestStemNodeInsertSameStem tests inserting values with the same stem
+func TestStemNodeInsertSameStem(t *testing.T) {
+ stem := make([]byte, 31)
+ for i := range stem {
+ stem[i] = byte(i)
+ }
+
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ // Insert another value with the same stem but different last byte
+ key := make([]byte, 32)
+ copy(key[:31], stem)
+ key[31] = 10
+ value := common.HexToHash("0x0202").Bytes()
+
+ newNode, err := node.Insert(key, value, nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert: %v", err)
+ }
+
+ // Should still be a StemNode
+ stemNode, ok := newNode.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", newNode)
+ }
+
+ // Check that both values are present
+ if !bytes.Equal(stemNode.Values[0], values[0]) {
+ t.Errorf("Value at index 0 mismatch")
+ }
+ if !bytes.Equal(stemNode.Values[10], value) {
+ t.Errorf("Value at index 10 mismatch")
+ }
+}
+
+// TestStemNodeInsertDifferentStem tests inserting values with different stems
+func TestStemNodeInsertDifferentStem(t *testing.T) {
+ stem1 := make([]byte, 31)
+ for i := range stem1 {
+ stem1[i] = 0x00
+ }
+
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+
+ node := &StemNode{
+ Stem: stem1,
+ Values: values[:],
+ depth: 0,
+ }
+
+ // Insert with a different stem (first bit different)
+ key := make([]byte, 32)
+ key[0] = 0x80 // First bit is 1 instead of 0
+ value := common.HexToHash("0x0202").Bytes()
+
+ newNode, err := node.Insert(key, value, nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert: %v", err)
+ }
+
+ // Should now be an InternalNode
+ internalNode, ok := newNode.(*InternalNode)
+ if !ok {
+ t.Fatalf("Expected InternalNode, got %T", newNode)
+ }
+
+ // Check depth
+ if internalNode.depth != 0 {
+ t.Errorf("Expected depth 0, got %d", internalNode.depth)
+ }
+
+ // Original stem should be on the left (bit 0)
+ leftStem, ok := internalNode.left.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected left child to be StemNode, got %T", internalNode.left)
+ }
+ if !bytes.Equal(leftStem.Stem, stem1) {
+ t.Errorf("Left stem mismatch")
+ }
+
+ // New stem should be on the right (bit 1)
+ rightStem, ok := internalNode.right.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected right child to be StemNode, got %T", internalNode.right)
+ }
+ if !bytes.Equal(rightStem.Stem, key[:31]) {
+ t.Errorf("Right stem mismatch")
+ }
+}
+
+// TestStemNodeInsertInvalidValueLength tests inserting value with invalid length
+func TestStemNodeInsertInvalidValueLength(t *testing.T) {
+ stem := make([]byte, 31)
+ var values [256][]byte
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ // Try to insert value with wrong length
+ key := make([]byte, 32)
+ copy(key[:31], stem)
+ invalidValue := []byte{1, 2, 3} // Not 32 bytes
+
+ _, err := node.Insert(key, invalidValue, nil, 0)
+ if err == nil {
+ t.Fatal("Expected error for invalid value length")
+ }
+
+ if err.Error() != "invalid insertion: value length" {
+ t.Errorf("Expected 'invalid insertion: value length' error, got: %v", err)
+ }
+}
+
+// TestStemNodeCopy tests the Copy method
+func TestStemNodeCopy(t *testing.T) {
+ stem := make([]byte, 31)
+ for i := range stem {
+ stem[i] = byte(i)
+ }
+
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+ values[255] = common.HexToHash("0x0202").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 10,
+ }
+
+ // Create a copy
+ copied := node.Copy()
+ copiedStem, ok := copied.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", copied)
+ }
+
+ // Check that values are equal but not the same slice
+ if !bytes.Equal(copiedStem.Stem, node.Stem) {
+ t.Errorf("Stem mismatch after copy")
+ }
+ if &copiedStem.Stem[0] == &node.Stem[0] {
+ t.Error("Stem slice not properly cloned")
+ }
+
+ // Check values
+ if !bytes.Equal(copiedStem.Values[0], node.Values[0]) {
+ t.Errorf("Value at index 0 mismatch after copy")
+ }
+ if !bytes.Equal(copiedStem.Values[255], node.Values[255]) {
+ t.Errorf("Value at index 255 mismatch after copy")
+ }
+
+ // Check that value slices are cloned
+ if copiedStem.Values[0] != nil && &copiedStem.Values[0][0] == &node.Values[0][0] {
+ t.Error("Value slice not properly cloned")
+ }
+
+ // Check depth
+ if copiedStem.depth != node.depth {
+ t.Errorf("Depth mismatch: expected %d, got %d", node.depth, copiedStem.depth)
+ }
+}
+
+// TestStemNodeHash tests the Hash method
+func TestStemNodeHash(t *testing.T) {
+ stem := make([]byte, 31)
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ hash1 := node.Hash()
+
+ // Hash should be deterministic
+ hash2 := node.Hash()
+ if hash1 != hash2 {
+ t.Errorf("Hash not deterministic: %x != %x", hash1, hash2)
+ }
+
+ // Changing a value should change the hash
+ node.Values[1] = common.HexToHash("0x0202").Bytes()
+ hash3 := node.Hash()
+ if hash1 == hash3 {
+ t.Error("Hash didn't change after modifying values")
+ }
+}
+
+// TestStemNodeGetValuesAtStem tests GetValuesAtStem method
+func TestStemNodeGetValuesAtStem(t *testing.T) {
+ stem := make([]byte, 31)
+ for i := range stem {
+ stem[i] = byte(i)
+ }
+
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+ values[10] = common.HexToHash("0x0202").Bytes()
+ values[255] = common.HexToHash("0x0303").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ // GetValuesAtStem with matching stem
+ retrievedValues, err := node.GetValuesAtStem(stem, nil)
+ if err != nil {
+ t.Fatalf("Failed to get values: %v", err)
+ }
+
+ // Check that all values match
+ for i := 0; i < 256; i++ {
+ if !bytes.Equal(retrievedValues[i], values[i]) {
+ t.Errorf("Value mismatch at index %d", i)
+ }
+ }
+
+ // GetValuesAtStem with different stem also returns the same values
+ // (implementation ignores the stem parameter)
+ differentStem := make([]byte, 31)
+ differentStem[0] = 0xFF
+
+ retrievedValues2, err := node.GetValuesAtStem(differentStem, nil)
+ if err != nil {
+ t.Fatalf("Failed to get values with different stem: %v", err)
+ }
+
+ // Should still return the same values (stem is ignored)
+ for i := 0; i < 256; i++ {
+ if !bytes.Equal(retrievedValues2[i], values[i]) {
+ t.Errorf("Value mismatch at index %d with different stem", i)
+ }
+ }
+}
+
+// TestStemNodeInsertValuesAtStem tests InsertValuesAtStem method
+func TestStemNodeInsertValuesAtStem(t *testing.T) {
+ stem := make([]byte, 31)
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ // Insert new values at the same stem
+ var newValues [256][]byte
+ newValues[1] = common.HexToHash("0x0202").Bytes()
+ newValues[2] = common.HexToHash("0x0303").Bytes()
+
+ newNode, err := node.InsertValuesAtStem(stem, newValues[:], nil, 0)
+ if err != nil {
+ t.Fatalf("Failed to insert values: %v", err)
+ }
+
+ stemNode, ok := newNode.(*StemNode)
+ if !ok {
+ t.Fatalf("Expected StemNode, got %T", newNode)
+ }
+
+ // Check that all values are present
+ if !bytes.Equal(stemNode.Values[0], values[0]) {
+ t.Error("Original value at index 0 missing")
+ }
+ if !bytes.Equal(stemNode.Values[1], newValues[1]) {
+ t.Error("New value at index 1 missing")
+ }
+ if !bytes.Equal(stemNode.Values[2], newValues[2]) {
+ t.Error("New value at index 2 missing")
+ }
+}
+
+// TestStemNodeGetHeight tests GetHeight method
+func TestStemNodeGetHeight(t *testing.T) {
+ node := &StemNode{
+ Stem: make([]byte, 31),
+ Values: make([][]byte, 256),
+ depth: 0,
+ }
+
+ height := node.GetHeight()
+ if height != 1 {
+ t.Errorf("Expected height 1, got %d", height)
+ }
+}
+
+// TestStemNodeCollectNodes tests CollectNodes method
+func TestStemNodeCollectNodes(t *testing.T) {
+ stem := make([]byte, 31)
+ var values [256][]byte
+ values[0] = common.HexToHash("0x0101").Bytes()
+
+ node := &StemNode{
+ Stem: stem,
+ Values: values[:],
+ depth: 0,
+ }
+
+ var collectedPaths [][]byte
+ var collectedNodes []BinaryNode
+
+ flushFn := func(path []byte, n BinaryNode) {
+ // Make a copy of the path
+ pathCopy := make([]byte, len(path))
+ copy(pathCopy, path)
+ collectedPaths = append(collectedPaths, pathCopy)
+ collectedNodes = append(collectedNodes, n)
+ }
+
+ err := node.CollectNodes([]byte{0, 1, 0}, flushFn)
+ if err != nil {
+ t.Fatalf("Failed to collect nodes: %v", err)
+ }
+
+ // Should have collected one node (itself)
+ if len(collectedNodes) != 1 {
+ t.Errorf("Expected 1 collected node, got %d", len(collectedNodes))
+ }
+
+ // Check that the collected node is the same
+ if collectedNodes[0] != node {
+ t.Error("Collected node doesn't match original")
+ }
+
+ // Check the path
+ if !bytes.Equal(collectedPaths[0], []byte{0, 1, 0}) {
+ t.Errorf("Path mismatch: expected [0, 1, 0], got %v", collectedPaths[0])
+ }
+}
diff --git a/trie/bintrie/trie.go b/trie/bintrie/trie.go
new file mode 100644
index 0000000000..0a8bd325f5
--- /dev/null
+++ b/trie/bintrie/trie.go
@@ -0,0 +1,353 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/trie"
+ "github.com/ethereum/go-ethereum/trie/trienode"
+ "github.com/ethereum/go-ethereum/triedb/database"
+ "github.com/holiman/uint256"
+)
+
+var errInvalidRootType = errors.New("invalid root type")
+
+// NewBinaryNode creates a new empty binary trie
+func NewBinaryNode() BinaryNode {
+ return Empty{}
+}
+
+// BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864.
+type BinaryTrie struct {
+ root BinaryNode
+ reader *trie.Reader
+ tracer *trie.PrevalueTracer
+}
+
+// ToDot converts the binary trie to a DOT language representation. Useful for debugging.
+func (t *BinaryTrie) ToDot() string {
+ t.root.Hash()
+ return ToDot(t.root)
+}
+
+// NewBinaryTrie creates a new binary trie.
+func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) {
+ reader, err := trie.NewReader(root, common.Hash{}, db)
+ if err != nil {
+ return nil, err
+ }
+ t := &BinaryTrie{
+ root: NewBinaryNode(),
+ reader: reader,
+ tracer: trie.NewPrevalueTracer(),
+ }
+ // Parse the root node if it's not empty
+ if root != types.EmptyBinaryHash && root != types.EmptyRootHash {
+ blob, err := t.nodeResolver(nil, root)
+ if err != nil {
+ return nil, err
+ }
+ node, err := DeserializeNode(blob, 0)
+ if err != nil {
+ return nil, err
+ }
+ t.root = node
+ }
+ return t, nil
+}
+
+// nodeResolver is a node resolver that reads nodes from the flatdb.
+func (t *BinaryTrie) nodeResolver(path []byte, hash common.Hash) ([]byte, error) {
+ // empty nodes will be serialized as common.Hash{}, so capture
+ // this special use case.
+ if hash == (common.Hash{}) {
+ return nil, nil // empty node
+ }
+ blob, err := t.reader.Node(path, hash)
+ if err != nil {
+ return nil, err
+ }
+ t.tracer.Put(path, blob)
+ return blob, nil
+}
+
+// GetKey returns the sha3 preimage of a hashed key that was previously used
+// to store a value.
+func (t *BinaryTrie) GetKey(key []byte) []byte {
+ return key
+}
+
+// GetWithHashedKey returns the value, assuming that the key has already
+// been hashed.
+func (t *BinaryTrie) GetWithHashedKey(key []byte) ([]byte, error) {
+ return t.root.Get(key, t.nodeResolver)
+}
+
+// GetAccount returns the account information for the given address.
+func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
+ var (
+ values [][]byte
+ err error
+ acc = &types.StateAccount{}
+ key = GetBinaryTreeKey(addr, zero[:])
+ )
+ switch r := t.root.(type) {
+ case *InternalNode:
+ values, err = r.GetValuesAtStem(key[:31], t.nodeResolver)
+ case *StemNode:
+ values = r.Values
+ case Empty:
+ return nil, nil
+ default:
+ // This will cover HashedNode but that should be fine since the
+ // root node should always be resolved.
+ return nil, errInvalidRootType
+ }
+ if err != nil {
+ return nil, fmt.Errorf("GetAccount (%x) error: %v", addr, err)
+ }
+
+ // The following code is required for the MPT->Binary conversion.
+ // An account can be partially migrated, where storage slots were moved to the binary
+ // but not yet the account. This means some account information as (header) storage slots
+ // are in the binary trie but basic account information must be read in the base tree (MPT).
+ // TODO: we can simplify this logic depending if the conversion is in progress or finished.
+ emptyAccount := true
+ for i := 0; values != nil && i <= CodeHashLeafKey && emptyAccount; i++ {
+ emptyAccount = emptyAccount && values[i] == nil
+ }
+ if emptyAccount {
+ return nil, nil
+ }
+
+ // If the account has been deleted, then values[10] will be 0 and not nil. If it has
+ // been recreated after that, then its code keccak will NOT be 0. So return `nil` if
+ // the nonce, and values[10], and code keccak is 0.
+ if bytes.Equal(values[BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[CodeHashLeafKey], zero[:]) {
+ return nil, nil
+ }
+
+ acc.Nonce = binary.BigEndian.Uint64(values[BasicDataLeafKey][BasicDataNonceOffset:])
+ var balance [16]byte
+ copy(balance[:], values[BasicDataLeafKey][BasicDataBalanceOffset:])
+ acc.Balance = new(uint256.Int).SetBytes(balance[:])
+ acc.CodeHash = values[CodeHashLeafKey]
+
+ return acc, nil
+}
+
+// GetStorage returns the value for key stored in the trie. The value bytes must
+// not be modified by the caller. If a node was not found in the database, a
+// trie.MissingNodeError is returned.
+func (t *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
+ return t.root.Get(GetBinaryTreeKey(addr, key), t.nodeResolver)
+}
+
+// UpdateAccount updates the account information for the given address.
+func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
+ var (
+ err error
+ basicData [32]byte
+ values = make([][]byte, NodeWidth)
+ stem = GetBinaryTreeKey(addr, zero[:])
+ )
+ binary.BigEndian.PutUint32(basicData[BasicDataCodeSizeOffset-1:], uint32(codeLen))
+ binary.BigEndian.PutUint64(basicData[BasicDataNonceOffset:], acc.Nonce)
+
+ // Because the balance is a max of 16 bytes, truncate
+ // the extra values. This happens in devmode, where
+ // 0xff**32 is allocated to the developer account.
+ balanceBytes := acc.Balance.Bytes()
+ // TODO: reduce the size of the allocation in devmode, then panic instead
+ // of truncating.
+ if len(balanceBytes) > 16 {
+ balanceBytes = balanceBytes[16:]
+ }
+ copy(basicData[32-len(balanceBytes):], balanceBytes[:])
+ values[BasicDataLeafKey] = basicData[:]
+ values[CodeHashLeafKey] = acc.CodeHash[:]
+
+ t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
+ return err
+}
+
+// UpdateStem updates the values for the given stem key.
+func (t *BinaryTrie) UpdateStem(key []byte, values [][]byte) error {
+ var err error
+ t.root, err = t.root.InsertValuesAtStem(key, values, t.nodeResolver, 0)
+ return err
+}
+
+// UpdateStorage associates key with value in the trie. If value has length zero, any
+// existing value is deleted from the trie. The value bytes must not be modified
+// by the caller while they are stored in the trie. If a node was not found in the
+// database, a trie.MissingNodeError is returned.
+func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) error {
+ k := GetBinaryTreeKeyStorageSlot(address, key)
+ var v [32]byte
+ if len(value) >= 32 {
+ copy(v[:], value[:32])
+ } else {
+ copy(v[32-len(value):], value[:])
+ }
+ root, err := t.root.Insert(k, v[:], t.nodeResolver, 0)
+ if err != nil {
+ return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
+ }
+ t.root = root
+ return nil
+}
+
+// DeleteAccount is a no-op as it is disabled in stateless.
+func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
+ return nil
+}
+
+// DeleteStorage removes any existing value for key from the trie. If a node was not
+// found in the database, a trie.MissingNodeError is returned.
+func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
+ k := GetBinaryTreeKey(addr, key)
+ var zero [32]byte
+ root, err := t.root.Insert(k, zero[:], t.nodeResolver, 0)
+ if err != nil {
+ return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
+ }
+ t.root = root
+ return nil
+}
+
+// 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.
+func (t *BinaryTrie) Hash() common.Hash {
+ return t.root.Hash()
+}
+
+// Commit writes all nodes to the trie's memory database, tracking the internal
+// and external (for account tries) references.
+func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
+ root := t.root.(*InternalNode)
+ nodeset := trienode.NewNodeSet(common.Hash{})
+
+ err := root.CollectNodes(nil, func(path []byte, node BinaryNode) {
+ serialized := SerializeNode(node)
+ nodeset.AddNode(path, trienode.NewNodeWithPrev(common.Hash{}, serialized, t.tracer.Get(path)))
+ })
+ if err != nil {
+ panic(fmt.Errorf("CollectNodes failed: %v", err))
+ }
+ // Serialize root commitment form
+ return t.Hash(), nodeset
+}
+
+// NodeIterator returns an iterator that returns nodes of the trie. Iteration
+// starts at the key after the given start key.
+func (t *BinaryTrie) NodeIterator(startKey []byte) (trie.NodeIterator, error) {
+ return newBinaryNodeIterator(t, nil)
+}
+
+// Prove constructs a Merkle proof for key. The result contains all encoded nodes
+// on the path to the value at key. The value itself is also included in the last
+// node and can be retrieved by verifying the proof.
+//
+// If the trie does not contain a value for key, the returned proof contains all
+// nodes of the longest existing prefix of the key (at least the root), ending
+// with the node that proves the absence of the key.
+func (t *BinaryTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
+ panic("not implemented")
+}
+
+// Copy creates a deep copy of the trie.
+func (t *BinaryTrie) Copy() *BinaryTrie {
+ return &BinaryTrie{
+ root: t.root.Copy(),
+ reader: t.reader,
+ tracer: t.tracer.Copy(),
+ }
+}
+
+// IsVerkle returns true if the trie is a Verkle tree.
+func (t *BinaryTrie) IsVerkle() bool {
+ // TODO @gballet This is technically NOT a verkle tree, but it has the same
+ // behavior and basic structure, so for all intents and purposes, it can be
+ // treated as such. Rename this when verkle gets removed.
+ return true
+}
+
+// UpdateContractCode updates the contract code into the trie.
+//
+// Note: the basic data leaf needs to have been previously created for this to work
+func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Hash, code []byte) error {
+ var (
+ chunks = trie.ChunkifyCode(code)
+ values [][]byte
+ key []byte
+ err error
+ )
+ for i, chunknr := 0, uint64(0); i < len(chunks); i, chunknr = i+32, chunknr+1 {
+ groupOffset := (chunknr + 128) % 256
+ if groupOffset == 0 /* start of new group */ || chunknr == 0 /* first chunk in header group */ {
+ values = make([][]byte, NodeWidth)
+ var offset [32]byte
+ binary.LittleEndian.PutUint64(offset[24:], chunknr+128)
+ key = GetBinaryTreeKey(addr, offset[:])
+ }
+ values[groupOffset] = chunks[i : i+32]
+
+ if groupOffset == 255 || len(chunks)-i <= 32 {
+ err = t.UpdateStem(key[:31], values)
+
+ if err != nil {
+ return fmt.Errorf("UpdateContractCode (addr=%x) error: %w", addr[:], err)
+ }
+ }
+ }
+ return nil
+}
+
+// PrefetchAccount attempts to resolve specific accounts from the database
+// to accelerate subsequent trie operations.
+func (t *BinaryTrie) PrefetchAccount(addresses []common.Address) error {
+ for _, addr := range addresses {
+ if _, err := t.GetAccount(addr); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// PrefetchStorage attempts to resolve specific storage slots from the database
+// to accelerate subsequent trie operations.
+func (t *BinaryTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
+ for _, key := range keys {
+ if _, err := t.GetStorage(addr, key); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// Witness returns a set containing all trie nodes that have been accessed.
+func (t *BinaryTrie) Witness() map[string][]byte {
+ panic("not implemented")
+}
diff --git a/trie/bintrie/trie_test.go b/trie/bintrie/trie_test.go
new file mode 100644
index 0000000000..84f7689549
--- /dev/null
+++ b/trie/bintrie/trie_test.go
@@ -0,0 +1,197 @@
+// Copyright 2025 go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package bintrie
+
+import (
+ "bytes"
+ "encoding/binary"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+)
+
+var (
+ zeroKey = [32]byte{}
+ oneKey = common.HexToHash("0101010101010101010101010101010101010101010101010101010101010101")
+ twoKey = common.HexToHash("0202020202020202020202020202020202020202020202020202020202020202")
+ threeKey = common.HexToHash("0303030303030303030303030303030303030303030303030303030303030303")
+ fourKey = common.HexToHash("0404040404040404040404040404040404040404040404040404040404040404")
+ ffKey = common.HexToHash("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
+)
+
+func TestSingleEntry(t *testing.T) {
+ tree := NewBinaryNode()
+ tree, err := tree.Insert(zeroKey[:], oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid depth")
+ }
+ expected := common.HexToHash("aab1060e04cb4f5dc6f697ae93156a95714debbf77d54238766adc5709282b6f")
+ got := tree.Hash()
+ if got != expected {
+ t.Fatalf("invalid tree root, got %x, want %x", got, expected)
+ }
+}
+
+func TestTwoEntriesDiffFirstBit(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(zeroKey[:], oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 2 {
+ t.Fatal("invalid height")
+ }
+ if tree.Hash() != common.HexToHash("dfc69c94013a8b3c65395625a719a87534a7cfd38719251ad8c8ea7fe79f065e") {
+ t.Fatal("invalid tree root")
+ }
+}
+
+func TestOneStemColocatedValues(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid height")
+ }
+}
+
+func TestTwoStemColocatedValues(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ // stem: 0...0
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // stem: 10...0
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 2 {
+ t.Fatal("invalid height")
+ }
+}
+
+func TestTwoKeysMatchFirst42Bits(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ // key1 and key 2 have the same prefix of 42 bits (b0*42+b1+b1) and differ after.
+ key1 := common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0").Bytes()
+ key2 := common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000").Bytes()
+ tree, err = tree.Insert(key1, oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(key2, twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1+42+1 {
+ t.Fatal("invalid height")
+ }
+}
+func TestInsertDuplicateKey(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ tree, err = tree.Insert(oneKey[:], oneKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tree, err = tree.Insert(oneKey[:], twoKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if tree.GetHeight() != 1 {
+ t.Fatal("invalid height")
+ }
+ // Verify that the value is updated
+ if !bytes.Equal(tree.(*StemNode).Values[1], twoKey[:]) {
+ t.Fatal("invalid height")
+ }
+}
+func TestLargeNumberOfEntries(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ for i := range 256 {
+ var key [32]byte
+ key[0] = byte(i)
+ tree, err = tree.Insert(key[:], ffKey[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ height := tree.GetHeight()
+ if height != 1+8 {
+ t.Fatalf("invalid height, wanted %d, got %d", 1+8, height)
+ }
+}
+
+func TestMerkleizeMultipleEntries(t *testing.T) {
+ var err error
+ tree := NewBinaryNode()
+ keys := [][]byte{
+ zeroKey[:],
+ common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ common.HexToHash("0100000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ common.HexToHash("8100000000000000000000000000000000000000000000000000000000000000").Bytes(),
+ }
+ for i, key := range keys {
+ var v [32]byte
+ binary.LittleEndian.PutUint64(v[:8], uint64(i))
+ tree, err = tree.Insert(key, v[:], nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ got := tree.Hash()
+ expected := common.HexToHash("9317155862f7a3867660ddd0966ff799a3d16aa4df1e70a7516eaa4a675191b5")
+ if got != expected {
+ t.Fatalf("invalid root, expected=%x, got = %x", expected, got)
+ }
+}
diff --git a/trie/committer.go b/trie/committer.go
index a040868c6c..2a2142e0ff 100644
--- a/trie/committer.go
+++ b/trie/committer.go
@@ -29,12 +29,12 @@ import (
// insertion order.
type committer struct {
nodes *trienode.NodeSet
- tracer *prevalueTracer
+ tracer *PrevalueTracer
collectLeaf bool
}
// newCommitter creates a new committer or picks one from the pool.
-func newCommitter(nodeset *trienode.NodeSet, tracer *prevalueTracer, collectLeaf bool) *committer {
+func newCommitter(nodeset *trienode.NodeSet, tracer *PrevalueTracer, collectLeaf bool) *committer {
return &committer{
nodes: nodeset,
tracer: tracer,
@@ -142,7 +142,7 @@ func (c *committer) store(path []byte, n node) node {
// The node is embedded in its parent, in other words, this node
// will not be stored in the database independently, mark it as
// deleted only if the node was existent in database before.
- origin := c.tracer.get(path)
+ origin := c.tracer.Get(path)
if len(origin) != 0 {
c.nodes.AddNode(path, trienode.NewDeletedWithPrev(origin))
}
@@ -150,7 +150,7 @@ func (c *committer) store(path []byte, n node) node {
}
// Collect the dirty node to nodeset for return.
nhash := common.BytesToHash(hash)
- c.nodes.AddNode(path, trienode.NewNodeWithPrev(nhash, nodeToBytes(n), c.tracer.get(path)))
+ c.nodes.AddNode(path, trienode.NewNodeWithPrev(nhash, nodeToBytes(n), c.tracer.Get(path)))
// Collect the corresponding leaf node if it's required. We don't check
// full node since it's impossible to store value in fullNode. The key
diff --git a/trie/iterator.go b/trie/iterator.go
index e6fedf2430..80298ce48f 100644
--- a/trie/iterator.go
+++ b/trie/iterator.go
@@ -405,7 +405,7 @@ func (it *nodeIterator) resolveHash(hash hashNode, path []byte) (node, error) {
// loaded blob will be tracked, while it's not required here since
// all loaded nodes won't be linked to trie at all and track nodes
// may lead to out-of-memory issue.
- blob, err := it.trie.reader.node(path, common.BytesToHash(hash))
+ blob, err := it.trie.reader.Node(path, common.BytesToHash(hash))
if err != nil {
return nil, err
}
@@ -426,7 +426,7 @@ func (it *nodeIterator) resolveBlob(hash hashNode, path []byte) ([]byte, error)
// loaded blob will be tracked, while it's not required here since
// all loaded nodes won't be linked to trie at all and track nodes
// may lead to out-of-memory issue.
- return it.trie.reader.node(path, common.BytesToHash(hash))
+ return it.trie.reader.Node(path, common.BytesToHash(hash))
}
func (st *nodeIteratorState) resolve(it *nodeIterator, path []byte) error {
diff --git a/trie/proof.go b/trie/proof.go
index f3ed417094..1a06ed5d5e 100644
--- a/trie/proof.go
+++ b/trie/proof.go
@@ -69,7 +69,7 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
// loaded blob will be tracked, while it's not required here since
// all loaded nodes won't be linked to trie at all and track nodes
// may lead to out-of-memory issue.
- blob, err := t.reader.node(prefix, common.BytesToHash(n))
+ blob, err := t.reader.Node(prefix, common.BytesToHash(n))
if err != nil {
log.Error("Unhandled trie error in Trie.Prove", "err", err)
return err
@@ -571,7 +571,7 @@ func VerifyRangeProof(rootHash common.Hash, firstKey []byte, keys [][]byte, valu
root: root,
reader: newEmptyReader(),
opTracer: newOpTracer(),
- prevalueTracer: newPrevalueTracer(),
+ prevalueTracer: NewPrevalueTracer(),
}
if empty {
tr.root = nil
diff --git a/trie/secure_trie.go b/trie/secure_trie.go
index 0424ecb6e5..7c7bd184bf 100644
--- a/trie/secure_trie.go
+++ b/trie/secure_trie.go
@@ -105,19 +105,6 @@ func (t *StateTrie) MustGet(key []byte) []byte {
return t.trie.MustGet(crypto.Keccak256(key))
}
-// GetStorage attempts to retrieve a storage slot with provided account address
-// and slot key. The value bytes must not be modified by the caller.
-// If the specified storage slot is not in the trie, nil will be returned.
-// If a trie node is not found in the database, a MissingNodeError is returned.
-func (t *StateTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) {
- enc, err := t.trie.Get(crypto.Keccak256(key))
- if err != nil || len(enc) == 0 {
- return nil, err
- }
- _, content, _, err := rlp.Split(enc)
- return content, err
-}
-
// GetAccount attempts to retrieve an account with provided account address.
// If the specified account is not in the trie, nil will be returned.
// If a trie node is not found in the database, a MissingNodeError is returned.
@@ -144,6 +131,39 @@ func (t *StateTrie) GetAccountByHash(addrHash common.Hash) (*types.StateAccount,
return ret, err
}
+// PrefetchAccount attempts to resolve specific accounts from the database
+// to accelerate subsequent trie operations.
+func (t *StateTrie) PrefetchAccount(addresses []common.Address) error {
+ var keys [][]byte
+ for _, addr := range addresses {
+ keys = append(keys, crypto.Keccak256(addr.Bytes()))
+ }
+ return t.trie.Prefetch(keys)
+}
+
+// GetStorage attempts to retrieve a storage slot with provided account address
+// and slot key. The value bytes must not be modified by the caller.
+// If the specified storage slot is not in the trie, nil will be returned.
+// If a trie node is not found in the database, a MissingNodeError is returned.
+func (t *StateTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) {
+ enc, err := t.trie.Get(crypto.Keccak256(key))
+ if err != nil || len(enc) == 0 {
+ return nil, err
+ }
+ _, content, _, err := rlp.Split(enc)
+ return content, err
+}
+
+// PrefetchStorage attempts to resolve specific storage slots from the database
+// to accelerate subsequent trie operations.
+func (t *StateTrie) PrefetchStorage(_ common.Address, keys [][]byte) error {
+ var keylist [][]byte
+ for _, key := range keys {
+ keylist = append(keylist, crypto.Keccak256(key))
+ }
+ return t.trie.Prefetch(keylist)
+}
+
// GetNode attempts to retrieve a trie node by compact-encoded path. It is not
// possible to use keybyte-encoding as the path might contain odd nibbles.
// If the specified trie node is not in the trie, nil will be returned.
@@ -253,7 +273,7 @@ func (t *StateTrie) GetKey(shaKey []byte) []byte {
}
// Witness returns a set containing all trie nodes that have been accessed.
-func (t *StateTrie) Witness() map[string]struct{} {
+func (t *StateTrie) Witness() map[string][]byte {
return t.trie.Witness()
}
diff --git a/trie/tracer.go b/trie/tracer.go
index 206e8aa20d..04122d1384 100644
--- a/trie/tracer.go
+++ b/trie/tracer.go
@@ -18,7 +18,7 @@ package trie
import (
"maps"
- "slices"
+ "sync"
)
// opTracer tracks the changes of trie nodes. During the trie operations,
@@ -94,39 +94,48 @@ func (t *opTracer) deletedList() [][]byte {
return paths
}
-// prevalueTracer tracks the original values of resolved trie nodes. Cached trie
+// PrevalueTracer tracks the original values of resolved trie nodes. Cached trie
// node values are expected to be immutable. A zero-size node value is treated as
// non-existent and should not occur in practice.
//
-// Note prevalueTracer is not thread-safe, callers should be responsible for
-// handling the concurrency issues by themselves.
-type prevalueTracer struct {
+// Note PrevalueTracer is thread-safe.
+type PrevalueTracer struct {
data map[string][]byte
+ lock sync.RWMutex
}
-// newPrevalueTracer initializes the tracer for capturing resolved trie nodes.
-func newPrevalueTracer() *prevalueTracer {
- return &prevalueTracer{
+// NewPrevalueTracer initializes the tracer for capturing resolved trie nodes.
+func NewPrevalueTracer() *PrevalueTracer {
+ return &PrevalueTracer{
data: make(map[string][]byte),
}
}
-// put tracks the newly loaded trie node and caches its RLP-encoded
+// Put tracks the newly loaded trie node and caches its RLP-encoded
// blob internally. Do not modify the value outside this function,
// as it is not deep-copied.
-func (t *prevalueTracer) put(path []byte, val []byte) {
+func (t *PrevalueTracer) Put(path []byte, val []byte) {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
t.data[string(path)] = val
}
-// get returns the cached trie node value. If the node is not found, nil will
+// Get returns the cached trie node value. If the node is not found, nil will
// be returned.
-func (t *prevalueTracer) get(path []byte) []byte {
+func (t *PrevalueTracer) Get(path []byte) []byte {
+ t.lock.RLock()
+ defer t.lock.RUnlock()
+
return t.data[string(path)]
}
-// hasList returns a list of flags indicating whether the corresponding trie nodes
+// HasList returns a list of flags indicating whether the corresponding trie nodes
// specified by the path exist in the trie.
-func (t *prevalueTracer) hasList(list [][]byte) []bool {
+func (t *PrevalueTracer) HasList(list [][]byte) []bool {
+ t.lock.RLock()
+ defer t.lock.RUnlock()
+
exists := make([]bool, 0, len(list))
for _, path := range list {
_, ok := t.data[string(path)]
@@ -135,20 +144,29 @@ func (t *prevalueTracer) hasList(list [][]byte) []bool {
return exists
}
-// values returns a list of values of the cached trie nodes.
-func (t *prevalueTracer) values() [][]byte {
- return slices.Collect(maps.Values(t.data))
+// Values returns a list of values of the cached trie nodes.
+func (t *PrevalueTracer) Values() map[string][]byte {
+ t.lock.RLock()
+ defer t.lock.RUnlock()
+
+ return maps.Clone(t.data)
}
-// reset resets the cached content in the prevalueTracer.
-func (t *prevalueTracer) reset() {
+// Reset resets the cached content in the prevalueTracer.
+func (t *PrevalueTracer) Reset() {
+ t.lock.Lock()
+ defer t.lock.Unlock()
+
clear(t.data)
}
-// copy returns a copied prevalueTracer instance.
-func (t *prevalueTracer) copy() *prevalueTracer {
+// Copy returns a copied prevalueTracer instance.
+func (t *PrevalueTracer) Copy() *PrevalueTracer {
+ t.lock.RLock()
+ defer t.lock.RUnlock()
+
// Shadow clone is used, as the cached trie node values are immutable
- return &prevalueTracer{
+ return &PrevalueTracer{
data: maps.Clone(t.data),
}
}
diff --git a/trie/tracer_test.go b/trie/tracer_test.go
index f2a4287461..695570fd0d 100644
--- a/trie/tracer_test.go
+++ b/trie/tracer_test.go
@@ -70,7 +70,6 @@ func testTrieOpTracer(t *testing.T, vals []struct{ k, v string }) {
}
insertSet := copySet(trie.opTracer.inserts) // copy before commit
deleteSet := copySet(trie.opTracer.deletes) // copy before commit
-
root, nodes := trie.Commit(false)
db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
diff --git a/trie/transition.go b/trie/transition.go
index ad3f782b75..da49c6cdc2 100644
--- a/trie/transition.go
+++ b/trie/transition.go
@@ -37,7 +37,7 @@ type TransitionTrie struct {
}
// NewTransitionTrie creates a new TransitionTrie.
-func NewTransitionTree(base *SecureTrie, overlay *VerkleTrie, st bool) *TransitionTrie {
+func NewTransitionTrie(base *SecureTrie, overlay *VerkleTrie, st bool) *TransitionTrie {
return &TransitionTrie{
overlay: overlay,
base: base,
@@ -78,6 +78,17 @@ func (t *TransitionTrie) GetStorage(addr common.Address, key []byte) ([]byte, er
return t.base.GetStorage(addr, key)
}
+// PrefetchStorage attempts to resolve specific storage slots from the database
+// to accelerate subsequent trie operations.
+func (t *TransitionTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
+ for _, key := range keys {
+ if _, err := t.GetStorage(addr, key); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// GetAccount abstract an account read from the trie.
func (t *TransitionTrie) GetAccount(address common.Address) (*types.StateAccount, error) {
data, err := t.overlay.GetAccount(address)
@@ -94,6 +105,17 @@ func (t *TransitionTrie) GetAccount(address common.Address) (*types.StateAccount
return t.base.GetAccount(address)
}
+// PrefetchAccount attempts to resolve specific accounts from the database
+// to accelerate subsequent trie operations.
+func (t *TransitionTrie) PrefetchAccount(addresses []common.Address) error {
+ for _, addr := range addresses {
+ if _, err := t.GetAccount(addr); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// UpdateStorage associates key with value in the trie. If value has length zero, any
// existing value is deleted from the trie. The value bytes must not be modified
// by the caller while they are stored in the trie.
@@ -173,7 +195,7 @@ func (t *TransitionTrie) IsVerkle() bool {
return true
}
-// UpdateStems updates a group of values, given the stem they are using. If
+// UpdateStem updates a group of values, given the stem they are using. If
// a value already exists, it is overwritten.
func (t *TransitionTrie) UpdateStem(key []byte, values [][]byte) error {
trie := t.overlay
@@ -200,6 +222,6 @@ func (t *TransitionTrie) UpdateContractCode(addr common.Address, codeHash common
}
// Witness returns a set containing all trie nodes that have been accessed.
-func (t *TransitionTrie) Witness() map[string]struct{} {
+func (t *TransitionTrie) Witness() map[string][]byte {
panic("not implemented")
}
diff --git a/trie/trie.go b/trie/trie.go
index 307036faa9..630462f8ca 100644
--- a/trie/trie.go
+++ b/trie/trie.go
@@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database"
+ "golang.org/x/sync/errgroup"
)
// Trie represents a Merkle Patricia Trie. Use New to create a trie that operates
@@ -54,11 +55,11 @@ type Trie struct {
uncommitted int
// reader is the handler trie can retrieve nodes from.
- reader *trieReader
+ reader *Reader
// Various tracers for capturing the modifications to trie
opTracer *opTracer
- prevalueTracer *prevalueTracer
+ prevalueTracer *PrevalueTracer
}
// newFlag returns the cache flag value for a newly created node.
@@ -76,7 +77,7 @@ func (t *Trie) Copy() *Trie {
uncommitted: t.uncommitted,
reader: t.reader,
opTracer: t.opTracer.copy(),
- prevalueTracer: t.prevalueTracer.copy(),
+ prevalueTracer: t.prevalueTracer.Copy(),
}
}
@@ -87,7 +88,7 @@ func (t *Trie) Copy() *Trie {
// empty, otherwise, the root node must be present in database or returns
// a MissingNodeError if not.
func New(id *ID, db database.NodeDatabase) (*Trie, error) {
- reader, err := newTrieReader(id.StateRoot, id.Owner, db)
+ reader, err := NewReader(id.StateRoot, id.Owner, db)
if err != nil {
return nil, err
}
@@ -95,7 +96,7 @@ func New(id *ID, db database.NodeDatabase) (*Trie, error) {
owner: id.Owner,
reader: reader,
opTracer: newOpTracer(),
- prevalueTracer: newPrevalueTracer(),
+ prevalueTracer: NewPrevalueTracer(),
}
if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash {
rootnode, err := trie.resolveAndTrack(id.Root[:], nil)
@@ -194,6 +195,51 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no
}
}
+// Prefetch attempts to resolve the leaves and intermediate trie nodes
+// specified by the key list in parallel. The results are silently
+// discarded to simplify the function.
+func (t *Trie) Prefetch(keylist [][]byte) error {
+ // Short circuit if the trie is already committed and not usable.
+ if t.committed {
+ return ErrCommitted
+ }
+ // Resolve the trie nodes sequentially if there are not too many
+ // trie nodes in the trie.
+ fn, ok := t.root.(*fullNode)
+ if !ok || len(keylist) < 16 {
+ for _, key := range keylist {
+ _, err := t.Get(key)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+ var (
+ keys = make(map[byte][][]byte)
+ eg errgroup.Group
+ )
+ for _, key := range keylist {
+ hkey := keybytesToHex(key)
+ keys[hkey[0]] = append(keys[hkey[0]], hkey)
+ }
+ for pos, ks := range keys {
+ eg.Go(func() error {
+ for _, k := range ks {
+ _, newnode, didResolve, err := t.get(fn.Children[pos], k, 1)
+ if err == nil && didResolve {
+ fn.Children[pos] = newnode
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ }
+ return eg.Wait()
+}
+
// MustGetNode is a wrapper of GetNode and will omit any encountered error but
// just print out an error message.
func (t *Trie) MustGetNode(path []byte) ([]byte, int) {
@@ -243,7 +289,7 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
if hash == nil {
return nil, origNode, 0, errors.New("non-consensus node")
}
- blob, err := t.reader.node(path, common.BytesToHash(hash))
+ blob, err := t.reader.Node(path, common.BytesToHash(hash))
return blob, origNode, 1, err
}
// Path still needs to be traversed, descend into children
@@ -609,11 +655,11 @@ func (t *Trie) resolve(n node, prefix []byte) (node, error) {
// node's original value. The rlp-encoded blob is preferred to be loaded from
// database because it's easy to decode node while complex to encode node to blob.
func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) {
- blob, err := t.reader.node(prefix, common.BytesToHash(n))
+ blob, err := t.reader.Node(prefix, common.BytesToHash(n))
if err != nil {
return nil, err
}
- t.prevalueTracer.put(prefix, blob)
+ t.prevalueTracer.Put(prefix, blob)
// The returned node blob won't be changed afterward. No need to
// deep-copy the slice.
@@ -627,7 +673,7 @@ func (t *Trie) deletedNodes() [][]byte {
var (
pos int
list = t.opTracer.deletedList()
- flags = t.prevalueTracer.hasList(list)
+ flags = t.prevalueTracer.HasList(list)
)
for i := 0; i < len(list); i++ {
if flags[i] {
@@ -665,7 +711,7 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
}
nodes := trienode.NewNodeSet(t.owner)
for _, path := range paths {
- nodes.AddNode(path, trienode.NewDeletedWithPrev(t.prevalueTracer.get(path)))
+ nodes.AddNode(path, trienode.NewDeletedWithPrev(t.prevalueTracer.Get(path)))
}
return types.EmptyRootHash, nodes // case (b)
}
@@ -683,7 +729,7 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
}
nodes := trienode.NewNodeSet(t.owner)
for _, path := range t.deletedNodes() {
- nodes.AddNode(path, trienode.NewDeletedWithPrev(t.prevalueTracer.get(path)))
+ nodes.AddNode(path, trienode.NewDeletedWithPrev(t.prevalueTracer.Get(path)))
}
// If the number of changes is below 100, we let one thread handle it
t.root = newCommitter(nodes, t.prevalueTracer, collectLeaf).Commit(t.root, t.uncommitted > 100)
@@ -706,16 +752,8 @@ func (t *Trie) hashRoot() []byte {
}
// Witness returns a set containing all trie nodes that have been accessed.
-func (t *Trie) Witness() map[string]struct{} {
- values := t.prevalueTracer.values()
- if len(values) == 0 {
- return nil
- }
- witness := make(map[string]struct{}, len(values))
- for _, val := range values {
- witness[string(val)] = struct{}{}
- }
- return witness
+func (t *Trie) Witness() map[string][]byte {
+ return t.prevalueTracer.Values()
}
// Reset drops the referenced root node and cleans all internal state.
@@ -725,6 +763,6 @@ func (t *Trie) Reset() {
t.unhashed = 0
t.uncommitted = 0
t.opTracer.reset()
- t.prevalueTracer.reset()
+ t.prevalueTracer.Reset()
t.committed = false
}
diff --git a/trie/trie_reader.go b/trie/trie_reader.go
index a42cdb0cf9..42fe4d72c7 100644
--- a/trie/trie_reader.go
+++ b/trie/trie_reader.go
@@ -22,39 +22,39 @@ import (
"github.com/ethereum/go-ethereum/triedb/database"
)
-// trieReader is a wrapper of the underlying node reader. It's not safe
+// Reader is a wrapper of the underlying database reader. It's not safe
// for concurrent usage.
-type trieReader struct {
+type Reader struct {
owner common.Hash
reader database.NodeReader
banned map[string]struct{} // Marker to prevent node from being accessed, for tests
}
-// newTrieReader initializes the trie reader with the given node reader.
-func newTrieReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*trieReader, error) {
+// NewReader initializes the trie reader with the given database reader.
+func NewReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*Reader, error) {
if stateRoot == (common.Hash{}) || stateRoot == types.EmptyRootHash {
- return &trieReader{owner: owner}, nil
+ return &Reader{owner: owner}, nil
}
reader, err := db.NodeReader(stateRoot)
if err != nil {
return nil, &MissingNodeError{Owner: owner, NodeHash: stateRoot, err: err}
}
- return &trieReader{owner: owner, reader: reader}, nil
+ return &Reader{owner: owner, reader: reader}, nil
}
// newEmptyReader initializes the pure in-memory reader. All read operations
// should be forbidden and returns the MissingNodeError.
-func newEmptyReader() *trieReader {
- return &trieReader{}
+func newEmptyReader() *Reader {
+ return &Reader{}
}
-// node retrieves the rlp-encoded trie node with the provided trie node
+// Node retrieves the rlp-encoded trie node with the provided trie node
// information. An MissingNodeError will be returned in case the node is
// not found or any error is encountered.
//
// Don't modify the returned byte slice since it's not deep-copied and
// still be referenced by database.
-func (r *trieReader) node(path []byte, hash common.Hash) ([]byte, error) {
+func (r *Reader) Node(path []byte, hash common.Hash) ([]byte, error) {
// Perform the logics in tests for preventing trie node access.
if r.banned != nil {
if _, ok := r.banned[string(path)]; ok {
diff --git a/trie/trie_test.go b/trie/trie_test.go
index 68759c37c0..22c3494f47 100644
--- a/trie/trie_test.go
+++ b/trie/trie_test.go
@@ -1499,3 +1499,83 @@ func testTrieCopyNewTrie(t *testing.T, entries []kv) {
t.Errorf("Hash mismatch: old %v, new %v", hash, tr.Hash())
}
}
+
+// goos: darwin
+// goarch: arm64
+// pkg: github.com/ethereum/go-ethereum/trie
+// cpu: Apple M1 Pro
+// BenchmarkTriePrefetch
+// BenchmarkTriePrefetch-8 9961 100706 ns/op
+func BenchmarkTriePrefetch(b *testing.B) {
+ db := newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme)
+ tr := NewEmpty(db)
+ vals := make(map[string]*kv)
+ for i := 0; i < 3000; i++ {
+ value := &kv{
+ k: randBytes(32),
+ v: randBytes(20),
+ t: false,
+ }
+ tr.MustUpdate(value.k, value.v)
+ vals[string(value.k)] = value
+ }
+ root, nodes := tr.Commit(false)
+ db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ tr, err := New(TrieID(root), db)
+ if err != nil {
+ b.Fatalf("Failed to open the trie")
+ }
+ var keys [][]byte
+ for k := range vals {
+ keys = append(keys, []byte(k))
+ if len(keys) > 64 {
+ break
+ }
+ }
+ tr.Prefetch(keys)
+ }
+}
+
+// goos: darwin
+// goarch: arm64
+// pkg: github.com/ethereum/go-ethereum/trie
+// cpu: Apple M1 Pro
+// BenchmarkTrieSeqPrefetch
+// BenchmarkTrieSeqPrefetch-8 12879 96710 ns/op
+func BenchmarkTrieSeqPrefetch(b *testing.B) {
+ db := newTestDatabase(rawdb.NewMemoryDatabase(), rawdb.HashScheme)
+ tr := NewEmpty(db)
+ vals := make(map[string]*kv)
+ for i := 0; i < 3000; i++ {
+ value := &kv{
+ k: randBytes(32),
+ v: randBytes(20),
+ t: false,
+ }
+ tr.MustUpdate(value.k, value.v)
+ vals[string(value.k)] = value
+ }
+ root, nodes := tr.Commit(false)
+ db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
+ b.ResetTimer()
+
+ for i := 0; i < b.N; i++ {
+ tr, err := New(TrieID(root), db)
+ if err != nil {
+ b.Fatalf("Failed to open the trie")
+ }
+ var keys [][]byte
+ for k := range vals {
+ keys = append(keys, []byte(k))
+ if len(keys) > 64 {
+ break
+ }
+ }
+ for _, k := range keys {
+ tr.Get(k)
+ }
+ }
+}
diff --git a/trie/trienode/node.go b/trie/trienode/node.go
index c83dc27cef..228a64f04c 100644
--- a/trie/trienode/node.go
+++ b/trie/trienode/node.go
@@ -259,11 +259,24 @@ func (set *MergedNodeSet) Merge(other *NodeSet) error {
return nil
}
-// Flatten returns a two-dimensional map for internal nodes.
-func (set *MergedNodeSet) Flatten() map[common.Hash]map[string]*Node {
+// Nodes returns a two-dimensional map for internal nodes.
+func (set *MergedNodeSet) Nodes() map[common.Hash]map[string]*Node {
nodes := make(map[common.Hash]map[string]*Node, len(set.Sets))
for owner, set := range set.Sets {
nodes[owner] = set.Nodes
}
return nodes
}
+
+// NodeAndOrigins returns a two-dimensional map for internal nodes along with
+// their original values.
+func (set *MergedNodeSet) NodeAndOrigins() (map[common.Hash]map[string]*Node, map[common.Hash]map[string][]byte) {
+ var (
+ nodes = make(map[common.Hash]map[string]*Node, len(set.Sets))
+ origins = make(map[common.Hash]map[string][]byte, len(set.Sets))
+ )
+ for owner, set := range set.Sets {
+ nodes[owner], origins[owner] = set.Nodes, set.Origins
+ }
+ return nodes, origins
+}
diff --git a/trie/verkle.go b/trie/verkle.go
index c89a8f1d36..186ac1f642 100644
--- a/trie/verkle.go
+++ b/trie/verkle.go
@@ -41,13 +41,13 @@ var (
type VerkleTrie struct {
root verkle.VerkleNode
cache *utils.PointCache
- reader *trieReader
- tracer *prevalueTracer
+ reader *Reader
+ tracer *PrevalueTracer
}
// NewVerkleTrie constructs a verkle tree based on the specified root hash.
func NewVerkleTrie(root common.Hash, db database.NodeDatabase, cache *utils.PointCache) (*VerkleTrie, error) {
- reader, err := newTrieReader(root, common.Hash{}, db)
+ reader, err := NewReader(root, common.Hash{}, db)
if err != nil {
return nil, err
}
@@ -55,7 +55,7 @@ func NewVerkleTrie(root common.Hash, db database.NodeDatabase, cache *utils.Poin
root: verkle.New(),
cache: cache,
reader: reader,
- tracer: newPrevalueTracer(),
+ tracer: NewPrevalueTracer(),
}
// Parse the root verkle node if it's not empty.
if root != types.EmptyVerkleHash && root != types.EmptyRootHash {
@@ -108,6 +108,17 @@ func (t *VerkleTrie) GetAccount(addr common.Address) (*types.StateAccount, error
return acc, nil
}
+// PrefetchAccount attempts to resolve specific accounts from the database
+// to accelerate subsequent trie operations.
+func (t *VerkleTrie) PrefetchAccount(addresses []common.Address) error {
+ for _, addr := range addresses {
+ if _, err := t.GetAccount(addr); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// GetStorage implements state.Trie, retrieving the storage slot with the specified
// account address and storage key. If the specified slot is not in the verkle tree,
// nil will be returned. If the tree is corrupted, an error will be returned.
@@ -120,6 +131,17 @@ func (t *VerkleTrie) GetStorage(addr common.Address, key []byte) ([]byte, error)
return common.TrimLeftZeroes(val), nil
}
+// PrefetchStorage attempts to resolve specific storage slots from the database
+// to accelerate subsequent trie operations.
+func (t *VerkleTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
+ for _, key := range keys {
+ if _, err := t.GetStorage(addr, key); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// UpdateAccount implements state.Trie, writing the provided account into the tree.
// If the tree is corrupted, an error will be returned.
func (t *VerkleTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
@@ -267,7 +289,7 @@ func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{})
for _, node := range nodes {
// Hash parameter is not used in pathdb
- nodeset.AddNode(node.Path, trienode.NewNodeWithPrev(common.Hash{}, node.SerializedBytes, t.tracer.get(node.Path)))
+ nodeset.AddNode(node.Path, trienode.NewNodeWithPrev(common.Hash{}, node.SerializedBytes, t.tracer.Get(node.Path)))
}
// Serialize root commitment form
return t.Hash(), nodeset
@@ -300,7 +322,7 @@ func (t *VerkleTrie) Copy() *VerkleTrie {
root: t.root.Copy(),
cache: t.cache,
reader: t.reader,
- tracer: t.tracer.copy(),
+ tracer: t.tracer.Copy(),
}
}
@@ -421,15 +443,15 @@ func (t *VerkleTrie) ToDot() string {
}
func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) {
- blob, err := t.reader.node(path, common.Hash{})
+ blob, err := t.reader.Node(path, common.Hash{})
if err != nil {
return nil, err
}
- t.tracer.put(path, blob)
+ t.tracer.Put(path, blob)
return blob, nil
}
// Witness returns a set containing all trie nodes that have been accessed.
-func (t *VerkleTrie) Witness() map[string]struct{} {
+func (t *VerkleTrie) Witness() map[string][]byte {
panic("not implemented")
}
diff --git a/triedb/database.go b/triedb/database.go
index e2f4334d6e..d2637bd909 100644
--- a/triedb/database.go
+++ b/triedb/database.go
@@ -375,3 +375,12 @@ func (db *Database) IsVerkle() bool {
func (db *Database) Disk() ethdb.Database {
return db.disk
}
+
+// SnapshotCompleted returns the indicator if the snapshot is completed.
+func (db *Database) SnapshotCompleted() bool {
+ pdb, ok := db.backend.(*pathdb.Database)
+ if !ok {
+ return false
+ }
+ return pdb.SnapshotCompleted()
+}
diff --git a/triedb/pathdb/config.go b/triedb/pathdb/config.go
new file mode 100644
index 0000000000..3745a63edd
--- /dev/null
+++ b/triedb/pathdb/config.go
@@ -0,0 +1,118 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package pathdb
+
+import (
+ "fmt"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/log"
+ "github.com/ethereum/go-ethereum/params"
+)
+
+const (
+ // defaultTrieCleanSize is the default memory allowance of clean trie cache.
+ defaultTrieCleanSize = 16 * 1024 * 1024
+
+ // defaultStateCleanSize is the default memory allowance of clean state cache.
+ defaultStateCleanSize = 16 * 1024 * 1024
+
+ // maxBufferSize is the maximum memory allowance of node buffer.
+ // Too large buffer will cause the system to pause for a long
+ // time when write happens. Also, the largest batch that pebble can
+ // support is 4GB, node will panic if batch size exceeds this limit.
+ maxBufferSize = 256 * 1024 * 1024
+
+ // defaultBufferSize is the default memory allowance of node buffer
+ // that aggregates the writes from above until it's flushed into the
+ // disk. It's meant to be used once the initial sync is finished.
+ // Do not increase the buffer size arbitrarily, otherwise the system
+ // pause time will increase when the database writes happen.
+ defaultBufferSize = 64 * 1024 * 1024
+)
+
+var (
+ // maxDiffLayers is the maximum diff layers allowed in the layer tree.
+ maxDiffLayers = 128
+)
+
+// Defaults contains default settings for Ethereum mainnet.
+var Defaults = &Config{
+ StateHistory: params.FullImmutabilityThreshold,
+ EnableStateIndexing: false,
+ TrieCleanSize: defaultTrieCleanSize,
+ StateCleanSize: defaultStateCleanSize,
+ WriteBufferSize: defaultBufferSize,
+}
+
+// ReadOnly is the config in order to open database in read only mode.
+var ReadOnly = &Config{
+ ReadOnly: true,
+ TrieCleanSize: defaultTrieCleanSize,
+ StateCleanSize: defaultStateCleanSize,
+}
+
+// Config contains the settings for database.
+type Config struct {
+ StateHistory uint64 // Number of recent blocks to maintain state history for, 0: full chain
+ EnableStateIndexing bool // Whether to enable state history indexing for external state access
+ TrieCleanSize int // Maximum memory allowance (in bytes) for caching clean trie data
+ StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
+ WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
+ ReadOnly bool // Flag whether the database is opened in read only mode
+ JournalDirectory string // Absolute path of journal directory (null means the journal data is persisted in key-value store)
+
+ // Testing configurations
+ SnapshotNoBuild bool // Flag Whether the state generation is disabled
+ NoAsyncFlush bool // Flag whether the background buffer flushing is disabled
+ NoAsyncGeneration bool // Flag whether the background generation is disabled
+}
+
+// sanitize checks the provided user configurations and changes anything that's
+// unreasonable or unworkable.
+func (c *Config) sanitize() *Config {
+ conf := *c
+ if conf.WriteBufferSize > maxBufferSize {
+ log.Warn("Sanitizing invalid node buffer size", "provided", common.StorageSize(conf.WriteBufferSize), "updated", common.StorageSize(maxBufferSize))
+ conf.WriteBufferSize = maxBufferSize
+ }
+ return &conf
+}
+
+// fields returns a list of attributes of config for printing.
+func (c *Config) fields() []interface{} {
+ var list []interface{}
+ if c.ReadOnly {
+ list = append(list, "readonly", true)
+ }
+ list = append(list, "triecache", common.StorageSize(c.TrieCleanSize))
+ list = append(list, "statecache", common.StorageSize(c.StateCleanSize))
+ list = append(list, "buffer", common.StorageSize(c.WriteBufferSize))
+
+ if c.StateHistory == 0 {
+ list = append(list, "state-history", "entire chain")
+ } else {
+ list = append(list, "state-history", fmt.Sprintf("last %d blocks", c.StateHistory))
+ }
+ if c.EnableStateIndexing {
+ list = append(list, "index-history", true)
+ }
+ if c.JournalDirectory != "" {
+ list = append(list, "journal-dir", c.JournalDirectory)
+ }
+ return list
+}
diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go
index e323a7449e..ae9574963e 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -31,37 +31,10 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
- "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-verkle"
)
-const (
- // defaultTrieCleanSize is the default memory allowance of clean trie cache.
- defaultTrieCleanSize = 16 * 1024 * 1024
-
- // defaultStateCleanSize is the default memory allowance of clean state cache.
- defaultStateCleanSize = 16 * 1024 * 1024
-
- // maxBufferSize is the maximum memory allowance of node buffer.
- // Too large buffer will cause the system to pause for a long
- // time when write happens. Also, the largest batch that pebble can
- // support is 4GB, node will panic if batch size exceeds this limit.
- maxBufferSize = 256 * 1024 * 1024
-
- // defaultBufferSize is the default memory allowance of node buffer
- // that aggregates the writes from above until it's flushed into the
- // disk. It's meant to be used once the initial sync is finished.
- // Do not increase the buffer size arbitrarily, otherwise the system
- // pause time will increase when the database writes happen.
- defaultBufferSize = 64 * 1024 * 1024
-)
-
-var (
- // maxDiffLayers is the maximum diff layers allowed in the layer tree.
- maxDiffLayers = 128
-)
-
// layer is the interface implemented by all state layers which includes some
// public methods and some additional methods for internal usage.
type layer interface {
@@ -105,7 +78,7 @@ type layer interface {
// the provided dirty trie nodes along with the state change set.
//
// Note, the maps are retained by the method to avoid copying everything.
- update(root common.Hash, id uint64, block uint64, nodes *nodeSet, states *StateSetWithOrigin) *diffLayer
+ update(root common.Hash, id uint64, block uint64, nodes *nodeSetWithOrigin, states *StateSetWithOrigin) *diffLayer
// journal commits an entire diff hierarchy to disk into a single journal entry.
// This is meant to be used during shutdown to persist the layer without
@@ -113,68 +86,6 @@ type layer interface {
journal(w io.Writer) error
}
-// Config contains the settings for database.
-type Config struct {
- StateHistory uint64 // Number of recent blocks to maintain state history for
- EnableStateIndexing bool // Whether to enable state history indexing for external state access
- TrieCleanSize int // Maximum memory allowance (in bytes) for caching clean trie nodes
- StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
- WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
- ReadOnly bool // Flag whether the database is opened in read only mode
- JournalDirectory string // Absolute path of journal directory (null means the journal data is persisted in key-value store)
-
- // Testing configurations
- SnapshotNoBuild bool // Flag Whether the state generation is allowed
- NoAsyncFlush bool // Flag whether the background buffer flushing is allowed
- NoAsyncGeneration bool // Flag whether the background generation is allowed
-}
-
-// sanitize checks the provided user configurations and changes anything that's
-// unreasonable or unworkable.
-func (c *Config) sanitize() *Config {
- conf := *c
- if conf.WriteBufferSize > maxBufferSize {
- log.Warn("Sanitizing invalid node buffer size", "provided", common.StorageSize(conf.WriteBufferSize), "updated", common.StorageSize(maxBufferSize))
- conf.WriteBufferSize = maxBufferSize
- }
- return &conf
-}
-
-// fields returns a list of attributes of config for printing.
-func (c *Config) fields() []interface{} {
- var list []interface{}
- if c.ReadOnly {
- list = append(list, "readonly", true)
- }
- if c.SnapshotNoBuild {
- list = append(list, "snapshot", false)
- }
- list = append(list, "triecache", common.StorageSize(c.TrieCleanSize))
- list = append(list, "statecache", common.StorageSize(c.StateCleanSize))
- list = append(list, "buffer", common.StorageSize(c.WriteBufferSize))
-
- if c.StateHistory == 0 {
- list = append(list, "history", "entire chain")
- } else {
- list = append(list, "history", fmt.Sprintf("last %d blocks", c.StateHistory))
- }
- if c.JournalDirectory != "" {
- list = append(list, "journal-dir", c.JournalDirectory)
- }
- return list
-}
-
-// Defaults contains default settings for Ethereum mainnet.
-var Defaults = &Config{
- StateHistory: params.FullImmutabilityThreshold,
- TrieCleanSize: defaultTrieCleanSize,
- StateCleanSize: defaultStateCleanSize,
- WriteBufferSize: defaultBufferSize,
-}
-
-// ReadOnly is the config in order to open database in read only mode.
-var ReadOnly = &Config{ReadOnly: true}
-
// nodeHasher is the function to compute the hash of supplied node blob.
type nodeHasher func([]byte) (common.Hash, error)
@@ -219,12 +130,14 @@ type Database struct {
isVerkle bool // Flag if database is used for verkle tree
hasher nodeHasher // Trie node hasher
- config *Config // Configuration for database
- diskdb ethdb.Database // Persistent storage for matured trie nodes
- tree *layerTree // The group for all known layers
- freezer ethdb.ResettableAncientStore // Freezer for storing trie histories, nil possible in tests
- lock sync.RWMutex // Lock to prevent mutations from happening at the same time
- indexer *historyIndexer // History indexer
+ config *Config // Configuration for database
+ diskdb ethdb.Database // Persistent storage for matured trie nodes
+ tree *layerTree // The group for all known layers
+
+ stateFreezer ethdb.ResettableAncientStore // Freezer for storing state histories, nil possible in tests
+ stateIndexer *historyIndexer // History indexer historical state data, nil possible
+
+ lock sync.RWMutex // Lock to prevent mutations from happening at the same time
}
// New attempts to load an already existing layer from a persistent key-value
@@ -275,8 +188,8 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
log.Crit("Failed to setup the generator", "err", err)
}
// TODO (rjl493456442) disable the background indexing in read-only mode
- if db.freezer != nil && db.config.EnableStateIndexing {
- db.indexer = newHistoryIndexer(db.diskdb, db.freezer, db.tree.bottom().stateID())
+ if db.stateFreezer != nil && db.config.EnableStateIndexing {
+ db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID())
log.Info("Enabled state history indexing")
}
fields := config.fields()
@@ -304,25 +217,26 @@ func (db *Database) repairHistory() error {
if err != nil {
log.Crit("Failed to open state history freezer", "err", err)
}
- db.freezer = freezer
+ db.stateFreezer = freezer
// Reset the entire state histories if the trie database is not initialized
// yet. This action is necessary because these state histories are not
// expected to exist without an initialized trie database.
id := db.tree.bottom().stateID()
if id == 0 {
- frozen, err := db.freezer.Ancients()
+ frozen, err := db.stateFreezer.Ancients()
if err != nil {
log.Crit("Failed to retrieve head of state history", "err", err)
}
if frozen != 0 {
- // TODO(rjl493456442) would be better to group them into a batch.
- //
// Purge all state history indexing data first
- rawdb.DeleteStateHistoryIndexMetadata(db.diskdb)
- rawdb.DeleteStateHistoryIndex(db.diskdb)
- err := db.freezer.Reset()
- if err != nil {
+ batch := db.diskdb.NewBatch()
+ rawdb.DeleteStateHistoryIndexMetadata(batch)
+ rawdb.DeleteStateHistoryIndex(batch)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to purge state history index", "err", err)
+ }
+ if err := db.stateFreezer.Reset(); err != nil {
log.Crit("Failed to reset state histories", "err", err)
}
log.Info("Truncated extraneous state history")
@@ -331,7 +245,7 @@ func (db *Database) repairHistory() error {
}
// Truncate the extra state histories above in freezer in case it's not
// aligned with the disk layer. It might happen after a unclean shutdown.
- pruned, err := truncateFromHead(db.diskdb, db.freezer, id)
+ pruned, err := truncateFromHead(db.stateFreezer, id)
if err != nil {
log.Crit("Failed to truncate extra state histories", "err", err)
}
@@ -419,7 +333,8 @@ func (db *Database) Update(root common.Hash, parentRoot common.Hash, block uint6
if err := db.modifyAllowed(); err != nil {
return err
}
- if err := db.tree.add(root, parentRoot, block, nodes, states); err != nil {
+ // TODO(rjl493456442) tracking the origins in the following PRs.
+ if err := db.tree.add(root, parentRoot, block, NewNodeSetWithOrigin(nodes.Nodes(), nil), states); err != nil {
return err
}
// Keep 128 diff layers in the memory, persistent layer is 129th.
@@ -507,13 +422,15 @@ func (db *Database) Enable(root common.Hash) error {
// all root->id mappings should be removed as well. Since
// mappings can be huge and might take a while to clear
// them, just leave them in disk and wait for overwriting.
- if db.freezer != nil {
- // TODO(rjl493456442) would be better to group them into a batch.
- //
+ if db.stateFreezer != nil {
// Purge all state history indexing data first
- rawdb.DeleteStateHistoryIndexMetadata(db.diskdb)
- rawdb.DeleteStateHistoryIndex(db.diskdb)
- if err := db.freezer.Reset(); err != nil {
+ batch.Reset()
+ rawdb.DeleteStateHistoryIndexMetadata(batch)
+ rawdb.DeleteStateHistoryIndex(batch)
+ if err := batch.Write(); err != nil {
+ return err
+ }
+ if err := db.stateFreezer.Reset(); err != nil {
return err
}
}
@@ -529,9 +446,9 @@ func (db *Database) Enable(root common.Hash) error {
// To ensure the history indexer always matches the current state, we must:
// 1. Close any existing indexer
// 2. Re-initialize the indexer so it starts indexing from the new state root.
- if db.indexer != nil && db.freezer != nil && db.config.EnableStateIndexing {
- db.indexer.close()
- db.indexer = newHistoryIndexer(db.diskdb, db.freezer, db.tree.bottom().stateID())
+ if db.stateIndexer != nil && db.stateFreezer != nil && db.config.EnableStateIndexing {
+ db.stateIndexer.close()
+ db.stateIndexer = newHistoryIndexer(db.diskdb, db.stateFreezer, db.tree.bottom().stateID())
log.Info("Re-enabled state history indexing")
}
log.Info("Rebuilt trie database", "root", root)
@@ -551,7 +468,7 @@ func (db *Database) Recover(root common.Hash) error {
if err := db.modifyAllowed(); err != nil {
return err
}
- if db.freezer == nil {
+ if db.stateFreezer == nil {
return errors.New("state rollback is non-supported")
}
// Short circuit if the target state is not recoverable
@@ -564,7 +481,7 @@ func (db *Database) Recover(root common.Hash) error {
dl = db.tree.bottom()
)
for dl.rootHash() != root {
- h, err := readHistory(db.freezer, dl.stateID())
+ h, err := readStateHistory(db.stateFreezer, dl.stateID())
if err != nil {
return err
}
@@ -585,7 +502,7 @@ func (db *Database) Recover(root common.Hash) error {
if err := db.diskdb.SyncKeyValue(); err != nil {
return err
}
- _, err := truncateFromHead(db.diskdb, db.freezer, dl.stateID())
+ _, err := truncateFromHead(db.stateFreezer, dl.stateID())
if err != nil {
return err
}
@@ -610,15 +527,15 @@ func (db *Database) Recoverable(root common.Hash) bool {
return false
}
// This is a temporary workaround for the unavailability of the freezer in
- // dev mode. As a consequence, the Pathdb loses the ability for deep reorg
+ // dev mode. As a consequence, the database loses the ability for deep reorg
// in certain cases.
// TODO(rjl493456442): Implement the in-memory ancient store.
- if db.freezer == nil {
+ if db.stateFreezer == nil {
return false
}
// Ensure the requested state is a canonical state and all state
- // histories in range [id+1, disklayer.ID] are present and complete.
- return checkHistories(db.freezer, *id+1, dl.stateID()-*id, func(m *meta) error {
+ // histories in range [id+1, dl.ID] are present and complete.
+ return checkStateHistories(db.stateFreezer, *id+1, dl.stateID()-*id, func(m *meta) error {
if m.parent != root {
return errors.New("unexpected state history")
}
@@ -646,14 +563,14 @@ func (db *Database) Close() error {
dl.resetCache() // release the memory held by clean cache
// Terminate the background state history indexer
- if db.indexer != nil {
- db.indexer.close()
+ if db.stateIndexer != nil {
+ db.stateIndexer.close()
}
// Close the attached state history freezer.
- if db.freezer == nil {
+ if db.stateFreezer == nil {
return nil
}
- return db.freezer.Close()
+ return db.stateFreezer.Close()
}
// Size returns the current storage size of the memory cache in front of the
@@ -704,7 +621,7 @@ func (db *Database) journalPath() string {
// End: State ID of the last history for the query. 0 implies the last available
// object is selected as the ending point. Note end is included in the query.
func (db *Database) AccountHistory(address common.Address, start, end uint64) (*HistoryStats, error) {
- return accountHistory(db.freezer, address, start, end)
+ return accountHistory(db.stateFreezer, address, start, end)
}
// StorageHistory inspects the storage history within the specified range.
@@ -717,22 +634,22 @@ func (db *Database) AccountHistory(address common.Address, start, end uint64) (*
//
// Note, slot refers to the hash of the raw slot key.
func (db *Database) StorageHistory(address common.Address, slot common.Hash, start uint64, end uint64) (*HistoryStats, error) {
- return storageHistory(db.freezer, address, slot, start, end)
+ return storageHistory(db.stateFreezer, address, slot, start, end)
}
// HistoryRange returns the block numbers associated with earliest and latest
// state history in the local store.
func (db *Database) HistoryRange() (uint64, uint64, error) {
- return historyRange(db.freezer)
+ return historyRange(db.stateFreezer)
}
// IndexProgress returns the indexing progress made so far. It provides the
// number of states that remain unindexed.
func (db *Database) IndexProgress() (uint64, error) {
- if db.indexer == nil {
+ if db.stateIndexer == nil {
return 0, nil
}
- return db.indexer.progress()
+ return db.stateIndexer.progress()
}
// AccountIterator creates a new account iterator for the specified root hash and
@@ -764,3 +681,14 @@ func (db *Database) StorageIterator(root common.Hash, account common.Hash, seek
}
return newFastStorageIterator(db, root, account, seek)
}
+
+// SnapshotCompleted returns the flag indicating if the snapshot generation is completed.
+func (db *Database) SnapshotCompleted() bool {
+ db.lock.RLock()
+ wait := db.waitSync
+ db.lock.RUnlock()
+ if wait {
+ return false
+ }
+ return db.tree.bottom().genComplete()
+}
diff --git a/triedb/pathdb/database_test.go b/triedb/pathdb/database_test.go
index e9a1850ee0..8cca7b1b3c 100644
--- a/triedb/pathdb/database_test.go
+++ b/triedb/pathdb/database_test.go
@@ -36,9 +36,10 @@ import (
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/holiman/uint256"
+ "golang.org/x/exp/maps"
)
-func updateTrie(db *Database, stateRoot common.Hash, addrHash common.Hash, root common.Hash, dirties map[common.Hash][]byte) (common.Hash, *trienode.NodeSet) {
+func updateTrie(db *Database, stateRoot common.Hash, addrHash common.Hash, root common.Hash, entries map[common.Hash][]byte) (common.Hash, *trienode.NodeSet) {
var id *trie.ID
if addrHash == (common.Hash{}) {
id = trie.StateTrieID(stateRoot)
@@ -49,13 +50,17 @@ func updateTrie(db *Database, stateRoot common.Hash, addrHash common.Hash, root
if err != nil {
panic(fmt.Errorf("failed to load trie, err: %w", err))
}
- for key, val := range dirties {
+ var deletes []common.Hash
+ for key, val := range entries {
if len(val) == 0 {
- tr.Delete(key.Bytes())
+ deletes = append(deletes, key)
} else {
tr.Update(key.Bytes(), val)
}
}
+ for _, key := range deletes {
+ tr.Delete(key.Bytes())
+ }
return tr.Commit(false)
}
@@ -72,16 +77,18 @@ const (
createAccountOp int = iota
modifyAccountOp
deleteAccountOp
+ resurrectAccountOp
opLen
)
+// genctx carries the generation context used within a single state transition.
type genctx struct {
stateRoot common.Hash
accounts map[common.Hash][]byte // Keyed by the hash of account address
storages map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
accountOrigin map[common.Address][]byte // Keyed by the account address
storageOrigin map[common.Address]map[common.Hash][]byte // Keyed by the account address and the hash of storage key
- nodes *trienode.MergedNodeSet
+ nodes *trienode.MergedNodeSet // Trie nodes produced from the state transition
}
func newCtx(stateRoot common.Hash) *genctx {
@@ -114,6 +121,8 @@ func (ctx *genctx) storageOriginSet(rawStorageKey bool, t *tester) map[common.Ad
type tester struct {
db *Database
roots []common.Hash
+ nodes []*trienode.MergedNodeSet
+ states []*StateSetWithOrigin
preimages map[common.Hash][]byte
// current state set
@@ -123,20 +132,57 @@ type tester struct {
// state snapshots
snapAccounts map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
+
+ // trienode snapshots
+ snapNodes map[common.Hash]*trienode.MergedNodeSet
}
-func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool, journalDir string) *tester {
+// testerConfig holds configuration parameters for running a test scenario.
+type testerConfig struct {
+ stateHistory uint64 // Number of historical states to retain
+ layers int // Number of state transitions to generate for
+ enableIndex bool // Enable state history indexing or not
+ journalDir string // Directory path for persisting journal files
+ isVerkle bool // Enables Verkle trie mode if true
+
+ writeBuffer *int // Optional, the size of memory allocated for write buffer
+ trieCache *int // Optional, the size of memory allocated for trie cache
+ stateCache *int // Optional, the size of memory allocated for state cache
+}
+
+func (c *testerConfig) trieCacheSize() int {
+ if c.trieCache != nil {
+ return *c.trieCache
+ }
+ return 256 * 1024
+}
+
+func (c *testerConfig) stateCacheSize() int {
+ if c.stateCache != nil {
+ return *c.stateCache
+ }
+ return 256 * 1024
+}
+
+func (c *testerConfig) writeBufferSize() int {
+ if c.writeBuffer != nil {
+ return *c.writeBuffer
+ }
+ return 256 * 1024
+}
+
+func newTester(t *testing.T, config *testerConfig) *tester {
var (
disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()})
db = New(disk, &Config{
- StateHistory: historyLimit,
- EnableStateIndexing: enableIndex,
- TrieCleanSize: 256 * 1024,
- StateCleanSize: 256 * 1024,
- WriteBufferSize: 256 * 1024,
+ StateHistory: config.stateHistory,
+ EnableStateIndexing: config.enableIndex,
+ TrieCleanSize: config.trieCacheSize(),
+ StateCleanSize: config.stateCacheSize(),
+ WriteBufferSize: config.writeBufferSize(),
NoAsyncFlush: true,
- JournalDirectory: journalDir,
- }, isVerkle)
+ JournalDirectory: config.journalDir,
+ }, config.isVerkle)
obj = &tester{
db: db,
@@ -145,9 +191,10 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, ena
storages: make(map[common.Hash]map[common.Hash][]byte),
snapAccounts: make(map[common.Hash]map[common.Hash][]byte),
snapStorages: make(map[common.Hash]map[common.Hash]map[common.Hash][]byte),
+ snapNodes: make(map[common.Hash]*trienode.MergedNodeSet),
}
)
- for i := 0; i < layers; i++ {
+ for i := 0; i < config.layers; i++ {
var parent = types.EmptyRootHash
if len(obj.roots) != 0 {
parent = obj.roots[len(obj.roots)-1]
@@ -158,6 +205,8 @@ func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, ena
panic(fmt.Errorf("failed to update state changes, err: %w", err))
}
obj.roots = append(obj.roots, root)
+ obj.nodes = append(obj.nodes, nodes)
+ obj.states = append(obj.states, states)
}
return obj
}
@@ -181,6 +230,8 @@ func (t *tester) extend(layers int) {
panic(fmt.Errorf("failed to update state changes, err: %w", err))
}
t.roots = append(t.roots, root)
+ t.nodes = append(t.nodes, nodes)
+ t.states = append(t.states, states)
}
}
@@ -270,10 +321,53 @@ func (t *tester) clearStorage(ctx *genctx, addr common.Address, root common.Hash
return root
}
+func (t *tester) resurrectStorage(ctx *genctx, addr common.Address, old map[common.Hash][]byte) common.Hash {
+ var (
+ addrHash = crypto.Keccak256Hash(addr.Bytes())
+ storage = make(map[common.Hash][]byte)
+ origin = make(map[common.Hash][]byte)
+ )
+ for i := 0; i < 3; i++ {
+ v, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(testrand.Bytes(32)))
+ key := testrand.Bytes(32)
+ hash := crypto.Keccak256Hash(key)
+ t.preimages[hash] = key
+
+ storage[hash] = v
+ origin[hash] = nil
+ }
+ var cnt int
+ for khash := range old {
+ cnt += 1
+ v, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(testrand.Bytes(32)))
+
+ storage[khash] = v
+ origin[khash] = old[khash]
+ if cnt >= 3 {
+ break
+ }
+ }
+ root, set := updateTrie(t.db, ctx.stateRoot, addrHash, types.EmptyRootHash, storage)
+
+ maps.Copy(ctx.storages[addrHash], storage)
+ if ctx.storageOrigin[addr] == nil {
+ ctx.storageOrigin[addr] = make(map[common.Hash][]byte)
+ }
+ for k, v := range origin {
+ if _, exists := ctx.storageOrigin[addr][k]; !exists {
+ ctx.storageOrigin[addr][k] = v
+ }
+ }
+ ctx.nodes.Merge(set)
+ return root
+}
+
func (t *tester) generate(parent common.Hash, rawStorageKey bool) (common.Hash, *trienode.MergedNodeSet, *StateSetWithOrigin) {
var (
- ctx = newCtx(parent)
- dirties = make(map[common.Hash]struct{})
+ ctx = newCtx(parent)
+ dirties = make(map[common.Hash]struct{})
+ deleted = make(map[common.Address]struct{})
+ resurrect = make(map[common.Address]struct{})
)
for i := 0; i < 20; i++ {
// Start with account creation always
@@ -336,6 +430,7 @@ func (t *tester) generate(parent common.Hash, rawStorageKey bool) (common.Hash,
continue
}
dirties[addrHash] = struct{}{}
+ deleted[addr] = struct{}{}
acct, _ := types.FullAccount(account)
if acct.Root != types.EmptyRootHash {
@@ -343,6 +438,25 @@ func (t *tester) generate(parent common.Hash, rawStorageKey bool) (common.Hash,
}
ctx.accounts[addrHash] = nil
ctx.accountOrigin[addr] = account
+
+ case resurrectAccountOp:
+ if len(deleted) == 0 {
+ continue
+ }
+ addresses := maps.Keys(deleted)
+ addr := addresses[rand.Intn(len(addresses))]
+ if _, exist := resurrect[addr]; exist {
+ continue
+ }
+ resurrect[addr] = struct{}{}
+
+ addrHash := crypto.Keccak256Hash(addr.Bytes())
+ root := t.resurrectStorage(ctx, addr, t.storages[addrHash])
+ ctx.accounts[addrHash] = types.SlimAccountRLP(generateAccount(root))
+ if _, exist := ctx.accountOrigin[addr]; !exist {
+ ctx.accountOrigin[addr] = nil
+ }
+ t.preimages[addrHash] = addr.Bytes()
}
}
root, set := updateTrie(t.db, parent, common.Hash{}, parent, ctx.accounts)
@@ -351,6 +465,7 @@ func (t *tester) generate(parent common.Hash, rawStorageKey bool) (common.Hash,
// Save state snapshot before commit
t.snapAccounts[parent] = copyAccounts(t.accounts)
t.snapStorages[parent] = copyStorages(t.storages)
+ t.snapNodes[parent] = ctx.nodes
// Commit all changes to live state set
for addrHash, account := range ctx.accounts {
@@ -426,7 +541,7 @@ func (t *tester) verifyHistory() error {
for i, root := range t.roots {
// The state history related to the state above disk layer should not exist.
if i > bottom {
- _, err := readHistory(t.db.freezer, uint64(i+1))
+ _, err := readStateHistory(t.db.stateFreezer, uint64(i+1))
if err == nil {
return errors.New("unexpected state history")
}
@@ -434,7 +549,7 @@ func (t *tester) verifyHistory() error {
}
// The state history related to the state below or equal to the disk layer
// should exist.
- obj, err := readHistory(t.db.freezer, uint64(i+1))
+ obj, err := readStateHistory(t.db.stateFreezer, uint64(i+1))
if err != nil {
return err
}
@@ -470,8 +585,7 @@ func TestDatabaseRollback(t *testing.T) {
maxDiffLayers = 128
}()
- // Verify state histories
- tester := newTester(t, 0, false, 32, false, "")
+ tester := newTester(t, &testerConfig{layers: 32})
defer tester.release()
if err := tester.verifyHistory(); err != nil {
@@ -505,7 +619,7 @@ func TestDatabaseRecoverable(t *testing.T) {
}()
var (
- tester = newTester(t, 0, false, 12, false, "")
+ tester = newTester(t, &testerConfig{layers: 12})
index = tester.bottomIndex()
)
defer tester.release()
@@ -526,7 +640,7 @@ func TestDatabaseRecoverable(t *testing.T) {
// Layers below current disk layer are recoverable
{tester.roots[index-1], true},
- // Disklayer itself is not recoverable, since it's
+ // Disk layer itself is not recoverable, since it's
// available for accessing.
{tester.roots[index], false},
@@ -542,6 +656,59 @@ func TestDatabaseRecoverable(t *testing.T) {
}
}
+func TestExecuteRollback(t *testing.T) {
+ // Redefine the diff layer depth allowance for faster testing.
+ maxDiffLayers = 4
+ defer func() {
+ maxDiffLayers = 128
+ }()
+
+ tester := newTester(t, &testerConfig{layers: 32})
+ defer tester.release()
+
+ // Revert database from top to bottom
+ for i := tester.bottomIndex(); i >= 0; i-- {
+ dl := tester.db.tree.bottom()
+ h, err := readStateHistory(tester.db.stateFreezer, dl.stateID())
+ if err != nil {
+ t.Fatalf("Failed to read history, err: %v", err)
+ }
+ nodes, err := apply(tester.db, h.meta.parent, h.meta.root, h.meta.version == stateHistoryV1, h.accounts, h.storages)
+ if err != nil {
+ t.Fatalf("Failed to apply history, err: %v", err)
+ }
+
+ // Verify the produced node set, ensuring they are aligned with the
+ // tracked dirty nodes.
+ want := tester.snapNodes[h.meta.parent]
+ if len(nodes) != len(want.Sets) {
+ t.Fatalf("Unexpected node sets, want: %d, got: %d", len(want.Sets), len(nodes))
+ }
+ for owner, setA := range nodes {
+ setB, ok := want.Sets[owner]
+ if !ok {
+ t.Fatalf("Excessive nodeset, %x", owner)
+ }
+ if len(setA) != len(setB.Origins) {
+ t.Fatalf("Unexpected origins, want: %d, got: %d", len(setA), len(setB.Origins))
+ }
+ for k, nA := range setA {
+ nB, ok := setB.Origins[k]
+ if !ok {
+ t.Fatalf("Excessive node, %v", []byte(k))
+ }
+ if !bytes.Equal(nA.Blob, nB) {
+ t.Fatalf("Unexpected node value, want: %v, got: %v", nA.Blob, nB)
+ }
+ }
+ }
+
+ if err := tester.db.Recover(h.meta.parent); err != nil {
+ t.Fatalf("Failed to recover db, err: %v", err)
+ }
+ }
+}
+
func TestDisable(t *testing.T) {
// Redefine the diff layer depth allowance for faster testing.
maxDiffLayers = 4
@@ -549,7 +716,7 @@ func TestDisable(t *testing.T) {
maxDiffLayers = 128
}()
- tester := newTester(t, 0, false, 32, false, "")
+ tester := newTester(t, &testerConfig{layers: 32})
defer tester.release()
stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
@@ -563,12 +730,8 @@ func TestDisable(t *testing.T) {
t.Fatalf("Failed to activate database: %v", err)
}
- // Ensure journal is deleted from disk
- if blob := rawdb.ReadTrieJournal(tester.db.diskdb); len(blob) != 0 {
- t.Fatal("Failed to clean journal")
- }
// Ensure all trie histories are removed
- n, err := tester.db.freezer.Ancients()
+ n, err := tester.db.stateFreezer.Ancients()
if err != nil {
t.Fatal("Failed to clean state history")
}
@@ -591,7 +754,7 @@ func TestCommit(t *testing.T) {
maxDiffLayers = 128
}()
- tester := newTester(t, 0, false, 12, false, "")
+ tester := newTester(t, &testerConfig{layers: 12})
defer tester.release()
if err := tester.db.Commit(tester.lastHash(), false); err != nil {
@@ -626,7 +789,7 @@ func testJournal(t *testing.T, journalDir string) {
maxDiffLayers = 128
}()
- tester := newTester(t, 0, false, 12, false, journalDir)
+ tester := newTester(t, &testerConfig{layers: 12, journalDir: journalDir})
defer tester.release()
if err := tester.db.Journal(tester.lastHash()); err != nil {
@@ -673,7 +836,7 @@ func testCorruptedJournal(t *testing.T, journalDir string, modifyFn func(databas
maxDiffLayers = 128
}()
- tester := newTester(t, 0, false, 12, false, journalDir)
+ tester := newTester(t, &testerConfig{layers: 12, journalDir: journalDir})
defer tester.release()
if err := tester.db.Journal(tester.lastHash()); err != nil {
@@ -718,13 +881,13 @@ func TestTailTruncateHistory(t *testing.T) {
maxDiffLayers = 128
}()
- tester := newTester(t, 10, false, 12, false, "")
+ tester := newTester(t, &testerConfig{layers: 12, stateHistory: 10})
defer tester.release()
tester.db.Close()
tester.db = New(tester.db.diskdb, &Config{StateHistory: 10}, false)
- head, err := tester.db.freezer.Ancients()
+ head, err := tester.db.stateFreezer.Ancients()
if err != nil {
t.Fatalf("Failed to obtain freezer head")
}
@@ -754,3 +917,107 @@ func copyStorages(set map[common.Hash]map[common.Hash][]byte) map[common.Hash]ma
}
return copied
}
+
+func TestDatabaseIndexRecovery(t *testing.T) {
+ maxDiffLayers = 4
+ defer func() {
+ maxDiffLayers = 128
+ }()
+
+ //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
+ writeBuffer := 512 * 1024
+ config := &testerConfig{
+ layers: 64,
+ enableIndex: true,
+ writeBuffer: &writeBuffer,
+ }
+ env := newTester(t, config)
+ defer env.release()
+
+ // Ensure the buffer in disk layer is not empty
+ var (
+ bRoot = env.db.tree.bottom().rootHash()
+ dRoot = crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(env.db.diskdb, nil))
+ )
+ for dRoot == bRoot {
+ env.extend(1)
+
+ bRoot = env.db.tree.bottom().rootHash()
+ dRoot = crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(env.db.diskdb, nil))
+ }
+ waitIndexing(env.db)
+
+ var (
+ dIndex int
+ roots = env.roots
+ hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
+ )
+ for i, root := range roots {
+ if root == dRoot {
+ dIndex = i
+ }
+ if root == bRoot {
+ break
+ }
+ if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // Terminate the database and mutate the journal, it's for simulating
+ // the unclean shutdown
+ env.db.Journal(env.lastHash())
+ env.db.Close()
+
+ // Mutate the journal in disk, it should be regarded as invalid
+ blob := rawdb.ReadTrieJournal(env.db.diskdb)
+ blob[0] = 0xa
+ rawdb.WriteTrieJournal(env.db.diskdb, blob)
+
+ // Reload the database, the extra state histories should be removed
+ env.db = New(env.db.diskdb, env.db.config, false)
+
+ for i := range roots {
+ _, err := readStateHistory(env.db.stateFreezer, uint64(i+1))
+ if i <= dIndex && err != nil {
+ t.Fatalf("State history is not found, %d", i)
+ }
+ if i > dIndex && err == nil {
+ t.Fatalf("Unexpected state history found, %d", i)
+ }
+ }
+ remain, err := env.db.IndexProgress()
+ if err != nil {
+ t.Fatalf("Failed to obtain the progress, %v", err)
+ }
+ if remain == 0 {
+ t.Fatalf("Unexpected progress remain, %d", remain)
+ }
+
+ // Apply new states on top, ensuring state indexing can respond correctly
+ for i := dIndex + 1; i < len(roots); i++ {
+ if err := env.db.Update(roots[i], roots[i-1], uint64(i), env.nodes[i], env.states[i]); err != nil {
+ panic(fmt.Errorf("failed to update state changes, err: %w", err))
+ }
+ }
+ remain, err = env.db.IndexProgress()
+ if err != nil {
+ t.Fatalf("Failed to obtain the progress, %v", err)
+ }
+ if remain != 0 {
+ t.Fatalf("Unexpected progress remain, %d", remain)
+ }
+ waitIndexing(env.db)
+
+ // Ensure the truncated state histories become accessible
+ bRoot = env.db.tree.bottom().rootHash()
+ hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
+ for i, root := range roots {
+ if root == bRoot {
+ break
+ }
+ if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go
index ac05b4a0fb..ae523c979c 100644
--- a/triedb/pathdb/difflayer.go
+++ b/triedb/pathdb/difflayer.go
@@ -34,7 +34,7 @@ type diffLayer struct {
root common.Hash // Root hash to which this layer diff belongs to
id uint64 // Corresponding state id
block uint64 // Associated block number
- nodes *nodeSet // Cached trie nodes indexed by owner and path
+ nodes *nodeSetWithOrigin // Cached trie nodes indexed by owner and path
states *StateSetWithOrigin // Associated state changes along with origin value
parent layer // Parent layer modified by this one, never nil, **can be changed**
@@ -42,7 +42,7 @@ type diffLayer struct {
}
// newDiffLayer creates a new diff layer on top of an existing layer.
-func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes *nodeSet, states *StateSetWithOrigin) *diffLayer {
+func newDiffLayer(parent layer, root common.Hash, id uint64, block uint64, nodes *nodeSetWithOrigin, states *StateSetWithOrigin) *diffLayer {
dl := &diffLayer{
root: root,
id: id,
@@ -151,7 +151,7 @@ func (dl *diffLayer) storage(accountHash, storageHash common.Hash, depth int) ([
// update implements the layer interface, creating a new layer on top of the
// existing layer tree with the specified data items.
-func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes *nodeSet, states *StateSetWithOrigin) *diffLayer {
+func (dl *diffLayer) update(root common.Hash, id uint64, block uint64, nodes *nodeSetWithOrigin, states *StateSetWithOrigin) *diffLayer {
return newDiffLayer(dl, root, id, block, nodes, states)
}
diff --git a/triedb/pathdb/difflayer_test.go b/triedb/pathdb/difflayer_test.go
index 83ed833486..0484fd9644 100644
--- a/triedb/pathdb/difflayer_test.go
+++ b/triedb/pathdb/difflayer_test.go
@@ -76,7 +76,7 @@ func benchmarkSearch(b *testing.B, depth int, total int) {
nblob = common.CopyBytes(blob)
}
}
- return newDiffLayer(parent, common.Hash{}, 0, 0, newNodeSet(nodes), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ return newDiffLayer(parent, common.Hash{}, 0, 0, NewNodeSetWithOrigin(nodes, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
}
var layer layer
layer = emptyLayer()
@@ -118,7 +118,7 @@ func BenchmarkPersist(b *testing.B) {
)
nodes[common.Hash{}][string(path)] = node
}
- return newDiffLayer(parent, common.Hash{}, 0, 0, newNodeSet(nodes), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ return newDiffLayer(parent, common.Hash{}, 0, 0, NewNodeSetWithOrigin(nodes, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
}
for i := 0; i < b.N; i++ {
b.StopTimer()
@@ -156,7 +156,7 @@ func BenchmarkJournal(b *testing.B) {
)
nodes[common.Hash{}][string(path)] = node
}
- return newDiffLayer(parent, common.Hash{}, 0, 0, newNodeSet(nodes), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ return newDiffLayer(parent, common.Hash{}, 0, 0, NewNodeSetWithOrigin(nodes, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
}
var layer layer
layer = emptyLayer()
diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go
index 06f0a7285f..2042e91611 100644
--- a/triedb/pathdb/disklayer.go
+++ b/triedb/pathdb/disklayer.go
@@ -319,10 +319,73 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
// update implements the layer interface, returning a new diff layer on top
// with the given state set.
-func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes *nodeSet, states *StateSetWithOrigin) *diffLayer {
+func (dl *diskLayer) update(root common.Hash, id uint64, block uint64, nodes *nodeSetWithOrigin, states *StateSetWithOrigin) *diffLayer {
return newDiffLayer(dl, root, id, block, nodes, states)
}
+// writeStateHistory stores the state history and indexes if indexing is
+// permitted.
+//
+// What's more, this function also returns a flag indicating whether the
+// buffer flushing is required, ensuring the persistent state ID is always
+// greater than or equal to the first history ID.
+func (dl *diskLayer) writeStateHistory(diff *diffLayer) (bool, error) {
+ // Short circuit if state history is not permitted
+ if dl.db.stateFreezer == nil {
+ return false, nil
+ }
+ // Bail out with an error if writing the state history fails.
+ // This can happen, for example, if the device is full.
+ err := writeStateHistory(dl.db.stateFreezer, diff)
+ if err != nil {
+ return false, err
+ }
+ // Notify the state history indexer for newly created history
+ if dl.db.stateIndexer != nil {
+ if err := dl.db.stateIndexer.extend(diff.stateID()); err != nil {
+ return false, err
+ }
+ }
+ // Determine if the persisted history object has exceeded the
+ // configured limitation.
+ limit := dl.db.config.StateHistory
+ if limit == 0 {
+ return false, nil
+ }
+ tail, err := dl.db.stateFreezer.Tail()
+ if err != nil {
+ return false, err
+ } // firstID = tail+1
+
+ // length = diff.stateID()-firstID+1 = diff.stateID()-tail
+ if diff.stateID()-tail <= limit {
+ return false, nil
+ }
+ newFirst := diff.stateID() - limit + 1 // the id of first history **after truncation**
+
+ // In a rare case where the ID of the first history object (after tail
+ // truncation) exceeds the persisted state ID, we must take corrective
+ // steps:
+ //
+ // - Skip tail truncation temporarily, avoid the scenario that associated
+ // history of persistent state is removed
+ //
+ // - Force a commit of the cached dirty states into persistent state
+ //
+ // These measures ensure the persisted state ID always remains greater
+ // than or equal to the first history ID.
+ if persistentID := rawdb.ReadPersistentStateID(dl.db.diskdb); persistentID < newFirst {
+ log.Debug("Skip tail truncation", "persistentID", persistentID, "tailID", tail+1, "headID", diff.stateID(), "limit", limit)
+ return true, nil
+ }
+ pruned, err := truncateFromTail(dl.db.stateFreezer, newFirst-1)
+ if err != nil {
+ return false, err
+ }
+ log.Debug("Pruned state history", "items", pruned, "tailid", newFirst)
+ return false, nil
+}
+
// commit merges the given bottom-most diff layer into the node buffer
// and returns a newly constructed disk layer. Note the current disk
// layer must be tagged as stale first to prevent re-access.
@@ -333,34 +396,9 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
// Construct and store the state history first. If crash happens after storing
// the state history but without flushing the corresponding states(journal),
// the stored state history will be truncated from head in the next restart.
- var (
- overflow bool
- oldest uint64
- )
- if dl.db.freezer != nil {
- // Bail out with an error if writing the state history fails.
- // This can happen, for example, if the device is full.
- err := writeHistory(dl.db.freezer, bottom)
- if err != nil {
- return nil, err
- }
- // Determine if the persisted history object has exceeded the configured
- // limitation, set the overflow as true if so.
- tail, err := dl.db.freezer.Tail()
- if err != nil {
- return nil, err
- }
- limit := dl.db.config.StateHistory
- if limit != 0 && bottom.stateID()-tail > limit {
- overflow = true
- oldest = bottom.stateID() - limit + 1 // track the id of history **after truncation**
- }
- // Notify the state history indexer for newly created history
- if dl.db.indexer != nil {
- if err := dl.db.indexer.extend(bottom.stateID()); err != nil {
- return nil, err
- }
- }
+ flush, err := dl.writeStateHistory(bottom)
+ if err != nil {
+ return nil, err
}
// Mark the diskLayer as stale before applying any mutations on top.
dl.stale = true
@@ -373,21 +411,13 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
}
rawdb.WriteStateID(dl.db.diskdb, bottom.rootHash(), bottom.stateID())
- // In a unique scenario where the ID of the oldest history object (after tail
- // truncation) surpasses the persisted state ID, we take the necessary action
- // of forcibly committing the cached dirty states to ensure that the persisted
- // state ID remains higher.
- persistedID := rawdb.ReadPersistentStateID(dl.db.diskdb)
- if !force && persistedID < oldest {
- force = true
- }
// Merge the trie nodes and flat states of the bottom-most diff layer into the
// buffer as the combined layer.
- combined := dl.buffer.commit(bottom.nodes, bottom.states.stateSet)
+ combined := dl.buffer.commit(bottom.nodes.nodeSet, bottom.states.stateSet)
// Terminate the background state snapshot generation before mutating the
// persistent state.
- if combined.full() || force {
+ if combined.full() || force || flush {
// Wait until the previous frozen buffer is fully flushed
if dl.frozen != nil {
if err := dl.frozen.waitFlush(); err != nil {
@@ -418,7 +448,7 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
// Freeze the live buffer and schedule background flushing
dl.frozen = combined
- dl.frozen.flush(bottom.root, dl.db.diskdb, dl.db.freezer, progress, dl.nodes, dl.states, bottom.stateID(), func() {
+ dl.frozen.flush(bottom.root, dl.db.diskdb, dl.db.stateFreezer, progress, dl.nodes, dl.states, bottom.stateID(), func() {
// Resume the background generation if it's not completed yet.
// The generator is assumed to be available if the progress is
// not nil.
@@ -431,8 +461,8 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
}
})
// Block until the frozen buffer is fully flushed out if the async flushing
- // is not allowed, or if the oldest history surpasses the persisted state ID.
- if dl.db.config.NoAsyncFlush || persistedID < oldest {
+ // is not allowed.
+ if dl.db.config.NoAsyncFlush {
if err := dl.frozen.waitFlush(); err != nil {
return nil, err
}
@@ -445,20 +475,11 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
if dl.generator != nil {
ndl.setGenerator(dl.generator)
}
- // To remove outdated history objects from the end, we set the 'tail' parameter
- // to 'oldest-1' due to the offset between the freezer index and the history ID.
- if overflow {
- pruned, err := truncateFromTail(ndl.db.diskdb, ndl.db.freezer, oldest-1)
- if err != nil {
- return nil, err
- }
- log.Debug("Pruned state history", "items", pruned, "tailid", oldest)
- }
return ndl, nil
}
// revert applies the given state history and return a reverted disk layer.
-func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
+func (dl *diskLayer) revert(h *stateHistory) (*diskLayer, error) {
start := time.Now()
if h.meta.root != dl.rootHash() {
return nil, errUnexpectedHistory
@@ -484,8 +505,8 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
dl.stale = true
// Unindex the corresponding state history
- if dl.db.indexer != nil {
- if err := dl.db.indexer.shorten(dl.id); err != nil {
+ if dl.db.stateIndexer != nil {
+ if err := dl.db.stateIndexer.shorten(dl.id); err != nil {
return nil, err
}
}
diff --git a/triedb/pathdb/execute.go b/triedb/pathdb/execute.go
index aa4bd8b44b..4c1cafec12 100644
--- a/triedb/pathdb/execute.go
+++ b/triedb/pathdb/execute.go
@@ -59,13 +59,19 @@ func apply(db database.NodeDatabase, prevRoot common.Hash, postRoot common.Hash,
rawStorageKey: rawStorageKey,
nodes: trienode.NewMergedNodeSet(),
}
+ var deletes []common.Address
for addr, account := range accounts {
- var err error
if len(account) == 0 {
- err = deleteAccount(ctx, db, addr)
+ deletes = append(deletes, addr)
} else {
- err = updateAccount(ctx, db, addr)
+ err := updateAccount(ctx, db, addr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to revert state, err: %w", err)
+ }
}
+ }
+ for _, addr := range deletes {
+ err := deleteAccount(ctx, db, addr)
if err != nil {
return nil, fmt.Errorf("failed to revert state, err: %w", err)
}
@@ -77,7 +83,7 @@ func apply(db database.NodeDatabase, prevRoot common.Hash, postRoot common.Hash,
if err := ctx.nodes.Merge(result); err != nil {
return nil, err
}
- return ctx.nodes.Flatten(), nil
+ return ctx.nodes.Nodes(), nil
}
// updateAccount the account was present in prev-state, and may or may not
@@ -108,17 +114,23 @@ func updateAccount(ctx *context, db database.NodeDatabase, addr common.Address)
if err != nil {
return err
}
+ var deletes []common.Hash
for key, val := range ctx.storages[addr] {
tkey := key
if ctx.rawStorageKey {
tkey = crypto.Keccak256Hash(key.Bytes())
}
- var err error
if len(val) == 0 {
- err = st.Delete(tkey.Bytes())
+ deletes = append(deletes, tkey)
} else {
- err = st.Update(tkey.Bytes(), val)
+ err := st.Update(tkey.Bytes(), val)
+ if err != nil {
+ return err
+ }
}
+ }
+ for _, tkey := range deletes {
+ err := st.Delete(tkey.Bytes())
if err != nil {
return err
}
diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go
index 47f224170d..bbedd52f34 100644
--- a/triedb/pathdb/history.go
+++ b/triedb/pathdb/history.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The go-ethereum Authors
+// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
@@ -12,592 +12,26 @@
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see .
+// along with the go-ethereum library. If not, see | State 1 |---->| ... |---->| State n |
-// +------------+ +---------+ +---------+ +---------+
-//
-// +-----------+ +------+ +-----------+
-// | History 1 |----> | ... |---->| History n |
-// +-----------+ +------+ +-----------+
-//
-// # Rollback
-//
-// If the system wants to roll back to a previous state n, it needs to ensure
-// all history objects from n+1 up to the current disk layer are existent. The
-// history objects are applied to the state in reverse order, starting from the
-// current disk layer.
-
-const (
- accountIndexSize = common.AddressLength + 13 // The length of encoded account index
- slotIndexSize = common.HashLength + 5 // The length of encoded slot index
- historyMetaSize = 9 + 2*common.HashLength // The length of encoded history meta
-
- stateHistoryV0 = uint8(0) // initial version of state history structure
- stateHistoryV1 = uint8(1) // use the storage slot raw key as the identifier instead of the key hash
- historyVersion = stateHistoryV1 // the default state history version
+var (
+ errHeadTruncationOutOfRange = errors.New("history head truncation out of range")
+ errTailTruncationOutOfRange = errors.New("history tail truncation out of range")
)
-// Each state history entry is consisted of five elements:
-//
-// # metadata
-// This object contains a few meta fields, such as the associated state root,
-// block number, version tag and so on. This object may contain an extra
-// accountHash list which means the storage changes belong to these accounts
-// are not complete due to large contract destruction. The incomplete history
-// can not be used for rollback and serving archive state request.
-//
-// # account index
-// This object contains some index information of account. For example, offset
-// and length indicate the location of the data belonging to the account. Besides,
-// storageOffset and storageSlots indicate the storage modification location
-// belonging to the account.
-//
-// The size of each account index is *fixed*, and all indexes are sorted
-// lexicographically. Thus binary search can be performed to quickly locate a
-// specific account.
-//
-// # account data
-// Account data is a concatenated byte stream composed of all account data.
-// The account data can be solved by the offset and length info indicated
-// by corresponding account index.
-//
-// fixed size
-// ^ ^
-// / \
-// +-----------------+-----------------+----------------+-----------------+
-// | Account index 1 | Account index 2 | ... | Account index N |
-// +-----------------+-----------------+----------------+-----------------+
-// |
-// | length
-// offset |----------------+
-// v v
-// +----------------+----------------+----------------+----------------+
-// | Account data 1 | Account data 2 | ... | Account data N |
-// +----------------+----------------+----------------+----------------+
-//
-// # storage index
-// This object is similar with account index. It's also fixed size and contains
-// the location info of storage slot data.
-//
-// # storage data
-// Storage data is a concatenated byte stream composed of all storage slot data.
-// The storage slot data can be solved by the location info indicated by
-// corresponding account index and storage slot index.
-//
-// fixed size
-// ^ ^
-// / \
-// +-----------------+-----------------+----------------+-----------------+
-// | Account index 1 | Account index 2 | ... | Account index N |
-// +-----------------+-----------------+----------------+-----------------+
-// |
-// | storage slots
-// storage offset |-----------------------------------------------------+
-// v v
-// +-----------------+-----------------+-----------------+
-// | storage index 1 | storage index 2 | storage index 3 |
-// +-----------------+-----------------+-----------------+
-// | length
-// offset |-------------+
-// v v
-// +-------------+
-// | slot data 1 |
-// +-------------+
-
-// accountIndex describes the metadata belonging to an account.
-type accountIndex struct {
- address common.Address // The address of account
- length uint8 // The length of account data, size limited by 255
- offset uint32 // The offset of item in account data table
- storageOffset uint32 // The offset of storage index in storage index table
- storageSlots uint32 // The number of mutated storage slots belonging to the account
-}
-
-// encode packs account index into byte stream.
-func (i *accountIndex) encode() []byte {
- var buf [accountIndexSize]byte
- copy(buf[:], i.address.Bytes())
- buf[common.AddressLength] = i.length
- binary.BigEndian.PutUint32(buf[common.AddressLength+1:], i.offset)
- binary.BigEndian.PutUint32(buf[common.AddressLength+5:], i.storageOffset)
- binary.BigEndian.PutUint32(buf[common.AddressLength+9:], i.storageSlots)
- return buf[:]
-}
-
-// decode unpacks account index from byte stream.
-func (i *accountIndex) decode(blob []byte) {
- i.address = common.BytesToAddress(blob[:common.AddressLength])
- i.length = blob[common.AddressLength]
- i.offset = binary.BigEndian.Uint32(blob[common.AddressLength+1:])
- i.storageOffset = binary.BigEndian.Uint32(blob[common.AddressLength+5:])
- i.storageSlots = binary.BigEndian.Uint32(blob[common.AddressLength+9:])
-}
-
-// slotIndex describes the metadata belonging to a storage slot.
-type slotIndex struct {
- // the identifier of the storage slot. Specifically
- // in v0, it's the hash of the raw storage slot key (32 bytes);
- // in v1, it's the raw storage slot key (32 bytes);
- id common.Hash
- length uint8 // The length of storage slot, up to 32 bytes defined in protocol
- offset uint32 // The offset of item in storage slot data table
-}
-
-// encode packs slot index into byte stream.
-func (i *slotIndex) encode() []byte {
- var buf [slotIndexSize]byte
- copy(buf[:common.HashLength], i.id.Bytes())
- buf[common.HashLength] = i.length
- binary.BigEndian.PutUint32(buf[common.HashLength+1:], i.offset)
- return buf[:]
-}
-
-// decode unpack slot index from the byte stream.
-func (i *slotIndex) decode(blob []byte) {
- i.id = common.BytesToHash(blob[:common.HashLength])
- i.length = blob[common.HashLength]
- i.offset = binary.BigEndian.Uint32(blob[common.HashLength+1:])
-}
-
-// meta describes the meta data of state history object.
-type meta struct {
- version uint8 // version tag of history object
- parent common.Hash // prev-state root before the state transition
- root common.Hash // post-state root after the state transition
- block uint64 // associated block number
-}
-
-// encode packs the meta object into byte stream.
-func (m *meta) encode() []byte {
- buf := make([]byte, historyMetaSize)
- buf[0] = m.version
- copy(buf[1:1+common.HashLength], m.parent.Bytes())
- copy(buf[1+common.HashLength:1+2*common.HashLength], m.root.Bytes())
- binary.BigEndian.PutUint64(buf[1+2*common.HashLength:historyMetaSize], m.block)
- return buf[:]
-}
-
-// decode unpacks the meta object from byte stream.
-func (m *meta) decode(blob []byte) error {
- if len(blob) < 1 {
- return errors.New("no version tag")
- }
- switch blob[0] {
- case stateHistoryV0, stateHistoryV1:
- if len(blob) != historyMetaSize {
- return fmt.Errorf("invalid state history meta, len: %d", len(blob))
- }
- m.version = blob[0]
- m.parent = common.BytesToHash(blob[1 : 1+common.HashLength])
- m.root = common.BytesToHash(blob[1+common.HashLength : 1+2*common.HashLength])
- m.block = binary.BigEndian.Uint64(blob[1+2*common.HashLength : historyMetaSize])
- return nil
- default:
- return fmt.Errorf("unknown version %d", blob[0])
- }
-}
-
-// history represents a set of state changes belong to a block along with
-// the metadata including the state roots involved in the state transition.
-// State history objects in disk are linked with each other by a unique id
-// (8-bytes integer), the oldest state history object can be pruned on demand
-// in order to control the storage size.
-type history struct {
- meta *meta // Meta data of history
- accounts map[common.Address][]byte // Account data keyed by its address hash
- accountList []common.Address // Sorted account hash list
- storages map[common.Address]map[common.Hash][]byte // Storage data keyed by its address hash and slot hash
- storageList map[common.Address][]common.Hash // Sorted slot hash list
-}
-
-// newHistory constructs the state history object with provided state change set.
-func newHistory(root common.Hash, parent common.Hash, block uint64, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, rawStorageKey bool) *history {
- var (
- accountList = slices.SortedFunc(maps.Keys(accounts), common.Address.Cmp)
- storageList = make(map[common.Address][]common.Hash)
- )
- for addr, slots := range storages {
- storageList[addr] = slices.SortedFunc(maps.Keys(slots), common.Hash.Cmp)
- }
- version := historyVersion
- if !rawStorageKey {
- version = stateHistoryV0
- }
- return &history{
- meta: &meta{
- version: version,
- parent: parent,
- root: root,
- block: block,
- },
- accounts: accounts,
- accountList: accountList,
- storages: storages,
- storageList: storageList,
- }
-}
-
-// stateSet returns the state set, keyed by the hash of the account address
-// and the hash of the storage slot key.
-func (h *history) stateSet() (map[common.Hash][]byte, map[common.Hash]map[common.Hash][]byte) {
- var (
- accounts = make(map[common.Hash][]byte)
- storages = make(map[common.Hash]map[common.Hash][]byte)
- )
- for addr, blob := range h.accounts {
- addrHash := crypto.Keccak256Hash(addr.Bytes())
- accounts[addrHash] = blob
-
- storage, exist := h.storages[addr]
- if !exist {
- continue
- }
- if h.meta.version == stateHistoryV0 {
- storages[addrHash] = storage
- } else {
- subset := make(map[common.Hash][]byte)
- for key, slot := range storage {
- subset[crypto.Keccak256Hash(key.Bytes())] = slot
- }
- storages[addrHash] = subset
- }
- }
- return accounts, storages
-}
-
-// encode serializes the state history and returns four byte streams represent
-// concatenated account/storage data, account/storage indexes respectively.
-func (h *history) encode() ([]byte, []byte, []byte, []byte) {
- var (
- slotNumber uint32 // the number of processed slots
- accountData []byte // the buffer for concatenated account data
- storageData []byte // the buffer for concatenated storage data
- accountIndexes []byte // the buffer for concatenated account index
- storageIndexes []byte // the buffer for concatenated storage index
- )
- for _, addr := range h.accountList {
- accIndex := accountIndex{
- address: addr,
- length: uint8(len(h.accounts[addr])),
- offset: uint32(len(accountData)),
- }
- slots, exist := h.storages[addr]
- if exist {
- // Encode storage slots in order
- for _, slotHash := range h.storageList[addr] {
- sIndex := slotIndex{
- id: slotHash,
- length: uint8(len(slots[slotHash])),
- offset: uint32(len(storageData)),
- }
- storageData = append(storageData, slots[slotHash]...)
- storageIndexes = append(storageIndexes, sIndex.encode()...)
- }
- // Fill up the storage meta in account index
- accIndex.storageOffset = slotNumber
- accIndex.storageSlots = uint32(len(slots))
- slotNumber += uint32(len(slots))
- }
- accountData = append(accountData, h.accounts[addr]...)
- accountIndexes = append(accountIndexes, accIndex.encode()...)
- }
- return accountData, storageData, accountIndexes, storageIndexes
-}
-
-// decoder wraps the byte streams for decoding with extra meta fields.
-type decoder struct {
- accountData []byte // the buffer for concatenated account data
- storageData []byte // the buffer for concatenated storage data
- accountIndexes []byte // the buffer for concatenated account index
- storageIndexes []byte // the buffer for concatenated storage index
-
- lastAccount *common.Address // the address of last resolved account
- lastAccountRead uint32 // the read-cursor position of account data
- lastSlotIndexRead uint32 // the read-cursor position of storage slot index
- lastSlotDataRead uint32 // the read-cursor position of storage slot data
-}
-
-// verify validates the provided byte streams for decoding state history. A few
-// checks will be performed to quickly detect data corruption. The byte stream
-// is regarded as corrupted if:
-//
-// - account indexes buffer is empty(empty state set is invalid)
-// - account indexes/storage indexer buffer is not aligned
-//
-// note, these situations are allowed:
-//
-// - empty account data: all accounts were not present
-// - empty storage set: no slots are modified
-func (r *decoder) verify() error {
- if len(r.accountIndexes)%accountIndexSize != 0 || len(r.accountIndexes) == 0 {
- return fmt.Errorf("invalid account index, len: %d", len(r.accountIndexes))
- }
- if len(r.storageIndexes)%slotIndexSize != 0 {
- return fmt.Errorf("invalid storage index, len: %d", len(r.storageIndexes))
- }
- return nil
-}
-
-// readAccount parses the account from the byte stream with specified position.
-func (r *decoder) readAccount(pos int) (accountIndex, []byte, error) {
- // Decode account index from the index byte stream.
- var index accountIndex
- if (pos+1)*accountIndexSize > len(r.accountIndexes) {
- return accountIndex{}, nil, errors.New("account data buffer is corrupted")
- }
- index.decode(r.accountIndexes[pos*accountIndexSize : (pos+1)*accountIndexSize])
-
- // Perform validation before parsing account data, ensure
- // - account is sorted in order in byte stream
- // - account data is strictly encoded with no gap inside
- // - account data is not out-of-slice
- if r.lastAccount != nil { // zero address is possible
- if bytes.Compare(r.lastAccount.Bytes(), index.address.Bytes()) >= 0 {
- return accountIndex{}, nil, errors.New("account is not in order")
- }
- }
- if index.offset != r.lastAccountRead {
- return accountIndex{}, nil, errors.New("account data buffer is gaped")
- }
- last := index.offset + uint32(index.length)
- if uint32(len(r.accountData)) < last {
- return accountIndex{}, nil, errors.New("account data buffer is corrupted")
- }
- data := r.accountData[index.offset:last]
-
- r.lastAccount = &index.address
- r.lastAccountRead = last
-
- return index, data, nil
-}
-
-// readStorage parses the storage slots from the byte stream with specified account.
-func (r *decoder) readStorage(accIndex accountIndex) ([]common.Hash, map[common.Hash][]byte, error) {
- var (
- last *common.Hash
- count = int(accIndex.storageSlots)
- list = make([]common.Hash, 0, count)
- storage = make(map[common.Hash][]byte, count)
- )
- for j := 0; j < count; j++ {
- var (
- index slotIndex
- start = (accIndex.storageOffset + uint32(j)) * uint32(slotIndexSize)
- end = (accIndex.storageOffset + uint32(j+1)) * uint32(slotIndexSize)
- )
- // Perform validation before parsing storage slot data, ensure
- // - slot index is not out-of-slice
- // - slot data is not out-of-slice
- // - slot is sorted in order in byte stream
- // - slot indexes is strictly encoded with no gap inside
- // - slot data is strictly encoded with no gap inside
- if start != r.lastSlotIndexRead {
- return nil, nil, errors.New("storage index buffer is gapped")
- }
- if uint32(len(r.storageIndexes)) < end {
- return nil, nil, errors.New("storage index buffer is corrupted")
- }
- index.decode(r.storageIndexes[start:end])
-
- if last != nil {
- if bytes.Compare(last.Bytes(), index.id.Bytes()) >= 0 {
- return nil, nil, fmt.Errorf("storage slot is not in order, last: %x, current: %x", *last, index.id)
- }
- }
- if index.offset != r.lastSlotDataRead {
- return nil, nil, errors.New("storage data buffer is gapped")
- }
- sEnd := index.offset + uint32(index.length)
- if uint32(len(r.storageData)) < sEnd {
- return nil, nil, errors.New("storage data buffer is corrupted")
- }
- storage[index.id] = r.storageData[r.lastSlotDataRead:sEnd]
- list = append(list, index.id)
-
- last = &index.id
- r.lastSlotIndexRead = end
- r.lastSlotDataRead = sEnd
- }
- return list, storage, nil
-}
-
-// decode deserializes the account and storage data from the provided byte stream.
-func (h *history) decode(accountData, storageData, accountIndexes, storageIndexes []byte) error {
- var (
- count = len(accountIndexes) / accountIndexSize
- accounts = make(map[common.Address][]byte, count)
- storages = make(map[common.Address]map[common.Hash][]byte)
- accountList = make([]common.Address, 0, count)
- storageList = make(map[common.Address][]common.Hash)
-
- r = &decoder{
- accountData: accountData,
- storageData: storageData,
- accountIndexes: accountIndexes,
- storageIndexes: storageIndexes,
- }
- )
- if err := r.verify(); err != nil {
- return err
- }
- for i := 0; i < count; i++ {
- // Resolve account first
- accIndex, accData, err := r.readAccount(i)
- if err != nil {
- return err
- }
- accounts[accIndex.address] = accData
- accountList = append(accountList, accIndex.address)
-
- // Resolve storage slots
- slotList, slotData, err := r.readStorage(accIndex)
- if err != nil {
- return err
- }
- if len(slotList) > 0 {
- storageList[accIndex.address] = slotList
- storages[accIndex.address] = slotData
- }
- }
- h.accounts = accounts
- h.accountList = accountList
- h.storages = storages
- h.storageList = storageList
- return nil
-}
-
-// readHistory reads and decodes the state history object by the given id.
-func readHistory(reader ethdb.AncientReader, id uint64) (*history, error) {
- mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
- if err != nil {
- return nil, err
- }
- var m meta
- if err := m.decode(mData); err != nil {
- return nil, err
- }
- h := history{meta: &m}
- if err := h.decode(accountData, storageData, accountIndexes, storageIndexes); err != nil {
- return nil, err
- }
- return &h, nil
-}
-
-// readHistories reads and decodes a list of state histories with the specific
-// history range.
-func readHistories(freezer ethdb.AncientReader, start uint64, count uint64) ([]*history, error) {
- var histories []*history
- metaList, aIndexList, sIndexList, aDataList, sDataList, err := rawdb.ReadStateHistoryList(freezer, start, count)
- if err != nil {
- return nil, err
- }
- for i := 0; i < len(metaList); i++ {
- var m meta
- if err := m.decode(metaList[i]); err != nil {
- return nil, err
- }
- h := history{meta: &m}
- if err := h.decode(aDataList[i], sDataList[i], aIndexList[i], sIndexList[i]); err != nil {
- return nil, err
- }
- histories = append(histories, &h)
- }
- return histories, nil
-}
-
-// writeHistory persists the state history with the provided state set.
-func writeHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
- // Short circuit if state set is not available.
- if dl.states == nil {
- return errors.New("state change set is not available")
- }
- var (
- start = time.Now()
- history = newHistory(dl.rootHash(), dl.parentLayer().rootHash(), dl.block, dl.states.accountOrigin, dl.states.storageOrigin, dl.states.rawStorageKey)
- )
- accountData, storageData, accountIndex, storageIndex := history.encode()
- dataSize := common.StorageSize(len(accountData) + len(storageData))
- indexSize := common.StorageSize(len(accountIndex) + len(storageIndex))
-
- // Write history data into five freezer table respectively.
- if err := rawdb.WriteStateHistory(writer, dl.stateID(), history.meta.encode(), accountIndex, storageIndex, accountData, storageData); err != nil {
- return err
- }
- historyDataBytesMeter.Mark(int64(dataSize))
- historyIndexBytesMeter.Mark(int64(indexSize))
- historyBuildTimeMeter.UpdateSince(start)
- log.Debug("Stored state history", "id", dl.stateID(), "block", dl.block, "data", dataSize, "index", indexSize, "elapsed", common.PrettyDuration(time.Since(start)))
-
- return nil
-}
-
-// checkHistories retrieves a batch of meta objects with the specified range
-// and performs the callback on each item.
-func checkHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
- for count > 0 {
- number := count
- if number > 10000 {
- number = 10000 // split the big read into small chunks
- }
- blobs, err := rawdb.ReadStateHistoryMetaList(reader, start, number)
- if err != nil {
- return err
- }
- for _, blob := range blobs {
- var dec meta
- if err := dec.decode(blob); err != nil {
- return err
- }
- if err := check(&dec); err != nil {
- return err
- }
- }
- count -= uint64(len(blobs))
- start += uint64(len(blobs))
- }
- return nil
-}
-
-// truncateFromHead removes the extra state histories from the head with the given
-// parameters. It returns the number of items removed from the head.
-func truncateFromHead(db ethdb.Batcher, store ethdb.AncientStore, nhead uint64) (int, error) {
+// truncateFromHead removes excess elements from the head of the freezer based
+// on the given parameters. It returns the number of items that were removed.
+func truncateFromHead(store ethdb.AncientStore, nhead uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
@@ -606,40 +40,28 @@ func truncateFromHead(db ethdb.Batcher, store ethdb.AncientStore, nhead uint64)
if err != nil {
return 0, err
}
- // Ensure that the truncation target falls within the specified range.
+ log.Info("Truncating from head", "ohead", ohead, "tail", otail, "nhead", nhead)
+
+ // Ensure that the truncation target falls within the valid range.
if ohead < nhead || nhead < otail {
- return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, nhead)
+ return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errHeadTruncationOutOfRange, otail, ohead, nhead)
}
// Short circuit if nothing to truncate.
if ohead == nhead {
return 0, nil
}
- // Load the meta objects in range [nhead+1, ohead]
- blobs, err := rawdb.ReadStateHistoryMetaList(store, nhead+1, ohead-nhead)
- if err != nil {
- return 0, err
- }
- batch := db.NewBatch()
- for _, blob := range blobs {
- var m meta
- if err := m.decode(blob); err != nil {
- return 0, err
- }
- rawdb.DeleteStateID(batch, m.root)
- }
- if err := batch.Write(); err != nil {
- return 0, err
- }
ohead, err = store.TruncateHead(nhead)
if err != nil {
return 0, err
}
+ // Associated root->id mappings are left in the database and wait
+ // for overwriting.
return int(ohead - nhead), nil
}
-// truncateFromTail removes the extra state histories from the tail with the given
-// parameters. It returns the number of items removed from the tail.
-func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64) (int, error) {
+// truncateFromTail removes excess elements from the end of the freezer based
+// on the given parameters. It returns the number of items that were removed.
+func truncateFromTail(store ethdb.AncientStore, ntail uint64) (int, error) {
ohead, err := store.Ancients()
if err != nil {
return 0, err
@@ -648,33 +70,18 @@ func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64)
if err != nil {
return 0, err
}
- // Ensure that the truncation target falls within the specified range.
+ // Ensure that the truncation target falls within the valid range.
if otail > ntail || ntail > ohead {
- return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, ntail)
+ return 0, fmt.Errorf("%w, tail: %d, head: %d, target: %d", errTailTruncationOutOfRange, otail, ohead, ntail)
}
// Short circuit if nothing to truncate.
if otail == ntail {
return 0, nil
}
- // Load the meta objects in range [otail+1, ntail]
- blobs, err := rawdb.ReadStateHistoryMetaList(store, otail+1, ntail-otail)
- if err != nil {
- return 0, err
- }
- batch := db.NewBatch()
- for _, blob := range blobs {
- var m meta
- if err := m.decode(blob); err != nil {
- return 0, err
- }
- rawdb.DeleteStateID(batch, m.root)
- }
- if err := batch.Write(); err != nil {
- return 0, err
- }
otail, err = store.TruncateTail(ntail)
if err != nil {
return 0, err
}
+ // Associated root->id mappings are left in the database.
return int(ntail - otail), nil
}
diff --git a/triedb/pathdb/history_index_test.go b/triedb/pathdb/history_index_test.go
index 7b24b86fd6..c83c33ffbd 100644
--- a/triedb/pathdb/history_index_test.go
+++ b/triedb/pathdb/history_index_test.go
@@ -180,7 +180,7 @@ func TestBatchIndexerWrite(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
batch = newBatchIndexer(db, false)
- histories = makeHistories(10)
+ histories = makeStateHistories(10)
)
for i, h := range histories {
if err := batch.process(h, uint64(i+1)); err != nil {
@@ -257,7 +257,7 @@ func TestBatchIndexerDelete(t *testing.T) {
var (
db = rawdb.NewMemoryDatabase()
bw = newBatchIndexer(db, false)
- histories = makeHistories(10)
+ histories = makeStateHistories(10)
)
// Index histories
for i, h := range histories {
diff --git a/triedb/pathdb/history_indexer.go b/triedb/pathdb/history_indexer.go
index 054d43e946..b4e89c3f17 100644
--- a/triedb/pathdb/history_indexer.go
+++ b/triedb/pathdb/history_indexer.go
@@ -93,7 +93,7 @@ func newBatchIndexer(db ethdb.KeyValueStore, delete bool) *batchIndexer {
// process iterates through the accounts and their associated storage slots in the
// state history, tracking the mapping between state and history IDs.
-func (b *batchIndexer) process(h *history, historyID uint64) error {
+func (b *batchIndexer) process(h *stateHistory, historyID uint64) error {
for _, address := range h.accountList {
addrHash := crypto.Keccak256Hash(address.Bytes())
b.counter += 1
@@ -241,7 +241,7 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient
}
return fmt.Errorf("history indexing is out of order, last: %s, requested: %d", last, historyID)
}
- h, err := readHistory(freezer, historyID)
+ h, err := readStateHistory(freezer, historyID)
if err != nil {
return err
}
@@ -271,7 +271,7 @@ func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancie
}
return fmt.Errorf("history unindexing is out of order, last: %s, requested: %d", last, historyID)
}
- h, err := readHistory(freezer, historyID)
+ h, err := readStateHistory(freezer, historyID)
if err != nil {
return err
}
@@ -322,15 +322,22 @@ func newIndexIniter(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, lastID
closed: make(chan struct{}),
}
// Load indexing progress
+ var recover bool
initer.last.Store(lastID)
metadata := loadIndexMetadata(disk)
if metadata != nil {
initer.indexed.Store(metadata.Last)
+ recover = metadata.Last > lastID
}
// Launch background indexer
initer.wg.Add(1)
- go initer.run(lastID)
+ if recover {
+ log.Info("History indexer is recovering", "history", lastID, "indexed", metadata.Last)
+ go initer.recover(lastID)
+ } else {
+ go initer.run(lastID)
+ }
return initer
}
@@ -364,8 +371,8 @@ func (i *indexIniter) remain() uint64 {
default:
last, indexed := i.last.Load(), i.indexed.Load()
if last < indexed {
- log.Error("Invalid state indexing range", "last", last, "indexed", indexed)
- return 0
+ log.Warn("State indexer is in recovery", "indexed", indexed, "last", last)
+ return indexed - last
}
return last - indexed
}
@@ -524,7 +531,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
if count > historyReadBatch {
count = historyReadBatch
}
- histories, err := readHistories(i.freezer, current, count)
+ histories, err := readStateHistories(i.freezer, current, count)
if err != nil {
// The history read might fall if the history is truncated from
// head due to revert operation.
@@ -543,12 +550,10 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
logged = time.Now()
var (
- left = lastID - current + 1
- done = current - beginID
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
+ left = lastID - current + 1
+ done = current - beginID
)
- // Override the ETA if larger than the largest until now
- eta := time.Duration(left/speed) * time.Millisecond
+ eta := common.CalculateETA(done, left, time.Since(start))
log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
}
}
@@ -571,6 +576,49 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
log.Info("Indexed state history", "from", beginID, "to", lastID, "elapsed", common.PrettyDuration(time.Since(start)))
}
+// recover handles unclean shutdown recovery. After an unclean shutdown, any
+// extra histories are typically truncated, while the corresponding history index
+// entries may still have been written. Ideally, we would unindex these histories
+// in reverse order, but there is no guarantee that the required histories will
+// still be available.
+//
+// As a workaround, indexIniter waits until the missing histories are regenerated
+// by chain recovery, under the assumption that the recovered histories will be
+// identical to the lost ones. Fork-awareness should be added in the future to
+// correctly handle histories affected by reorgs.
+func (i *indexIniter) recover(lastID uint64) {
+ defer i.wg.Done()
+
+ for {
+ select {
+ case signal := <-i.interrupt:
+ newLastID := signal.newLastID
+ if newLastID != lastID+1 && newLastID != lastID-1 {
+ signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", lastID, newLastID)
+ continue
+ }
+
+ // Update the last indexed flag
+ lastID = newLastID
+ signal.result <- nil
+ i.last.Store(newLastID)
+ log.Debug("Updated history index flag", "last", lastID)
+
+ // Terminate the recovery routine once the histories are fully aligned
+ // with the index data, indicating that index initialization is complete.
+ metadata := loadIndexMetadata(i.disk)
+ if metadata != nil && metadata.Last == lastID {
+ close(i.done)
+ log.Info("History indexer is recovered", "last", lastID)
+ return
+ }
+
+ case <-i.closed:
+ return
+ }
+ }
+}
+
// historyIndexer manages the indexing and unindexing of state histories,
// providing access to historical states.
//
@@ -598,14 +646,17 @@ func checkVersion(disk ethdb.KeyValueStore) {
if err == nil && m.Version == stateIndexVersion {
return
}
- // TODO(rjl493456442) would be better to group them into a batch.
- rawdb.DeleteStateHistoryIndexMetadata(disk)
- rawdb.DeleteStateHistoryIndex(disk)
-
version := "unknown"
if err == nil {
version = fmt.Sprintf("%d", m.Version)
}
+
+ batch := disk.NewBatch()
+ rawdb.DeleteStateHistoryIndexMetadata(batch)
+ rawdb.DeleteStateHistoryIndex(batch)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed to purge state history index", "err", err)
+ }
log.Info("Cleaned up obsolete state history index", "version", version, "want", stateIndexVersion)
}
diff --git a/triedb/pathdb/history_indexer_test.go b/triedb/pathdb/history_indexer_test.go
index abfcafc945..96c87ccb1b 100644
--- a/triedb/pathdb/history_indexer_test.go
+++ b/triedb/pathdb/history_indexer_test.go
@@ -32,7 +32,7 @@ func TestHistoryIndexerShortenDeadlock(t *testing.T) {
freezer, _ := rawdb.NewStateFreezer(t.TempDir(), false, false)
defer freezer.Close()
- histories := makeHistories(100)
+ histories := makeStateHistories(100)
for i, h := range histories {
accountData, storageData, accountIndex, storageIndex := h.encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), h.meta.encode(), accountIndex, storageIndex, accountData, storageData)
diff --git a/triedb/pathdb/history_inspect.go b/triedb/pathdb/history_inspect.go
index 9458e2478b..9b4eea27b4 100644
--- a/triedb/pathdb/history_inspect.go
+++ b/triedb/pathdb/history_inspect.go
@@ -61,7 +61,7 @@ func sanitizeRange(start, end uint64, freezer ethdb.AncientReader) (uint64, uint
return first, last, nil
}
-func inspectHistory(freezer ethdb.AncientReader, start, end uint64, onHistory func(*history, *HistoryStats)) (*HistoryStats, error) {
+func inspectHistory(freezer ethdb.AncientReader, start, end uint64, onHistory func(*stateHistory, *HistoryStats)) (*HistoryStats, error) {
var (
stats = &HistoryStats{}
init = time.Now()
@@ -74,7 +74,7 @@ func inspectHistory(freezer ethdb.AncientReader, start, end uint64, onHistory fu
for id := start; id <= end; id += 1 {
// The entire history object is decoded, although it's unnecessary for
// account inspection. TODO(rjl493456442) optimization is worthwhile.
- h, err := readHistory(freezer, id)
+ h, err := readStateHistory(freezer, id)
if err != nil {
return nil, err
}
@@ -98,7 +98,7 @@ func inspectHistory(freezer ethdb.AncientReader, start, end uint64, onHistory fu
// accountHistory inspects the account history within the range.
func accountHistory(freezer ethdb.AncientReader, address common.Address, start, end uint64) (*HistoryStats, error) {
- return inspectHistory(freezer, start, end, func(h *history, stats *HistoryStats) {
+ return inspectHistory(freezer, start, end, func(h *stateHistory, stats *HistoryStats) {
blob, exists := h.accounts[address]
if !exists {
return
@@ -111,7 +111,7 @@ func accountHistory(freezer ethdb.AncientReader, address common.Address, start,
// storageHistory inspects the storage history within the range.
func storageHistory(freezer ethdb.AncientReader, address common.Address, slot common.Hash, start uint64, end uint64) (*HistoryStats, error) {
slotHash := crypto.Keccak256Hash(slot.Bytes())
- return inspectHistory(freezer, start, end, func(h *history, stats *HistoryStats) {
+ return inspectHistory(freezer, start, end, func(h *stateHistory, stats *HistoryStats) {
slots, exists := h.storages[address]
if !exists {
return
@@ -145,11 +145,11 @@ func historyRange(freezer ethdb.AncientReader) (uint64, uint64, error) {
}
last := head - 1
- fh, err := readHistory(freezer, first)
+ fh, err := readStateHistory(freezer, first)
if err != nil {
return 0, 0, err
}
- lh, err := readHistory(freezer, last)
+ lh, err := readStateHistory(freezer, last)
if err != nil {
return 0, 0, err
}
diff --git a/triedb/pathdb/history_reader.go b/triedb/pathdb/history_reader.go
index d0ecdf035f..a11297b3f6 100644
--- a/triedb/pathdb/history_reader.go
+++ b/triedb/pathdb/history_reader.go
@@ -320,11 +320,12 @@ func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint6
tail, err := r.freezer.Tail()
if err != nil {
return nil, err
- }
- // stateID == tail is allowed, as the first history object preserved
- // is tail+1
+ } // firstID = tail+1
+
+ // stateID+1 == firstID is allowed, as all the subsequent state histories
+ // are present with no gap inside.
if stateID < tail {
- return nil, errors.New("historical state has been pruned")
+ return nil, fmt.Errorf("historical state has been pruned, first: %d, state: %d", tail+1, stateID)
}
// To serve the request, all state histories from stateID+1 to lastID
diff --git a/triedb/pathdb/history_reader_test.go b/triedb/pathdb/history_reader_test.go
index 4eb93fb9c9..75c5f701f9 100644
--- a/triedb/pathdb/history_reader_test.go
+++ b/triedb/pathdb/history_reader_test.go
@@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/internal/testrand"
)
func waitIndexing(db *Database) {
@@ -36,11 +37,29 @@ func waitIndexing(db *Database) {
}
}
-func checkHistoricState(env *tester, root common.Hash, hr *historyReader) error {
- // Short circuit if the historical state is no longer available
- if rawdb.ReadStateID(env.db.diskdb, root) == nil {
+func stateAvail(id uint64, env *tester) bool {
+ if env.db.config.StateHistory == 0 {
+ return true
+ }
+ dl := env.db.tree.bottom()
+ if dl.stateID() <= env.db.config.StateHistory {
+ return true
+ }
+ firstID := dl.stateID() - env.db.config.StateHistory + 1
+
+ return id+1 >= firstID
+}
+
+func checkHistoricalState(env *tester, root common.Hash, id uint64, hr *historyReader) error {
+ if !stateAvail(id, env) {
return nil
}
+
+ // Short circuit if the historical state is no longer available
+ if rawdb.ReadStateID(env.db.diskdb, root) == nil {
+ return fmt.Errorf("state not found %d %x", id, root)
+ }
+
var (
dl = env.db.tree.bottom()
stateID = rawdb.ReadStateID(env.db.diskdb, root)
@@ -124,22 +143,27 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
defer func() {
maxDiffLayers = 128
}()
- //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
- env := newTester(t, historyLimit, false, 64, true, "")
+ //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
+ config := &testerConfig{
+ stateHistory: historyLimit,
+ layers: 64,
+ enableIndex: true,
+ }
+ env := newTester(t, config)
defer env.release()
waitIndexing(env.db)
var (
roots = env.roots
- dRoot = env.db.tree.bottom().rootHash()
- hr = newHistoryReader(env.db.diskdb, env.db.freezer)
+ dl = env.db.tree.bottom()
+ hr = newHistoryReader(env.db.diskdb, env.db.stateFreezer)
)
- for _, root := range roots {
- if root == dRoot {
+ for i, root := range roots {
+ if root == dl.rootHash() {
break
}
- if err := checkHistoricState(env, root, hr); err != nil {
+ if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
t.Fatal(err)
}
}
@@ -148,12 +172,46 @@ func testHistoryReader(t *testing.T, historyLimit uint64) {
env.extend(4)
waitIndexing(env.db)
- for _, root := range roots {
- if root == dRoot {
+ for i, root := range roots {
+ if root == dl.rootHash() {
break
}
- if err := checkHistoricState(env, root, hr); err != nil {
+ if err := checkHistoricalState(env, root, uint64(i+1), hr); err != nil {
t.Fatal(err)
}
}
}
+
+func TestHistoricalStateReader(t *testing.T) {
+ maxDiffLayers = 4
+ defer func() {
+ maxDiffLayers = 128
+ }()
+
+ //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
+ config := &testerConfig{
+ stateHistory: 0,
+ layers: 64,
+ enableIndex: true,
+ }
+ env := newTester(t, config)
+ defer env.release()
+ waitIndexing(env.db)
+
+ // non-canonical state
+ fakeRoot := testrand.Hash()
+ rawdb.WriteStateID(env.db.diskdb, fakeRoot, 10)
+
+ _, err := env.db.HistoricReader(fakeRoot)
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ t.Log(err)
+
+ // canonical state
+ realRoot := env.roots[9]
+ _, err = env.db.HistoricReader(realRoot)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+}
diff --git a/triedb/pathdb/history_state.go b/triedb/pathdb/history_state.go
new file mode 100644
index 0000000000..3bb69a7f4d
--- /dev/null
+++ b/triedb/pathdb/history_state.go
@@ -0,0 +1,610 @@
+// Copyright 2023 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package pathdb
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "maps"
+ "slices"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/rawdb"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/ethdb"
+ "github.com/ethereum/go-ethereum/log"
+)
+
+// State history records the state changes involved in executing a block. The
+// state can be reverted to the previous version by applying the associated
+// history object (state reverse diff). State history objects are kept to
+// guarantee that the system can perform state rollbacks in case of deep reorg.
+//
+// Each state transition will generate a state history object. Note that not
+// every block has a corresponding state history object. If a block performs
+// no state changes whatsoever, no state is created for it. Each state history
+// will have a sequentially increasing number acting as its unique identifier.
+//
+// The state history is written to disk (ancient store) when the corresponding
+// diff layer is merged into the disk layer. At the same time, system can prune
+// the oldest histories according to config.
+//
+// Disk State
+// ^
+// |
+// +------------+ +---------+ +---------+ +---------+
+// | Init State |---->| State 1 |---->| ... |---->| State n |
+// +------------+ +---------+ +---------+ +---------+
+//
+// +-----------+ +------+ +-----------+
+// | History 1 |----> | ... |---->| History n |
+// +-----------+ +------+ +-----------+
+//
+// # Rollback
+//
+// If the system wants to roll back to a previous state n, it needs to ensure
+// all history objects from n+1 up to the current disk layer are existent. The
+// history objects are applied to the state in reverse order, starting from the
+// current disk layer.
+
+const (
+ accountIndexSize = common.AddressLength + 13 // The length of encoded account index
+ slotIndexSize = common.HashLength + 5 // The length of encoded slot index
+ historyMetaSize = 9 + 2*common.HashLength // The length of encoded history meta
+
+ stateHistoryV0 = uint8(0) // initial version of state history structure
+ stateHistoryV1 = uint8(1) // use the storage slot raw key as the identifier instead of the key hash
+ historyVersion = stateHistoryV1 // the default state history version
+)
+
+// Each state history entry is consisted of five elements:
+//
+// # metadata
+// This object contains a few meta fields, such as the associated state root,
+// block number, version tag and so on. This object may contain an extra
+// accountHash list which means the storage changes belong to these accounts
+// are not complete due to large contract destruction. The incomplete history
+// can not be used for rollback and serving archive state request.
+//
+// # account index
+// This object contains some index information of account. For example, offset
+// and length indicate the location of the data belonging to the account. Besides,
+// storageOffset and storageSlots indicate the storage modification location
+// belonging to the account.
+//
+// The size of each account index is *fixed*, and all indexes are sorted
+// lexicographically. Thus binary search can be performed to quickly locate a
+// specific account.
+//
+// # account data
+// Account data is a concatenated byte stream composed of all account data.
+// The account data can be solved by the offset and length info indicated
+// by corresponding account index.
+//
+// fixed size
+// ^ ^
+// / \
+// +-----------------+-----------------+----------------+-----------------+
+// | Account index 1 | Account index 2 | ... | Account index N |
+// +-----------------+-----------------+----------------+-----------------+
+// |
+// | length
+// offset |----------------+
+// v v
+// +----------------+----------------+----------------+----------------+
+// | Account data 1 | Account data 2 | ... | Account data N |
+// +----------------+----------------+----------------+----------------+
+//
+// # storage index
+// This object is similar with account index. It's also fixed size and contains
+// the location info of storage slot data.
+//
+// # storage data
+// Storage data is a concatenated byte stream composed of all storage slot data.
+// The storage slot data can be solved by the location info indicated by
+// corresponding account index and storage slot index.
+//
+// fixed size
+// ^ ^
+// / \
+// +-----------------+-----------------+----------------+-----------------+
+// | Account index 1 | Account index 2 | ... | Account index N |
+// +-----------------+-----------------+----------------+-----------------+
+// |
+// | storage slots
+// storage offset |-----------------------------------------------------+
+// v v
+// +-----------------+-----------------+-----------------+
+// | storage index 1 | storage index 2 | storage index 3 |
+// +-----------------+-----------------+-----------------+
+// | length
+// offset |-------------+
+// v v
+// +-------------+
+// | slot data 1 |
+// +-------------+
+
+// accountIndex describes the metadata belonging to an account.
+type accountIndex struct {
+ address common.Address // The address of account
+ length uint8 // The length of account data, size limited by 255
+ offset uint32 // The offset of item in account data table
+ storageOffset uint32 // The offset of storage index in storage index table
+ storageSlots uint32 // The number of mutated storage slots belonging to the account
+}
+
+// encode packs account index into byte stream.
+func (i *accountIndex) encode() []byte {
+ var buf [accountIndexSize]byte
+ copy(buf[:], i.address.Bytes())
+ buf[common.AddressLength] = i.length
+ binary.BigEndian.PutUint32(buf[common.AddressLength+1:], i.offset)
+ binary.BigEndian.PutUint32(buf[common.AddressLength+5:], i.storageOffset)
+ binary.BigEndian.PutUint32(buf[common.AddressLength+9:], i.storageSlots)
+ return buf[:]
+}
+
+// decode unpacks account index from byte stream.
+func (i *accountIndex) decode(blob []byte) {
+ i.address = common.BytesToAddress(blob[:common.AddressLength])
+ i.length = blob[common.AddressLength]
+ i.offset = binary.BigEndian.Uint32(blob[common.AddressLength+1:])
+ i.storageOffset = binary.BigEndian.Uint32(blob[common.AddressLength+5:])
+ i.storageSlots = binary.BigEndian.Uint32(blob[common.AddressLength+9:])
+}
+
+// slotIndex describes the metadata belonging to a storage slot.
+type slotIndex struct {
+ // the identifier of the storage slot. Specifically
+ // in v0, it's the hash of the raw storage slot key (32 bytes);
+ // in v1, it's the raw storage slot key (32 bytes);
+ id common.Hash
+ length uint8 // The length of storage slot, up to 32 bytes defined in protocol
+ offset uint32 // The offset of item in storage slot data table
+}
+
+// encode packs slot index into byte stream.
+func (i *slotIndex) encode() []byte {
+ var buf [slotIndexSize]byte
+ copy(buf[:common.HashLength], i.id.Bytes())
+ buf[common.HashLength] = i.length
+ binary.BigEndian.PutUint32(buf[common.HashLength+1:], i.offset)
+ return buf[:]
+}
+
+// decode unpack slot index from the byte stream.
+func (i *slotIndex) decode(blob []byte) {
+ i.id = common.BytesToHash(blob[:common.HashLength])
+ i.length = blob[common.HashLength]
+ i.offset = binary.BigEndian.Uint32(blob[common.HashLength+1:])
+}
+
+// meta describes the meta data of state history object.
+type meta struct {
+ version uint8 // version tag of history object
+ parent common.Hash // prev-state root before the state transition
+ root common.Hash // post-state root after the state transition
+ block uint64 // associated block number
+}
+
+// encode packs the meta object into byte stream.
+func (m *meta) encode() []byte {
+ buf := make([]byte, historyMetaSize)
+ buf[0] = m.version
+ copy(buf[1:1+common.HashLength], m.parent.Bytes())
+ copy(buf[1+common.HashLength:1+2*common.HashLength], m.root.Bytes())
+ binary.BigEndian.PutUint64(buf[1+2*common.HashLength:historyMetaSize], m.block)
+ return buf[:]
+}
+
+// decode unpacks the meta object from byte stream.
+func (m *meta) decode(blob []byte) error {
+ if len(blob) < 1 {
+ return errors.New("no version tag")
+ }
+ switch blob[0] {
+ case stateHistoryV0, stateHistoryV1:
+ if len(blob) != historyMetaSize {
+ return fmt.Errorf("invalid state history meta, len: %d", len(blob))
+ }
+ m.version = blob[0]
+ m.parent = common.BytesToHash(blob[1 : 1+common.HashLength])
+ m.root = common.BytesToHash(blob[1+common.HashLength : 1+2*common.HashLength])
+ m.block = binary.BigEndian.Uint64(blob[1+2*common.HashLength : historyMetaSize])
+ return nil
+ default:
+ return fmt.Errorf("unknown version %d", blob[0])
+ }
+}
+
+// stateHistory represents a set of state changes belong to a block along with
+// the metadata including the state roots involved in the state transition.
+//
+// State history objects in disk are linked with each other by a unique id
+// (8-bytes integer), the oldest state history object can be pruned on demand
+// in order to control the storage size.
+type stateHistory struct {
+ meta *meta // Meta data of history
+ accounts map[common.Address][]byte // Account data keyed by its address hash
+ accountList []common.Address // Sorted account hash list
+ storages map[common.Address]map[common.Hash][]byte // Storage data keyed by its address hash and slot hash
+ storageList map[common.Address][]common.Hash // Sorted slot hash list
+}
+
+// newStateHistory constructs the state history object with provided states.
+func newStateHistory(root common.Hash, parent common.Hash, block uint64, accounts map[common.Address][]byte, storages map[common.Address]map[common.Hash][]byte, rawStorageKey bool) *stateHistory {
+ var (
+ accountList = slices.SortedFunc(maps.Keys(accounts), common.Address.Cmp)
+ storageList = make(map[common.Address][]common.Hash)
+ )
+ for addr, slots := range storages {
+ storageList[addr] = slices.SortedFunc(maps.Keys(slots), common.Hash.Cmp)
+ }
+ version := historyVersion
+ if !rawStorageKey {
+ version = stateHistoryV0
+ }
+ return &stateHistory{
+ meta: &meta{
+ version: version,
+ parent: parent,
+ root: root,
+ block: block,
+ },
+ accounts: accounts,
+ accountList: accountList,
+ storages: storages,
+ storageList: storageList,
+ }
+}
+
+// stateSet returns the state set, keyed by the hash of the account address
+// and the hash of the storage slot key.
+func (h *stateHistory) stateSet() (map[common.Hash][]byte, map[common.Hash]map[common.Hash][]byte) {
+ var (
+ accounts = make(map[common.Hash][]byte)
+ storages = make(map[common.Hash]map[common.Hash][]byte)
+ )
+ for addr, blob := range h.accounts {
+ addrHash := crypto.Keccak256Hash(addr.Bytes())
+ accounts[addrHash] = blob
+
+ storage, exist := h.storages[addr]
+ if !exist {
+ continue
+ }
+ if h.meta.version == stateHistoryV0 {
+ storages[addrHash] = storage
+ } else {
+ subset := make(map[common.Hash][]byte)
+ for key, slot := range storage {
+ subset[crypto.Keccak256Hash(key.Bytes())] = slot
+ }
+ storages[addrHash] = subset
+ }
+ }
+ return accounts, storages
+}
+
+// encode serializes the state history and returns four byte streams represent
+// concatenated account/storage data, account/storage indexes respectively.
+func (h *stateHistory) encode() ([]byte, []byte, []byte, []byte) {
+ var (
+ slotNumber uint32 // the number of processed slots
+ accountData []byte // the buffer for concatenated account data
+ storageData []byte // the buffer for concatenated storage data
+ accountIndexes []byte // the buffer for concatenated account index
+ storageIndexes []byte // the buffer for concatenated storage index
+ )
+ for _, addr := range h.accountList {
+ accIndex := accountIndex{
+ address: addr,
+ length: uint8(len(h.accounts[addr])),
+ offset: uint32(len(accountData)),
+ }
+ slots, exist := h.storages[addr]
+ if exist {
+ // Encode storage slots in order
+ for _, slotHash := range h.storageList[addr] {
+ sIndex := slotIndex{
+ id: slotHash,
+ length: uint8(len(slots[slotHash])),
+ offset: uint32(len(storageData)),
+ }
+ storageData = append(storageData, slots[slotHash]...)
+ storageIndexes = append(storageIndexes, sIndex.encode()...)
+ }
+ // Fill up the storage meta in account index
+ accIndex.storageOffset = slotNumber
+ accIndex.storageSlots = uint32(len(slots))
+ slotNumber += uint32(len(slots))
+ }
+ accountData = append(accountData, h.accounts[addr]...)
+ accountIndexes = append(accountIndexes, accIndex.encode()...)
+ }
+ return accountData, storageData, accountIndexes, storageIndexes
+}
+
+// decoder wraps the byte streams for decoding with extra meta fields.
+type decoder struct {
+ accountData []byte // the buffer for concatenated account data
+ storageData []byte // the buffer for concatenated storage data
+ accountIndexes []byte // the buffer for concatenated account index
+ storageIndexes []byte // the buffer for concatenated storage index
+
+ lastAccount *common.Address // the address of last resolved account
+ lastAccountRead uint32 // the read-cursor position of account data
+ lastSlotIndexRead uint32 // the read-cursor position of storage slot index
+ lastSlotDataRead uint32 // the read-cursor position of storage slot data
+}
+
+// verify validates the provided byte streams for decoding state history. A few
+// checks will be performed to quickly detect data corruption. The byte stream
+// is regarded as corrupted if:
+//
+// - account indexes buffer is empty(empty state set is invalid)
+// - account indexes/storage indexer buffer is not aligned
+//
+// note, these situations are allowed:
+//
+// - empty account data: all accounts were not present
+// - empty storage set: no slots are modified
+func (r *decoder) verify() error {
+ if len(r.accountIndexes)%accountIndexSize != 0 || len(r.accountIndexes) == 0 {
+ return fmt.Errorf("invalid account index, len: %d", len(r.accountIndexes))
+ }
+ if len(r.storageIndexes)%slotIndexSize != 0 {
+ return fmt.Errorf("invalid storage index, len: %d", len(r.storageIndexes))
+ }
+ return nil
+}
+
+// readAccount parses the account from the byte stream with specified position.
+func (r *decoder) readAccount(pos int) (accountIndex, []byte, error) {
+ // Decode account index from the index byte stream.
+ var index accountIndex
+ if (pos+1)*accountIndexSize > len(r.accountIndexes) {
+ return accountIndex{}, nil, errors.New("account data buffer is corrupted")
+ }
+ index.decode(r.accountIndexes[pos*accountIndexSize : (pos+1)*accountIndexSize])
+
+ // Perform validation before parsing account data, ensure
+ // - account is sorted in order in byte stream
+ // - account data is strictly encoded with no gap inside
+ // - account data is not out-of-slice
+ if r.lastAccount != nil { // zero address is possible
+ if bytes.Compare(r.lastAccount.Bytes(), index.address.Bytes()) >= 0 {
+ return accountIndex{}, nil, errors.New("account is not in order")
+ }
+ }
+ if index.offset != r.lastAccountRead {
+ return accountIndex{}, nil, errors.New("account data buffer is gaped")
+ }
+ last := index.offset + uint32(index.length)
+ if uint32(len(r.accountData)) < last {
+ return accountIndex{}, nil, errors.New("account data buffer is corrupted")
+ }
+ data := r.accountData[index.offset:last]
+
+ r.lastAccount = &index.address
+ r.lastAccountRead = last
+
+ return index, data, nil
+}
+
+// readStorage parses the storage slots from the byte stream with specified account.
+func (r *decoder) readStorage(accIndex accountIndex) ([]common.Hash, map[common.Hash][]byte, error) {
+ var (
+ last *common.Hash
+ count = int(accIndex.storageSlots)
+ list = make([]common.Hash, 0, count)
+ storage = make(map[common.Hash][]byte, count)
+ )
+ for j := 0; j < count; j++ {
+ var (
+ index slotIndex
+ start = (accIndex.storageOffset + uint32(j)) * uint32(slotIndexSize)
+ end = (accIndex.storageOffset + uint32(j+1)) * uint32(slotIndexSize)
+ )
+ // Perform validation before parsing storage slot data, ensure
+ // - slot index is not out-of-slice
+ // - slot data is not out-of-slice
+ // - slot is sorted in order in byte stream
+ // - slot indexes is strictly encoded with no gap inside
+ // - slot data is strictly encoded with no gap inside
+ if start != r.lastSlotIndexRead {
+ return nil, nil, errors.New("storage index buffer is gapped")
+ }
+ if uint32(len(r.storageIndexes)) < end {
+ return nil, nil, errors.New("storage index buffer is corrupted")
+ }
+ index.decode(r.storageIndexes[start:end])
+
+ if last != nil {
+ if bytes.Compare(last.Bytes(), index.id.Bytes()) >= 0 {
+ return nil, nil, fmt.Errorf("storage slot is not in order, last: %x, current: %x", *last, index.id)
+ }
+ }
+ if index.offset != r.lastSlotDataRead {
+ return nil, nil, errors.New("storage data buffer is gapped")
+ }
+ sEnd := index.offset + uint32(index.length)
+ if uint32(len(r.storageData)) < sEnd {
+ return nil, nil, errors.New("storage data buffer is corrupted")
+ }
+ storage[index.id] = r.storageData[r.lastSlotDataRead:sEnd]
+ list = append(list, index.id)
+
+ last = &index.id
+ r.lastSlotIndexRead = end
+ r.lastSlotDataRead = sEnd
+ }
+ return list, storage, nil
+}
+
+// decode deserializes the account and storage data from the provided byte stream.
+func (h *stateHistory) decode(accountData, storageData, accountIndexes, storageIndexes []byte) error {
+ var (
+ count = len(accountIndexes) / accountIndexSize
+ accounts = make(map[common.Address][]byte, count)
+ storages = make(map[common.Address]map[common.Hash][]byte)
+ accountList = make([]common.Address, 0, count)
+ storageList = make(map[common.Address][]common.Hash)
+
+ r = &decoder{
+ accountData: accountData,
+ storageData: storageData,
+ accountIndexes: accountIndexes,
+ storageIndexes: storageIndexes,
+ }
+ )
+ if err := r.verify(); err != nil {
+ return err
+ }
+ for i := 0; i < count; i++ {
+ // Resolve account first
+ accIndex, accData, err := r.readAccount(i)
+ if err != nil {
+ return err
+ }
+ accounts[accIndex.address] = accData
+ accountList = append(accountList, accIndex.address)
+
+ // Resolve storage slots
+ slotList, slotData, err := r.readStorage(accIndex)
+ if err != nil {
+ return err
+ }
+ if len(slotList) > 0 {
+ storageList[accIndex.address] = slotList
+ storages[accIndex.address] = slotData
+ }
+ }
+ h.accounts = accounts
+ h.accountList = accountList
+ h.storages = storages
+ h.storageList = storageList
+ return nil
+}
+
+// readStateHistoryMeta reads the metadata of state history with the specified id.
+func readStateHistoryMeta(reader ethdb.AncientReader, id uint64) (*meta, error) {
+ data := rawdb.ReadStateHistoryMeta(reader, id)
+ if len(data) == 0 {
+ return nil, fmt.Errorf("metadata is not found, %d", id)
+ }
+ var m meta
+ err := m.decode(data)
+ if err != nil {
+ return nil, err
+ }
+ return &m, nil
+}
+
+// readStateHistory reads a single state history records with the specified id.
+func readStateHistory(reader ethdb.AncientReader, id uint64) (*stateHistory, error) {
+ mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
+ if err != nil {
+ return nil, err
+ }
+ var m meta
+ if err := m.decode(mData); err != nil {
+ return nil, err
+ }
+ h := stateHistory{meta: &m}
+ if err := h.decode(accountData, storageData, accountIndexes, storageIndexes); err != nil {
+ return nil, err
+ }
+ return &h, nil
+}
+
+// readStateHistories reads a list of state history records within the specified range.
+func readStateHistories(freezer ethdb.AncientReader, start uint64, count uint64) ([]*stateHistory, error) {
+ var histories []*stateHistory
+ metaList, aIndexList, sIndexList, aDataList, sDataList, err := rawdb.ReadStateHistoryList(freezer, start, count)
+ if err != nil {
+ return nil, err
+ }
+ for i := 0; i < len(metaList); i++ {
+ var m meta
+ if err := m.decode(metaList[i]); err != nil {
+ return nil, err
+ }
+ h := stateHistory{meta: &m}
+ if err := h.decode(aDataList[i], sDataList[i], aIndexList[i], sIndexList[i]); err != nil {
+ return nil, err
+ }
+ histories = append(histories, &h)
+ }
+ return histories, nil
+}
+
+// writeStateHistory persists the state history associated with the given diff layer.
+func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
+ // Short circuit if state set is not available.
+ if dl.states == nil {
+ return errors.New("state change set is not available")
+ }
+ var (
+ start = time.Now()
+ history = newStateHistory(dl.rootHash(), dl.parentLayer().rootHash(), dl.block, dl.states.accountOrigin, dl.states.storageOrigin, dl.states.rawStorageKey)
+ )
+ accountData, storageData, accountIndex, storageIndex := history.encode()
+ dataSize := common.StorageSize(len(accountData) + len(storageData))
+ indexSize := common.StorageSize(len(accountIndex) + len(storageIndex))
+
+ // Write history data into five freezer table respectively.
+ if err := rawdb.WriteStateHistory(writer, dl.stateID(), history.meta.encode(), accountIndex, storageIndex, accountData, storageData); err != nil {
+ return err
+ }
+ historyDataBytesMeter.Mark(int64(dataSize))
+ historyIndexBytesMeter.Mark(int64(indexSize))
+ historyBuildTimeMeter.UpdateSince(start)
+ log.Debug("Stored state history", "id", dl.stateID(), "block", dl.block, "data", dataSize, "index", indexSize, "elapsed", common.PrettyDuration(time.Since(start)))
+
+ return nil
+}
+
+// checkStateHistories retrieves a batch of metadata objects with the specified
+// range and performs the callback on each item.
+func checkStateHistories(reader ethdb.AncientReader, start, count uint64, check func(*meta) error) error {
+ for count > 0 {
+ number := count
+ if number > 10000 {
+ number = 10000 // split the big read into small chunks
+ }
+ blobs, err := rawdb.ReadStateHistoryMetaList(reader, start, number)
+ if err != nil {
+ return err
+ }
+ for _, blob := range blobs {
+ var dec meta
+ if err := dec.decode(blob); err != nil {
+ return err
+ }
+ if err := check(&dec); err != nil {
+ return err
+ }
+ }
+ count -= uint64(len(blobs))
+ start += uint64(len(blobs))
+ }
+ return nil
+}
diff --git a/triedb/pathdb/history_test.go b/triedb/pathdb/history_state_test.go
similarity index 65%
rename from triedb/pathdb/history_test.go
rename to triedb/pathdb/history_state_test.go
index 2928d19d74..4a777111ea 100644
--- a/triedb/pathdb/history_test.go
+++ b/triedb/pathdb/history_state_test.go
@@ -18,6 +18,7 @@ package pathdb
import (
"bytes"
+ "errors"
"fmt"
"reflect"
"testing"
@@ -49,36 +50,36 @@ func randomStateSet(n int) (map[common.Address][]byte, map[common.Address]map[co
return accounts, storages
}
-func makeHistory(rawStorageKey bool) *history {
+func makeStateHistory(rawStorageKey bool) *stateHistory {
accounts, storages := randomStateSet(3)
- return newHistory(testrand.Hash(), types.EmptyRootHash, 0, accounts, storages, rawStorageKey)
+ return newStateHistory(testrand.Hash(), types.EmptyRootHash, 0, accounts, storages, rawStorageKey)
}
-func makeHistories(n int) []*history {
+func makeStateHistories(n int) []*stateHistory {
var (
parent = types.EmptyRootHash
- result []*history
+ result []*stateHistory
)
for i := 0; i < n; i++ {
root := testrand.Hash()
accounts, storages := randomStateSet(3)
- h := newHistory(root, parent, uint64(i), accounts, storages, false)
+ h := newStateHistory(root, parent, uint64(i), accounts, storages, false)
parent = root
result = append(result, h)
}
return result
}
-func TestEncodeDecodeHistory(t *testing.T) {
- testEncodeDecodeHistory(t, false)
- testEncodeDecodeHistory(t, true)
+func TestEncodeDecodeStateHistory(t *testing.T) {
+ testEncodeDecodeStateHistory(t, false)
+ testEncodeDecodeStateHistory(t, true)
}
-func testEncodeDecodeHistory(t *testing.T, rawStorageKey bool) {
+func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
var (
m meta
- dec history
- obj = makeHistory(rawStorageKey)
+ dec stateHistory
+ obj = makeStateHistory(rawStorageKey)
)
// check if meta data can be correctly encode/decode
blob := obj.meta.encode()
@@ -108,7 +109,7 @@ func testEncodeDecodeHistory(t *testing.T, rawStorageKey bool) {
}
}
-func checkHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, id uint64, root common.Hash, exist bool) {
+func checkStateHistory(t *testing.T, freezer ethdb.AncientReader, id uint64, exist bool) {
blob := rawdb.ReadStateHistoryMeta(freezer, id)
if exist && len(blob) == 0 {
t.Fatalf("Failed to load trie history, %d", id)
@@ -116,25 +117,17 @@ func checkHistory(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientRe
if !exist && len(blob) != 0 {
t.Fatalf("Unexpected trie history, %d", id)
}
- if exist && rawdb.ReadStateID(db, root) == nil {
- t.Fatalf("Root->ID mapping is not found, %d", id)
- }
- if !exist && rawdb.ReadStateID(db, root) != nil {
- t.Fatalf("Unexpected root->ID mapping, %d", id)
+}
+
+func checkHistoriesInRange(t *testing.T, freezer ethdb.AncientReader, from, to uint64, exist bool) {
+ for i := from; i <= to; i = i + 1 {
+ checkStateHistory(t, freezer, i, exist)
}
}
-func checkHistoriesInRange(t *testing.T, db ethdb.KeyValueReader, freezer ethdb.AncientReader, from, to uint64, roots []common.Hash, exist bool) {
- for i, j := from, 0; i <= to; i, j = i+1, j+1 {
- checkHistory(t, db, freezer, i, roots[j], exist)
- }
-}
-
-func TestTruncateHeadHistory(t *testing.T) {
+func TestTruncateHeadStateHistory(t *testing.T) {
var (
- roots []common.Hash
- hs = makeHistories(10)
- db = rawdb.NewMemoryDatabase()
+ hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@@ -142,27 +135,23 @@ func TestTruncateHeadHistory(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
- rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
- roots = append(roots, hs[i].meta.root)
}
for size := len(hs); size > 0; size-- {
- pruned, err := truncateFromHead(db, freezer, uint64(size-1))
+ pruned, err := truncateFromHead(freezer, uint64(size-1))
if err != nil {
t.Fatalf("Failed to truncate from head %v", err)
}
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
- checkHistoriesInRange(t, db, freezer, uint64(size), uint64(10), roots[size-1:], false)
- checkHistoriesInRange(t, db, freezer, uint64(1), uint64(size-1), roots[:size-1], true)
+ checkHistoriesInRange(t, freezer, uint64(size), uint64(10), false)
+ checkHistoriesInRange(t, freezer, uint64(1), uint64(size-1), true)
}
}
-func TestTruncateTailHistory(t *testing.T) {
+func TestTruncateTailStateHistory(t *testing.T) {
var (
- roots []common.Hash
- hs = makeHistories(10)
- db = rawdb.NewMemoryDatabase()
+ hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@@ -170,20 +159,18 @@ func TestTruncateTailHistory(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
- rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
- roots = append(roots, hs[i].meta.root)
}
for newTail := 1; newTail < len(hs); newTail++ {
- pruned, _ := truncateFromTail(db, freezer, uint64(newTail))
+ pruned, _ := truncateFromTail(freezer, uint64(newTail))
if pruned != 1 {
t.Error("Unexpected pruned items", "want", 1, "got", pruned)
}
- checkHistoriesInRange(t, db, freezer, uint64(1), uint64(newTail), roots[:newTail], false)
- checkHistoriesInRange(t, db, freezer, uint64(newTail+1), uint64(10), roots[newTail:], true)
+ checkHistoriesInRange(t, freezer, uint64(1), uint64(newTail), false)
+ checkHistoriesInRange(t, freezer, uint64(newTail+1), uint64(10), true)
}
}
-func TestTruncateTailHistories(t *testing.T) {
+func TestTruncateTailStateHistories(t *testing.T) {
var cases = []struct {
limit uint64
expPruned int
@@ -191,21 +178,29 @@ func TestTruncateTailHistories(t *testing.T) {
minUnpruned uint64
empty bool
}{
+ // history: id [10]
{
- 1, 9, 9, 10, false,
+ limit: 1,
+ expPruned: 9,
+ maxPruned: 9, minUnpruned: 10, empty: false,
},
+ // history: none
{
- 0, 10, 10, 0 /* no meaning */, true,
+ limit: 0,
+ expPruned: 10,
+ empty: true,
},
+ // history: id [1:10]
{
- 10, 0, 0, 1, false,
+ limit: 10,
+ expPruned: 0,
+ maxPruned: 0,
+ minUnpruned: 1,
},
}
for i, c := range cases {
var (
- roots []common.Hash
- hs = makeHistories(10)
- db = rawdb.NewMemoryDatabase()
+ hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir()+fmt.Sprintf("%d", i), false, false)
)
defer freezer.Close()
@@ -213,27 +208,23 @@ func TestTruncateTailHistories(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
- rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
- roots = append(roots, hs[i].meta.root)
}
- pruned, _ := truncateFromTail(db, freezer, uint64(10)-c.limit)
+ pruned, _ := truncateFromTail(freezer, uint64(10)-c.limit)
if pruned != c.expPruned {
t.Error("Unexpected pruned items", "want", c.expPruned, "got", pruned)
}
if c.empty {
- checkHistoriesInRange(t, db, freezer, uint64(1), uint64(10), roots, false)
+ checkHistoriesInRange(t, freezer, uint64(1), uint64(10), false)
} else {
- tail := 10 - int(c.limit)
- checkHistoriesInRange(t, db, freezer, uint64(1), c.maxPruned, roots[:tail], false)
- checkHistoriesInRange(t, db, freezer, c.minUnpruned, uint64(10), roots[tail:], true)
+ checkHistoriesInRange(t, freezer, uint64(1), c.maxPruned, false)
+ checkHistoriesInRange(t, freezer, c.minUnpruned, uint64(10), true)
}
}
}
func TestTruncateOutOfRange(t *testing.T) {
var (
- hs = makeHistories(10)
- db = rawdb.NewMemoryDatabase()
+ hs = makeStateHistories(10)
freezer, _ = rawdb.NewStateFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
@@ -241,9 +232,8 @@ func TestTruncateOutOfRange(t *testing.T) {
for i := 0; i < len(hs); i++ {
accountData, storageData, accountIndex, storageIndex := hs[i].encode()
rawdb.WriteStateHistory(freezer, uint64(i+1), hs[i].meta.encode(), accountIndex, storageIndex, accountData, storageData)
- rawdb.WriteStateID(db, hs[i].meta.root, uint64(i+1))
}
- truncateFromTail(db, freezer, uint64(len(hs)/2))
+ truncateFromTail(freezer, uint64(len(hs)/2))
// Ensure of-out-range truncations are rejected correctly.
head, _ := freezer.Ancients()
@@ -255,20 +245,20 @@ func TestTruncateOutOfRange(t *testing.T) {
expErr error
}{
{0, head, nil}, // nothing to delete
- {0, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
- {0, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
+ {0, head + 1, errHeadTruncationOutOfRange},
+ {0, tail - 1, errHeadTruncationOutOfRange},
{1, tail, nil}, // nothing to delete
- {1, head + 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, head+1)},
- {1, tail - 1, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", tail, head, tail-1)},
+ {1, head + 1, errTailTruncationOutOfRange},
+ {1, tail - 1, errTailTruncationOutOfRange},
}
for _, c := range cases {
var gotErr error
if c.mode == 0 {
- _, gotErr = truncateFromHead(db, freezer, c.target)
+ _, gotErr = truncateFromHead(freezer, c.target)
} else {
- _, gotErr = truncateFromTail(db, freezer, c.target)
+ _, gotErr = truncateFromTail(freezer, c.target)
}
- if !reflect.DeepEqual(gotErr, c.expErr) {
+ if !errors.Is(gotErr, c.expErr) {
t.Errorf("Unexpected error, want: %v, got: %v", c.expErr, gotErr)
}
}
diff --git a/triedb/pathdb/iterator.go b/triedb/pathdb/iterator.go
index 84ea08ddd3..8ca8247206 100644
--- a/triedb/pathdb/iterator.go
+++ b/triedb/pathdb/iterator.go
@@ -309,7 +309,7 @@ type diskStorageIterator struct {
it ethdb.Iterator
}
-// StorageIterator creates a storage iterator over the persistent state.
+// newDiskStorageIterator creates a storage iterator over the persistent state.
func newDiskStorageIterator(db ethdb.KeyValueStore, account common.Hash, seek common.Hash) StorageIterator {
pos := common.TrimRightZeroes(seek[:])
return &diskStorageIterator{
diff --git a/triedb/pathdb/journal.go b/triedb/pathdb/journal.go
index 4639932763..02bdef5d34 100644
--- a/triedb/pathdb/journal.go
+++ b/triedb/pathdb/journal.go
@@ -229,7 +229,7 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) {
return nil, fmt.Errorf("load block number: %v", err)
}
// Read in-memory trie nodes from journal
- var nodes nodeSet
+ var nodes nodeSetWithOrigin
if err := nodes.decode(r); err != nil {
return nil, err
}
@@ -267,7 +267,7 @@ func (dl *diskLayer) journal(w io.Writer) error {
if err := dl.buffer.states.encode(w); err != nil {
return err
}
- log.Debug("Journaled pathdb disk layer", "root", dl.root)
+ log.Debug("Journaled pathdb disk layer", "root", dl.root, "id", dl.id)
return nil
}
@@ -333,7 +333,16 @@ func (db *Database) Journal(root common.Hash) error {
if db.readOnly {
return errDatabaseReadOnly
}
-
+ // Forcibly sync the ancient store before persisting the in-memory layers.
+ // This prevents an edge case where the in-memory layers are persisted
+ // but the ancient store is not properly closed, resulting in recent writes
+ // being lost. After a restart, the ancient store would then be misaligned
+ // with the disk layer, causing data corruption.
+ if db.stateFreezer != nil {
+ if err := db.stateFreezer.SyncAncient(); err != nil {
+ return err
+ }
+ }
// Store the journal into the database and return
var (
file *os.File
diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go
index b2f3f7f37d..ec45257db5 100644
--- a/triedb/pathdb/layertree.go
+++ b/triedb/pathdb/layertree.go
@@ -22,7 +22,6 @@ import (
"sync"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/trie/trienode"
)
// layerTree is a group of state layers identified by the state root.
@@ -142,7 +141,7 @@ func (tree *layerTree) len() int {
}
// add inserts a new layer into the tree if it can be linked to an existing old parent.
-func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint64, nodes *trienode.MergedNodeSet, states *StateSetWithOrigin) error {
+func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint64, nodes *nodeSetWithOrigin, states *StateSetWithOrigin) error {
// Reject noop updates to avoid self-loops. This is a special case that can
// happen for clique networks and proof-of-stake networks where empty blocks
// don't modify the state (0 block subsidy).
@@ -156,7 +155,7 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6
if parent == nil {
return fmt.Errorf("triedb parent [%#x] layer missing", parentRoot)
}
- l := parent.update(root, parent.stateID()+1, block, newNodeSet(nodes.Flatten()), states)
+ l := parent.update(root, parent.stateID()+1, block, nodes, states)
tree.lock.Lock()
defer tree.lock.Unlock()
diff --git a/triedb/pathdb/layertree_test.go b/triedb/pathdb/layertree_test.go
index a76d60ba5b..a74c6eb045 100644
--- a/triedb/pathdb/layertree_test.go
+++ b/triedb/pathdb/layertree_test.go
@@ -22,7 +22,6 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
- "github.com/ethereum/go-ethereum/trie/trienode"
)
func newTestLayerTree() *layerTree {
@@ -45,9 +44,9 @@ func TestLayerCap(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -66,9 +65,9 @@ func TestLayerCap(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -86,9 +85,9 @@ func TestLayerCap(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -106,12 +105,12 @@ func TestLayerCap(t *testing.T) {
// ->C2'->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -131,12 +130,12 @@ func TestLayerCap(t *testing.T) {
// ->C2'->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -155,11 +154,11 @@ func TestLayerCap(t *testing.T) {
// ->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
// Chain:
@@ -213,8 +212,8 @@ func TestBaseLayer(t *testing.T) {
// C1->C2->C3 (HEAD)
{
func() {
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
},
common.Hash{0x1},
},
@@ -230,9 +229,9 @@ func TestBaseLayer(t *testing.T) {
// C4->C5->C6 (HEAD)
{
func() {
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x5}, common.Hash{0x4}, 4, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x6}, common.Hash{0x5}, 5, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x5}, common.Hash{0x4}, 4, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x6}, common.Hash{0x5}, 5, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
tr.cap(common.Hash{0x6}, 2)
},
common.Hash{0x4},
@@ -258,7 +257,7 @@ func TestDescendant(t *testing.T) {
// C1->C2 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -269,7 +268,7 @@ func TestDescendant(t *testing.T) {
// Chain:
// C1->C2->C3 (HEAD)
op: func(tr *layerTree) {
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
},
snapshotB: map[common.Hash]map[common.Hash]struct{}{
common.Hash{0x1}: {
@@ -286,9 +285,9 @@ func TestDescendant(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -325,9 +324,9 @@ func TestDescendant(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -360,9 +359,9 @@ func TestDescendant(t *testing.T) {
// C1->C2->C3->C4 (HEAD)
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -392,12 +391,12 @@ func TestDescendant(t *testing.T) {
// ->C2'->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -445,12 +444,12 @@ func TestDescendant(t *testing.T) {
// ->C2'->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2a}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2a}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2b}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2b}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -494,11 +493,11 @@ func TestDescendant(t *testing.T) {
// ->C3'->C4'
init: func() *layerTree {
tr := newTestLayerTree()
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
- tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3a}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4a}, common.Hash{0x3a}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x3b}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
+ tr.add(common.Hash{0x4b}, common.Hash{0x3b}, 3, NewNodeSetWithOrigin(nil, nil), NewStateSetWithOrigin(nil, nil, nil, nil, false))
return tr
},
snapshotA: map[common.Hash]map[common.Hash]struct{}{
@@ -580,11 +579,11 @@ func TestAccountLookup(t *testing.T) {
// Chain:
// C1->C2->C3->C4 (HEAD)
tr := newTestLayerTree() // base = 0x1
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), nil, nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xb"), nil, nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa", "0xc"), nil, nil, nil, false))
var cases = []struct {
@@ -734,11 +733,11 @@ func TestStorageLookup(t *testing.T) {
// Chain:
// C1->C2->C3->C4 (HEAD)
tr := newTestLayerTree() // base = 0x1
- tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x2}, common.Hash{0x1}, 1, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil), nil, nil, false))
- tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x3}, common.Hash{0x2}, 2, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x2"}}, nil), nil, nil, false))
- tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, trienode.NewMergedNodeSet(),
+ tr.add(common.Hash{0x4}, common.Hash{0x3}, 3, NewNodeSetWithOrigin(nil, nil),
NewStateSetWithOrigin(randomAccountSet("0xa"), randomStorageSet([]string{"0xa"}, [][]string{{"0x1", "0x3"}}, nil), nil, nil, false))
var cases = []struct {
diff --git a/triedb/pathdb/nodes.go b/triedb/pathdb/nodes.go
index f90bd0f01c..c6f9e7aece 100644
--- a/triedb/pathdb/nodes.go
+++ b/triedb/pathdb/nodes.go
@@ -18,6 +18,7 @@ package pathdb
import (
"bytes"
+ "errors"
"fmt"
"io"
"maps"
@@ -301,3 +302,125 @@ func (s *nodeSet) dbsize() int {
}
return m + int(s.size)
}
+
+// nodeSetWithOrigin wraps the node set with additional original values of the
+// mutated trie nodes.
+type nodeSetWithOrigin struct {
+ *nodeSet
+
+ // nodeOrigin represents the trie nodes before the state transition. It's keyed
+ // by the account address hash and node path. The nil value means the trie node
+ // was not present.
+ nodeOrigin map[common.Hash]map[string][]byte
+
+ // memory size of the state data (accountNodeOrigin and storageNodeOrigin)
+ size uint64
+}
+
+// NewNodeSetWithOrigin constructs the state set with the provided data.
+func NewNodeSetWithOrigin(nodes map[common.Hash]map[string]*trienode.Node, origins map[common.Hash]map[string][]byte) *nodeSetWithOrigin {
+ // Don't panic for the lazy callers, initialize the nil maps instead.
+ if origins == nil {
+ origins = make(map[common.Hash]map[string][]byte)
+ }
+ set := &nodeSetWithOrigin{
+ nodeSet: newNodeSet(nodes),
+ nodeOrigin: origins,
+ }
+ set.computeSize()
+ return set
+}
+
+// computeSize calculates the database size of the held trie nodes.
+func (s *nodeSetWithOrigin) computeSize() {
+ var size int
+ for owner, slots := range s.nodeOrigin {
+ prefixLen := common.HashLength
+ if owner == (common.Hash{}) {
+ prefixLen = 0
+ }
+ for path, data := range slots {
+ size += prefixLen + len(path) + len(data)
+ }
+ }
+ s.size = s.nodeSet.size + uint64(size)
+}
+
+// encode serializes the content of node set into the provided writer.
+func (s *nodeSetWithOrigin) encode(w io.Writer) error {
+ // Encode node set
+ if err := s.nodeSet.encode(w); err != nil {
+ return err
+ }
+ // Short circuit if the origins are not tracked
+ if len(s.nodeOrigin) == 0 {
+ return nil
+ }
+
+ // Encode node origins
+ nodes := make([]journalNodes, 0, len(s.nodeOrigin))
+ for owner, subset := range s.nodeOrigin {
+ entry := journalNodes{
+ Owner: owner,
+ Nodes: make([]journalNode, 0, len(subset)),
+ }
+ for path, node := range subset {
+ entry.Nodes = append(entry.Nodes, journalNode{
+ Path: []byte(path),
+ Blob: node,
+ })
+ }
+ nodes = append(nodes, entry)
+ }
+ return rlp.Encode(w, nodes)
+}
+
+// hasOrigin returns whether the origin data set exists in the rlp stream.
+// It's a workaround for backward compatibility.
+func (s *nodeSetWithOrigin) hasOrigin(r *rlp.Stream) (bool, error) {
+ kind, _, err := r.Kind()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ return false, nil
+ }
+ return false, err
+ }
+ // If the type of next element in the RLP stream is:
+ // - `rlp.List`: represents the original value of trienodes;
+ // - others, like `boolean`: represent a field in the following state data set;
+ return kind == rlp.List, nil
+}
+
+// decode deserializes the content from the rlp stream into the node set.
+func (s *nodeSetWithOrigin) decode(r *rlp.Stream) error {
+ if s.nodeSet == nil {
+ s.nodeSet = &nodeSet{}
+ }
+ if err := s.nodeSet.decode(r); err != nil {
+ return err
+ }
+
+ // Decode node origins
+ s.nodeOrigin = make(map[common.Hash]map[string][]byte)
+ if hasOrigin, err := s.hasOrigin(r); err != nil {
+ return err
+ } else if hasOrigin {
+ var encoded []journalNodes
+ if err := r.Decode(&encoded); err != nil {
+ return fmt.Errorf("load nodes: %v", err)
+ }
+ for _, entry := range encoded {
+ subset := make(map[string][]byte, len(entry.Nodes))
+ for _, n := range entry.Nodes {
+ if len(n.Blob) > 0 {
+ subset[string(n.Path)] = n.Blob
+ } else {
+ subset[string(n.Path)] = nil
+ }
+ }
+ s.nodeOrigin[entry.Owner] = subset
+ }
+ }
+ s.computeSize()
+ return nil
+}
diff --git a/triedb/pathdb/nodes_test.go b/triedb/pathdb/nodes_test.go
new file mode 100644
index 0000000000..483dc4b1a6
--- /dev/null
+++ b/triedb/pathdb/nodes_test.go
@@ -0,0 +1,128 @@
+// Copyright 2025 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package pathdb
+
+import (
+ "bytes"
+ "reflect"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/rlp"
+ "github.com/ethereum/go-ethereum/trie/trienode"
+)
+
+func TestNodeSetEncode(t *testing.T) {
+ nodes := make(map[common.Hash]map[string]*trienode.Node)
+ nodes[common.Hash{}] = map[string]*trienode.Node{
+ "": trienode.New(crypto.Keccak256Hash([]byte{0x0}), []byte{0x0}),
+ "1": trienode.New(crypto.Keccak256Hash([]byte{0x1}), []byte{0x1}),
+ "2": trienode.New(crypto.Keccak256Hash([]byte{0x2}), []byte{0x2}),
+ }
+ nodes[common.Hash{0x1}] = map[string]*trienode.Node{
+ "": trienode.New(crypto.Keccak256Hash([]byte{0x0}), []byte{0x0}),
+ "1": trienode.New(crypto.Keccak256Hash([]byte{0x1}), []byte{0x1}),
+ "2": trienode.New(crypto.Keccak256Hash([]byte{0x2}), []byte{0x2}),
+ }
+ s := newNodeSet(nodes)
+
+ buf := bytes.NewBuffer(nil)
+ if err := s.encode(buf); err != nil {
+ t.Fatalf("Failed to encode states, %v", err)
+ }
+ var dec nodeSet
+ if err := dec.decode(rlp.NewStream(buf, 0)); err != nil {
+ t.Fatalf("Failed to decode states, %v", err)
+ }
+ if !reflect.DeepEqual(s.accountNodes, dec.accountNodes) {
+ t.Fatal("Unexpected account data")
+ }
+ if !reflect.DeepEqual(s.storageNodes, dec.storageNodes) {
+ t.Fatal("Unexpected storage data")
+ }
+}
+
+func TestNodeSetWithOriginEncode(t *testing.T) {
+ nodes := make(map[common.Hash]map[string]*trienode.Node)
+ nodes[common.Hash{}] = map[string]*trienode.Node{
+ "": trienode.New(crypto.Keccak256Hash([]byte{0x0}), []byte{0x0}),
+ "1": trienode.New(crypto.Keccak256Hash([]byte{0x1}), []byte{0x1}),
+ "2": trienode.New(crypto.Keccak256Hash([]byte{0x2}), []byte{0x2}),
+ }
+ nodes[common.Hash{0x1}] = map[string]*trienode.Node{
+ "": trienode.New(crypto.Keccak256Hash([]byte{0x0}), []byte{0x0}),
+ "1": trienode.New(crypto.Keccak256Hash([]byte{0x1}), []byte{0x1}),
+ "2": trienode.New(crypto.Keccak256Hash([]byte{0x2}), []byte{0x2}),
+ }
+ origins := make(map[common.Hash]map[string][]byte)
+ origins[common.Hash{}] = map[string][]byte{
+ "": nil,
+ "1": {0x1},
+ "2": {0x2},
+ }
+ origins[common.Hash{0x1}] = map[string][]byte{
+ "": nil,
+ "1": {0x1},
+ "2": {0x2},
+ }
+
+ // Encode with origin set
+ s := NewNodeSetWithOrigin(nodes, origins)
+
+ buf := bytes.NewBuffer(nil)
+ if err := s.encode(buf); err != nil {
+ t.Fatalf("Failed to encode states, %v", err)
+ }
+ var dec nodeSetWithOrigin
+ if err := dec.decode(rlp.NewStream(buf, 0)); err != nil {
+ t.Fatalf("Failed to decode states, %v", err)
+ }
+ if !reflect.DeepEqual(s.accountNodes, dec.accountNodes) {
+ t.Fatal("Unexpected account data")
+ }
+ if !reflect.DeepEqual(s.storageNodes, dec.storageNodes) {
+ t.Fatal("Unexpected storage data")
+ }
+ if !reflect.DeepEqual(s.nodeOrigin, dec.nodeOrigin) {
+ t.Fatal("Unexpected node origin data")
+ }
+
+ // Encode without origin set
+ s = NewNodeSetWithOrigin(nodes, nil)
+
+ buf = bytes.NewBuffer(nil)
+ if err := s.encode(buf); err != nil {
+ t.Fatalf("Failed to encode states, %v", err)
+ }
+ var dec2 nodeSetWithOrigin
+ if err := dec2.decode(rlp.NewStream(buf, 0)); err != nil {
+ t.Fatalf("Failed to decode states, %v", err)
+ }
+ if !reflect.DeepEqual(s.accountNodes, dec2.accountNodes) {
+ t.Fatal("Unexpected account data")
+ }
+ if !reflect.DeepEqual(s.storageNodes, dec2.storageNodes) {
+ t.Fatal("Unexpected storage data")
+ }
+ if len(dec2.nodeOrigin) != 0 {
+ t.Fatal("unexpected node origin data")
+ }
+ if dec2.size != s.size {
+ t.Fatalf("Unexpected data size, got: %d, want: %d", dec2.size, s.size)
+ }
+}
diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go
index b7b18f13f9..842ac0972e 100644
--- a/triedb/pathdb/reader.go
+++ b/triedb/pathdb/reader.go
@@ -207,30 +207,34 @@ type HistoricalStateReader struct {
// HistoricReader constructs a reader for accessing the requested historic state.
func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, error) {
// Bail out if the state history hasn't been fully indexed
- if db.indexer == nil || !db.indexer.inited() {
+ if db.stateIndexer == nil || db.stateFreezer == nil {
+ return nil, fmt.Errorf("historical state %x is not available", root)
+ }
+ if !db.stateIndexer.inited() {
return nil, errors.New("state histories haven't been fully indexed yet")
}
- if db.freezer == nil {
- return nil, errors.New("state histories are not available")
- }
- // States at the current disk layer or above are directly accessible via
- // db.StateReader.
+ // - States at the current disk layer or above are directly accessible
+ // via `db.StateReader`.
//
- // States older than the current disk layer (including the disk layer
- // itself) are available through historic state access.
- //
- // Note: the requested state may refer to a stale historic state that has
- // already been pruned. This function does not validate availability, as
- // underlying states may be pruned dynamically. Validity is checked during
- // each actual state retrieval.
+ // - States older than the current disk layer (including the disk layer
+ // itself) are available via `db.HistoricReader`.
id := rawdb.ReadStateID(db.diskdb, root)
if id == nil {
return nil, fmt.Errorf("state %#x is not available", root)
}
+ // Ensure the requested state is canonical, historical states on side chain
+ // are not accessible.
+ meta, err := readStateHistoryMeta(db.stateFreezer, *id+1)
+ if err != nil {
+ return nil, err // e.g., the referred state history has been pruned
+ }
+ if meta.parent != root {
+ return nil, fmt.Errorf("state %#x is not canonincal", root)
+ }
return &HistoricalStateReader{
id: *id,
db: db,
- reader: newHistoryReader(db.diskdb, db.freezer),
+ reader: newHistoryReader(db.diskdb, db.stateFreezer),
}, nil
}
diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go
index bc638a569e..dc737c3b53 100644
--- a/triedb/pathdb/states.go
+++ b/triedb/pathdb/states.go
@@ -181,7 +181,7 @@ func (s *stateSet) accountList() []common.Hash {
return list
}
-// StorageList returns a sorted list of all storage slot hashes in this state set
+// storageList returns a sorted list of all storage slot hashes in this state set
// for the given account. The returned list will include the hash of deleted
// storage slot.
//
diff --git a/triedb/pathdb/verifier.go b/triedb/pathdb/verifier.go
index 2d6f72925b..a69b10f4f3 100644
--- a/triedb/pathdb/verifier.go
+++ b/triedb/pathdb/verifier.go
@@ -166,20 +166,17 @@ func (stat *generateStats) report() {
// If there's progress on the account trie, estimate the time to finish crawling it
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
var (
- left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
- speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- eta = time.Duration(left/speed) * time.Millisecond
+ left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
+ eta = common.CalculateETA(done, left, time.Since(stat.start))
)
// If there are large contract crawls in progress, estimate their finish time
for acc, head := range stat.slotsHead {
start := stat.slotsStart[acc]
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
- var (
- left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
- speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
- )
+ left := math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
+
// Override the ETA if larger than the largest until now
- if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
+ if slotETA := common.CalculateETA(done, left, time.Since(start)); eta < slotETA {
eta = slotETA
}
}
diff --git a/version/version.go b/version/version.go
index 30c81b804b..092735ff1f 100644
--- a/version/version.go
+++ b/version/version.go
@@ -19,6 +19,6 @@ package version
const (
Major = 1 // Major version component of the current release
Minor = 16 // Minor version component of the current release
- Patch = 3 // Patch version component of the current release
+ Patch = 4 // Patch version component of the current release
Meta = "unstable" // Version metadata to append to the version string
)