Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 59 additions & 13 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ type stateObject struct {
// object was previously existent and is being deployed as a contract within
// the current transaction.
newContract bool

// storageTrieSizeBytes holds an approximate storage trie "size" computed as
// the sum of RLP-encoded standalone trie node blobs (NodeBlob sizes) for the
// current storage trie root.
storageTrieSizeBytes uint64
storageTrieSizeBytesInit bool
}

// empty returns whether the account is considered empty.
Expand Down Expand Up @@ -114,6 +120,7 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s

func (s *stateObject) markSelfdestructed() {
s.selfDestructed = true
s.storageTrieSizeBytesInit = false
}

func (s *stateObject) touch() {
Expand Down Expand Up @@ -155,6 +162,42 @@ func (s *stateObject) getPrefetchedTrie() Trie {
return s.db.prefetcher.trie(s.addrHash, s.data.Root)
}

// storageTrieSize walks the entire storage trie and returns an approximate size
// metric as the sum of RLP-encoded standalone trie node blobs (NodeBlob sizes).
// The result is cached per state object within the current execution scope to
// avoid redundant traversals. If the trie can't be loaded or iterated, the
// method returns false to signal that the size is unavailable.
func (s *stateObject) storageTrieSize() (uint64, bool) {
if s.storageTrieSizeBytesInit {
return s.storageTrieSizeBytes, true
}
tr, err := s.getTrie()
if err != nil {
log.Error("Failed to open storage trie", "address", s.address, "err", err)
return 0, false
}
it, err := tr.NodeIterator(nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be extremely expensive. The largest contract on mainnet is ~92G.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for comment, its just PoC exploration. We will most likely have background task or we will sync from genesis and calculating state size growth only via SetState.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With utility I made just for this, I managed to iterate the whole db for like 48 hours.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious how would it be possible to calculate the trie state diff on the fly per SSTORE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly state diff but for example if you sync from genesis you can use https://github.com/0xPolygon/bor/blob/master/core/state/state_object.go#L222 to track the state growth per account in which case heavy function like storageTrieSize wont be needed at all. Ofcourse storageTrieSize accounts for some metadata generated on disk, which we can account for with some formula too.

if err != nil {
log.Error("Failed to create storage trie iterator", "address", s.address, "err", err)
return 0, false
}
var size uint64
for it.Next(true) {
blob := it.NodeBlob()
if blob == nil {
continue
}
size += uint64(len(blob))
}
if err := it.Error(); err != nil {
log.Error("Failed to iterate storage trie", "address", s.address, "err", err)
return 0, false
}
s.storageTrieSizeBytes = size
s.storageTrieSizeBytesInit = true
return size, true
}

// GetState retrieves a value associated with the given storage key.
func (s *stateObject) GetState(key common.Hash) common.Hash {
value, _ := s.getState(key)
Expand Down Expand Up @@ -235,6 +278,7 @@ func (s *stateObject) SetState(key, value common.Hash) common.Hash {
// setState updates a value in account dirty storage. The dirtiness will be
// removed if the value being set equals to the original value.
func (s *stateObject) setState(key common.Hash, value common.Hash, origin common.Hash) {
s.storageTrieSizeBytesInit = false
// Storage slot is set back to its original value, undo the dirty marker
if value == origin {
delete(s.dirtyStorage, key)
Expand Down Expand Up @@ -491,19 +535,21 @@ func (s *stateObject) setBalance(amount *uint256.Int) {

func (s *stateObject) deepCopy(db *StateDB) *stateObject {
obj := &stateObject{
db: db,
address: s.address,
addrHash: s.addrHash,
origin: s.origin,
data: s.data,
code: s.code,
originStorage: s.originStorage.Copy(),
pendingStorage: s.pendingStorage.Copy(),
dirtyStorage: s.dirtyStorage.Copy(),
uncommittedStorage: s.uncommittedStorage.Copy(),
dirtyCode: s.dirtyCode,
selfDestructed: s.selfDestructed,
newContract: s.newContract,
db: db,
address: s.address,
addrHash: s.addrHash,
origin: s.origin,
data: s.data,
code: s.code,
originStorage: s.originStorage.Copy(),
pendingStorage: s.pendingStorage.Copy(),
dirtyStorage: s.dirtyStorage.Copy(),
uncommittedStorage: s.uncommittedStorage.Copy(),
dirtyCode: s.dirtyCode,
selfDestructed: s.selfDestructed,
newContract: s.newContract,
storageTrieSizeBytes: s.storageTrieSizeBytes,
storageTrieSizeBytesInit: s.storageTrieSizeBytesInit,
}
if s.trie != nil {
obj.trie = mustCopyTrie(s.trie)
Expand Down
12 changes: 12 additions & 0 deletions core/state/statedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,18 @@ func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) commo
})
}

// StorageTrieSize walks the storage trie of the provided account and returns an
// approximate size metric based on the sum of standalone trie node blobs
// (NodeBlob sizes). If the trie cannot be accessed (e.g. missing nodes), the
// size is reported as unavailable (false).
func (s *StateDB) StorageTrieSize(addr common.Address) (uint64, bool) {
stateObject := s.getStateObject(addr)
if stateObject == nil {
return 0, false
}
return stateObject.storageTrieSize()
}

// Database retrieves the low level database supporting the lower level trie ops.
func (s *StateDB) Database() Database {
return s.db
Expand Down
4 changes: 4 additions & 0 deletions core/state/statedb_hooked.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func (s *hookedStateDB) GetStorageRoot(addr common.Address) common.Hash {
return s.inner.GetStorageRoot(addr)
}

func (s *hookedStateDB) StorageTrieSize(addr common.Address) (uint64, bool) {
return s.inner.StorageTrieSize(addr)
}

func (s *hookedStateDB) GetTransientState(addr common.Address, key common.Hash) common.Hash {
return s.inner.GetTransientState(addr, key)
}
Expand Down
2 changes: 0 additions & 2 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,6 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas, evm.Config.Tracer)
} else {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests)
contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr))

Expand Down
26 changes: 15 additions & 11 deletions core/vm/gas_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
y, x = stack.Back(1), stack.Back(0)
current = evm.StateDB.GetState(contract.Address(), x.Bytes32())
)
// Optional extra gas based on storage trie size (disabled by default).
extra := chargeStorageTrieGas(evm, contract.Address())
// The legacy gas metering only takes into consideration the current state
// Legacy rules should be applied if we are in Petersburg (removal of EIP-1283)
// OR Constantinople is not active
Expand All @@ -115,12 +117,12 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
// 3. From a non-zero to a non-zero (CHANGE)
switch {
case current == (common.Hash{}) && y.Sign() != 0: // 0 => non 0
return params.SstoreSetGas, nil
return params.SstoreSetGas + extra, nil
case current != (common.Hash{}) && y.Sign() == 0: // non 0 => 0
evm.StateDB.AddRefund(params.SstoreRefundGas)
return params.SstoreClearGas, nil
return params.SstoreClearGas + extra, nil
default: // non 0 => non 0 (or 0 => 0)
return params.SstoreResetGas, nil
return params.SstoreResetGas + extra, nil
}
}

