Skip to content

Commit 29025ab

Browse files
committed
core, eth: track issuance on disk, add rpc sub for it
1 parent b8b91b2 commit 29025ab

File tree

6 files changed

+181
-29
lines changed

6 files changed

+181
-29
lines changed

core/blockchain.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/ethereum/go-ethereum/common/mclock"
3232
"github.com/ethereum/go-ethereum/common/prque"
3333
"github.com/ethereum/go-ethereum/consensus"
34+
"github.com/ethereum/go-ethereum/core/issuance"
3435
"github.com/ethereum/go-ethereum/core/rawdb"
3536
"github.com/ethereum/go-ethereum/core/state"
3637
"github.com/ethereum/go-ethereum/core/state/snapshot"
@@ -1185,7 +1186,7 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
11851186

11861187
// writeBlockWithState writes block, metadata and corresponding state data to the
11871188
// database.
1188-
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB) error {
1189+
func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB) error {
11891190
// Calculate the total difficulty of the block
11901191
ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1)
11911192
if ptd == nil {
@@ -1263,6 +1264,22 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
12631264
}
12641265
}
12651266
}
1267+
// If Ether issuance tracking is enabled, do it before emitting events
1268+
if bc.vmConfig.EnableIssuanceRecording {
1269+
// Note, this code path is opt-in for data analysis nodes, so speed
1270+
// is not really relevant, simplicity and containment much more so.
1271+
parent := rawdb.ReadHeader(bc.db, block.ParentHash(), block.NumberU64()-1)
1272+
if parent == nil {
1273+
log.Error("Failed to retrieve parent for issuance", "err", err)
1274+
} else {
1275+
issuance, err := issuance.Issuance(block, parent, bc.stateCache.TrieDB(), bc.chainConfig)
1276+
if err != nil {
1277+
log.Error("Failed to record Ether issuance", "err", err)
1278+
} else {
1279+
rawdb.WriteIssuance(bc.db, block.NumberU64(), block.Hash(), issuance)
1280+
}
1281+
}
1282+
}
12661283
return nil
12671284
}
12681285

