Skip to content

Commit baf5433

Browse files
committed
Optimize snapshot traversal for EVM transactions
1 parent fe062d1 commit baf5433

File tree

3 files changed

+78
-58
lines changed

3 files changed

+78
-58
lines changed

fvm/evm/emulator/state/delta.go

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ var _ types.HotView = &DeltaView{}
6767

6868
// NewDeltaView constructs a new delta view
6969
func NewDeltaView(parent types.ReadOnlyView) *DeltaView {
70+
// The root node will be of type `BaseView`, so we should
71+
// check if we are dealing with `DeltaView`, for parent
72+
// skipping
73+
dt, ok := parent.(*DeltaView)
74+
if ok && !dt.hasData() {
75+
// If my parent doesn't have any writes, I can just
76+
// delegate "upstream" queries to its parent
77+
parent = dt.parent
78+
}
79+
7080
return &DeltaView{
7181
parent: parent,
7282

@@ -478,21 +488,14 @@ func (d *DeltaView) SubRefund(amount uint64) error {
478488

479489
// AddressInAccessList checks if the address is in the access list of
480490
// the current view.
481-
// NOTE: Due to resource constraints (such as CPU & memory), and the
482-
// high-frequency usage of this function from EVM, we do not look up
483-
// the parents until the root view or until we find a view that has
484-
// the address in its local access list.
485-
// As an optimization, the `StateDB.AddressInAccessList` is responsible
486-
// for optimally traversing the views, to check if the address is in
487-
// the access list.
488491
func (d *DeltaView) AddressInAccessList(addr gethCommon.Address) bool {
489492
if d.accessListAddresses != nil {
490493
_, addressFound := d.accessListAddresses[addr]
491494
if addressFound {
492495
return true
493496
}
494497
}
495-
return false
498+
return d.parent.AddressInAccessList(addr)
496499
}
497500

498501
// AddAddressToAccessList adds an address to the access list
@@ -508,13 +511,6 @@ func (d *DeltaView) AddAddressToAccessList(addr gethCommon.Address) bool {
508511

509512
// SlotInAccessList checks if the slot is in the access list of the
510513
// current view.
511-
// NOTE: Due to resource constraints (such as CPU & memory), and the
512-
// high-frequency usage of this function from EVM, we do not look up
513-
// the parents until the root view or until we find a view that has
514-
// the slot in its local access list.
515-
// As an optimization, the `StateDB.SlotInAccessList` is responsible
516-
// for optimally traversing the views, to check if the slot is in
517-
// the access list.
518514
func (d *DeltaView) SlotInAccessList(sk types.SlotAddress) (addressOk bool, slotOk bool) {
519515
addressFound := d.AddressInAccessList(sk.Address)
520516
if d.accessListSlots != nil {
@@ -523,7 +519,8 @@ func (d *DeltaView) SlotInAccessList(sk types.SlotAddress) (addressOk bool, slot
523519
return addressFound, true
524520
}
525521
}
526-
return addressFound, false
522+
_, slotFound := d.parent.SlotInAccessList(sk)
523+
return addressFound, slotFound
527524
}
528525

529526
// AddSlotToAccessList adds a slot to the access list
@@ -584,3 +581,64 @@ func (d *DeltaView) DirtySlots() map[types.SlotAddress]struct{} {
584581
}
585582
return dirtySlots
586583
}
584+
585+
// hasData returns whether any state modifications are recorded in this view
586+
func (d *DeltaView) hasData() bool {
587+
if len(d.dirtyAddresses) > 0 {
588+
return true
589+
}
590+
591+
if len(d.created) > 0 {
592+
return true
593+
}
594+
595+
if len(d.newContract) > 0 {
596+
return true
597+
}
598+
599+
if len(d.toBeDestructed) > 0 {
600+
return true
601+
}
602+
603+
if len(d.recreated) > 0 {
604+
return true
605+
}
606+
607+
if len(d.balances) > 0 {
608+
return true
609+
}
610+
611+
if len(d.nonces) > 0 {
612+
return true
613+
}
614+
615+
if len(d.codes) > 0 {
616+
return true
617+
}
618+
619+
if len(d.codeHashes) > 0 {
620+
return true
621+
}
622+
623+
if len(d.slots) > 0 {
624+
return true
625+
}
626+
627+
if len(d.transient) > 0 {
628+
return true
629+
}
630+
631+
if len(d.accessListAddresses) > 0 {
632+
return true
633+
}
634+
635+
if len(d.accessListSlots) > 0 {
636+
return true
637+
}
638+
639+
if d.refund > 0 {
640+
return true
641+
}
642+
643+
return false
644+
}

fvm/evm/emulator/state/delta_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ func TestDeltaView(t *testing.T) {
496496
})
497497

498498
// check address through parent
499-
require.False(t, view.AddressInAccessList(addr1))
499+
require.True(t, view.AddressInAccessList(addr1))
500500

501501
// add addr 2 to the list
502502
require.False(t, view.AddressInAccessList(addr2))
@@ -511,7 +511,7 @@ func TestDeltaView(t *testing.T) {
511511
// check slot through parent
512512
addrFound, slotFound := view.SlotInAccessList(slot1)
513513
require.False(t, addrFound)
514-
require.False(t, slotFound)
514+
require.True(t, slotFound)
515515

516516
// add slot 2 to the list
517517
addrFound, slotFound = view.SlotInAccessList(slot2)

fvm/evm/emulator/state/stateDB.go

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -324,50 +324,12 @@ func (db *StateDB) SetTransientState(addr gethCommon.Address, key, value gethCom
324324

325325
// AddressInAccessList checks if an address is in the access list
326326
func (db *StateDB) AddressInAccessList(addr gethCommon.Address) bool {
327-
// For each static call / call / delegate call, the EVM will create
328-
// a snapshot, so that it can revert to it in case of execution errors,
329-
// such as out of gas etc, using `Snapshot` & `RevertToSnapshot`.
330-
// This can create a long list of views, in the order of 4K for certain
331-
// large transactions. To avoid performance issues with DeltaView checking parents,
332-
// which causes deep stacks and function call overhead, we use a plain for-loop instead.
333-
// We iterate through the views in ascending order (from lowest to highest) as an optimization.
334-
// Since addresses are typically added to the AccessList early during transaction execution,
335-
// this allows us to return early when the needed addresses are found in the initial views.
336-
end := len(db.views)
337-
for i := range end {
338-
view := db.views[i]
339-
if view.AddressInAccessList(addr) {
340-
return true
341-
}
342-
}
343-
344-
return false
327+
return db.latestView().AddressInAccessList(addr)
345328
}
346329

347330
// SlotInAccessList checks if the given (address,slot) is in the access list
348331
func (db *StateDB) SlotInAccessList(addr gethCommon.Address, key gethCommon.Hash) (addressOk bool, slotOk bool) {
349-
slotKey := types.SlotAddress{Address: addr, Key: key}
350-
351-
// For each static call / call / delegate call, the EVM will create
352-
// a snapshot, so that it can revert to it in case of execution errors,
353-
// such as out of gas etc, using `Snapshot` & `RevertToSnapshot`.
354-
// This can create a long list of views, in the order of 4K for certain
355-
// large transactions. To avoid performance issues with DeltaView checking parents,
356-
// which causes deep stacks and function call overhead, we use a plain for-loop instead.
357-
// We iterate through the views in ascending order (from lowest to highest) as an optimization.
358-
// Since slots are typically added to the AccessList early during transaction execution,
359-
// this allows us to return early when the needed slots are found in the initial views.
360-
addressFound := false
361-
end := len(db.views)
362-
for i := range end {
363-
view := db.views[i]
364-
addressFound, slotFound := view.SlotInAccessList(slotKey)
365-
if slotFound {
366-
return addressFound, true
367-
}
368-
}
369-
370-
return addressFound, false
332+
return db.latestView().SlotInAccessList(types.SlotAddress{Address: addr, Key: key})
371333
}
372334

373335
// AddAddressToAccessList adds the given address to the access list.

0 commit comments

Comments
 (0)