Skip to content

Commit 9a34687

Browse files
authored
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.
1 parent e58c785 commit 9a34687

File tree

7 files changed

+73
-27
lines changed

7 files changed

+73
-27
lines changed

core/blockchain.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,15 +1609,22 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.
16091609
if err := blockBatch.Write(); err != nil {
16101610
log.Crit("Failed to write block into disk", "err", err)
16111611
}
1612-
// Commit all cached state changes into underlying memory database.
1613-
root, stateUpdate, err := statedb.CommitWithUpdate(block.NumberU64(), bc.chainConfig.IsEIP158(block.Number()), bc.chainConfig.IsCancun(block.Number(), block.Time()))
1612+
1613+
var (
1614+
err error
1615+
root common.Hash
1616+
isEIP158 = bc.chainConfig.IsEIP158(block.Number())
1617+
isCancun = bc.chainConfig.IsCancun(block.Number(), block.Time())
1618+
)
1619+
if bc.stateSizer == nil {
1620+
root, err = statedb.Commit(block.NumberU64(), isEIP158, isCancun)
1621+
} else {
1622+
root, err = statedb.CommitAndTrack(block.NumberU64(), isEIP158, isCancun, bc.stateSizer)
1623+
}
16141624
if err != nil {
16151625
return err
16161626
}
1617-
// Emit the state update to the state sizestats if it's active
1618-
if bc.stateSizer != nil {
1619-
bc.stateSizer.Notify(stateUpdate)
1620-
}
1627+
16211628
// If node is running in path mode, skip explicit gc operation
16221629
// which is unnecessary in this mode.
16231630
if bc.triedb.Scheme() == rawdb.PathScheme {

core/state/reader.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ import (
4040

4141
// ContractCodeReader defines the interface for accessing contract code.
4242
type ContractCodeReader interface {
43+
// Has returns the flag indicating whether the contract code with
44+
// specified address and hash exists or not.
45+
Has(addr common.Address, codeHash common.Hash) bool
46+
4347
// Code retrieves a particular contract's code.
4448
//
4549
// - 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)
170174
return len(code), nil
171175
}
172176

177+
// Has returns the flag indicating whether the contract code with
178+
// specified address and hash exists or not.
179+
func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool {
180+
code, _ := r.Code(addr, codeHash)
181+
return len(code) > 0
182+
}
183+
173184
// flatReader wraps a database state reader and is safe for concurrent access.
174185
type flatReader struct {
175186
reader database.StateReader

core/state/state_sizer.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,14 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
243243
}
244244
}
245245

246-
// Measure code changes. Note that the reported contract code size may be slightly
247-
// inaccurate due to database deduplication (code is stored by its hash). However,
248-
// this deviation is negligible and acceptable for measurement purposes.
246+
codeExists := make(map[common.Hash]struct{})
249247
for _, code := range update.codes {
248+
if _, ok := codeExists[code.hash]; ok || code.exists {
249+
continue
250+
}
250251
stats.ContractCodes += 1
251252
stats.ContractCodeBytes += codeKeySize + int64(len(code.blob))
253+
codeExists[code.hash] = struct{}{}
252254
}
253255
return stats, nil
254256
}