@@ -1280,7 +1297,7 @@ func (bc *BlockChain) WriteBlockAndSetHead(block *types.Block, receipts []*types
12801297
// and also it applies the given block as the new chain head. This function expects
12811298
// the chain mutex to be held.
12821299
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) {
1283-
if err := bc.writeBlockWithState(block, receipts, logs, state); err != nil {
1300+
if err := bc.writeBlockWithState(block, receipts, state); err != nil {
12841301
return NonStatTy, err
12851302
}
12861303
currentBlock := bc.CurrentBlock()
@@ -1643,13 +1660,10 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool)
16431660
var status WriteStatus
16441661
if !setHead {
16451662
// Don't set the head, only insert the block
1646-
err = bc.writeBlockWithState(block, receipts, logs, statedb)
1663+
err = bc.writeBlockWithState(block, receipts, statedb)
16471664
} else {
16481665
status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false)
16491666
}
1650-
if _, err := bc.issuance(block, parent); err != nil {
1651-
log.Error("Failed to calculate Ether issuance: %v", err)
1652-
}
16531667
atomic.StoreUint32(&followupInterrupt, 1)
16541668
if err != nil {
16551669
return it.index, err

core/issuance.go renamed to core/issuance/issuance.go

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Lesser General Public License
1515
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
1616

17-
package core
17+
package issuance
1818

1919
import (
2020
"fmt"
@@ -24,14 +24,15 @@ import (
2424
"github.com/ethereum/go-ethereum/consensus/ethash"
2525
"github.com/ethereum/go-ethereum/core/types"
2626
"github.com/ethereum/go-ethereum/log"
27+
"github.com/ethereum/go-ethereum/params"
2728
"github.com/ethereum/go-ethereum/rlp"
2829
"github.com/ethereum/go-ethereum/trie"
2930
)
3031

31-
// issuance calculates the Ether issuance (or burn) across two state tries. In
32+
// Issuance calculates the Ether issuance (or burn) across two state tries. In
3233
// normal mode of operation, the expectation is to calculate the issuance between
3334
// two consecutive blocks.
34-
func (bc *BlockChain) issuance(block *types.Block, parent *types.Header) (*big.Int, error) {
35+
func Issuance(block *types.Block, parent *types.Header, db *trie.Database, config *params.ChainConfig) (*big.Int, error) {
3536
var (
3637
issuance = new(big.Int)
3738
start = time.Now()
@@ -40,11 +41,11 @@ func (bc *BlockChain) issuance(block *types.Block, parent *types.Header) (*big.I
4041
if block.ParentHash() != parent.Hash() {
4142
return nil, fmt.Errorf("parent hash mismatch: have %s, want %s", block.ParentHash().Hex(), parent.Hash().Hex())
4243
}
43-
src, err := trie.New(parent.Root, bc.stateCache.TrieDB())
44+
src, err := trie.New(parent.Root, db)
4445
if err != nil {
4546
return nil, fmt.Errorf("failed to open source trie: %v", err)
4647
}
47-
dst, err := trie.New(block.Root(), bc.stateCache.TrieDB())
48+
dst, err := trie.New(block.Root(), db)
4849
if err != nil {
4950
return nil, fmt.Errorf("failed to open destination trie: %v", err)
5051
}
@@ -58,7 +59,6 @@ func (bc *BlockChain) issuance(block *types.Block, parent *types.Header) (*big.I
5859
panic(err)
5960
}
6061
issuance.Add(issuance, acc.Balance)
61-
//fmt.Printf("%#x: +%v\n", fwdIt.Key, acc.Balance)
6262
}
6363
// Gather all the changes across from destination to source
6464
rewDiffIt, _ := trie.NewDifferenceIterator(dst.NodeIterator(nil), src.NodeIterator(nil))
@@ -70,21 +70,37 @@ func (bc *BlockChain) issuance(block *types.Block, parent *types.Header) (*big.I
7070
panic(err)
7171
}
7272
issuance.Sub(issuance, acc.Balance)
73-
//fmt.Printf("%#x: -%v\n", rewIt.Key, acc.Balance)
7473
}
7574
// Calculate the block subsidy based on chain rules and progression
76-
var (
77-
subsidy = new(big.Int)
78-
uncles = new(big.Int)
79-
)
75+
subsidy, uncles, burn := Subsidy(block, config)
76+
77+
// Calculate the difference between the "calculated" and "crawled" issuance
78+
diff := new(big.Int).Set(issuance)
79+
diff.Sub(diff, subsidy)
80+
diff.Sub(diff, uncles)
81+
diff.Add(diff, burn)
82+
83+
log.Info("Calculated issuance for block", "number", block.Number(), "hash", block.Hash(), "state", issuance, "subsidy", subsidy, "uncles", uncles, "burn", burn, "diff", diff, "elapsed", time.Since(start))
84+
return issuance, nil
85+
}
86+
87+
// Subsidy calculates the block mining and uncle subsidy as well as the 1559 burn
88+
// solely based on header fields. This method is a very accurate approximation of
89+
// the true issuance, but cannot take into account Ether burns via selfdestructs,
90+
// so it will always be ever so slightly off.
91+
func Subsidy(block *types.Block, config *params.ChainConfig) (subsidy *big.Int, uncles *big.Int, burn *big.Int) {
92+
// Calculate the block subsidy based on chain rules and progression
93+
subsidy = new(big.Int)
94+
uncles = new(big.Int)
95+
8096
// Select the correct block reward based on chain progression
81-
if bc.chainConfig.Ethash != nil {
97+
if config.Ethash != nil {
8298
if block.Difficulty().BitLen() != 0 {
8399
subsidy = ethash.FrontierBlockReward
84-
if bc.chainConfig.IsByzantium(block.Number()) {
100+
if config.IsByzantium(block.Number()) {
85101
subsidy = ethash.ByzantiumBlockReward
86102
}
87-
if bc.chainConfig.IsConstantinople(block.Number()) {
103+
if config.IsConstantinople(block.Number()) {
88104
subsidy = ethash.ConstantinopleBlockReward
89105
}
90106
}
@@ -107,16 +123,10 @@ func (bc *BlockChain) issuance(block *types.Block, parent *types.Header) (*big.I
107123
uncles.Add(uncles, r)
108124
}
109125
}
110-
burn := new(big.Int)
126+
// Calculate the burn based on chain rules and progression
127+
burn = new(big.Int)
111128
if block.BaseFee() != nil {
112129
burn = new(big.Int).Mul(new(big.Int).SetUint64(block.GasUsed()), block.BaseFee())
113130
}
114-
// Calculate the difference between the "calculated" and "crawled" issuance
115-
diff := new(big.Int).Set(issuance)
116-
diff.Sub(diff, subsidy)
117-
diff.Sub(diff, uncles)
118-
diff.Add(diff, burn)
119-
120-
log.Info("Calculated issuance for block", "number", block.Number(), "hash", block.Hash(), "state", issuance, "subsidy", subsidy, "uncles", uncles, "burn", burn, "diff", diff, "elapsed", time.Since(start))
121-
return issuance, nil
131+
return subsidy, uncles, burn
122132
}

core/rawdb/accessors_metadata.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package rawdb
1818

1919
import (
2020
"encoding/json"
21+
"math/big"
2122
"time"
2223

2324
"github.com/ethereum/go-ethereum/common"
@@ -186,3 +187,22 @@ func WriteTransitionStatus(db ethdb.KeyValueWriter, data []byte) {
186187
log.Crit("Failed to store the eth2 transition status", "err", err)
187188
}
188189
}
190+
191+
// ReadIssuance retrieves the amount of Ether (in Wei) issued (or burnt) in a
192+
// specific block. If unavailable for the specific block (non full synced node),
193+
// nil will be returned.
194+
func ReadIssuance(db ethdb.KeyValueReader, number uint64, hash common.Hash) *big.Int {
195+
data, _ := db.Get(issuanceKey(number, hash))
196+
if len(data) == 0 {
197+
return nil
198+
}
199+
return new(big.Int).SetBytes(data)
200+
}
201+
202+
// WriteIssuance stores the amount of Ether (in wei) issued (or burnt) in a
203+
// specific block.
204+
func WriteIssuance(db ethdb.KeyValueWriter, number uint64, hash common.Hash, issuance *big.Int) {
205+
if err := db.Put(issuanceKey(number, hash), issuance.Bytes()); err != nil {
206+
log.Crit("Failed to store block issuance", "err", err)
207+
}
208+
}

core/rawdb/database.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
339339
bloomBits stat
340340
beaconHeaders stat
341341
cliqueSnaps stat
342+
issuanceDiffs stat
342343

343344
// Ancient store statistics
344345
ancientHeadersSize common.StorageSize
@@ -400,6 +401,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
400401
bloomBits.Add(size)
401402
case bytes.HasPrefix(key, skeletonHeaderPrefix) && len(key) == (len(skeletonHeaderPrefix)+8):
402403
beaconHeaders.Add(size)
404+
case bytes.HasPrefix(key, issuancePrefix) && len(key) == (len(issuancePrefix)+8+common.HashLength):
405+
issuanceDiffs.Add(size)
403406
case bytes.HasPrefix(key, []byte("clique-")) && len(key) == 7+common.HashLength:
404407
cliqueSnaps.Add(size)
405408
case bytes.HasPrefix(key, []byte("cht-")) ||
@@ -464,6 +467,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
464467
{"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()},
465468
{"Key-Value store", "Beacon sync headers", beaconHeaders.Size(), beaconHeaders.Count()},
466469
{"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()},
470+
{"Key-Value store", "Issuance counters", issuanceDiffs.Size(), issuanceDiffs.Count()},
467471
{"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()},
468472
{"Ancient store", "Headers", ancientHeadersSize.String(), ancients.String()},
469473
{"Ancient store", "Bodies", ancientBodiesSize.String(), ancients.String()},

core/rawdb/schema.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ var (
9797
CodePrefix = []byte("c") // CodePrefix + code hash -> account code
9898
skeletonHeaderPrefix = []byte("S") // skeletonHeaderPrefix + num (uint64 big endian) -> header
9999

100+
issuancePrefix = []byte("e") // issuancePrefix + num (uint64 big endian) + hash -> wei diff
101+
100102
PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage
101103
configPrefix = []byte("ethereum-config-") // config prefix for the db
102104
genesisPrefix = []byte("ethereum-genesis-") // genesis state prefix for the db
@@ -248,3 +250,8 @@ func configKey(hash common.Hash) []byte {
248250
func genesisKey(hash common.Hash) []byte {
249251
return append(genesisPrefix, hash.Bytes()...)
250252
}
253+
254+
// issuanceKey = issuancePrefix + num (uint64 big endian) + hash
255+
func issuanceKey(number uint64, hash common.Hash) []byte {
256+
return append(append(issuancePrefix, encodeBlockNumber(number)...), hash.Bytes()...)
257+
}

eth/api.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/ethereum/go-ethereum/common"
3232
"github.com/ethereum/go-ethereum/common/hexutil"
3333
"github.com/ethereum/go-ethereum/core"
34+
"github.com/ethereum/go-ethereum/core/issuance"
3435
"github.com/ethereum/go-ethereum/core/rawdb"
3536
"github.com/ethereum/go-ethereum/core/state"
3637
"github.com/ethereum/go-ethereum/core/types"
@@ -67,6 +68,102 @@ func (api *PublicEthereumAPI) Hashrate() hexutil.Uint64 {
6768
return hexutil.Uint64(api.e.Miner().Hashrate())
6869
}
6970

71+
// Issuance send a notification each time a new block is appended to the chain
72+
// with various counters about Ether issuance: the state diff (if available),
73+
// block and uncle subsidy, 1559 burn.
74+
func (api *PublicEthereumAPI) Issuance(ctx context.Context, from uint64) (*rpc.Subscription, error) {
75+
// If issuance tracking is not explcitly enabled, refuse to service this
76+
// endpoint. Although we could enable the simple calculations, it might
77+
// end up as an unexpected load on RPC providers, so let's not surprise.
78+
if !api.e.config.EnableIssuanceRecording {
79+
return nil, errors.New("issuance recording not enabled")
80+
}
81+
config := api.e.blockchain.Config()
82+
83+
// Issuance recording enabled, create a subscription to stream through
84+
notifier, supported := rpc.NotifierFromContext(ctx)
85+
if !supported {
86+
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
87+
}
88+
rpcSub := notifier.CreateSubscription()
89+
90+
// Define an internal type for issuance notifications
91+
type issuanceNotification struct {
92+
Number uint64 `json:"blockNumber"`
93+
Hash common.Hash `json:"blockHash"`
94+
Issuance *big.Int `json:"totalIssuance"`
95+
Subsidy *big.Int `json:"subsidyMining"`
96+
Uncles *big.Int `json:"subsidyUncles"`
97+
Burn *big.Int `json:"burnProtocol"`
98+
Destruct *big.Int `json:"burnDestruct"`
99+
}
100+
101+
// Define a method to convert a block into an issuance notification
102+
service := func(block *types.Block) {
103+
// Retrieve the state-crawled issuance - if available
104+
crawled := rawdb.ReadIssuance(api.e.chainDb, block.NumberU64(), block.Hash())
105+
106+
// Calculate the subsidy from the block's contents
107+
subsidy, uncles, burn := issuance.Subsidy(block, config)
108+
109+
// Calculate the difference between the "calculated" and "crawled" issuance
110+
var diff *big.Int
111+
if crawled != nil {
112+
diff = new(big.Int).Set(crawled)
113+
diff.Sub(diff, subsidy)
114+
diff.Sub(diff, uncles)
115+
diff.Add(diff, burn)
116+
}
117+
// Push the issuance to the user
118+
notifier.Notify(rpcSub.ID, &issuanceNotification{
119+
Number: block.NumberU64(),
120+
Hash: block.Hash(),
121+
Issuance: crawled,
122+
Subsidy: subsidy,
123+
Uncles: uncles,
124+
Burn: burn,
125+
Destruct: diff,
126+
})
127+
}
128+
go func() {
129+
// Iterate over all blocks from the requested source up to head and push
130+
// out historical issuance values to the user. Checking the head after
131+
// each iteration is a bit heavy, but it's not really relevant compared
132+
// to pulling blocks from disk, so this keeps thing simpler to switch
133+
// from historicla blocks to live blocks.
134+
for number := from; number <= api.e.blockchain.CurrentBlock().NumberU64(); number++ {
135+
block := rawdb.ReadBlock(api.e.chainDb, rawdb.ReadCanonicalHash(api.e.chainDb, number), number)
136+
if block == nil {
137+
log.Error("Missing block for issuane reporting", "number", number)
138+
return
139+
}
140+
service(block)
141+
}
142+
// Subscribe to chain events and keep emitting issuances on all branches
143+
canonBlocks := make(chan core.ChainEvent)
144+
canonBlocksSub := api.e.blockchain.SubscribeChainEvent(canonBlocks)
145+
defer canonBlocksSub.Unsubscribe()
146+
147+
sideBlocks := make(chan core.ChainSideEvent)
148+
sideBlocksSub := api.e.blockchain.SubscribeChainSideEvent(sideBlocks)
149+
defer sideBlocksSub.Unsubscribe()
150+
151+
for {
152+
select {
153+
case event := <-canonBlocks:
154+
service(event.Block)
155+
case event := <-sideBlocks:
156+
service(event.Block)
157+
case <-rpcSub.Err():
158+
return
159+
case <-notifier.Closed():
160+
return
161+
}
162+
}
163+
}()
164+
return rpcSub, nil
165+
}
166+
70167
// PublicMinerAPI provides an API to control the miner.
71168
// It offers only methods that operate on data that pose no security risk when it is publicly accessible.
72169
type PublicMinerAPI struct {

0 commit comments

Comments
 (0)