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 [![Go Report Card](https://goreportcard.com/badge/github.com/ethereum/go-ethereum)](https://goreportcard.com/report/github.com/ethereum/go-ethereum) [![Travis](https://app.travis-ci.com/ethereum/go-ethereum.svg?branch=master)](https://app.travis-ci.com/github/ethereum/go-ethereum) [![Discord](https://img.shields.io/badge/discord-join%20chat-blue.svg)](https://discord.gg/nthXNEv) +[![Twitter](https://img.shields.io/twitter/follow/go_ethereum)](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 )