Expand All @@ -140,20 +142,20 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
// (2.2.2.2.) Otherwise, add 4800 gas to refund counter.
value := common.Hash(y.Bytes32())
if current == value { // noop (1)
return params.NetSstoreNoopGas, nil
return params.NetSstoreNoopGas + extra, nil
}

original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32())
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return params.NetSstoreInitGas, nil
return params.NetSstoreInitGas + extra, nil
}

if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(params.NetSstoreClearRefund)
}

return params.NetSstoreCleanGas, nil // write existing slot (2.1.2)
return params.NetSstoreCleanGas + extra, nil // write existing slot (2.1.2)
}

if original != (common.Hash{}) {
Expand All @@ -172,7 +174,7 @@ func gasSStore(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySi
}
}

return params.NetSstoreDirtyGas, nil
return params.NetSstoreDirtyGas + extra, nil
}

// Here come the EIP2200 rules:
Expand Down Expand Up @@ -200,24 +202,26 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
y, x = stack.Back(1), stack.Back(0)
current = evm.StateDB.GetState(contract.Address(), x.Bytes32())
)
// Optional extra gas based on storage trie size (disabled by default).
extra := chargeStorageTrieGas(evm, contract.Address())

value := common.Hash(y.Bytes32())

if current == value { // noop (1)
return params.SloadGasEIP2200, nil
return params.SloadGasEIP2200 + extra, nil
}

original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32())
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return params.SstoreSetGasEIP2200, nil
return params.SstoreSetGasEIP2200 + extra, nil
}

if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP2200)
}

return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
return params.SstoreResetGasEIP2200 + extra, nil // write existing slot (2.1.2)
}

if original != (common.Hash{}) {
Expand All @@ -236,7 +240,7 @@ func gasSStoreEIP2200(evm *EVM, contract *Contract, stack *Stack, mem *Memory, m
}
}

return params.SloadGasEIP2200, nil // dirty update (2.2)
return params.SloadGasEIP2200 + extra, nil // dirty update (2.2)
}