core/state/state_sizer_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func TestSizeTracker(t *testing.T) {
5858
state.AddBalance(addr3, uint256.NewInt(3000), tracing.BalanceChangeUnspecified)
5959
state.SetNonce(addr3, 3, tracing.NonceChangeUnspecified)
6060

61-
currentRoot, _, err := state.CommitWithUpdate(1, true, false)
61+
currentRoot, err := state.Commit(1, true, false)
6262
if err != nil {
6363
t.Fatalf("Failed to commit initial state: %v", err)
6464
}
@@ -83,7 +83,7 @@ func TestSizeTracker(t *testing.T) {
8383
if i%3 == 0 {
8484
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
8585
}
86-
root, _, err := newState.CommitWithUpdate(blockNum, true, false)
86+
root, err := newState.Commit(blockNum, true, false)
8787
if err != nil {
8888
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
8989
}
@@ -154,21 +154,22 @@ func TestSizeTracker(t *testing.T) {
154154
if i%3 == 0 {
155155
newState.SetCode(testAddr, []byte{byte(i), 0x60, 0x80, byte(i + 1), 0x52}, tracing.CodeChangeUnspecified)
156156
}
157-
root, update, err := newState.CommitWithUpdate(blockNum, true, false)
157+
ret, err := newState.commitAndFlush(blockNum, true, false, true)
158158
if err != nil {
159159
t.Fatalf("Failed to commit state at block %d: %v", blockNum, err)
160160
}
161-
if err := tdb.Commit(root, false); err != nil {
161+
tracker.Notify(ret)
162+
163+
if err := tdb.Commit(ret.root, false); err != nil {
162164
t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
163165
}
164166

165-
diff, err := calSizeStats(update)
167+
diff, err := calSizeStats(ret)
166168
if err != nil {
167169
t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
168170
}
169171
trackedUpdates = append(trackedUpdates, diff)
170-
tracker.Notify(update)
171-
currentRoot = root
172+
currentRoot = ret.root
172173
}
173174
finalRoot := rawdb.ReadSnapshotRoot(db)
174175

core/state/statedb.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,11 +1317,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
13171317

13181318
// commitAndFlush is a wrapper of commit which also commits the state mutations
13191319
// to the configured data stores.
1320-
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (*stateUpdate, error) {
1320+
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, dedupCode bool) (*stateUpdate, error) {
13211321
ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
13221322
if err != nil {
13231323
return nil, err
13241324
}
1325+
1326+
if dedupCode {
1327+
ret.markCodeExistence(s.reader)
1328+
}
1329+
13251330
// Commit dirty contract code if any exists
13261331
if db := s.db.TrieDB().Disk(); db != nil && len(ret.codes) > 0 {
13271332
batch := db.NewBatch()
@@ -1376,21 +1381,21 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag
13761381
// no empty accounts left that could be deleted by EIP-158, storage wiping
13771382
// should not occur.
13781383
func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, error) {
1379-
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping)
1384+
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, false)
13801385
if err != nil {
13811386
return common.Hash{}, err
13821387
}
13831388
return ret.root, nil
13841389
}
13851390

1386-
// CommitWithUpdate writes the state mutations and returns both the root hash and the state update.
1387-
// This is useful for tracking state changes at the blockchain level.
1388-
func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *stateUpdate, error) {
1389-
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping)
1391+
// CommitAndTrack writes the state mutations and notifies the size tracker of the state changes.
1392+
func (s *StateDB) CommitAndTrack(block uint64, deleteEmptyObjects bool, noStorageWiping bool, sizer *SizeTracker) (common.Hash, error) {
1393+
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true)
13901394
if err != nil {
1391-
return common.Hash{}, nil, err
1395+
return common.Hash{}, err
13921396
}
1393-
return ret.root, ret, nil
1397+
sizer.Notify(ret)
1398+
return ret.root, nil
13941399
}
13951400

13961401
// Prepare handles the preparatory steps for executing a state transition with.

core/state/statedb_fuzz_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ func (test *stateTest) run() bool {
228228
} else {
229229
state.IntermediateRoot(true) // call intermediateRoot at the transaction boundary
230230
}
231-
ret, err := state.commitAndFlush(0, true, false) // call commit at the block boundary
231+
ret, err := state.commitAndFlush(0, true, false, false) // call commit at the block boundary
232232
if err != nil {
233233
panic(err)
234234
}

core/state/stateupdate.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import (
2626

2727
// contractCode represents a contract code with associated metadata.
2828
type contractCode struct {
29-
hash common.Hash // hash is the cryptographic hash of the contract code.
30-
blob []byte // blob is the binary representation of the contract code.
29+
hash common.Hash // hash is the cryptographic hash of the contract code.
30+
blob []byte // blob is the binary representation of the contract code.
31+
exists bool // flag whether the code has been existent
3132
}
3233

3334
// accountDelete represents an operation for deleting an Ethereum account.
@@ -190,3 +191,22 @@ func (sc *stateUpdate) stateSet() *triedb.StateSet {
190191
RawStorageKey: sc.rawStorageKey,
191192
}
192193
}
194+
195+
// markCodeExistence determines whether each piece of contract code referenced
196+
// in this state update actually exists.
197+
//
198+
// Note: This operation is expensive and not needed during normal state transitions.
199+
// It is only required when SizeTracker is enabled to produce accurate state
200+
// statistics.
201+
func (sc *stateUpdate) markCodeExistence(reader ContractCodeReader) {
202+
cache := make(map[common.Hash]bool)
203+
for addr, code := range sc.codes {
204+
if exists, ok := cache[code.hash]; ok {
205+
code.exists = exists
206+
continue
207+
}
208+
res := reader.Has(addr, code.hash)
209+
cache[code.hash] = res
210+
code.exists = res
211+
}
212+
}

0 commit comments

Comments
 (0)