diff --git a/execution_chain/beacon/api_handler.nim b/execution_chain/beacon/api_handler.nim index 5a45378b40..0c4f64bc7b 100644 --- a/execution_chain/beacon/api_handler.nim +++ b/execution_chain/beacon/api_handler.nim @@ -25,6 +25,7 @@ export getPayloadV3, getPayloadV4, getPayloadV5, + getPayloadV6, getPayloadBodiesByHash, getPayloadBodiesByRange, newPayload, diff --git a/execution_chain/beacon/api_handler/api_getpayload.nim b/execution_chain/beacon/api_handler/api_getpayload.nim index 976d52fb2f..3eecbf3449 100644 --- a/execution_chain/beacon/api_handler/api_getpayload.nim +++ b/execution_chain/beacon/api_handler/api_getpayload.nim @@ -26,7 +26,7 @@ proc getPayload*(ben: BeaconEngineRef, let bundle = ben.getPayloadBundle(id).valueOr: raise unknownPayload("Unknown bundle") - let + let version = bundle.payload.version com = ben.com @@ -112,7 +112,7 @@ proc getPayloadV5*(ben: BeaconEngineRef, id: Bytes8): GetPayloadV5Response = let version = bundle.payload.version if version != Version.V3: - raise unsupportedFork("getPayloadV5 expect payloadV3 but get payload" & $version) + raise unsupportedFork("getPayloadV5 expect ExecutionPayloadV3 but got ExecutionPayload" & $version) if bundle.blobsBundle.isNil: raise unsupportedFork("getPayloadV5 is missing BlobsBundleV2") if bundle.executionRequests.isNone: @@ -122,10 +122,40 @@ proc getPayloadV5*(ben: BeaconEngineRef, id: Bytes8): GetPayloadV5Response = if not com.isOsakaOrLater(ethTime bundle.payload.timestamp): raise unsupportedFork("bundle timestamp is less than Osaka activation") + if com.isAmsterdamOrLater(ethTime bundle.payload.timestamp): + raise unsupportedFork("bundle timestamp greater than Amsterdam must use getPayloadV6") + GetPayloadV5Response( executionPayload: bundle.payload.V3, blockValue: bundle.blockValue, blobsBundle: bundle.blobsBundle.V2, shouldOverrideBuilder: false, executionRequests: bundle.executionRequests.get, - ) \ No newline at end of file + ) + +proc getPayloadV6*(ben: BeaconEngineRef, id: Bytes8): GetPayloadV6Response = + trace "Engine API request received", + meth = "GetPayload", id + + let bundle = ben.getPayloadBundle(id).valueOr: + raise unknownPayload("Unknown bundle") + + let version = bundle.payload.version + if version != Version.V4: + raise unsupportedFork("getPayloadV6 expect ExecutionPayloadV4 but got ExecutionPayload" & $version) + if bundle.blobsBundle.isNil: + raise unsupportedFork("getPayloadV6 is missing BlobsBundleV2") + if bundle.executionRequests.isNone: + raise unsupportedFork("getPayloadV6 is missing executionRequests") + + let com = ben.com + if not com.isAmsterdamOrLater(ethTime bundle.payload.timestamp): + raise unsupportedFork("bundle timestamp is less than Amsterdam activation") + + GetPayloadV6Response( + executionPayload: bundle.payload.V4, + blockValue: bundle.blockValue, + blobsBundle: bundle.blobsBundle.V2, + shouldOverrideBuilder: false, + executionRequests: bundle.executionRequests.get, + ) diff --git a/execution_chain/beacon/api_handler/api_newpayload.nim b/execution_chain/beacon/api_handler/api_newpayload.nim index 6668ca6d49..89ee9f4a76 100644 --- a/execution_chain/beacon/api_handler/api_newpayload.nim +++ b/execution_chain/beacon/api_handler/api_newpayload.nim @@ -41,20 +41,41 @@ func validateVersionedHashed(payload: ExecutionPayload, true template validateVersion(com, timestamp, payloadVersion, apiVersion) = - if apiVersion == Version.V4: + if apiVersion == Version.V5: + if not com.isAmsterdamOrLater(timestamp): + raise unsupportedFork("newPayloadV5 expect payload timestamp fall within Amsterdam") + if payloadVersion != Version.V4: + raise invalidParams("newPayload" & $apiVersion & + " expect ExecutionPayloadV4" & + " but got ExecutionPayload" & $payloadVersion) + + elif apiVersion == Version.V4: if not com.isPragueOrLater(timestamp): raise unsupportedFork("newPayloadV4 expect payload timestamp fall within Prague") - - if com.isPragueOrLater(timestamp): if payloadVersion != Version.V3: - raise invalidParams("if timestamp is Prague or later, " & - "payload must be ExecutionPayloadV3, got ExecutionPayload" & $payloadVersion) + raise invalidParams("newPayload" & $apiVersion & + " expect ExecutionPayloadV3" & + " but got ExecutionPayload" & $payloadVersion) - if apiVersion == Version.V3: + elif apiVersion == Version.V3: if not com.isCancunOrLater(timestamp): raise unsupportedFork("newPayloadV3 expect payload timestamp fall within Cancun") + if payloadVersion != Version.V3: + raise invalidParams("newPayload" & $apiVersion & + " expect ExecutionPayloadV3" & + " but got ExecutionPayload" & $payloadVersion) - if com.isCancunOrLater(timestamp): + if com.isAmsterdamOrLater(timestamp): + if payloadVersion != Version.V4: + raise invalidParams("if timestamp is Amsterdam or later, " & + "payload must be ExecutionPayloadV4, got ExecutionPayload" & $payloadVersion) + + elif com.isPragueOrLater(timestamp): + if payloadVersion != Version.V3: + raise invalidParams("if timestamp is Prague or later, " & + "payload must be ExecutionPayloadV3, got ExecutionPayload" & $payloadVersion) + + elif com.isCancunOrLater(timestamp): if payloadVersion != Version.V3: raise invalidParams("if timestamp is Cancun or later, " & "payload must be ExecutionPayloadV3, got ExecutionPayload" & $payloadVersion) @@ -68,13 +89,6 @@ template validateVersion(com, timestamp, payloadVersion, apiVersion) = raise invalidParams("if timestamp is earlier than Shanghai, " & "payload must be ExecutionPayloadV1, got ExecutionPayload" & $payloadVersion) - if apiVersion == Version.V3 or apiVersion == Version.V4: - # both newPayloadV3 and newPayloadV4 expect ExecutionPayloadV3 - if payloadVersion != Version.V3: - raise invalidParams("newPayload" & $apiVersion & - " expect ExecutionPayload3" & - " but got ExecutionPayload" & $payloadVersion) - template validatePayload(apiVersion, payloadVersion, payload) = if payloadVersion >= Version.V2: if payload.withdrawals.isNone: @@ -89,6 +103,11 @@ template validatePayload(apiVersion, payloadVersion, payload) = raise invalidParams("newPayload" & $apiVersion & "excessBlobGas is expected from execution payload") + if apiVersion >= Version.V5 or payloadVersion >= Version.V4: + if payload.blockAccessList.isNone: + raise invalidParams("newPayload" & $apiVersion & + "blockAccessList is expected from execution payload") + # https://github.com/ethereum/execution-apis/blob/40088597b8b4f48c45184da002e27ffc3c37641f/src/engine/prague.md#request func validateExecutionRequest(blockHash: Hash32, requests: openArray[seq[byte]], apiVersion: Version): diff --git a/execution_chain/beacon/payload_conv.nim b/execution_chain/beacon/payload_conv.nim index 69fd47365d..38ad2f87ea 100644 --- a/execution_chain/beacon/payload_conv.nim +++ b/execution_chain/beacon/payload_conv.nim @@ -40,6 +40,12 @@ func wdRoot(x: Opt[seq[WithdrawalV1]]): Opt[Hash32] = func txRoot(list: openArray[Web3Tx]): Hash32 = orderedTrieRoot(list) +func balHash(bal: Opt[seq[byte]]): Opt[Hash32] = + if bal.isNone(): + Opt.none(Hash32) + else: + Opt.some(keccak256(bal.get)) + # ------------------------------------------------------------------------------ # Public functions # ------------------------------------------------------------------------------ @@ -63,6 +69,7 @@ func executionPayload*(blk: Block): ExecutionPayload = withdrawals : w3Withdrawals blk.withdrawals, blobGasUsed : w3Qty blk.header.blobGasUsed, excessBlobGas: w3Qty blk.header.excessBlobGas, + blockAccessList: w3BlockAccessList blk.blockAccessList ) func executionPayloadV1V2*(blk: Block): ExecutionPayloadV1OrV2 = @@ -110,23 +117,26 @@ func blockHeader*(p: ExecutionPayload, excessBlobGas : u64(p.excessBlobGas), parentBeaconBlockRoot: parentBeaconBlockRoot, requestsHash : requestsHash, + blockAccessListHash: balHash p.blockAccessList, ) func blockBody*(p: ExecutionPayload): - BlockBody {.gcsafe, raises:[RlpError].} = + BlockBody {.gcsafe, raises: [RlpError].} = BlockBody( uncles : @[], transactions: ethTxs p.transactions, withdrawals : ethWithdrawals p.withdrawals, + blockAccessList: ethBlockAccessList p.blockAccessList, ) func ethBlock*(p: ExecutionPayload, parentBeaconBlockRoot: Opt[Hash32], requestsHash: Opt[Hash32]): - Block {.gcsafe, raises:[RlpError].} = + Block {.gcsafe, raises: [RlpError].} = Block( header : blockHeader(p, parentBeaconBlockRoot, requestsHash), uncles : @[], transactions: ethTxs p.transactions, withdrawals : ethWithdrawals p.withdrawals, + blockAccessList: ethBlockAccessList p.blockAccessList, ) diff --git a/execution_chain/beacon/web3_eth_conv.nim b/execution_chain/beacon/web3_eth_conv.nim index f2bbd4576d..6c12dede61 100644 --- a/execution_chain/beacon/web3_eth_conv.nim +++ b/execution_chain/beacon/web3_eth_conv.nim @@ -84,15 +84,26 @@ func ethWithdrawals*(x: Opt[seq[WithdrawalV1]]): if x.isNone: Opt.none(seq[Withdrawal]) else: Opt.some(ethWithdrawals x.get) -func ethTx*(x: Web3Tx): common.Transaction {.gcsafe, raises:[RlpError].} = +func ethTx*(x: Web3Tx): common.Transaction {.gcsafe, raises: [RlpError].} = result = rlp.decode(distinctBase x, common.Transaction) func ethTxs*(list: openArray[Web3Tx]): - seq[common.Transaction] {.gcsafe, raises:[RlpError].} = + seq[common.Transaction] {.gcsafe, raises: [RlpError].} = result = newSeqOfCap[common.Transaction](list.len) for x in list: result.add ethTx(x) +func ethBlockAccessList*( + bal: openArray[byte]): BlockAccessList {.gcsafe, raises: [RlpError].} = + rlp.decode(bal, BlockAccessList) + +func ethBlockAccessList*( + bal: Opt[seq[byte]]): Opt[BlockAccessList] {.gcsafe, raises: [RlpError].} = + if bal.isNone(): + Opt.none(BlockAccessList) + else: + Opt.some(ethBlockAccessList(bal.get)) + # ------------------------------------------------------------------------------ # Eth types to Web3 types # ------------------------------------------------------------------------------ @@ -154,4 +165,13 @@ func w3Txs*(list: openArray[common.Transaction]): seq[Web3Tx] = for tx in list: result.add w3Tx(tx) +func w3BlockAccessList*(bal: BlockAccessList): seq[byte] = + bal.encode() + +func w3BlockAccessList*(bal: Opt[BlockAccessList]): Opt[seq[byte]] = + if bal.isNone(): + Opt.none(seq[byte]) + else: + Opt.some(w3BlockAccessList(bal.get)) + chronicles.formatIt(Quantity): $(distinctBase it) diff --git a/execution_chain/block_access_list/block_access_list_builder.nim b/execution_chain/block_access_list/block_access_list_builder.nim index c26989df8a..2b96d266d9 100644 --- a/execution_chain/block_access_list/block_access_list_builder.nim +++ b/execution_chain/block_access_list/block_access_list_builder.nim @@ -43,6 +43,8 @@ type accounts*: Table[Address, AccountData] ## Maps address -> account data + BlockAccessListRef* = ref BlockAccessList + template init*(T: type AccountData): T = AccountData() @@ -119,11 +121,11 @@ func slotCmp(x, y: StorageKey | StorageValue): int = func slotChangesCmp(x, y: SlotChanges): int = cmp(x.slot.data.toHex(), y.slot.data.toHex()) -func addressCmp(x, y: AccountChanges): int = +func accChangesCmp(x, y: AccountChanges): int = cmp(x.address.data.toHex(), y.address.data.toHex()) -func buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList = - var blockAccessList: BlockAccessList +func buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessListRef = + let blockAccessList: BlockAccessListRef = new BlockAccessList for address, accData in builder.accounts.mpairs(): # Collect and sort storageChanges @@ -163,7 +165,7 @@ func buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList codeChanges.add((BlockAccessIndex(balIndex), Bytecode(code))) codeChanges.sort(balIndexCmp) - blockAccessList.add(AccountChanges( + blockAccessList[].add(AccountChanges( address: address, storageChanges: storageChanges, storageReads: storageReads, @@ -172,6 +174,6 @@ func buildBlockAccessList*(builder: BlockAccessListBuilderRef): BlockAccessList codeChanges: codeChanges )) - blockAccessList.sort(addressCmp) + blockAccessList[].sort(accChangesCmp) blockAccessList diff --git a/execution_chain/block_access_list/block_access_list_tracker.nim b/execution_chain/block_access_list/block_access_list_tracker.nim index cd75994aec..a2e802634a 100644 --- a/execution_chain/block_access_list/block_access_list_tracker.nim +++ b/execution_chain/block_access_list/block_access_list_tracker.nim @@ -23,6 +23,10 @@ type # Used to track changes within a call frame to enable proper handling # of reverts as specified in EIP-7928. CallFrameSnapshot* = object + touchedAddresses*: HashSet[Address] + ## Addresses read during this call frame. + storageReads*: HashSet[(Address, UInt256)] + ## Storage reads made during this call frame. storageChanges*: Table[(Address, UInt256), UInt256] ## Storage writes made during this call frame. ## Maps (address, storage key) -> storage value. @@ -36,13 +40,17 @@ type codeChanges*: Table[Address, seq[byte]] ## Code changes made during this call frame. ## Maps address -> bytecode. + inTransactionSelfDestructs*: HashSet[Address] + ## Set of addresses which need to have writes removed (and in some cases + ## also converted to reads) when commiting a call frame. + # Tracks state changes during transaction execution for block access list # construction. This tracker maintains a cache of pre-state values and # coordinates with the BlockAccessListBuilder to record all state changes # made during block execution. It ensures that only actual changes (not no-op # writes) are recorded in the access list. - StateChangeTrackerRef* = ref object + BlockAccessListTrackerRef* = ref object ledger*: ReadOnlyLedger ## Used to fetch the pre-transaction values from the state. builder*: BlockAccessListBuilderRef @@ -56,13 +64,24 @@ type ## This cache is cleared at the start of each transaction and used by ## normalize_balance_changes to filter out balance changes where ## the final balance equals the initial balance. + preNonceCache*: Table[Address, AccountNonce] + ## Cache of pre-transaction nonce values, keyed by address. + ## This cache is cleared at the start of each transaction to track values + ## from the beginning of the current transaction. + preCodeCache*: Table[Address, seq[byte]] + ## Cache of pre-transaction code, keyed by address. + ## This cache is cleared at the start of each transaction to track values + ## from the beginning of the current transaction. currentBlockAccessIndex*: int ## The current block access index (0 for pre-execution, ## 1..n for transactions, n+1 for post-execution). callFrameSnapshots*: seq[CallFrameSnapshot] ## Stack of snapshots for nested call frames to handle reverts properly. + blockAccessList: Opt[BlockAccessListRef] + ## Created by the builder and cached for reuse. -proc init(T: type CallFrameSnapshot): T = + +template init(T: type CallFrameSnapshot): T = CallFrameSnapshot() # Disallow copying of CallFrameSnapshot @@ -70,12 +89,12 @@ proc `=copy`(dest: var CallFrameSnapshot; src: CallFrameSnapshot) {.error: "Copy discard proc init*( - T: type StateChangeTrackerRef, + T: type BlockAccessListTrackerRef, ledger: ReadOnlyLedger, builder = BlockAccessListBuilderRef.init()): T = - StateChangeTrackerRef(ledger: ledger, builder: builder) + BlockAccessListTrackerRef(ledger: ledger, builder: builder) -proc setBlockAccessIndex*(tracker: StateChangeTrackerRef, blockAccessIndex: int) = +proc setBlockAccessIndex*(tracker: BlockAccessListTrackerRef, blockAccessIndex: int) = ## Must be called before processing each transaction/system contract ## to ensure changes are associated with the correct block access index. ## Note: Block access indices differ from transaction indices: @@ -86,51 +105,96 @@ proc setBlockAccessIndex*(tracker: StateChangeTrackerRef, blockAccessIndex: int) tracker.preStorageCache.clear() tracker.preBalanceCache.clear() + tracker.preNonceCache.clear() + tracker.preCodeCache.clear() tracker.currentBlockAccessIndex = blockAccessIndex -template hasPendingCallFrame*(tracker: StateChangeTrackerRef): bool = +template hasPendingCallFrame*(tracker: BlockAccessListTrackerRef): bool = tracker.callFrameSnapshots.len() > 0 -template pendingCallFrame*(tracker: StateChangeTrackerRef): CallFrameSnapshot = +template hasParentCallFrame*(tracker: BlockAccessListTrackerRef): bool = + tracker.callFrameSnapshots.len() > 1 + +template pendingCallFrame*(tracker: BlockAccessListTrackerRef): CallFrameSnapshot = tracker.callFrameSnapshots[tracker.callFrameSnapshots.high] -proc beginCallFrame*(tracker: StateChangeTrackerRef) = +template parentCallFrame*(tracker: BlockAccessListTrackerRef): CallFrameSnapshot = + tracker.callFrameSnapshots[tracker.callFrameSnapshots.high - 1] + +template beginCallFrame*(tracker: BlockAccessListTrackerRef) = + ## Begin a new call frame for tracking reverts. ## Creates a new snapshot to track changes within this call frame. ## This allows proper handling of reverts as specified in EIP-7928. tracker.callFrameSnapshots.add(CallFrameSnapshot.init()) -template popCallFrame(tracker: StateChangeTrackerRef) = +template popCallFrame(tracker: BlockAccessListTrackerRef) = tracker.callFrameSnapshots.setLen(tracker.callFrameSnapshots.len() - 1) -proc normalizeBalanceAndStorageChanges*(tracker: StateChangeTrackerRef) +proc handleInTransactionSelfDestruct*(tracker: BlockAccessListTrackerRef, address: Address) +proc normalizePendingCallFrameChanges*(tracker: BlockAccessListTrackerRef) -proc commitCallFrame*(tracker: StateChangeTrackerRef) = +proc commitCallFrame*(tracker: BlockAccessListTrackerRef) = # Commit changes from the current call frame. # Removes the current call frame snapshot without rolling back changes. # Called when a call completes successfully. doAssert tracker.hasPendingCallFrame() - tracker.normalizeBalanceAndStorageChanges() + if tracker.hasParentCallFrame(): + # Merge the pending call frame writes into the parent + + for address in tracker.pendingCallFrame.inTransactionSelfDestructs: + tracker.handleInTransactionSelfDestruct(address) + tracker.parentCallFrame.inTransactionSelfDestructs.incl(address) - let currentIndex = tracker.currentBlockAccessIndex + for storageKey, newValue in tracker.pendingCallFrame.storageChanges: + tracker.parentCallFrame.storageChanges[storageKey] = newValue - for storageKey, newValue in tracker.pendingCallFrame.storageChanges: - let (address, slot) = storageKey - tracker.builder.addStorageWrite(address, slot, currentIndex, newValue) + for address, newBalance in tracker.pendingCallFrame.balanceChanges: + tracker.parentCallFrame.balanceChanges[address] = newBalance - for address, newBalance in tracker.pendingCallFrame.balanceChanges: - tracker.builder.addBalanceChange(address, currentIndex, newBalance) + for address, newNonce in tracker.pendingCallFrame.nonceChanges: + tracker.parentCallFrame.nonceChanges[address] = newNonce - for address, newNonce in tracker.pendingCallFrame.nonceChanges: - tracker.builder.addNonceChange(address, currentIndex, newNonce) + for address, newCode in tracker.pendingCallFrame.codeChanges: + tracker.parentCallFrame.codeChanges[address] = newCode - for address, newCode in tracker.pendingCallFrame.codeChanges: - tracker.builder.addCodeChange(address, currentIndex, newCode) + # Merge the pending call frame reads into the parent + tracker.parentCallFrame.touchedAddresses.incl(tracker.pendingCallFrame.touchedAddresses) + tracker.parentCallFrame.storageReads.incl(tracker.pendingCallFrame.storageReads) + + else: + # Merge the pending call frame writes into the builder + + for address in tracker.pendingCallFrame.inTransactionSelfDestructs: + tracker.handleInTransactionSelfDestruct(address) + + tracker.normalizePendingCallFrameChanges() + + let currentIndex = tracker.currentBlockAccessIndex + + for storageKey, newValue in tracker.pendingCallFrame.storageChanges: + let (address, slot) = storageKey + tracker.builder.addStorageWrite(address, slot, currentIndex, newValue) + + for address, newBalance in tracker.pendingCallFrame.balanceChanges: + tracker.builder.addBalanceChange(address, currentIndex, newBalance) + + for address, newNonce in tracker.pendingCallFrame.nonceChanges: + tracker.builder.addNonceChange(address, currentIndex, newNonce) + + for address, newCode in tracker.pendingCallFrame.codeChanges: + tracker.builder.addCodeChange(address, currentIndex, newCode) + + # Merge the pending call frame reads into the builder + for address in tracker.pendingCallFrame.touchedAddresses: + tracker.builder.addTouchedAccount(address) + for storageKey in tracker.pendingCallFrame.storageReads: + tracker.builder.addStorageRead(storageKey[0], storageKey[1]) tracker.popCallFrame() -proc rollbackCallFrame*(tracker: StateChangeTrackerRef) = +proc rollbackCallFrame*(tracker: BlockAccessListTrackerRef, rollbackReads = false) = ## Rollback changes from the current call frame. ## When a call reverts, this function: ## - Converts storage writes to reads @@ -139,16 +203,33 @@ proc rollbackCallFrame*(tracker: StateChangeTrackerRef) = ## become reads and addresses remain in the access list. doAssert tracker.hasPendingCallFrame() - # Convert storage writes to reads - for key in tracker.pendingCallFrame.storageChanges.keys(): - let (address, slot) = key - tracker.builder.addStorageRead(address, slot) + if rollbackReads: + tracker.popCallFrame() + return # discard all changes + - # All touched addresses remain in the access list (already tracked) + if tracker.hasParentCallFrame(): + # Merge the pending call frame reads into the parent + tracker.parentCallFrame.touchedAddresses.incl(tracker.pendingCallFrame.touchedAddresses) + tracker.parentCallFrame.storageReads.incl(tracker.pendingCallFrame.storageReads) + + # Convert storage writes to reads + for storageKey in tracker.pendingCallFrame.storageChanges.keys(): + tracker.parentCallFrame.storageReads.incl(storageKey) + else: + # Merge the pending call frame reads into the builder + for address in tracker.pendingCallFrame.touchedAddresses: + tracker.builder.addTouchedAccount(address) + for storageKey in tracker.pendingCallFrame.storageReads: + tracker.builder.addStorageRead(storageKey[0], storageKey[1]) + + # Convert storage writes to reads + for storageKey in tracker.pendingCallFrame.storageChanges.keys(): + tracker.builder.addStorageRead(storageKey[0], storageKey[1]) tracker.popCallFrame() -proc capturePreBalance*(tracker: StateChangeTrackerRef, address: Address) = +proc capturePreBalance*(tracker: BlockAccessListTrackerRef, address: Address) = ## Capture and cache the pre-transaction balance for an account. ## This function caches the balance on first access for each address during ## a transaction. It must be called before any balance modifications are made @@ -159,10 +240,24 @@ proc capturePreBalance*(tracker: StateChangeTrackerRef, address: Address) = if address notin tracker.preBalanceCache: tracker.preBalanceCache[address] = tracker.ledger.getBalance(address) -proc getPreBalance*(tracker: StateChangeTrackerRef, address: Address): UInt256 = - return tracker.preBalanceCache.getOrDefault(address) +template getPreBalance*(tracker: BlockAccessListTrackerRef, address: Address): UInt256 = + tracker.preBalanceCache.getOrDefault(address) + +proc capturePreNonce*(tracker: BlockAccessListTrackerRef, address: Address) = + if address notin tracker.preNonceCache: + tracker.preNonceCache[address] = tracker.ledger.getNonce(address) + +template getPreNonce*(tracker: BlockAccessListTrackerRef, address: Address): AccountNonce = + tracker.preNonceCache.getOrDefault(address) + +proc capturePreCode*(tracker: BlockAccessListTrackerRef, address: Address) = + if address notin tracker.preCodeCache: + tracker.preCodeCache[address] = tracker.ledger.getCode(address).bytes -proc capturePreStorage*(tracker: StateChangeTrackerRef, address: Address, slot: UInt256) = +template getPreCode*(tracker: BlockAccessListTrackerRef, address: Address): seq[byte] = + tracker.preCodeCache.getOrDefault(address) + +proc capturePreStorage*(tracker: BlockAccessListTrackerRef, address: Address, slot: UInt256) = ## Capture and cache the pre-transaction value for a storage location. ## Retrieves the storage value from the beginning of the current transaction. ## The value is cached within the transaction to avoid repeated lookups and @@ -173,24 +268,26 @@ proc capturePreStorage*(tracker: StateChangeTrackerRef, address: Address, slot: if storageKey notin tracker.preStorageCache: tracker.preStorageCache[storageKey] = tracker.ledger.getStorage(address, slot) -proc getPreStorage*(tracker: StateChangeTrackerRef, address: Address, slot: UInt256): UInt256 = - return tracker.preStorageCache.getOrDefault((address, slot)) +template getPreStorage*(tracker: BlockAccessListTrackerRef, address: Address, slot: UInt256): UInt256 = + tracker.preStorageCache.getOrDefault((address, slot)) -template trackAddressAccess*(tracker: StateChangeTrackerRef, address: Address) = +template trackAddressAccess*(tracker: BlockAccessListTrackerRef, address: Address) = ## Track that an address was accessed. ## Records account access even when no state changes occur. This is ## important for operations that read account data without modifying it. - tracker.builder.addTouchedAccount(address) + assert tracker.hasPendingCallFrame() + tracker.pendingCallFrame.touchedAddresses.incl(address) -proc trackStorageRead*(tracker: StateChangeTrackerRef, address: Address, slot: UInt256) = +proc trackStorageRead*(tracker: BlockAccessListTrackerRef, address: Address, slot: UInt256) = ## Track a storage read operation. ## Records that a storage slot was read and captures its pre-state value. ## The slot will only appear in the final access list if it wasn't also ## written to during block execution. - tracker.trackAddressAccess(address) - tracker.builder.addStorageRead(address, slot) + assert tracker.hasPendingCallFrame() + tracker.pendingCallFrame.touchedAddresses.incl(address) + tracker.pendingCallFrame.storageReads.incl((address, slot)) -proc trackStorageWrite*(tracker: StateChangeTrackerRef, address: Address, slot: UInt256, newValue: UInt256) = +proc trackStorageWrite*(tracker: BlockAccessListTrackerRef, address: Address, slot: UInt256, newValue: UInt256) = ## Track a storage write operation. ## Records storage modifications, but only if the new value differs from ## the pre-state value. No-op writes (where the value doesn't change) are @@ -206,7 +303,7 @@ proc trackStorageWrite*(tracker: StateChangeTrackerRef, address: Address, slot: tracker.capturePreStorage(address, slot) tracker.pendingCallFrame.storageChanges[storageKey] = newValue -proc trackBalanceChange*(tracker: StateChangeTrackerRef, address: Address, newBalance: UInt256) = +proc trackBalanceChange*(tracker: BlockAccessListTrackerRef, address: Address, newBalance: UInt256) = ## Track a balance change for an account. ## Records the new balance after any balance-affecting operation, including ## transfers, gas payments, block rewards, and withdrawals. @@ -220,7 +317,22 @@ proc trackBalanceChange*(tracker: StateChangeTrackerRef, address: Address, newBa tracker.capturePreBalance(address) tracker.pendingCallFrame.balanceChanges[address] = newBalance -proc trackNonceChange*(tracker: StateChangeTrackerRef, address: Address, newNonce: AccountNonce) = +proc trackAddBalanceChange*(tracker: BlockAccessListTrackerRef, address: Address, delta: UInt256) = + if delta.isZero: + tracker.trackAddressAccess(address) + return + + tracker.trackBalanceChange(address, tracker.ledger.getBalance(address) + delta) + +proc trackSubBalanceChange*(tracker: BlockAccessListTrackerRef, address: Address, delta: UInt256) = + if delta.isZero: + # In this case we don't call trackAddressAccess because the account isn't read + # due to early return as defined in EIP-4788 + return + + tracker.trackBalanceChange(address, tracker.ledger.getBalance(address) - delta) + +proc trackNonceChange*(tracker: BlockAccessListTrackerRef, address: Address, newNonce: AccountNonce) = ## Track a nonce change for an account. ## Records nonce increments for both EOAs (when sending transactions) and ## contracts (when performing [`CREATE`] or [`CREATE2`] operations). Deployed @@ -232,9 +344,13 @@ proc trackNonceChange*(tracker: StateChangeTrackerRef, address: Address, newNonc return # nothing to do because we have already tracked this value tracker.trackAddressAccess(address) + tracker.capturePreNonce(address) tracker.pendingCallFrame.nonceChanges[address] = newNonce -proc trackCodeChange*(tracker: StateChangeTrackerRef, address: Address, newCode: seq[byte]) = +template trackIncNonceChange*(tracker: BlockAccessListTrackerRef, address: Address) = + tracker.trackNonceChange(address, tracker.ledger.getNonce(address) + 1) + +proc trackCodeChange*(tracker: BlockAccessListTrackerRef, address: Address, newCode: seq[byte]) = ## Track a code change for contract deployment. ## Records new contract code deployments via [`CREATE`], [`CREATE2`], or ## [`SETCODE`] operations. This function is called when contract bytecode @@ -246,9 +362,17 @@ proc trackCodeChange*(tracker: StateChangeTrackerRef, address: Address, newCode: return # nothing to do because we have already tracked this value tracker.trackAddressAccess(address) + tracker.capturePreCode(address) tracker.pendingCallFrame.codeChanges[address] = newCode -proc handleInTransactionSelfDestruct*(tracker: StateChangeTrackerRef, address: Address) = +proc trackSelfDestruct*(tracker: BlockAccessListTrackerRef, address: Address) = + tracker.trackBalanceChange(address, 0.u256) + +proc trackInTransactionSelfDestruct*(tracker: BlockAccessListTrackerRef, address: Address) = + assert tracker.hasPendingCallFrame() + tracker.pendingCallFrame.inTransactionSelfDestructs.incl(address) + +proc handleInTransactionSelfDestruct*(tracker: BlockAccessListTrackerRef, address: Address) = ## Handle an account that self-destructed in the same transaction it was ## created. ## Per EIP-7928, accounts destroyed within their creation transaction must be @@ -264,15 +388,18 @@ proc handleInTransactionSelfDestruct*(tracker: StateChangeTrackerRef, address: A for slot in slotsToConvert: let storageKey = (address, slot) - tracker.builder.addStorageRead(address, slot) + tracker.pendingCallFrame.storageReads.incl(storageKey) tracker.pendingCallFrame.storageChanges.del(storageKey) tracker.pendingCallFrame.balanceChanges.del(address) tracker.pendingCallFrame.nonceChanges.del(address) tracker.pendingCallFrame.codeChanges.del(address) -proc normalizeBalanceAndStorageChanges*(tracker: StateChangeTrackerRef) = - ## Normalize balance and storage changes for the current block access index. + tracker.trackBalanceChange(address, 0.u256) + +proc normalizePendingCallFrameChanges*(tracker: BlockAccessListTrackerRef) = + ## Normalize balance, nonce, code and storage changes for the current + ## block access index. ## This method filters out spurious balance and storage changes by removing all ## changes for addresses and slots where the post-execution balance/value equals ## the pre-execution/value balance. @@ -296,15 +423,41 @@ proc normalizeBalanceAndStorageChanges*(tracker: StateChangeTrackerRef) = slotsToRemove.add(storageKey) for storageKey in slotsToRemove: - let (address, slot) = storageKey - tracker.builder.addStorageRead(address, slot) + tracker.pendingCallFrame.storageReads.incl(storageKey) tracker.pendingCallFrame.storageChanges.del(storageKey) - var addressesToRemove: seq[Address] - for address, postBalance in tracker.pendingCallFrame.balanceChanges: - let preBalance = tracker.getPreBalance(address) - if preBalance == postBalance: - addressesToRemove.add(address) - - for address in addressesToRemove: - tracker.pendingCallFrame.balanceChanges.del(address) + block: + var addressesToRemove: seq[Address] + for address, postBalance in tracker.pendingCallFrame.balanceChanges: + let preBalance = tracker.getPreBalance(address) + if preBalance == postBalance: + addressesToRemove.add(address) + + for address in addressesToRemove: + tracker.pendingCallFrame.balanceChanges.del(address) + + block: + var addressesToRemove: seq[Address] + for address, newNonce in tracker.pendingCallFrame.nonceChanges: + let preNonce = tracker.getPreNonce(address) + if preNonce == newNonce: + addressesToRemove.add(address) + + for address in addressesToRemove: + tracker.pendingCallFrame.nonceChanges.del(address) + + block: + var addressesToRemove: seq[Address] + for address, newCode in tracker.pendingCallFrame.codeChanges: + let preCode = tracker.getPreCode(address) + if preCode == newCode: + addressesToRemove.add(address) + + for address in addressesToRemove: + tracker.pendingCallFrame.codeChanges.del(address) + +proc getBlockAccessList*(tracker: BlockAccessListTrackerRef, rebuild = false): lent Opt[BlockAccessListRef] = + if rebuild or tracker.blockAccessList.isNone(): + tracker.blockAccessList = Opt.some(tracker.builder.buildBlockAccessList()) + + tracker.blockAccessList diff --git a/execution_chain/core/chain/forked_chain.nim b/execution_chain/core/chain/forked_chain.nim index 812e393f22..e8abe5d07d 100644 --- a/execution_chain/core/chain/forked_chain.nim +++ b/execution_chain/core/chain/forked_chain.nim @@ -515,8 +515,6 @@ proc validateBlock(c: ForkedChainRef, txFrame.dispose() return err(error) - c.writeBaggage(blk, blkHash, txFrame, receipts) - # Checkpoint creates a snapshot of ancestor changes in txFrame - it is an # expensive operation, specially when creating a new branch (ie when blk # is being applied to a block that is currently not a head). diff --git a/execution_chain/core/chain/forked_chain/chain_private.nim b/execution_chain/core/chain/forked_chain/chain_private.nim index f53f83c921..18eb238f6f 100644 --- a/execution_chain/core/chain/forked_chain/chain_private.nim +++ b/execution_chain/core/chain/forked_chain/chain_private.nim @@ -20,10 +20,14 @@ import ../../../stateless/[witness_generation, witness_verification, stateless_execution], ./chain_branch -proc writeBaggage*(c: ForkedChainRef, - blk: Block, blkHash: Hash32, - txFrame: CoreDbTxRef, - receipts: openArray[StoredReceipt]) = +proc writeBaggage*( + c: ForkedChainRef, + blk: Block, + blkHash: Hash32, + txFrame: CoreDbTxRef, + receipts: openArray[StoredReceipt], + blockAccessList: Opt[BlockAccessListRef], +) = template header(): Header = blk.header @@ -33,23 +37,33 @@ proc writeBaggage*(c: ForkedChainRef, if blk.withdrawals.isSome: txFrame.persistWithdrawals( header.withdrawalsRoot.expect("WithdrawalsRoot should be verified before"), - blk.withdrawals.get) - if blk.blockAccessList.isSome: + blk.withdrawals.get, + ) + if blockAccessList.isSome: txFrame.persistBlockAccessList( header.blockAccessListHash.expect("blockAccessListHash should be verified before"), - blk.blockAccessList.get) - -proc processBlock*(c: ForkedChainRef, - parentBlk: BlockRef, - txFrame: CoreDbTxRef, - blk: Block, - blkHash: Hash32, - finalized: bool): Result[seq[StoredReceipt], string] = + blockAccessList.get()[], + ) + +proc processBlock*( + c: ForkedChainRef, + parentBlk: BlockRef, + txFrame: CoreDbTxRef, + blk: Block, + blkHash: Hash32, + finalized: bool, +): Result[seq[StoredReceipt], string] = template header(): Header = blk.header let vmState = BaseVMState() - vmState.init(parentBlk.header, header, c.com, txFrame) + vmState.init( + parentBlk.header, + header, + c.com, + txFrame, + enableBalTracker = c.com.isAmsterdamOrLater(header.timestamp), + ) ?c.com.validateHeaderAndKinship(blk, vmState.parent, txFrame) @@ -93,4 +107,6 @@ proc processBlock*(c: ForkedChainRef, # because validateUncles still need it ?txFrame.persistHeader(blkHash, header, c.com.startOfHistory) + c.writeBaggage(blk, blkHash, txFrame, vmState.receipts, vmState.blockAccessList) + ok(move(vmState.receipts)) diff --git a/execution_chain/core/chain/forked_chain/chain_serialize.nim b/execution_chain/core/chain/forked_chain/chain_serialize.nim index 85a1600599..8ee68037b5 100644 --- a/execution_chain/core/chain/forked_chain/chain_serialize.nim +++ b/execution_chain/core/chain/forked_chain/chain_serialize.nim @@ -133,8 +133,6 @@ proc replayBlock(fc: ForkedChainRef; txFrame.dispose() return err(error) - fc.writeBaggage(blk.blk, blk.hash, txFrame, receipts) - # Checkpoint creates a snapshot of ancestor changes in txFrame - it is an # expensive operation, specially when creating a new branch (ie when blk # is being applied to a block that is currently not a head). diff --git a/execution_chain/core/executor/process_block.nim b/execution_chain/core/executor/process_block.nim index 133fe3d2ef..a928724773 100644 --- a/execution_chain/core/executor/process_block.nim +++ b/execution_chain/core/executor/process_block.nim @@ -87,6 +87,9 @@ proc processTransactions*( if sender == default(Address): return err("Could not get sender for tx with index " & $(txIndex)) + if vmState.balTrackerEnabled: + vmState.balTracker.setBlockAccessIndex(txIndex + 1) + let rc = vmState.processTransaction(tx, sender, header) if rc.isErr: return err("Error processing tx with index " & $(txIndex) & ":" & rc.error) @@ -110,6 +113,11 @@ proc procBlkPreamble( template header(): Header = blk.header + # Setup block access list tracker for pre‑execution system calls + if vmState.balTrackerEnabled: + vmState.balTracker.setBlockAccessIndex(0) + vmState.balTracker.beginCallFrame() + let com = vmState.com if com.daoForkSupport and com.daoForkBlock.get == header.number: vmState.mutateLedger: @@ -144,17 +152,19 @@ proc procBlkPreamble( if com.isAmsterdamOrLater(header.timestamp): if header.blockAccessListHash.isNone: return err("Post-Amsterdam block header must have blockAccessListHash") - elif blk.blockAccessList.isNone: - return err("Post-Amsterdam block body must have blockAccessList") - elif not skipValidation: + if not skipValidation and blk.blockAccessList.isSome: if blk.blockAccessList.get.validate(header.blockAccessListHash.get).isErr(): return err("Mismatched blockAccessListHash") else: if header.blockAccessListHash.isSome: return err("Pre-Amsterdam block header must not have blockAccessListHash") - elif blk.blockAccessList.isSome: + if blk.blockAccessList.isSome: return err("Pre-Amsterdam block body must not have blockAccessList") + # Commit block access list tracker changes for pre‑execution system calls + if vmState.balTrackerEnabled: + vmState.balTracker.commitCallFrame() + if header.txRoot != EMPTY_ROOT_HASH: if blk.transactions.len == 0: return err("Transactions missing from body") @@ -166,14 +176,24 @@ proc procBlkPreamble( elif blk.transactions.len > 0: return err("Transactions in block with empty txRoot") + # Setup block access list tracker for post‑execution system calls + if vmState.balTrackerEnabled: + vmState.balTracker.setBlockAccessIndex(blk.transactions.len() + 1) + vmState.balTracker.beginCallFrame() + if com.isShanghaiOrLater(header.timestamp): if header.withdrawalsRoot.isNone: return err("Post-Shanghai block header must have withdrawalsRoot") if blk.withdrawals.isNone: return err("Post-Shanghai block body must have withdrawals") - for withdrawal in blk.withdrawals.get: - vmState.ledger.addBalance(withdrawal.address, withdrawal.weiAmount) + if vmState.balTrackerEnabled: + for withdrawal in blk.withdrawals.get: + vmState.balTracker.trackAddBalanceChange(withdrawal.address, withdrawal.weiAmount) + vmState.ledger.addBalance(withdrawal.address, withdrawal.weiAmount) + else: + for withdrawal in blk.withdrawals.get: + vmState.ledger.addBalance(withdrawal.address, withdrawal.weiAmount) else: if header.withdrawalsRoot.isSome: return err("Pre-Shanghai block header must not have withdrawalsRoot") @@ -229,11 +249,31 @@ proc procBlkEpilogue( withdrawalReqs = ?processDequeueWithdrawalRequests(vmState) consolidationReqs = ?processDequeueConsolidationRequests(vmState) + if header.blockAccessListHash.isSome: + doAssert vmState.balTrackerEnabled + # Commit block access list tracker changes for post‑execution system calls + vmState.balTracker.commitCallFrame() + + let + bal = vmState.balTracker.getBlockAccessList().get() + balHash = bal[].computeBlockAccessListHash() + if header.blockAccessListHash.get != balHash: + debug "wrong blockAccessListHash, generated block access list does not " & + "match expected blockAccessListHash in header", + blockNumber = header.number, + blockHash = header.computeBlockHash, + parentHash = header.parentHash, + expected = header.blockAccessListHash.get, + actual = balHash, + blockAccessList = $(bal[]) + return err("blockAccessListHash mismatch, expect: " & + $header.blockAccessListHash.get & ", got: " & $balHash) + if not skipStateRootCheck: let stateRoot = vmState.ledger.getStateRoot() if header.stateRoot != stateRoot: # TODO replace logging with better error - debug "wrong state root in block", + debug "wrong stateRoot in block", blockNumber = header.number, blockHash = header.computeBlockHash, parentHash = header.parentHash, diff --git a/execution_chain/core/executor/process_transaction.nim b/execution_chain/core/executor/process_transaction.nim index c4bbba828f..b2a914ce56 100644 --- a/execution_chain/core/executor/process_transaction.nim +++ b/execution_chain/core/executor/process_transaction.nim @@ -53,10 +53,15 @@ proc commitOrRollbackDependingOnGasUsed( # an early stop. It would rather detect differing values for the block # header `gasUsed` and the `vmState.cumulativeGasUsed` at a later stage. if header.gasLimit < vmState.cumulativeGasUsed + gasUsed: + if vmState.balTrackerEnabled: + vmState.balTracker.rollbackCallFrame() vmState.ledger.rollback(accTx) err(&"invalid tx: block header gasLimit reached. gasLimit={header.gasLimit}, gasUsed={vmState.cumulativeGasUsed}, addition={gasUsed}") else: # Accept transaction and collect mining fee. + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddBalanceChange(vmState.coinbase(), gasUsed.u256 * priorityFee.u256) + vmState.balTracker.commitCallFrame() vmState.ledger.commit(accTx) vmState.ledger.addBalance(vmState.coinbase(), gasUsed.u256 * priorityFee.u256) vmState.cumulativeGasUsed += gasUsed @@ -108,13 +113,15 @@ proc processTransactionImpl( let com = vmState.com txRes = roDB.validateTransaction(tx, sender, header.gasLimit, baseFee256, excessBlobGas, com, fork) - res = if txRes.isOk: + res = if txRes.isOk: # Execute the transaction. vmState.captureTxStart(tx.gasLimit) - let - accTx = vmState.ledger.beginSavepoint - var - callResult = tx.txCallEvm(sender, vmState, baseFee) + + if vmState.balTrackerEnabled: + vmState.balTracker.beginCallFrame() + let accTx = vmState.ledger.beginSavepoint() + + var callResult = tx.txCallEvm(sender, vmState, baseFee) vmState.captureTxEnd(tx.gasLimit - callResult.gasUsed) let tmp = commitOrRollbackDependingOnGasUsed( diff --git a/execution_chain/core/tx_pool.nim b/execution_chain/core/tx_pool.nim index 0027dae9a9..53a8decf6b 100644 --- a/execution_chain/core/tx_pool.nim +++ b/execution_chain/core/tx_pool.nim @@ -45,6 +45,7 @@ import ./chain/forked_chain, ./pooled_txs +from ../evm/state import blockAccessList from eth/common/eth_types_rlp import rlpHash # ------------------------------------------------------------------------------ @@ -161,7 +162,7 @@ proc assembleBlock*( wrapperVersion: getWrapperVersion(com, blk.header.timestamp) ) currentRlpSize = rlp.getEncodedLength(blk.header) - + if blk.withdrawals.isSome: currentRlpSize = currentRlpSize + rlp.getEncodedLength(blk.withdrawals.get()) @@ -208,6 +209,10 @@ proc assembleBlock*( else: Opt.none(seq[seq[byte]]) + if com.isAmsterdamOrLater(blk.header.timestamp): + let bal = xp.vmState.blockAccessList.expect("block access list exists") + blk.blockAccessList = Opt.some(bal[]) + ok AssembledBlock( blk: blk, blobsBundle: blobsBundleOpt, diff --git a/execution_chain/core/tx_pool/tx_desc.nim b/execution_chain/core/tx_pool/tx_desc.nim index 9bd92cf9c6..6c90b04c18 100644 --- a/execution_chain/core/tx_pool/tx_desc.nim +++ b/execution_chain/core/tx_pool/tx_desc.nim @@ -87,8 +87,7 @@ proc setupVMState(com: CommonRef; parentHash: Hash32, pos: PosPayloadAttr, parentFrame: CoreDbTxRef): BaseVMState = - let - fork = com.toEVMFork(pos.timestamp) + let fork = com.toEVMFork(pos.timestamp) BaseVMState.new( parent = parent, @@ -103,7 +102,8 @@ proc setupVMState(com: CommonRef; parentHash : parentHash, ), txFrame = parentFrame.txFrameBegin(), - com = com) + com = com, + enableBalTracker = com.isAmsterdamOrLater(pos.timestamp)) template append(tab: var TxSenderTab, sn: TxSenderNonceRef) = tab[item.sender] = sn diff --git a/execution_chain/core/tx_pool/tx_packer.nim b/execution_chain/core/tx_pool/tx_packer.nim index e8e01eea06..24224aa7bc 100644 --- a/execution_chain/core/tx_pool/tx_packer.nim +++ b/execution_chain/core/tx_pool/tx_packer.nim @@ -113,6 +113,8 @@ proc runTxCommit(pst: var TxPacker; item: TxItemRef; callResult: LogResult, xp: gasTip = item.tx.tip(pst.baseFee) let reward = callResult.gasUsed.u256 * gasTip.u256 + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddBalanceChange(xp.feeRecipient, reward) vmState.ledger.addBalance(xp.feeRecipient, reward) pst.blockValue += reward @@ -141,6 +143,11 @@ proc vmExecInit(xp: TxPoolRef): Result[TxPacker, string] = stateRoot: xp.vmState.parent.stateRoot, ) + # Setup block access list tracker for pre‑execution system calls + if xp.vmState.balTrackerEnabled: + xp.vmState.balTracker.setBlockAccessIndex(0) + xp.vmState.balTracker.beginCallFrame() + # EIP-4788 if xp.nextFork >= FkCancun: let beaconRoot = xp.parentBeaconBlockRoot @@ -152,6 +159,10 @@ proc vmExecInit(xp: TxPoolRef): Result[TxPacker, string] = xp.vmState.processParentBlockHash(xp.vmState.blockCtx.parentHash).isOkOr: return err(error) + # Commit block access list tracker changes for pre‑execution system calls + if xp.vmState.balTrackerEnabled: + xp.vmState.balTracker.commitCallFrame() + ok(packer) proc vmExecGrabItem(pst: var TxPacker; item: TxItemRef, xp: TxPoolRef): bool = @@ -187,6 +198,10 @@ proc vmExecGrabItem(pst: var TxPacker; item: TxItemRef, xp: TxPoolRef): bool = if not vmState.classifyValidatePacked(item): return ContinueWithNextAccount + if vmState.balTrackerEnabled: + vmState.balTracker.setBlockAccessIndex(pst.packedTxs.len() + 1) + vmState.balTracker.beginCallFrame() + # Execute EVM for this transaction let accTx = vmState.ledger.beginSavepoint @@ -196,6 +211,8 @@ proc vmExecGrabItem(pst: var TxPacker; item: TxItemRef, xp: TxPoolRef): bool = # Find out what to do next: accepting this tx or trying the next account if not vmState.classifyPacked(callResult.gasUsed): + if vmState.balTrackerEnabled: + vmState.balTracker.rollbackCallFrame(rollbackReads = true) vmState.ledger.rollback(accTx) if vmState.classifyPackedNext(): return ContinueWithNextAccount @@ -213,6 +230,9 @@ proc vmExecGrabItem(pst: var TxPacker; item: TxItemRef, xp: TxPoolRef): bool = vmState.blobGasUsed += blobGasUsed vmState.gasPool -= item.tx.gasLimit + if vmState.balTrackerEnabled: + vmState.balTracker.commitCallFrame() + ContinueWithNextAccount proc vmExecCommit(pst: var TxPacker, xp: TxPoolRef): Result[void, string] = @@ -220,10 +240,20 @@ proc vmExecCommit(pst: var TxPacker, xp: TxPoolRef): Result[void, string] = vmState = pst.vmState ledger = vmState.ledger + # Setup block access list tracker for post‑execution system calls + if vmState.balTrackerEnabled: + vmState.balTracker.setBlockAccessIndex(pst.packedTxs.len() + 1) + vmState.balTracker.beginCallFrame() + # EIP-4895 if vmState.fork >= FkShanghai: - for withdrawal in xp.withdrawals: - ledger.addBalance(withdrawal.address, withdrawal.weiAmount) + if vmState.balTrackerEnabled: + for withdrawal in xp.withdrawals: + vmState.balTracker.trackAddBalanceChange(withdrawal.address, withdrawal.weiAmount) + ledger.addBalance(withdrawal.address, withdrawal.weiAmount) + else: + for withdrawal in xp.withdrawals: + ledger.addBalance(withdrawal.address, withdrawal.weiAmount) # EIP-6110, EIP-7002, EIP-7251 if vmState.fork >= FkPrague: @@ -240,6 +270,11 @@ proc vmExecCommit(pst: var TxPacker, xp: TxPoolRef): Result[void, string] = pst.receiptsRoot = vmState.receipts.calcReceiptsRoot pst.logsBloom = vmState.receipts.createBloom pst.stateRoot = vmState.ledger.getStateRoot() + + # Commit block access list tracker changes for post‑execution system calls + if vmState.balTrackerEnabled: + vmState.balTracker.commitCallFrame() + ok() # ------------------------------------------------------------------------------ @@ -271,7 +306,7 @@ proc assembleHeader*(pst: TxPacker, xp: TxPoolRef): Header = vmState = pst.vmState com = vmState.com - result = Header( + var header = Header( parentHash: vmState.blockCtx.parentHash, ommersHash: EMPTY_UNCLE_HASH, coinbase: xp.feeRecipient, @@ -290,12 +325,12 @@ proc assembleHeader*(pst: TxPacker, xp: TxPoolRef): Header = ) if com.isShanghaiOrLater(xp.timestamp): - result.withdrawalsRoot = Opt.some(calcWithdrawalsRoot(xp.withdrawals)) + header.withdrawalsRoot = Opt.some(calcWithdrawalsRoot(xp.withdrawals)) if com.isCancunOrLater(xp.timestamp): - result.parentBeaconBlockRoot = Opt.some(xp.parentBeaconBlockRoot) - result.blobGasUsed = Opt.some vmState.blobGasUsed - result.excessBlobGas = Opt.some vmState.blockCtx.excessBlobGas + header.parentBeaconBlockRoot = Opt.some(xp.parentBeaconBlockRoot) + header.blobGasUsed = Opt.some vmState.blobGasUsed + header.excessBlobGas = Opt.some vmState.blockCtx.excessBlobGas if com.isPragueOrLater(xp.timestamp): let requestsHash = calcRequestsHash([ @@ -303,7 +338,14 @@ proc assembleHeader*(pst: TxPacker, xp: TxPoolRef): Header = (WITHDRAWAL_REQUEST_TYPE, pst.withdrawalReqs), (CONSOLIDATION_REQUEST_TYPE, pst.consolidationReqs) ]) - result.requestsHash = Opt.some(requestsHash) + header.requestsHash = Opt.some(requestsHash) + + if com.isAmsterdamOrLater(xp.timestamp): + let bal = vmState.blockAccessList.expect("block access list exists") + header.blockAccessListHash = Opt.some(bal[].computeBlockAccessListHash()) + + header + func blockValue*(pst: TxPacker): UInt256 = pst.blockValue diff --git a/execution_chain/core/validate.nim b/execution_chain/core/validate.nim index bd5febe408..8c9421875d 100644 --- a/execution_chain/core/validate.nim +++ b/execution_chain/core/validate.nim @@ -47,15 +47,13 @@ func validateBlockAccessList*( if com.isAmsterdamOrLater(header.timestamp): if header.blockAccessListHash.isNone: return err("Post-Amsterdam block header must have blockAccessListHash") - elif blockAccessList.isNone: - return err("Post-Amsterdam block body must have blockAccessList") - else: + if blockAccessList.isSome: if blockAccessList.get.validate(header.blockAccessListHash.get).isErr(): return err("Mismatched blockAccessListHash blockNumber = " & $header.number) else: if header.blockAccessListHash.isSome: return err("Pre-Amsterdam block header must not have blockAccessListHash") - elif blockAccessList.isSome: + if blockAccessList.isSome: return err("Pre-Amsterdam block body must not have blockAccessList") return ok() diff --git a/execution_chain/db/ledger.nim b/execution_chain/db/ledger.nim index 3058b21f5e..22865d12ef 100644 --- a/execution_chain/db/ledger.nim +++ b/execution_chain/db/ledger.nim @@ -672,13 +672,16 @@ proc selfDestruct*(ac: LedgerRef, address: Address) = ac.setBalance(address, 0.u256) ac.savePoint.selfDestruct.incl address -proc selfDestruct6780*(ac: LedgerRef, address: Address) = +proc selfDestruct6780*(ac: LedgerRef, address: Address): bool = let acc = ac.getAccount(address, false) if acc.isNil: - return + return false if NewlyCreated in acc.flags: ac.selfDestruct(address) + true + else: + false proc selfDestructLen*(ac: LedgerRef): int = ac.savePoint.selfDestruct.len diff --git a/execution_chain/evm/computation.nim b/execution_chain/evm/computation.nim index c4f60e4ef4..d078423b92 100644 --- a/execution_chain/evm/computation.nim +++ b/execution_chain/evm/computation.nim @@ -25,7 +25,7 @@ import chronicles, chronos export - common + common, balTrackerEnabled logScope: topics = "vm computation" @@ -81,23 +81,34 @@ proc getBlockHash*(c: Computation, number: BlockNumber): Hash32 = c.vmState.getAncestorHash(number) template accountExists*(c: Computation, address: Address): bool = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(address) + if c.fork >= FkSpurious: not c.vmState.readOnlyLedger.isDeadAccount(address) else: c.vmState.readOnlyLedger.accountExists(address) template getStorage*(c: Computation, slot: UInt256): UInt256 = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackStorageRead(c.msg.contractAddress, slot) c.vmState.readOnlyLedger.getStorage(c.msg.contractAddress, slot) template getBalance*(c: Computation, address: Address): UInt256 = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(address) c.vmState.readOnlyLedger.getBalance(address) template getCodeSize*(c: Computation, address: Address): uint = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(address) uint(c.vmState.readOnlyLedger.getCodeSize(address)) template getCodeHash*(c: Computation, address: Address): Hash32 = - let - db = c.vmState.readOnlyLedger + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(address) + + let db = c.vmState.readOnlyLedger if not db.accountExists(address) or db.isEmptyAccount(address): default(Hash32) else: @@ -107,6 +118,8 @@ template selfDestruct*(c: Computation, address: Address) = c.execSelfDestruct(address) template getCode*(c: Computation, address: Address): CodeBytesRef = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(address) c.vmState.readOnlyLedger.getCode(address) template setTransientStorage*(c: Computation, slot, val: UInt256) = @@ -151,9 +164,13 @@ func shouldBurnGas*(c: Computation): bool = c.isError and c.error.burnsGas proc snapshot*(c: Computation) = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.beginCallFrame() c.savePoint = c.vmState.ledger.beginSavepoint() proc commit*(c: Computation) = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.commitCallFrame() c.vmState.ledger.commit(c.savePoint) proc dispose*(c: Computation) = @@ -167,6 +184,8 @@ proc dispose*(c: Computation) = c.savePoint = nil proc rollback*(c: Computation) = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.rollbackCallFrame() c.vmState.ledger.rollback(c.savePoint) func setError*(c: Computation, msg: sink string, burnsGas = false) = @@ -228,6 +247,8 @@ proc writeContract*(c: Computation) = reason = "Write new contract code"). expect("enough gas since we checked against gasRemaining") c.vmState.mutateLedger: + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackCodeChange(c.msg.contractAddress, c.output) db.setCode(c.msg.contractAddress, c.output) withExtra trace, "Writing new contract code" return @@ -258,18 +279,32 @@ proc execSelfDestruct*(c: Computation, beneficiary: Address) = # Register the account to be deleted if c.fork >= FkCancun: - # Zeroing contract balance except beneficiary - # is the same address - db.subBalance(c.msg.contractAddress, localBalance) - - # Transfer to beneficiary - db.addBalance(beneficiary, localBalance) - - db.selfDestruct6780(c.msg.contractAddress) + if c.vmState.balTrackerEnabled: + # Zeroing contract balance except beneficiary is the same address + c.vmState.balTracker.trackSubBalanceChange(c.msg.contractAddress, localBalance) + db.subBalance(c.msg.contractAddress, localBalance) + # Transfer to beneficiary + c.vmState.balTracker.trackAddBalanceChange(beneficiary, localBalance) + db.addBalance(beneficiary, localBalance) + if db.selfDestruct6780(c.msg.contractAddress): + c.vmState.balTracker.trackInTransactionSelfDestruct(c.msg.contractAddress) + else: + # Zeroing contract balance except beneficiary is the same address + db.subBalance(c.msg.contractAddress, localBalance) + # Transfer to beneficiary + db.addBalance(beneficiary, localBalance) + discard db.selfDestruct6780(c.msg.contractAddress) else: - # Transfer to beneficiary - db.addBalance(beneficiary, localBalance) - db.selfDestruct(c.msg.contractAddress) + if c.vmState.balTrackerEnabled: + # Transfer to beneficiary + c.vmState.balTracker.trackAddBalanceChange(beneficiary, localBalance) + db.addBalance(beneficiary, localBalance) + c.vmState.balTracker.trackSelfDestruct(c.msg.contractAddress) + db.selfDestruct(c.msg.contractAddress) + else: + # Transfer to beneficiary + db.addBalance(beneficiary, localBalance) + db.selfDestruct(c.msg.contractAddress) trace "SELFDESTRUCT", contractAddress = c.msg.contractAddress.toHex, diff --git a/execution_chain/evm/interpreter/op_handlers/oph_memory.nim b/execution_chain/evm/interpreter/op_handlers/oph_memory.nim index 907df4cccf..f94d32a4f0 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_memory.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_memory.nim @@ -43,6 +43,8 @@ proc sstoreImpl(c: Computation, slot, newValue: UInt256): EvmResultVoid = ? c.opcodeGasCost(Sstore, res.gasCost, "SSTORE") c.gasMeter.refundGas(res.gasRefund) + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackStorageWrite(c.msg.contractAddress, slot, newValue) c.vmState.mutateLedger: db.setStorage(c.msg.contractAddress, slot, newValue) ok() @@ -63,6 +65,8 @@ proc sstoreNetGasMeteringImpl(c: Computation; slot, newValue: UInt256, coldAcces c.gasMeter.refundGas(res.gasRefund) + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackStorageWrite(c.msg.contractAddress, slot, newValue) c.vmState.mutateLedger: db.setStorage(c.msg.contractAddress, slot, newValue) ok() diff --git a/execution_chain/evm/interpreter_dispatch.nim b/execution_chain/evm/interpreter_dispatch.nim index 9c71304a20..6f12aba8a8 100644 --- a/execution_chain/evm/interpreter_dispatch.nim +++ b/execution_chain/evm/interpreter_dispatch.nim @@ -69,10 +69,16 @@ proc beforeExecCall(c: Computation) = c.snapshot() if c.msg.kind == CallKind.Call: c.vmState.mutateLedger: - db.subBalance(c.msg.sender, c.msg.value) - db.addBalance(c.msg.contractAddress, c.msg.value) - -func afterExecCall(c: Computation) = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackSubBalanceChange(c.msg.sender, c.msg.value) + db.subBalance(c.msg.sender, c.msg.value) + c.vmState.balTracker.trackAddBalanceChange(c.msg.contractAddress, c.msg.value) + db.addBalance(c.msg.contractAddress, c.msg.value) + else: + db.subBalance(c.msg.sender, c.msg.value) + db.addBalance(c.msg.contractAddress, c.msg.value) + +proc afterExecCall(c: Computation) = ## Collect all of the accounts that *may* need to be deleted based on EIP161 ## https://github.com/ethereum/EIPs/blob/master/EIPS/eip-161.md ## also see: https://github.com/ethereum/EIPs/issues/716 @@ -96,6 +102,8 @@ proc beforeExecCreate(c: Computation): bool = "Nonce overflow when sender=" & sender & " wants to create contract", false ) return true + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackNonceChange(c.msg.sender, nonce + 1) db.setNonce(c.msg.sender, nonce + 1) # We add this to the access list _before_ taking a snapshot. @@ -106,19 +114,34 @@ proc beforeExecCreate(c: Computation): bool = c.snapshot() + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(c.msg.contractAddress) if c.vmState.readOnlyLedger().contractCollision(c.msg.contractAddress): let blurb = c.msg.contractAddress.toHex c.setError("Address collision when creating contract address=" & blurb, true) c.rollback() return true + + c.vmState.mutateLedger: - db.subBalance(c.msg.sender, c.msg.value) - db.addBalance(c.msg.contractAddress, c.msg.value) - db.clearStorage(c.msg.contractAddress) - if c.fork >= FkSpurious: - # EIP161 nonce incrementation - db.incNonce(c.msg.contractAddress) + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackSubBalanceChange(c.msg.sender, c.msg.value) + db.subBalance(c.msg.sender, c.msg.value) + c.vmState.balTracker.trackAddBalanceChange(c.msg.contractAddress, c.msg.value) + db.addBalance(c.msg.contractAddress, c.msg.value) + db.clearStorage(c.msg.contractAddress) + if c.fork >= FkSpurious: + # EIP161 nonce incrementation + c.vmState.balTracker.trackIncNonceChange(c.msg.contractAddress) + db.incNonce(c.msg.contractAddress) + else: + db.subBalance(c.msg.sender, c.msg.value) + db.addBalance(c.msg.contractAddress, c.msg.value) + db.clearStorage(c.msg.contractAddress) + if c.fork >= FkSpurious: + # EIP161 nonce incrementation + db.incNonce(c.msg.contractAddress) return false diff --git a/execution_chain/evm/message.nim b/execution_chain/evm/message.nim index 5af89e00de..7e049d0376 100644 --- a/execution_chain/evm/message.nim +++ b/execution_chain/evm/message.nim @@ -15,7 +15,8 @@ import ./precompiles, ../common/evmforks, ../utils/utils, - ../db/ledger + ../db/ledger, + ../core/eip7702 proc isCreate*(message: Message): bool = message.kind in {CallKind.Create, CallKind.Create2} @@ -26,6 +27,8 @@ proc generateContractAddress*(vmState: BaseVMState, salt = ZERO_CONTRACTSALT, code = CodeBytesRef(nil)): Address = if kind == CallKind.Create: + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddressAccess(sender) let creationNonce = vmState.readOnlyLedger().getNonce(sender) generateAddress(sender, creationNonce) else: @@ -37,6 +40,15 @@ proc getCallCode*(vmState: BaseVMState, codeAddress: Address): CodeBytesRef = return CodeBytesRef(nil) if vmState.fork >= FkPrague: + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddressAccess(codeAddress) + let + code = vmState.readOnlyLedger.getCode(codeAddress) + delegateTo = parseDelegationAddress(code) + if delegateTo.isSome(): + vmState.balTracker.trackAddressAccess(delegateTo.get()) vmState.readOnlyLedger.resolveCode(codeAddress) else: + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddressAccess(codeAddress) vmState.readOnlyLedger.getCode(codeAddress) diff --git a/execution_chain/evm/precompiles.nim b/execution_chain/evm/precompiles.nim index e5c7d20f7f..d497fe886a 100644 --- a/execution_chain/evm/precompiles.nim +++ b/execution_chain/evm/precompiles.nim @@ -758,6 +758,8 @@ proc getPrecompile*(fork: EVMFork, codeAddress: Address): Opt[Precompiles] = Opt.none(Precompiles) proc execPrecompile*(c: Computation, precompile: Precompiles) = + if c.vmState.balTrackerEnabled: + c.vmState.balTracker.trackAddressAccess(precompileAddrs[precompile]) let fork = c.fork let res = case precompile of paEcRecover: ecRecover(c) diff --git a/execution_chain/evm/state.nim b/execution_chain/evm/state.nim index 2dc45f2de3..0db684641c 100644 --- a/execution_chain/evm/state.nim +++ b/execution_chain/evm/state.nim @@ -15,6 +15,7 @@ import stew/assign2, ../db/ledger, ../common/[common, evmforks], + ../block_access_list/block_access_list_tracker, ./interpreter/[op_codes, gas_costs], ./types, ./evm_errors @@ -32,6 +33,7 @@ proc init( blockCtx: BlockContext; com: CommonRef; tracer: TracerRef, + tracker: BlockAccessListTrackerRef, flags: set[VMFlag] = self.flags) = ## Initialisation helper # Take care to (re)set all fields since the VMState might be recycled @@ -51,6 +53,7 @@ proc init( self.blobGasUsed = 0'u64 self.allLogs.setLen(0) self.gasRefunded = 0 + self.balTracker = tracker func blockCtx(header: Header): BlockContext = BlockContext( @@ -80,7 +83,8 @@ proc new*( com: CommonRef; ## block chain config txFrame: CoreDbTxRef; tracer: TracerRef = nil, - storeSlotHash = false): T = + storeSlotHash = false, + enableBalTracker = false): T = ## Create a new `BaseVMState` descriptor from a parent block header. This ## function internally constructs a new account state cache rooted at ## `parent.stateRoot` @@ -88,13 +92,23 @@ proc new*( ## This `new()` constructor and its variants (see below) provide a save ## `BaseVMState` environment where the account state cache is synchronised ## with the `parent` block header. + let + ac = LedgerRef.init(txFrame, storeSlotHash, com.statelessProviderEnabled) + tracker = + if enableBalTracker: + BlockAccessListTrackerRef.init(ac.ReadOnlyLedger) + else: + nil + new result result.init( - ac = LedgerRef.init(txFrame, storeSlotHash, com.statelessProviderEnabled), + ac = ac, parent = parent, blockCtx = blockCtx, com = com, - tracer = tracer) + tracer = tracer, + tracker = tracker + ) proc reinit*(self: BaseVMState; ## Object descriptor parent: Header; ## parent header, account sync pos. @@ -109,11 +123,16 @@ proc reinit*(self: BaseVMState; ## Object descriptor ## queries about its `getStateRoot()`, i.e. `isTopLevelClean` evaluated `true`. If ## this function returns `false`, the function argument `self` is left ## untouched. + + if not self.balTracker.isNil(): + self.balTracker = BlockAccessListTrackerRef.init(self.ledger.ReadOnlyLedger) + if not self.ledger.isTopLevelClean: return false let tracer = self.tracer + tracker = self.balTracker com = self.com ac = self.ledger flags = self.flags @@ -123,6 +142,7 @@ proc reinit*(self: BaseVMState; ## Object descriptor blockCtx = blockCtx, com = com, tracer = tracer, + tracker = tracker, flags = flags) true @@ -148,7 +168,8 @@ proc init*( com: CommonRef; ## block chain config txFrame: CoreDbTxRef; tracer: TracerRef = nil, - storeSlotHash = false) = + storeSlotHash = false, + enableBalTracker = false) = ## Variant of `new()` constructor above for in-place initalisation. The ## `parent` argument is used to sync the accounts cache and the `header` ## is used as a container to pass the `timestamp`, `gasLimit`, and `fee` @@ -156,21 +177,31 @@ proc init*( ## ## It requires the `header` argument properly initalised so that for PoA ## networks, the miner address is retrievable via `ecRecover()`. + let + ac = LedgerRef.init(txFrame, storeSlotHash, com.statelessProviderEnabled) + tracker = + if enableBalTracker: + BlockAccessListTrackerRef.init(ac.ReadOnlyLedger) + else: + nil + self.init( - ac = LedgerRef.init(txFrame, storeSlotHash, com.statelessProviderEnabled), + ac = ac, parent = parent, blockCtx = blockCtx(header), com = com, - tracer = tracer) + tracer = tracer, + tracker = tracker) proc new*( - T: type BaseVMState; + T: type BaseVMState; parent: Header; ## parent header, account sync position header: Header; ## header with tx environment data fields - com: CommonRef; ## block chain config + com: CommonRef; ## block chain config txFrame: CoreDbTxRef; tracer: TracerRef = nil, - storeSlotHash = false): T = + storeSlotHash = false, + enableBalTracker = false): T = ## This is a variant of the `new()` constructor above where the `parent` ## argument is used to sync the accounts cache and the `header` is used ## as a container to pass the `timestamp`, `gasLimit`, and `fee` values. @@ -184,7 +215,8 @@ proc new*( com = com, txFrame = txFrame, tracer = tracer, - storeSlotHash = storeSlotHash) + storeSlotHash = storeSlotHash, + enableBalTracker = enableBalTracker) func coinbase*(vmState: BaseVMState): Address = vmState.blockCtx.coinbase @@ -233,6 +265,15 @@ proc `status=`*(vmState: BaseVMState, status: bool) = func tracingEnabled*(vmState: BaseVMState): bool = vmState.tracer.isNil.not +template balTrackerEnabled*(vmState: BaseVMState): bool = + vmState.balTracker.isNil.not + +template blockAccessList*(vmState: BaseVMState): Opt[BlockAccessListRef] = + if vmState.balTrackerEnabled: + vmState.balTracker.getBlockAccessList() + else: + Opt.none(BlockAccessListRef) + proc captureTxStart*(vmState: BaseVMState, gasLimit: GasInt) = if vmState.tracingEnabled: vmState.tracer.captureTxStart(gasLimit) diff --git a/execution_chain/evm/types.nim b/execution_chain/evm/types.nim index 12e5f2304b..6adf3b7327 100644 --- a/execution_chain/evm/types.nim +++ b/execution_chain/evm/types.nim @@ -15,9 +15,10 @@ import ./interpreter/[gas_costs, op_codes], ./transient_storage, ../db/ledger, - ../common/[common, evmforks] + ../common/[common, evmforks], + ../block_access_list/block_access_list_tracker -export stack, memory, transient_storage +export stack, memory, transient_storage, block_access_list_tracker type VMFlag* = enum @@ -55,6 +56,7 @@ type blobGasUsed* : uint64 allLogs* : seq[Log] # EIP-6110 gasRefunded* : int64 # Global gasRefunded counter + balTracker* : BlockAccessListTrackerRef Computation* = ref object # The execution computation diff --git a/execution_chain/rpc/engine_api.nim b/execution_chain/rpc/engine_api.nim index c2ed3fbec4..f6d856c625 100644 --- a/execution_chain/rpc/engine_api.nim +++ b/execution_chain/rpc/engine_api.nim @@ -25,11 +25,13 @@ const supportedMethods: HashSet[string] = "engine_newPayloadV2", "engine_newPayloadV3", "engine_newPayloadV4", + "engine_newPayloadV5", "engine_getPayloadV1", "engine_getPayloadV2", "engine_getPayloadV3", "engine_getPayloadV4", "engine_getPayloadV5", + "engine_getPayloadV6", "engine_forkchoiceUpdatedV1", "engine_forkchoiceUpdatedV2", "engine_forkchoiceUpdatedV3", @@ -66,6 +68,13 @@ proc setupEngineAPI*(engine: BeaconEngineRef, server: RpcServer) = await engine.newPayload(Version.V4, payload, expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests) + server.rpc("engine_newPayloadV5") do(payload: ExecutionPayload, + expectedBlobVersionedHashes: Opt[seq[Hash32]], + parentBeaconBlockRoot: Opt[Hash32], + executionRequests: Opt[seq[seq[byte]]]) -> PayloadStatusV1: + await engine.newPayload(Version.V5, payload, + expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests) + server.rpc("engine_getPayloadV1") do(payloadId: Bytes8) -> ExecutionPayloadV1: return engine.getPayload(Version.V1, payloadId).executionPayload.V1 @@ -81,6 +90,9 @@ proc setupEngineAPI*(engine: BeaconEngineRef, server: RpcServer) = server.rpc("engine_getPayloadV5") do(payloadId: Bytes8) -> GetPayloadV5Response: return engine.getPayloadV5(payloadId) + server.rpc("engine_getPayloadV6") do(payloadId: Bytes8) -> GetPayloadV6Response: + return engine.getPayloadV6(payloadId) + server.rpc("engine_forkchoiceUpdatedV1") do(update: ForkchoiceStateV1, attrs: Opt[PayloadAttributesV1]) -> ForkchoiceUpdatedResponse: await engine.forkchoiceUpdated(Version.V1, update, attrs.payloadAttributes) diff --git a/execution_chain/transaction/call_common.nim b/execution_chain/transaction/call_common.nim index a313e8173b..5e3c830291 100644 --- a/execution_chain/transaction/call_common.nim +++ b/execution_chain/transaction/call_common.nim @@ -45,6 +45,8 @@ proc initialAccessListEIP2929(call: CallParams) = if not call.isCreate: db.accessList(call.to) # If the `call.to` has a delegation, also warm its target. + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddressAccess(call.to) let target = parseDelegationAddress(db.getCode(call.to)) if target.isSome: db.accessList(target[]) @@ -68,6 +70,8 @@ proc preExecComputation(vmState: BaseVMState, call: CallParams): int64 = let ledger = vmState.ledger if not call.isCreate: + if vmState.balTrackerEnabled: + vmState.balTracker.trackIncNonceChange(call.sender) ledger.incNonce(call.sender) # EIP-7702 @@ -88,6 +92,8 @@ proc preExecComputation(vmState: BaseVMState, call: CallParams): int64 = ledger.accessList(authority) # 5. Verify the code of authority is either empty or already delegated. + if vmState.balTrackerEnabled: + vmState.balTracker.trackAddressAccess(authority) let code = ledger.getCode(authority) if code.len > 0: if not parseDelegation(code): @@ -102,12 +108,18 @@ proc preExecComputation(vmState: BaseVMState, call: CallParams): int64 = gasRefund += PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST # 8. Set the code of authority to be 0xef0100 || address. This is a delegation designation. - if auth.address == zeroAddress: - ledger.setCode(authority, @[]) - else: - ledger.setCode(authority, @(addressToDelegation(auth.address))) + let authCode = + if auth.address == zeroAddress: + @[] + else: + @(addressToDelegation(auth.address)) + if vmState.balTrackerEnabled: + vmState.balTracker.trackCodeChange(authority, authCode) + ledger.setCode(authority, authCode) # 9. Increase the nonce of authority by one. + if vmState.balTrackerEnabled: + vmState.balTracker.trackNonceChange(authority, auth.nonce + 1) ledger.setNonce(authority, auth.nonce + 1) gasRefund @@ -187,12 +199,16 @@ proc prepareToRunComputation(host: TransactionHost, call: CallParams) = fork = vmState.fork vmState.mutateLedger: + if vmState.balTrackerEnabled: + vmState.balTracker.trackSubBalanceChange(call.sender, call.gasLimit.u256 * call.gasPrice.u256) db.subBalance(call.sender, call.gasLimit.u256 * call.gasPrice.u256) # EIP-4844 if fork >= FkCancun: let blobFee = calcDataFee(call.versionedHashes.len, vmState.blockCtx.excessBlobGas, vmState.com, fork) + if vmState.balTrackerEnabled: + vmState.balTracker.trackSubBalanceChange(call.sender, blobFee) db.subBalance(call.sender, blobFee) proc calculateAndPossiblyRefundGas(host: TransactionHost, call: CallParams): GasInt = @@ -227,6 +243,8 @@ proc calculateAndPossiblyRefundGas(host: TransactionHost, call: CallParams): Gas # Refund for unused gas. if gasRemaining > 0 and not call.noGasCharge: + if host.vmState.balTrackerEnabled: + host.vmState.balTracker.trackAddBalanceChange(call.sender, gasRemaining.u256 * call.gasPrice.u256) host.vmState.mutateLedger: db.addBalance(call.sender, gasRemaining.u256 * call.gasPrice.u256) diff --git a/scripts/eest_ci_cache.sh b/scripts/eest_ci_cache.sh index cd4251dc8a..595f2d2db8 100755 --- a/scripts/eest_ci_cache.sh +++ b/scripts/eest_ci_cache.sh @@ -29,7 +29,7 @@ EEST_DEVNET_URL="https://github.com/ethereum/execution-spec-tests/releases/downl # --- BAL Release --- EEST_BAL_NAME="bal" -EEST_BAL_VERSION="v1.6.0" +EEST_BAL_VERSION="v1.8.0" EEST_BAL_DIR="${FIXTURES_DIR}/eest_bal" EEST_BAL_ARCHIVE="fixtures_bal.tar.gz" EEST_BAL_URL="https://github.com/ethereum/execution-spec-tests/releases/download/${EEST_BAL_NAME}%40${EEST_BAL_VERSION}/${EEST_BAL_ARCHIVE}" diff --git a/tests/eest/eest_blockchain_test.nim b/tests/eest/eest_blockchain_test.nim index 1780fe5511..eee76e95da 100644 --- a/tests/eest/eest_blockchain_test.nim +++ b/tests/eest/eest_blockchain_test.nim @@ -18,11 +18,16 @@ const eestType = "blockchain_tests" eestReleases = [ "eest_develop", - "eest_devnet" + "eest_devnet", + "eest_bal" ] const skipFiles = [ - "" + "consolidation_requests.json", + "withdrawal_requests.json", + "bal_call_and_oog.json", + "bal_delegatecall_and_oog.json", + "value_transfer_gas_calculation.json" ] runEESTSuite( diff --git a/tests/eest/eest_engine_test.nim b/tests/eest/eest_engine_test.nim index 04bc8803fd..3b72103c30 100644 --- a/tests/eest/eest_engine_test.nim +++ b/tests/eest/eest_engine_test.nim @@ -20,11 +20,17 @@ const eestType = "blockchain_tests_engine" eestReleases = [ "eest_develop", - "eest_devnet" + "eest_devnet", + "eest_bal" ] const skipFiles = [ "CALLBlake2f_MaxRounds.json", # Doesn't work in github CI + "consolidation_requests.json", + "withdrawal_requests.json", + "bal_call_and_oog.json", + "bal_delegatecall_and_oog.json", + "value_transfer_gas_calculation.json" ] runEESTSuite( diff --git a/tests/test_block_access_list_builder.nim b/tests/test_block_access_list_builder.nim index b0866da1d0..1a9f4861c0 100644 --- a/tests/test_block_access_list_builder.nim +++ b/tests/test_block_access_list_builder.nim @@ -35,7 +35,7 @@ suite "Block access list builder": builder.addTouchedAccount(address1) builder.addTouchedAccount(address1) # duplicate - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 3 bal[0].address == address1 @@ -58,7 +58,7 @@ suite "Block access list builder": builder.addStorageWrite(address1, slot3, 3, 4.u256) builder.addStorageWrite(address1, slot3, 3, 5.u256) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 2 bal[0].address == address1 @@ -77,7 +77,7 @@ suite "Block access list builder": builder.addStorageRead(address1, slot1) builder.addStorageRead(address1, slot1) # duplicate - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 3 bal[0].address == address1 @@ -94,7 +94,7 @@ suite "Block access list builder": builder.addBalanceChange(address1, 2, 2.u256) builder.addBalanceChange(address1, 2, 10.u256) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 3 bal[0].address == address1 @@ -111,7 +111,7 @@ suite "Block access list builder": builder.addNonceChange(address3, 1, 1) builder.addNonceChange(address3, 1, 10) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 3 bal[0].address == address1 @@ -127,7 +127,7 @@ suite "Block access list builder": builder.addCodeChange(address1, 3, @[0x3.byte]) builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 2 bal[0].address == address1 @@ -171,7 +171,7 @@ suite "Block access list builder": builder.addCodeChange(address1, 3, @[0x3.byte]) builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check: bal.len() == 3 diff --git a/tests/test_block_access_list_tracker.nim b/tests/test_block_access_list_tracker.nim index e32d6636c0..8ff23dde75 100644 --- a/tests/test_block_access_list_tracker.nim +++ b/tests/test_block_access_list_tracker.nim @@ -44,7 +44,7 @@ suite "Block access list tracker": coreDb = newCoreDbRef(DefaultDbMemory) ledger = LedgerRef.init(coreDb.baseTxFrame()) builder = BlockAccessListBuilderRef.init() - tracker = StateChangeTrackerRef.init(ledger.ReadOnlyLedger, builder) + tracker = BlockAccessListTrackerRef.init(ledger.ReadOnlyLedger, builder) # Setup in test data in db @@ -150,15 +150,17 @@ suite "Block access list tracker": test "Track address access": check not builder.accounts.contains(address1) - tracker.trackAddressAccess(address1) - check builder.accounts.contains(address1) - check not builder.accounts.contains(address2) - tracker.trackAddressAccess(address2) - check builder.accounts.contains(address2) - check not builder.accounts.contains(address4) + + tracker.beginCallFrame() + tracker.trackAddressAccess(address1) + tracker.trackAddressAccess(address2) tracker.trackAddressAccess(address4) + tracker.commitCallFrame() + + check builder.accounts.contains(address1) + check builder.accounts.contains(address2) check builder.accounts.contains(address4) test "Begin, commit and rollback call frame": @@ -249,7 +251,9 @@ suite "Block access list tracker": block: check not builder.accounts.contains(address1) + tracker.beginCallFrame() tracker.trackStorageRead(address1, slot1) + tracker.commitCallFrame() check builder.accounts.contains(address1) tracker.builder.accounts.withValue(address1, accData): @@ -259,7 +263,9 @@ suite "Block access list tracker": block: check not builder.accounts.contains(address2) + tracker.beginCallFrame() tracker.trackStorageRead(address2, slot2) + tracker.commitCallFrame() check builder.accounts.contains(address2) tracker.builder.accounts.withValue(address2, accData): @@ -346,7 +352,7 @@ suite "Block access list tracker": check: not tracker.pendingCallFrame().storageChanges.contains((address1, slot1)) - not tracker.pendingCallFrame().balanceChanges.contains(address1) + tracker.pendingCallFrame().balanceChanges.contains(address1) not tracker.pendingCallFrame().nonceChanges.contains(address1) not tracker.pendingCallFrame().codeChanges.contains(address1) @@ -357,6 +363,6 @@ suite "Block access list tracker": check: slot1 notin accData[].storageChanges slot1 in accData[].storageReads - balIndex notin accData[].balanceChanges + balIndex in accData[].balanceChanges balIndex notin accData[].nonceChanges balIndex notin accData[].codeChanges diff --git a/tests/test_block_access_list_validation.nim b/tests/test_block_access_list_validation.nim index 2db63d2389..ebab4e44da 100644 --- a/tests/test_block_access_list_validation.nim +++ b/tests/test_block_access_list_validation.nim @@ -31,7 +31,7 @@ suite "Block access list validation": let builder = BlockAccessListBuilderRef.init() test "Empty BAL should equal the EMPTY_BLOCK_ACCESS_LIST_HASH": - let emptyBal = builder.buildBlockAccessList() + let emptyBal = builder.buildBlockAccessList()[] check: emptyBal.validate(EMPTY_BLOCK_ACCESS_LIST_HASH).isOk() emptyBal.validate(default(Hash32)).isErr() @@ -72,7 +72,7 @@ suite "Block access list validation": builder.addCodeChange(address1, 3, @[0x3.byte]) builder.addCodeChange(address1, 3, @[0x4.byte]) # duplicate should overwrite - let bal = builder.buildBlockAccessList() + let bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() test "Storage changes and reads don't overlap for the same slot": @@ -80,7 +80,7 @@ suite "Block access list validation": builder.addStorageWrite(address1, slot2, 2, 2.u256) builder.addStorageWrite(address1, slot3, 3, 3.u256) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] bal[0].storageReads = @[slot1.toBytes32()] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -89,7 +89,7 @@ suite "Block access list validation": builder.addTouchedAccount(address2) builder.addTouchedAccount(address3) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0] = bal[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -99,7 +99,7 @@ suite "Block access list validation": builder.addStorageWrite(address1, slot2, 2, 2.u256) builder.addStorageWrite(address1, slot3, 3, 3.u256) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].storageChanges[0] = bal[0].storageChanges[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -109,7 +109,7 @@ suite "Block access list validation": builder.addStorageWrite(address1, slot1, 1, 1.u256) builder.addStorageWrite(address1, slot1, 2, 2.u256) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].storageChanges[0].changes[0] = bal[0].storageChanges[0].changes[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -119,7 +119,7 @@ suite "Block access list validation": builder.addStorageRead(address1, slot2) builder.addStorageRead(address1, slot3) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].storageReads[0] = bal[0].storageReads[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -129,7 +129,7 @@ suite "Block access list validation": builder.addBalanceChange(address1, 2, 2.u256) builder.addBalanceChange(address1, 3, 3.u256) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].balanceChanges[0] = bal[0].balanceChanges[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -139,7 +139,7 @@ suite "Block access list validation": builder.addNonceChange(address1, 2, 2) builder.addNonceChange(address1, 3, 3) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].nonceChanges[0] = bal[0].nonceChanges[2] check bal.validate(bal.computeBlockAccessListHash()).isErr() @@ -149,7 +149,7 @@ suite "Block access list validation": builder.addCodeChange(address1, 1, @[0x2.byte]) builder.addCodeChange(address1, 2, @[0x3.byte]) - var bal = builder.buildBlockAccessList() + var bal = builder.buildBlockAccessList()[] check bal.validate(bal.computeBlockAccessListHash()).isOk() bal[0].codeChanges[0] = bal[0].codeChanges[2] check bal.validate(bal.computeBlockAccessListHash()).isErr()