func makeGasLog(n uint64) gasFunc {
Expand Down
5 changes: 5 additions & 0 deletions core/vm/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ type StateDB interface {
GetState(common.Address, common.Hash) common.Hash
SetState(common.Address, common.Hash, common.Hash) common.Hash
GetStorageRoot(addr common.Address) common.Hash
// StorageTrieSize returns an implementation-defined metric for the storage trie
// size of an account. When available, it should be based on stored trie node
// blobs (sum of NodeBlob sizes) to reflect database footprint rather than
// leaf payload size.
StorageTrieSize(addr common.Address) (uint64, bool)

GetTransientState(addr common.Address, key common.Hash) common.Hash
SetTransientState(addr common.Address, key, value common.Hash)
Expand Down
52 changes: 46 additions & 6 deletions core/vm/operations_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package vm

import (
"errors"
"math/bits"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
Expand All @@ -26,12 +27,49 @@ import (
"github.com/ethereum/go-ethereum/params"
)

// Storage-trie gas charging parameters.
//
// These are consensus-affecting; keep disabled (0) unless activated by fork.
// The metric used is StateDB.StorageTrieSize which is NodeBlob-based bytes.
const (
// storageTrieLogStepGas is the gas charged per logarithmic step.
// 0 disables trie-size charging.
storageTrieLogStepGas uint64 = 1

// storageTrieFreeBytes is the size (in NodeBlob-bytes) below which no extra
// gas is charged.
storageTrieFreeBytes uint64 = 256 * 1024
)

// chargeStorageTrieGas returns additional gas to charge based on the storage
// trie's NodeBlob-byte size.
//
// The caller is expected to add the returned value to the opcode gas.
func chargeStorageTrieGas(evm *EVM, storageOwner common.Address) uint64 {
if storageTrieLogStepGas == 0 {
return 0
}
size, ok := evm.StateDB.StorageTrieSize(storageOwner)
if !ok || size <= storageTrieFreeBytes {
return 0
}
ratio := size / storageTrieFreeBytes
if ratio == 0 {
return 0
}
// floor(log2(ratio)) via bit length.
steps := uint64(bits.Len64(ratio) - 1)
return steps * storageTrieLogStepGas
}

func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
// If we fail the minimum gas availability invariant, fail (0)
if contract.Gas <= params.SstoreSentryGasEIP2200 {
return 0, errors.New("not enough gas for reentrancy sentry")
}
// Optional extra gas based on storage trie size (disabled by default).
extra := chargeStorageTrieGas(evm, contract.Address())
// Gas sentry honoured, do the actual gas calculation based on the stored value
var (
y, x = stack.Back(1), stack.peek()
Expand All @@ -51,21 +89,21 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
if current == value { // noop (1)
// EIP 2200 original clause:
// return params.SloadGasEIP2200, nil
return cost + params.WarmStorageReadCostEIP2929, nil // SLOAD_GAS
return cost + params.WarmStorageReadCostEIP2929 + extra, nil // SLOAD_GAS
}

original := evm.StateDB.GetCommittedState(contract.Address(), x.Bytes32())
if original == current {
if original == (common.Hash{}) { // create slot (2.1.1)
return cost + params.SstoreSetGasEIP2200, nil
return cost + params.SstoreSetGasEIP2200 + extra, nil
}

if value == (common.Hash{}) { // delete slot (2.1.2b)
evm.StateDB.AddRefund(clearingRefund)
}
// EIP-2200 original clause:
// return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929), nil // write existing slot (2.1.2)
return cost + (params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) + extra, nil // write existing slot (2.1.2)
}

if original != (common.Hash{}) {
Expand All @@ -92,7 +130,7 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
}
// EIP-2200 original clause:
//return params.SloadGasEIP2200, nil // dirty update (2.2)
return cost + params.WarmStorageReadCostEIP2929, nil // dirty update (2.2)
return cost + params.WarmStorageReadCostEIP2929 + extra, nil // dirty update (2.2)
}
}

Expand All @@ -104,15 +142,17 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
loc := stack.peek()
slot := common.Hash(loc.Bytes32())
// Optional extra gas based on storage trie size (disabled by default).
extra := chargeStorageTrieGas(evm, contract.Address())
// Check slot presence in the access list
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
// If the caller cannot afford the cost, this change will be rolled back
// If he does afford it, we can skip checking the same thing later on, during execution
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
return params.ColdSloadCostEIP2929, nil
return params.ColdSloadCostEIP2929 + extra, nil
}

return params.WarmStorageReadCostEIP2929, nil
return params.WarmStorageReadCostEIP2929 + extra, nil
}

// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
Expand Down
Loading