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
}