diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 83b91f7f8d5..80eed08cf9a 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -37,6 +37,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" @@ -922,6 +923,10 @@ func (fb *filterBackend) ServiceFilter(ctx context.Context, ms *bloombits.Matche panic("not supported") } +func (fb *filterBackend) Config() *ethconfig.Config { + panic("not supported") +} + func (fb *filterBackend) ChainConfig() *params.ChainConfig { panic("not supported") } diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 5046906c0a9..653276876be 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -30,7 +30,7 @@ import ( ) const ( - ipcAPIs = "admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 rpc:1.0 txpool:1.0 web3:1.0" + ipcAPIs = "admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 geth:1.0 miner:1.0 net:1.0 rpc:1.0 txpool:1.0 web3:1.0" httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0" ) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 8ada24829cf..ee7ae468acc 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -70,6 +70,7 @@ var ( utils.OverrideCancun, utils.OverrideVerkle, utils.EnablePersonal, + utils.SupplyDeltaFlag, utils.TxPoolLocalsFlag, utils.TxPoolNoLocalsFlag, utils.TxPoolJournalFlag, diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index fafc8276047..4fbd26ca0a7 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/supply" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/flags" @@ -150,6 +151,18 @@ as the backend data source, making this command a lot faster. The argument is interpreted as block number or hash. If none is provided, the latest block is used. +`, + }, + { + Name: "crawl-supply", + Usage: "Calculate the ether supply at a specific block", + Action: crawlSupply, + Category: "MISCELLANEOUS COMMANDS", + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Description: ` +geth snapshot crawl-supply +will traverse the whole state from the given root and accumulate all the ether +balances to calculate the total supply. `, }, }, @@ -605,3 +618,35 @@ func checkAccount(ctx *cli.Context) error { log.Info("Checked the snapshot journalled storage", "time", common.PrettyDuration(time.Since(start))) return nil } + +func crawlSupply(ctx *cli.Context) error { + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + chaindb := utils.MakeChainDatabase(ctx, stack, true) + headBlock := rawdb.ReadHeadBlock(chaindb) + if headBlock == nil { + log.Error("Failed to load head block") + return errors.New("no head block") + } + if ctx.NArg() > 1 { + log.Error("Too many arguments given") + return errors.New("too many arguments") + } + snapConfig := snapshot.Config{ + CacheSize: 256, + Recovery: false, + NoBuild: false, + AsyncBuild: false, + } + snaptree, err := snapshot.New(snapConfig, chaindb, trie.NewDatabase(chaindb), headBlock.Root()) + if err != nil { + log.Error("Failed to open snapshot tree", "err", err) + return err + } + if _, err = supply.Supply(headBlock.Header(), snaptree); err != nil { + log.Error("Failed to calculate current supply", "err", err) + return err + } + return nil +} diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 8961e350ae5..5f296fa520c 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -911,6 +911,10 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: metrics.DefaultConfig.InfluxDBOrganization, Category: flags.MetricsCategory, } + SupplyDeltaFlag = &cli.BoolFlag{ + Name: "supplydelta", + Usage: "Track Ether supply deltas (don't use in production)", + } ) var ( @@ -1724,6 +1728,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } + if ctx.IsSet(SupplyDeltaFlag.Name) { + cfg.EnableSupplyDeltaRecording = ctx.Bool(SupplyDeltaFlag.Name) + } // Override any default configs for hard coded networks. switch { case ctx.Bool(MainnetFlag.Name): diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index ad36f21ca94..71dd827ccf5 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -488,7 +488,7 @@ func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.H // Finalize implements consensus.Engine, accumulating the block and uncle rewards. func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, withdrawals []*types.Withdrawal) { // Accumulate any block and uncle rewards - accumulateRewards(chain.Config(), state, header, uncles) + applyRewards(chain.Config(), state, header, uncles) } // FinalizeAndAssemble implements consensus.Engine, accumulating the block and @@ -543,10 +543,19 @@ var ( big32 = big.NewInt(32) ) -// AccumulateRewards credits the coinbase of the given block with the mining -// reward. The total reward consists of the static block reward and rewards for -// included uncles. The coinbase of each uncle block is also rewarded. -func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { +// applyRewards credits the coinbase of the given block with the mining reward. +func applyRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { + f := func(h *types.Header, amt *big.Int) { + state.AddBalance(h.Coinbase, amt) + } + AccumulateRewards(config, header, uncles, f, f) +} + +// AccumulateRewards is a generic function that allows the caller to decide how +// to apply rewards. The total reward consists of the static block reward and +// rewards for included uncles. The coinbase of each uncle block is also +// rewarded. +func AccumulateRewards(config *params.ChainConfig, header *types.Header, uncles []*types.Header, accUncleReward, accTotalReward func(*types.Header, *big.Int)) { // Select the correct block reward based on chain progression blockReward := FrontierBlockReward if config.IsByzantium(header.Number) { @@ -563,10 +572,10 @@ func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header r.Sub(r, header.Number) r.Mul(r, blockReward) r.Div(r, big8) - state.AddBalance(uncle.Coinbase, r) + accUncleReward(uncle, r) r.Div(blockReward, big32) reward.Add(reward, r) } - state.AddBalance(header.Coinbase, reward) + accTotalReward(header, reward) } diff --git a/core/blockchain.go b/core/blockchain.go index b760a301fb8..e097bc30e71 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -36,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/supply" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" @@ -1392,6 +1393,36 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. } bc.triedb.Dereference(root) } + // If Ether supply delta tracking is enabled, do it before emitting events + if bc.vmConfig.EnableSupplyDeltaRecording { + // Note, this code path is opt-in for data analysis nodes, so speed + // is not really relevant, simplicity and containment much more so. + parent := rawdb.ReadHeader(bc.db, block.ParentHash(), block.NumberU64()-1) + if parent == nil { + log.Error("Failed to retrieve parent for supply delta", "err", err) + } else { + start := time.Now() + + supplyDelta, err := supply.Delta(parent, block.Header(), bc.stateCache.TrieDB()) + if err != nil { + log.Error("Failed to record Ether supply delta", "err", err) + } else { + rawdb.WriteSupplyDelta(bc.db, block.NumberU64(), block.Hash(), supplyDelta) + } + + // Calculate the block coinbaseReward based on chain rules and progression. + rewards, withdrawals := supply.Issuance(block, bc.chainConfig) + burn := supply.Burn(block.Header()) + + // Calculate the difference between the "calculated" and "crawled" supply delta. + diff := new(big.Int).Set(supplyDelta) + diff.Sub(diff, rewards) + diff.Sub(diff, withdrawals) + diff.Add(diff, burn) + + log.Info("Calculated supply delta for block", "number", block.Number(), "hash", block.Hash(), "supplydelta", supplyDelta, "rewards", rewards, "burn", burn, "withdrawals", withdrawals, "diff", diff, "elapsed", time.Since(start)) + } + } return nil } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index e626dbb5f72..e6aaf60705a 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/supply" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" @@ -4341,3 +4342,81 @@ func TestEIP3651(t *testing.T) { t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual) } } + +func TestDelta(t *testing.T) { + var ( + aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa") + engine = beacon.NewFaker() + + // A sender who makes transactions, has some funds + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + funds = big.NewInt(params.Ether) + gspec = &Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: GenesisAlloc{ + addr1: {Balance: funds}, + // The address 0xAAAA self-destructs + aa: { + Code: []byte{ + byte(vm.ADDRESS), + byte(vm.SELFDESTRUCT), + }, + Nonce: 0, + Balance: big.NewInt(41), + }, + }, + } + ) + + gspec.Config.TerminalTotalDifficulty = common.Big0 + gspec.Config.TerminalTotalDifficultyPassed = true + gspec.Config.ShanghaiTime = u64(0) + signer := types.LatestSigner(gspec.Config) + + db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{1}) + + // One transaction to 0xAAAA + txdata := &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: 0, + To: &aa, + Value: common.Big1, + Gas: 50000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + } + tx := types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key1) + + b.AddTx(tx) + b.AddWithdrawal(&types.Withdrawal{Amount: 1337}) + }) + + var ( + parent = gspec.ToBlock().Header() + block = blocks[0] + ) + + got, err := supply.Delta(parent, block.Header(), trie.NewDatabase(db)) + if err != nil { + t.Fatalf("failed to calculate delta: %v", err) + } + + // Calculate delta, w/o self-destructs + rewards, withdrawals := supply.Issuance(block, gspec.Config) + burn := supply.Burn(block.Header()) + + want := new(big.Int) + want.Add(want, rewards) + want.Add(want, withdrawals) + want.Sub(want, burn) + + // Now account for self-destructed amount. + want.Sub(want, big.NewInt(42)) + + if want.Cmp(got) != 0 { + t.Fatalf("incorrect delta calculated: want %d, got %d", want, got) + } +} diff --git a/core/rawdb/accessors_metadata.go b/core/rawdb/accessors_metadata.go index 2ff29d1add9..2ce2b676780 100644 --- a/core/rawdb/accessors_metadata.go +++ b/core/rawdb/accessors_metadata.go @@ -18,6 +18,7 @@ package rawdb import ( "encoding/json" + "math/big" "time" "github.com/ethereum/go-ethereum/common" @@ -187,3 +188,38 @@ func WriteTransitionStatus(db ethdb.KeyValueWriter, data []byte) { log.Crit("Failed to store the eth2 transition status", "err", err) } } + +// ReadSupplyDelta retrieves the amount of Ether (in Wei) issued (or burnt) in a +// specific block. If unavailable for the specific block (non full synced node), +// nil will be returned. +func ReadSupplyDelta(db ethdb.KeyValueReader, number uint64, hash common.Hash) *big.Int { + blob, _ := db.Get(supplyDeltaKey(number, hash)) + if len(blob) < 2 { + return nil + } + // Since negative big ints can't be encoded to bytes directly, use a dirty + // hack to store the negativift flag in the first byte (0 == positive, + // 1 == negative) + supplyDelta := new(big.Int).SetBytes(blob[1:]) + if blob[0] == 1 { + supplyDelta.Neg(supplyDelta) + } + return supplyDelta +} + +// WriteSupplyDelta stores the amount of Ether (in wei) issued (or burnt) in a +// specific block. +func WriteSupplyDelta(db ethdb.KeyValueWriter, number uint64, hash common.Hash, supplyDelta *big.Int) { + // Since negative big ints can't be encoded to bytes directly, use a dirty + // hack to store the negativift flag in the first byte (0 == positive, + // 1 == negative) + blob := []byte{0} + if supplyDelta.Sign() < 0 { + blob[0] = 1 + } + blob = append(blob, supplyDelta.Bytes()...) + + if err := db.Put(supplyDeltaKey(number, hash), blob); err != nil { + log.Crit("Failed to store block supply delta", "err", err) + } +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index e864bcb2e88..c9518b5795e 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -457,21 +457,22 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { logged = time.Now() // Key-value store statistics - headers stat - bodies stat - receipts stat - tds stat - numHashPairings stat - hashNumPairings stat - tries stat - codes stat - txLookups stat - accountSnaps stat - storageSnaps stat - preimages stat - bloomBits stat - beaconHeaders stat - cliqueSnaps stat + headers stat + bodies stat + receipts stat + tds stat + numHashPairings stat + hashNumPairings stat + tries stat + codes stat + txLookups stat + accountSnaps stat + storageSnaps stat + preimages stat + bloomBits stat + beaconHeaders stat + cliqueSnaps stat + supplyDeltaDiffs stat // Les statistic chtTrieNodes stat @@ -527,6 +528,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8): beaconHeaders.Add(size) case bytes.HasPrefix(key, CliqueSnapshotPrefix) && len(key) == 7+common.HashLength: + case bytes.HasPrefix(key, supplyDeltaPrefix) && len(key) == (len(supplyDeltaPrefix)+8+common.HashLength): + supplyDeltaDiffs.Add(size) cliqueSnaps.Add(size) case bytes.HasPrefix(key, ChtTablePrefix) || bytes.HasPrefix(key, ChtIndexTablePrefix) || @@ -577,6 +580,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"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", "Supply delta counters", supplyDeltaDiffs.Size(), supplyDeltaDiffs.Count()}, {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, {"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()}, {"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()}, diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 18722ed5d4c..f352b733b0f 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -105,6 +105,8 @@ var ( trieNodeAccountPrefix = []byte("A") // trieNodeAccountPrefix + hexPath -> trie node trieNodeStoragePrefix = []byte("O") // trieNodeStoragePrefix + accountHash + hexPath -> trie node + supplyDeltaPrefix = []byte("e") // supplyDeltaPrefix + num (uint64 big endian) + hash -> wei diff + PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage configPrefix = []byte("ethereum-config-") // config prefix for the db genesisPrefix = []byte("ethereum-genesis-") // genesis state prefix for the db @@ -294,3 +296,8 @@ func IsStorageTrieNode(key []byte) (bool, common.Hash, []byte) { accountHash := common.BytesToHash(key[len(trieNodeStoragePrefix) : len(trieNodeStoragePrefix)+common.HashLength]) return true, accountHash, key[len(trieNodeStoragePrefix)+common.HashLength:] } + +// supplyDeltaKey = supplyDeltaPrefix + num (uint64 big endian) + hash +func supplyDeltaKey(number uint64, hash common.Hash) []byte { + return append(append(supplyDeltaPrefix, encodeBlockNumber(number)...), hash.Bytes()...) +} diff --git a/core/supply/delta.go b/core/supply/delta.go new file mode 100644 index 00000000000..0d11216a73b --- /dev/null +++ b/core/supply/delta.go @@ -0,0 +1,103 @@ +// 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 supply + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +// Delta calculates the ether delta across two state tries. That is, the +// issuance minus the ether destroyed. +func Delta(src, dst *types.Header, db *trie.Database) (*big.Int, error) { + // Open src and dst tries. + srcTrie, err := trie.New(trie.StateTrieID(src.Root), db) + if err != nil { + return nil, fmt.Errorf("failed to open source trie: %v", err) + } + dstTrie, err := trie.New(trie.StateTrieID(dst.Root), db) + if err != nil { + return nil, fmt.Errorf("failed to open destination trie: %v", err) + } + delta := new(big.Int) + + // Gather all the changes across from source to destination. + fwdDiffIt, _ := trie.NewDifferenceIterator(srcTrie.MustNodeIterator(nil), dstTrie.MustNodeIterator(nil)) + fwdIt := trie.NewIterator(fwdDiffIt) + + for fwdIt.Next() { + acc := new(types.StateAccount) + if err := rlp.DecodeBytes(fwdIt.Value, acc); err != nil { + panic(err) + } + delta.Add(delta, acc.Balance) + } + // Gather all the changes across from destination to source. + revDiffIt, _ := trie.NewDifferenceIterator(dstTrie.MustNodeIterator(nil), srcTrie.MustNodeIterator(nil)) + revIt := trie.NewIterator(revDiffIt) + + for revIt.Next() { + acc := new(types.StateAccount) + if err := rlp.DecodeBytes(revIt.Value, acc); err != nil { + panic(err) + } + delta.Sub(delta, acc.Balance) + } + + return delta, nil +} + +// Issuance calculates the amount of ether issued by the protocol. There are +// currently two ways for ether to be creates, the first is from block rewards +// and the second is via withdrawals. +func Issuance(block *types.Block, config *params.ChainConfig) (*big.Int, *big.Int) { + var ( + rewards = new(big.Int) + withdrawals = new(big.Int) + ) + // If block is ethash, calculate the coinbase and uncle rewards. + if config.Ethash != nil && block.Difficulty().BitLen() != 0 { + acc := func(h *types.Header, amt *big.Int) { + rewards.Add(rewards, amt) + } + ethash.AccumulateRewards(config, block.Header(), block.Uncles(), acc, acc) + } + // Sum up withdrawals. + for _, w := range block.Withdrawals() { + withdrawals.Add(withdrawals, newGwei(w.Amount)) + } + return rewards, withdrawals +} + +// Burn calculates the amount of ether burned due to EIP-1559 base fee. +func Burn(header *types.Header) *big.Int { + burn := new(big.Int) + if header.BaseFee != nil { + burn = new(big.Int).Mul(new(big.Int).SetUint64(header.GasUsed), header.BaseFee) + } + return burn +} + +func newGwei(n uint64) *big.Int { + return new(big.Int).Mul(big.NewInt(int64(n)), big.NewInt(params.GWei)) +} diff --git a/core/supply/supply.go b/core/supply/supply.go new file mode 100644 index 00000000000..aaf880650cb --- /dev/null +++ b/core/supply/supply.go @@ -0,0 +1,61 @@ +// 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 supply + +import ( + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// Supply crawls the state snapshot at a given header and gathers all the account +// balances to sum into the total ether supply. +func Supply(header *types.Header, snaptree *snapshot.Tree) (*big.Int, error) { + accIt, err := snaptree.AccountIterator(header.Root, common.Hash{}) + if err != nil { + return nil, err + } + defer accIt.Release() + + log.Info("Ether supply counting started", "block", header.Number, "hash", header.Hash(), "root", header.Root) + + var ( + start = time.Now() + logged = time.Now() + accounts uint64 + ) + supply := big.NewInt(0) + for accIt.Next() { + account, err := types.FullAccount(accIt.Account()) + if err != nil { + return nil, err + } + supply.Add(supply, account.Balance) + accounts++ + if time.Since(logged) > 8*time.Second { + log.Info("Ether supply counting in progress", "at", accIt.Hash(), "accounts", accounts, "supply", supply, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + log.Info("Ether supply counting complete", "block", header.Number, "hash", header.Hash(), "root", header.Root, "accounts", accounts, "supply", supply, "elapsed", common.PrettyDuration(time.Since(start))) + + return supply, nil +} diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 873337850e6..9ad3650cee9 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -25,10 +25,11 @@ import ( // Config are the configuration options for the Interpreter type Config struct { - Tracer EVMLogger // Opcode logger - NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) - EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages - ExtraEips []int // Additional EIPS that are to be enabled + Tracer EVMLogger // Opcode logger + NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) + EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages + EnableSupplyDeltaRecording bool // Enables recording Ether supply delta counters + ExtraEips []int // Additional EIPS that are to be enabled } // ScopeContext contains the things that are per-call, such as stack and memory, diff --git a/eth/api_backend.go b/eth/api_backend.go index 80f5bcee614..3db5d9c38d8 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" @@ -50,6 +51,10 @@ type EthAPIBackend struct { gpo *gasprice.Oracle } +func (b *EthAPIBackend) Config() *ethconfig.Config { + return b.eth.config +} + // ChainConfig returns the active chain configuration. func (b *EthAPIBackend) ChainConfig() *params.ChainConfig { return b.eth.blockchain.Config() diff --git a/eth/backend.go b/eth/backend.go index 63bd864b21e..0ad837d4025 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -180,7 +180,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } var ( vmConfig = vm.Config{ - EnablePreimageRecording: config.EnablePreimageRecording, + EnablePreimageRecording: config.EnablePreimageRecording, + EnableSupplyDeltaRecording: config.EnableSupplyDeltaRecording, } cacheConfig = &core.CacheConfig{ TrieCleanLimit: config.TrieCleanCache, diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 5de0055b519..e95db2d8281 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -140,6 +140,9 @@ type Config struct { // Miscellaneous options DocRoot string `toml:"-"` + // Enables tracking Ether supply deltas during block processing. + EnableSupplyDeltaRecording bool + // RPCGasCap is the global gas cap for eth-call variants. RPCGasCap uint64 diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 82b2d5b6b00..81237b88acc 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -51,6 +51,7 @@ func (c Config) MarshalTOML() (interface{}, error) { RPCTxFeeCap float64 OverrideCancun *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"` + EnableSupplyDeltaRecording bool } var enc Config enc.Genesis = c.Genesis @@ -88,6 +89,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RPCTxFeeCap = c.RPCTxFeeCap enc.OverrideCancun = c.OverrideCancun enc.OverrideVerkle = c.OverrideVerkle + enc.EnableSupplyDeltaRecording = c.EnableSupplyDeltaRecording return &enc, nil } @@ -129,6 +131,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RPCTxFeeCap *float64 OverrideCancun *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"` + EnableSupplyDeltaRecording *bool } var dec Config if err := unmarshal(&dec); err != nil { @@ -239,5 +242,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideVerkle != nil { c.OverrideVerkle = dec.OverrideVerkle } + if dec.EnableSupplyDeltaRecording != nil { + c.EnableSupplyDeltaRecording = *dec.EnableSupplyDeltaRecording + } return nil } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 4d8c7e07074..5eb905445dd 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -40,6 +40,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/blocktest" @@ -206,6 +207,7 @@ type testBackend struct { db ethdb.Database chain *core.BlockChain pending *types.Block + config *ethconfig.Config } func newTestBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) *testBackend { @@ -230,7 +232,7 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i i t.Fatalf("block %d: failed to insert into chain: %v", n, err) } - backend := &testBackend{db: db, chain: chain} + backend := &testBackend{db: db, chain: chain, config: ðconfig.Config{EnableSupplyDeltaRecording: true}} return backend } @@ -379,6 +381,7 @@ func (b testBackend) TxPoolContentFrom(addr common.Address) ([]*types.Transactio func (b testBackend) SubscribeNewTxsEvent(events chan<- core.NewTxsEvent) event.Subscription { panic("implement me") } +func (b testBackend) Config() *ethconfig.Config { return b.config } func (b testBackend) ChainConfig() *params.ChainConfig { return b.chain.Config() } func (b testBackend) Engine() consensus.Engine { return b.chain.Engine() } func (b testBackend) GetLogs(ctx context.Context, blockHash common.Hash, number uint64) ([][]*types.Log, error) { diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 458fb811eda..6fefc4b89f8 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" @@ -84,6 +85,7 @@ type Backend interface { TxPoolContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription + Config() *ethconfig.Config ChainConfig() *params.ChainConfig Engine() consensus.Engine @@ -99,30 +101,33 @@ type Backend interface { ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) } -func GetAPIs(apiBackend Backend) []rpc.API { +func GetAPIs(backend Backend) []rpc.API { nonceLock := new(AddrLocker) return []rpc.API{ { Namespace: "eth", - Service: NewEthereumAPI(apiBackend), + Service: NewEthereumAPI(backend), }, { Namespace: "eth", - Service: NewBlockChainAPI(apiBackend), + Service: NewBlockChainAPI(backend), }, { Namespace: "eth", - Service: NewTransactionAPI(apiBackend, nonceLock), + Service: NewTransactionAPI(backend, nonceLock), }, { Namespace: "txpool", - Service: NewTxPoolAPI(apiBackend), + Service: NewTxPoolAPI(backend), }, { Namespace: "debug", - Service: NewDebugAPI(apiBackend), + Service: NewDebugAPI(backend), }, { Namespace: "eth", - Service: NewEthereumAccountAPI(apiBackend.AccountManager()), + Service: NewEthereumAccountAPI(backend.AccountManager()), }, { Namespace: "personal", - Service: NewPersonalAccountAPI(apiBackend, nonceLock), + Service: NewPersonalAccountAPI(backend, nonceLock), + }, { + Namespace: "geth", + Service: NewGethAPI(backend), }, } } diff --git a/internal/ethapi/geth_api.go b/internal/ethapi/geth_api.go new file mode 100644 index 00000000000..266a955075e --- /dev/null +++ b/internal/ethapi/geth_api.go @@ -0,0 +1,141 @@ +// 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 ethapi + +import ( + "context" + "errors" + "math/big" + + "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/supply" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +// GethAPI is the collection of geth-specific APIs exposed over the geth +// namespace. +type GethAPI struct { + b Backend +} + +// NewDebugAPI creates a new instance of DebugAPI. +func NewGethAPI(b Backend) *GethAPI { + return &GethAPI{b: b} +} + +// SupplyDelta send a notification each time a new block is appended to the chain +// with various counters about Ether supply delta: the state diff (if +// available), block and uncle subsidy, 1559 burn. +func (api *GethAPI) SupplyDelta(ctx context.Context, from uint64) (*rpc.Subscription, error) { + // If supply delta tracking is not explcitly enabled, refuse to service this + // endpoint. Although we could enable the simple calculations, it might + // end up as an unexpected load on RPC providers, so let's not surprise. + if !api.b.Config().EnableSupplyDeltaRecording { + return nil, errors.New("supply delta recording not enabled") + } + config := api.b.ChainConfig() + + // Supply delta recording enabled, create a subscription to stream through + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported + } + rpcSub := notifier.CreateSubscription() + + // Define an internal type for supply delta notifications + type supplyDeltaNotification struct { + Number uint64 `json:"block"` + Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash"` + SupplyDelta *big.Int `json:"supplyDelta"` + Reward *big.Int `json:"reward"` + Withdrawals *big.Int `json:"withdrawals"` + Burn *big.Int `json:"burn"` + Destruct *big.Int `json:"destruct"` + } + // Define a method to convert a block into an supply delta notification + service := func(block *types.Block) { + // Retrieve the state-crawled supply delta - if available + crawled := rawdb.ReadSupplyDelta(api.b.ChainDb(), block.NumberU64(), block.Hash()) + + // Calculate the issuance and burn from the block's contents + rewards, withdrawals := supply.Issuance(block, config) + burn := supply.Burn(block.Header()) + + // Calculate the difference between the "calculated" and "crawled" supply delta + var diff *big.Int + if crawled != nil { + diff = new(big.Int).Set(crawled) + diff.Sub(diff, rewards) + diff.Sub(diff, withdrawals) + diff.Add(diff, burn) + } + // Push the supply delta to the user + notifier.Notify(rpcSub.ID, &supplyDeltaNotification{ + Number: block.NumberU64(), + Hash: block.Hash(), + ParentHash: block.ParentHash(), + SupplyDelta: crawled, + Reward: rewards, + Withdrawals: withdrawals, + Burn: burn, + Destruct: diff, + }) + } + go func() { + // Iterate over all blocks from the requested source up to head and push + // out historical supply delta values to the user. Checking the head after + // each iteration is a bit heavy, but it's not really relevant compared + // to pulling blocks from disk, so this keeps thing simpler to switch + // from historical blocks to live blocks. + for number := from; number <= api.b.CurrentBlock().Number.Uint64(); number++ { + block := rawdb.ReadBlock(api.b.ChainDb(), rawdb.ReadCanonicalHash(api.b.ChainDb(), number), number) + if block == nil { + log.Error("Missing block for supply delta reporting", "number", number) + return + } + service(block) + } + // Subscribe to chain events and keep emitting supply deltas on all + // branches + canonBlocks := make(chan core.ChainEvent) + canonBlocksSub := api.b.SubscribeChainEvent(canonBlocks) + defer canonBlocksSub.Unsubscribe() + + sideBlocks := make(chan core.ChainSideEvent) + sideBlocksSub := api.b.SubscribeChainSideEvent(sideBlocks) + defer sideBlocksSub.Unsubscribe() + + for { + select { + case event := <-canonBlocks: + service(event.Block) + case event := <-sideBlocks: + service(event.Block) + case <-rpcSub.Err(): + return + case <-notifier.Closed(): + return + } + } + }() + return rpcSub, nil +} diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 9161d5e681f..bba5f2aea6f 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" @@ -255,6 +256,7 @@ func (b *backendMock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { } func (b *backendMock) CurrentHeader() *types.Header { return b.current } func (b *backendMock) ChainConfig() *params.ChainConfig { return b.config } +func (b *backendMock) Config() *ethconfig.Config { panic("not supported") } // Other methods needed to implement Backend interface. func (b *backendMock) SyncProgress() ethereum.SyncProgress { return ethereum.SyncProgress{} } diff --git a/les/api_backend.go b/les/api_backend.go index 3e9dbadce86..b3bd7e935de 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" @@ -48,6 +49,10 @@ type LesApiBackend struct { gpo *gasprice.Oracle } +func (b *LesApiBackend) Config() *ethconfig.Config { + return b.eth.config +} + func (b *LesApiBackend) ChainConfig() *params.ChainConfig { return b.eth.chainConfig }