mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-07 23:48:36 +00:00
core/state: fix incorrect contract code state metrics (#33376)
## Description This PR fixes incorrect contract code state metrics by ensuring duplicate codes are not counted towards the reported results. ## Rationale The contract code metrics don't consider database deduplication. The current implementation assumes that the results are only **slightly inaccurate**, but this is not true, especially for data collection efforts that started from the genesis block.
This commit is contained in:
parent
e58c785424
commit
9a346873b8
7 changed files with 73 additions and 27 deletions
|
|
@ -1609,15 +1609,22 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
|
||||||
if err := blockBatch.Write(); err != nil {
|
if err := blockBatch.Write(); err != nil {
|
||||||
log.Crit("Failed to write block into disk", "err", err)
|
log.Crit("Failed to write block into disk", "err", err)
|
||||||
}
|
}
|
||||||
// Commit all cached state changes into underlying memory database.
|
|
||||||
root, stateUpdate, err := statedb.CommitWithUpdate(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number()), bc.chainConfig.IsCancun(block.Number(), block.Time()))
|
var (
|
||||||
|
err error
|
||||||
|
root common.Hash
|
||||||
|
isEIP158 = bc.chainConfig.IsEIP158(block.Number())
|
||||||
|
isCancun = bc.chainConfig.IsCancun(block.Number(), block.Time())
|
||||||
|
)
|
||||||
|
if bc.stateSizer == nil {
|
||||||
|
root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun)
|
||||||
|
} else {
|
||||||
|
root, err = statedb.CommitAndTrack(block.NumberU64(), isEIP158, isCancun, bc.stateSizer)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// If node is running in path mode, skip explicit gc operation
|
||||||
// which is unnecessary in this mode.
|
// which is unnecessary in this mode.
|
||||||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ import (
|
||||||
|
|
||||||
// ContractCodeReader defines the interface for accessing contract code.
|
// ContractCodeReader defines the interface for accessing contract code.
|
||||||
type ContractCodeReader interface {
|
type ContractCodeReader interface {
|
||||||
|
// Has returns the flag indicating whether the contract code with
|
||||||
|
// specified address and hash exists or not.
|
||||||
|
Has(addr common.Address, codeHash common.Hash) bool
|
||||||
|
|
||||||
// Code retrieves a particular contract's code.
|
// Code retrieves a particular contract's code.
|
||||||
//
|
//
|
||||||
// - Returns nil code along with nil error if the requested contract code
|
// - Returns nil code along with nil error if the requested contract code
|
||||||
|
|
@ -170,6 +174,13 @@ func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash)
|
||||||
return len(code), nil
|
return len(code), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Has returns the flag indicating whether the contract code with
|
||||||
|
// specified address and hash exists or not.
|
||||||
|
func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool {
|
||||||
|
code, _ := r.Code(addr, codeHash)
|
||||||
|
return len(code) > 0
|
||||||
|
}
|
||||||
|
|
||||||
// flatReader wraps a database state reader and is safe for concurrent access.
|
// flatReader wraps a database state reader and is safe for concurrent access.
|
||||||
type flatReader struct {
|
type flatReader struct {
|
||||||
reader database.StateReader
|
reader database.StateReader
|
||||||
|
|
|
||||||
|
|
@ -243,12 +243,14 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure code changes. Note that the reported contract code size may be slightly
|
codeExists := make(map[common.Hash]struct{})
|
||||||
// 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 {
|
for _, code := range update.codes {
|
||||||
|
if _, ok := codeExists[code.hash]; ok || code.exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
stats.ContractCodes += 1
|
stats.ContractCodes += 1
|
||||||
stats.ContractCodeBytes += codeKeySize + int64(len(code.blob))
|
stats.ContractCodeBytes += codeKeySize + int64(len(code.blob))
|
||||||
|
codeExists[code.hash] = struct{}{}
|
||||||
}
|
}
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func TestSizeTracker(t *testing.T) {
|
||||||
state.AddBalance(addr3, uint256.NewInt(3000), tracing.BalanceChangeUnspecified)
|
state.AddBalance(addr3, uint256.NewInt(3000), tracing.BalanceChangeUnspecified)
|
||||||
state.SetNonce(addr3, 3, tracing.NonceChangeUnspecified)
|
state.SetNonce(addr3, 3, tracing.NonceChangeUnspecified)
|
||||||
|
|
||||||
currentRoot, _, err := state.CommitWithUpdate(1, true, false)
|
currentRoot, err := state.Commit(1, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to commit initial state: %v", err)
|
t.Fatalf("Failed to commit initial state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ func TestSizeTracker(t *testing.T) {
|
||||||
if i%3 == 0 {
|
if i%3 == 0 {
|
||||||
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
|
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
|
||||||
}
|
}
|
||||||
root, _, err := newState.CommitWithUpdate(blockNum, true, false)
|
root, err := newState.Commit(blockNum, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
|
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
|
||||||
}
|
}
|
||||||
|
|
@ -154,21 +154,22 @@ func TestSizeTracker(t *testing.T) {
|
||||||
if i%3 == 0 {
|
if i%3 == 0 {
|
||||||
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
|
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
|
||||||
}
|
}
|
||||||
root, update, err := newState.CommitWithUpdate(blockNum, true, false)
|
ret, err := newState.commitAndFlush(blockNum, true, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
|
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
|
||||||
}
|
}
|
||||||
if err := tdb.Commit(root, false); err != nil {
|
tracker.Notify(ret)
|
||||||
|
|
||||||
|
if err := tdb.Commit(ret.root, false); err != nil {
|
||||||
t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
|
t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
diff, err := calSizeStats(update)
|
diff, err := calSizeStats(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
|
t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
|
||||||
}
|
}
|
||||||
trackedUpdates = append(trackedUpdates, diff)
|
trackedUpdates = append(trackedUpdates, diff)
|
||||||
tracker.Notify(update)
|
currentRoot = ret.root
|
||||||
currentRoot = root
|
|
||||||
}
|
}
|
||||||
finalRoot := rawdb.ReadSnapshotRoot(db)
|
finalRoot := rawdb.ReadSnapshotRoot(db)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1317,11 +1317,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
|
||||||
|
|
||||||
// commitAndFlush is a wrapper of commit which also commits the state mutations
|
// commitAndFlush is a wrapper of commit which also commits the state mutations
|
||||||
// to the configured data stores.
|
// to the configured data stores.
|
||||||
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (*stateUpdate, error) {
|
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, dedupCode bool) (*stateUpdate, error) {
|
||||||
ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
|
ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dedupCode {
|
||||||
|
ret.markCodeExistence(s.reader)
|
||||||
|
}
|
||||||
|
|
||||||
// Commit dirty contract code if any exists
|
// Commit dirty contract code if any exists
|
||||||
if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 {
|
if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 {
|
||||||
batch := db.NewBatch()
|
batch := db.NewBatch()
|
||||||
|
|
@ -1376,21 +1381,21 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag
|
||||||
// no empty accounts left that could be deleted by EIP-158, storage wiping
|
// no empty accounts left that could be deleted by EIP-158, storage wiping
|
||||||
// should not occur.
|
// should not occur.
|
||||||
func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) {
|
func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) {
|
||||||
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping)
|
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
return common.Hash{}, err
|
||||||
}
|
}
|
||||||
return ret.root, nil
|
return ret.root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitWithUpdate writes the state mutations and returns both the root hash and the state update.
|
// CommitAndTrack writes the state mutations and notifies the size tracker of the state changes.
|
||||||
// This is useful for tracking state changes at the blockchain level.
|
func (s *StateDB) CommitAndTrack(block uint64, deleteEmptyObjects bool, noStorageWiping bool, sizer *SizeTracker) (common.Hash, error) {
|
||||||
func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *stateUpdate, error) {
|
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true)
|
||||||
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, nil, err
|
return common.Hash{}, err
|
||||||
}
|
}
|
||||||
return ret.root, ret, nil
|
sizer.Notify(ret)
|
||||||
|
return ret.root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare handles the preparatory steps for executing a state transition with.
|
// Prepare handles the preparatory steps for executing a state transition with.
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ func (test *stateTest) run() bool {
|
||||||
} else {
|
} else {
|
||||||
state.IntermediateRoot(true) // call intermediateRoot at the transaction boundary
|
state.IntermediateRoot(true) // call intermediateRoot at the transaction boundary
|
||||||
}
|
}
|
||||||
ret, err := state.commitAndFlush(0, true, false) // call commit at the block boundary
|
ret, err := state.commitAndFlush(0, true, false, false) // call commit at the block boundary
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ import (
|
||||||
|
|
||||||
// contractCode represents a contract code with associated metadata.
|
// contractCode represents a contract code with associated metadata.
|
||||||
type contractCode struct {
|
type contractCode struct {
|
||||||
hash common.Hash // hash is the cryptographic hash of the contract code.
|
hash common.Hash // hash is the cryptographic hash of the contract code.
|
||||||
blob []byte // blob is the binary representation of the contract code.
|
blob []byte // blob is the binary representation of the contract code.
|
||||||
|
exists bool // flag whether the code has been existent
|
||||||
}
|
}
|
||||||
|
|
||||||
// accountDelete represents an operation for deleting an Ethereum account.
|
// accountDelete represents an operation for deleting an Ethereum account.
|
||||||
|
|
@ -190,3 +191,22 @@ func (sc *stateUpdate) stateSet() *triedb.StateSet {
|
||||||
RawStorageKey: sc.rawStorageKey,
|
RawStorageKey: sc.rawStorageKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// markCodeExistence determines whether each piece of contract code referenced
|
||||||
|
// in this state update actually exists.
|
||||||
|
//
|
||||||
|
// Note: This operation is expensive and not needed during normal state transitions.
|
||||||
|
// It is only required when SizeTracker is enabled to produce accurate state
|
||||||
|
// statistics.
|
||||||
|
func (sc *stateUpdate) markCodeExistence(reader ContractCodeReader) {
|
||||||
|
cache := make(map[common.Hash]bool)
|
||||||
|
for addr, code := range sc.codes {
|
||||||
|
if exists, ok := cache[code.hash]; ok {
|
||||||
|
code.exists = exists
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res := reader.Has(addr, code.hash)
|
||||||
|
cache[code.hash] = res
|
||||||
|
code.exists = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue