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 7c739debdf..aae593ca0d 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 4fc32523ea..83258c1f8c 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() @@ -116,11 +118,11 @@ func balIndexCmp(x, y: StorageChange | BalanceChange | NonceChange | CodeChange) func slotChangesCmp(x, y: SlotChanges): int = cmp(x.slot, y.slot) -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 @@ -160,7 +162,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, @@ -169,6 +171,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..3c3b8526f4 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). @@ -663,6 +661,7 @@ proc init*( fcuSafe: fcuSafe, baseQueue: initDeque[BlockRef](), lastBaseLogTime: EthTime.now(), + badBlocks: LruCache[Hash32, (Block, Opt[BlockAccessListRef])].init(100), ) # updateFinalized will stop ancestor lineage @@ -886,6 +885,12 @@ proc latestBlock*(c: ForkedChainRef): Block = return c.baseTxFrame.getEthBlock(c.latest.hash).expect("baseBlock exists") c.latest.blk +proc getBadBlocks*(c: ForkedChainRef): seq[(Block, Opt[BlockAccessListRef])] = + var blks: seq[(Block, Opt[BlockAccessListRef])] + for badBlock in c.badBlocks.values(): + blks.add(badBlock) + blks + proc headerByNumber*(c: ForkedChainRef, number: BlockNumber): Result[Header, string] = if number > c.latest.number: return err("Requested block number not exists: " & $number) diff --git a/execution_chain/core/chain/forked_chain/chain_desc.nim b/execution_chain/core/chain/forked_chain/chain_desc.nim index 09d11cc475..ff7c2720e6 100644 --- a/execution_chain/core/chain/forked_chain/chain_desc.nim +++ b/execution_chain/core/chain/forked_chain/chain_desc.nim @@ -13,13 +13,16 @@ import std/[tables, deques], chronos, + minilru, ../../../common, ../../../db/[core_db, fcu_db], ../../../portal/portal, ./block_quarantine, ./chain_branch -export tables +from ../../../block_access_list/block_access_list_builder import BlockAccessListRef + +export tables, minilru type QueueItem* = object @@ -99,6 +102,11 @@ type # Prevent async re-entrancy messing up FC state # on both `importBlock` and `forkChoice`. + badBlocks*: LruCache[Hash32, (Block, Opt[BlockAccessListRef])] + # Recent blocks that failed validation for any reason, + # indexed by block hash and containing a tuple of the block + # and the generated block access list. + # ------------------------------------------------------------------------------ # These functions are private to ForkedChainRef # ------------------------------------------------------------------------------ diff --git a/execution_chain/core/chain/forked_chain/chain_private.nim b/execution_chain/core/chain/forked_chain/chain_private.nim index 30da560ad0..c47c73897c 100644 --- a/execution_chain/core/chain/forked_chain/chain_private.nim +++ b/execution_chain/core/chain/forked_chain/chain_private.nim @@ -20,50 +20,86 @@ 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], + generatedBal: Opt[BlockAccessListRef], +) = template header(): Header = blk.header txFrame.persistTransactions(header.number, header.txRoot, blk.transactions) txFrame.persistReceipts(header.receiptsRoot, receipts) discard txFrame.persistUncles(blk.uncles) + if blk.withdrawals.isSome: txFrame.persistWithdrawals( header.withdrawalsRoot.expect("WithdrawalsRoot should be verified before"), - blk.withdrawals.get) + blk.withdrawals.get, + ) + if blk.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] = + blkHash, + blk.blockAccessList.get(), + ) + elif generatedBal.isSome: + txFrame.persistBlockAccessList( + blkHash, + generatedBal.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 = (not finalized or blk.blockAccessList.isNone()) and + c.com.isAmsterdamOrLater(header.timestamp), + ) - ?c.com.validateHeaderAndKinship(blk, vmState.parent, txFrame) + c.com.validateHeaderAndKinship(blk, vmState.parent, txFrame).isOkOr: + c.badBlocks.put(blkHash, (blk, vmState.blockAccessList)) + return err(error) template processBlock(): auto = - # When processing a finalized block, we optimistically assume that the state - # root will check out and delay such validation for when it's time to persist - # changes to disk - ?vmState.processBlock( + vmState.processBlock( blk, skipValidation = false, skipReceipts = false, skipUncles = true, - skipStateRootCheck = finalized and not c.eagerStateRoot - ) + # When processing a finalized block, we optimistically assume that the state + # root will check out and delay such validation for when it's time to persist + # changes to disk + skipStateRootCheck = finalized and not c.eagerStateRoot, + # Depending on the BAL retention period of clients, finalized blocks might + # be received without a BAL. In this case we skip checking the block BAL + # against the header bal hash. + skipPreExecBalCheck = finalized and blk.blockAccessList.isNone(), + # Finalized blocks are known to be canonical and therefore the bal hash + # in the header is known to be valid and so it should be good enough to + # simply check that the provide block BAL (when skipPreExecBalCheck = false) + # matches the header bal hash. In this case the post execution check can be + # skipped. + skipPostExecBalCheck = finalized and blk.blockAccessList.isSome(), + ).isOkOr: + c.badBlocks.put(blkHash, (blk, vmState.blockAccessList)) + return err(error) if not vmState.com.statelessProviderEnabled: processBlock() @@ -92,4 +128,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/chain/persist_blocks.nim b/execution_chain/core/chain/persist_blocks.nim index aad1060228..6a7fe5d713 100644 --- a/execution_chain/core/chain/persist_blocks.nim +++ b/execution_chain/core/chain/persist_blocks.nim @@ -69,9 +69,14 @@ proc getVmState( parent = ?txFrame.getBlockHeader(header.parentHash) doAssert txFrame.getSavedStateBlockNumber() == parent.number - vmState.init(parent, header, p.com, txFrame, storeSlotHash = storeSlotHash) + + vmState.init(parent, header, p.com, txFrame, storeSlotHash = storeSlotHash, + enableBalTracker = FullValidation in p.flags and + p.com.isAmsterdamOrLater(header.timestamp)) + p.vmState = vmState assign(p.parent, parent) + else: if header.number != p.parent.number + 1: return err("Only linear histories supported by Persister") @@ -157,6 +162,8 @@ proc persistBlock*(p: var Persister, blk: Block): Result[void, string] = skipReceipts = skipValidation and PersistReceipts notin p.flags, skipUncles = PersistUncles notin p.flags, skipStateRootCheck = skipValidation, + skipPreExecBalCheck = true, + skipPostExecBalCheck = skipValidation, ) if not vmState.com.statelessProviderEnabled: diff --git a/execution_chain/core/executor/process_block.nim b/execution_chain/core/executor/process_block.nim index 79fddf4ac6..3828a1b5ff 100644 --- a/execution_chain/core/executor/process_block.nim +++ b/execution_chain/core/executor/process_block.nim @@ -97,6 +97,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) @@ -114,11 +117,16 @@ proc processTransactions*( proc procBlkPreamble( vmState: BaseVMState, blk: Block, - skipValidation, skipReceipts, skipUncles: bool, + skipValidation, skipReceipts, skipUncles: bool, skipPreExecBalCheck: bool, ): Result[void, string] = 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: @@ -153,17 +161,21 @@ 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 skipPreExecBalCheck: + if blk.blockAccessList.isNone: + return err("Post-Amsterdam block body must have blockAccessList") 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") @@ -175,14 +187,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") @@ -215,6 +237,7 @@ proc procBlkEpilogue( skipValidation: bool, skipReceipts: bool, skipStateRootCheck: bool, + skipPostExecBalCheck: bool ): Result[void, string] = template header(): Header = blk.header @@ -238,11 +261,31 @@ proc procBlkEpilogue( withdrawalReqs = ?processDequeueWithdrawalRequests(vmState) consolidationReqs = ?processDequeueConsolidationRequests(vmState) + if not skipPostExecBalCheck and vmState.com.isAmsterdamOrLater(header.timestamp): + 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, @@ -301,19 +344,21 @@ proc procBlkEpilogue( proc processBlock*( vmState: BaseVMState, ## Parent environment of header/body block blk: Block, ## Header/body block to add to the blockchain - skipValidation: bool = false, - skipReceipts: bool = false, - skipUncles: bool = false, - skipStateRootCheck: bool = false, + skipValidation = false, + skipReceipts = false, + skipUncles = false, + skipStateRootCheck = false, + skipPreExecBalCheck = false, + skipPostExecBalCheck = false, ): Result[void, string] = ## Generalised function to processes `blk` for any network. - ?vmState.procBlkPreamble(blk, skipValidation, skipReceipts, skipUncles) + ?vmState.procBlkPreamble(blk, skipValidation, skipReceipts, skipUncles, skipPreExecBalCheck) # EIP-3675: no reward for miner in POA/POS if not vmState.com.proofOfStake(blk.header, vmState.ledger.txFrame): vmState.calculateReward(blk.header, blk.uncles) - ?vmState.procBlkEpilogue(blk, skipValidation, skipReceipts, skipStateRootCheck) + ?vmState.procBlkEpilogue(blk, skipValidation, skipReceipts, skipStateRootCheck, skipPostExecBalCheck) ok() 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/core_db/core_apps.nim b/execution_chain/db/core_db/core_apps.nim index 8d1ebc5f07..f09b4f2633 100644 --- a/execution_chain/db/core_db/core_apps.nim +++ b/execution_chain/db/core_db/core_apps.nim @@ -410,18 +410,16 @@ proc getTransactions*( return ok(move(res)) proc persistBlockAccessList*( - db: CoreDbTxRef, blockAccessListHash: Hash32, bal: BlockAccessList) = - db.put(blockAccessListHashKey(blockAccessListHash).toOpenArray, bal.encode()) + db: CoreDbTxRef, blockHash: Hash32, bal: BlockAccessList) = + db.put(blockHashToBlockAccessListKey(blockHash).toOpenArray, bal.encode()) .expect("persistBlockAccessList should succeed") proc getBlockAccessList*( - db: CoreDbTxRef, - blockAccessListHash: Hash32): Result[Opt[BlockAccessList], string] = - if blockAccessListHash == EMPTY_BLOCK_ACCESS_LIST_HASH: - return ok(Opt.some(default(BlockAccessList))) - - let balBytes = db.getOrEmpty(blockAccessListHashKey(blockAccessListHash).toOpenArray).valueOr: - return err("getBlockAccessList: " & $$error) + db: CoreDbTxRef, blockHash: Hash32): Result[Opt[BlockAccessList], string] = + let balBytes = db.getOrEmpty( + blockHashToBlockAccessListKey(blockHash).toOpenArray + ).valueOr: + return err("getBlockAccessList: " & $$error) if balBytes == EmptyBlob: return ok(Opt.none(BlockAccessList)) 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/db/storage_types.nim b/execution_chain/db/storage_types.nim index a2606fd6ec..843b3fdb25 100644 --- a/execution_chain/db/storage_types.nim +++ b/execution_chain/db/storage_types.nim @@ -111,7 +111,7 @@ func blockHashToWitnessKey*(h: Hash32): DbKey {.inline.} = result.data[1 .. 32] = h.data result.dataEndPos = uint8 32 -func blockAccessListHashKey*(h: Hash32): DbKey {.inline.} = +func blockHashToBlockAccessListKey*(h: Hash32): DbKey {.inline.} = result.data[0] = byte ord(blockAccessList) result.data[1 .. 32] = h.data result.dataEndPos = uint8 32 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/gas_costs.nim b/execution_chain/evm/interpreter/gas_costs.nim index b0484a18c3..4b3c01a59e 100644 --- a/execution_chain/evm/interpreter/gas_costs.nim +++ b/execution_chain/evm/interpreter/gas_costs.nim @@ -66,9 +66,10 @@ type # - μ: a value popped from the stack or its size. kind*: Op - isNewAccount*: bool + isNewAccount*: proc(): bool {.gcsafe, raises: [].} gasLeft*: GasInt - gasCallEIPs*: GasInt + gasCallEIP2929*: proc(): GasInt {.gcsafe, raises: [].} + gasCallDelegate*: proc(): GasInt {.gcsafe, raises: [].} contractGas*: UInt256 currentMemSize*: GasNatural memOffset*: GasNatural @@ -351,7 +352,7 @@ template gasCosts(fork: EVMFork, prefix, ResultGasCostsName: untyped) = static(FeeSchedule[GasLogData]) * memLength + static(4 * FeeSchedule[GasLogTopic])) - func `prefix gasCall`(value: UInt256, params: GasParams): EvmResult[CallGasResult] {.nimcall.} = + proc `prefix gasCall`(value: UInt256, params: GasParams): EvmResult[CallGasResult] {.nimcall.} = # From the Yellow Paper, going through the equation from bottom to top # https://ethereum.github.io/yellowpaper/paper.pdf#appendix.H # @@ -373,20 +374,30 @@ template gasCosts(fork: EVMFork, prefix, ResultGasCostsName: untyped) = # the current implementation - https://github.com/ethereum/EIPs/issues/8 # Both gasCost and childGasLimit are always on positive side - - var gasLeft = params.gasLeft - if gasLeft < params.gasCallEIPs: - return err(opErr(OutOfGas)) - gasLeft -= params.gasCallEIPs - - var gasCost: GasInt = `prefix gasMemoryExpansion`( + var + gasLeft = params.gasLeft + gasCost: GasInt = `prefix gasMemoryExpansion`( params.currentMemSize, params.memOffset, params.memLength) - var childGasLimit: GasInt + # Cxfer + if not value.isZero and params.kind in {Call, CallCode}: + gasCost += static(GasInt(FeeSchedule[GasCallValue])) + # Cextra + gasCost += static(GasInt(FeeSchedule[GasCall])) + + gasCost += params.gasCallEIP2929() + + if gasLeft < gasCost: + return err(opErr(OutOfGas)) + + gasCost += params.gasCallDelegate() + + if gasLeft < gasCost: + return err(opErr(OutOfGas)) # Cnew_account - if params.isNewAccount and params.kind == Call: + if params.isNewAccount() and params.kind == Call: when fork < FkSpurious: # Pre-EIP161 all account creation calls consumed 25000 gas. gasCost += static(GasInt(FeeSchedule[GasNewAccount])) @@ -397,18 +408,13 @@ template gasCosts(fork: EVMFork, prefix, ResultGasCostsName: untyped) = if not value.isZero: gasCost += static(GasInt(FeeSchedule[GasNewAccount])) - # Cxfer - if not value.isZero and params.kind in {Call, CallCode}: - gasCost += static(GasInt(FeeSchedule[GasCallValue])) - - # Cextra - gasCost += static(GasInt(FeeSchedule[GasCall])) - if gasLeft < gasCost: return err(opErr(OutOfGas)) + gasLeft -= gasCost # Cgascap + var childGasLimit: GasInt when fork >= FkTangerine: # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md let gas = `prefix all_but_one_64th`(gasLeft) @@ -422,11 +428,10 @@ template gasCosts(fork: EVMFork, prefix, ResultGasCostsName: untyped) = return err(gasErr(GasIntOverflow)) childGasLimit = params.contractGas.truncate(GasInt) - if gasCost.u256 + childGasLimit.u256 + params.gasCallEIPs.u256 > high(GasInt).u256: + if gasCost.u256 + childGasLimit.u256 > high(GasInt).u256: return err(gasErr(GasIntOverflow)) gasCost += childGasLimit - gasCost += params.gasCallEIPs # Ccallgas - Gas sent to the child message if not value.isZero and params.kind in {Call, CallCode}: diff --git a/execution_chain/evm/interpreter/op_handlers/oph_call.nim b/execution_chain/evm/interpreter/op_handlers/oph_call.nim index 6f36fbd20b..e34d40c57a 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_call.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_call.nim @@ -55,7 +55,8 @@ type memOffset: int memLength: int contractAddress: Address - gasCallEIPs: GasInt + gasCallEIP2929: proc(): GasInt {.gcsafe, raises: [].} + gasCallDelegate: proc(): GasInt {.gcsafe, raises: [].} proc gasCallEIP2929(c: Computation, address: Address): GasInt = c.vmState.mutateLedger: @@ -81,16 +82,27 @@ proc updateStackAndParams(q: var LocalParams; c: Computation) = q.memOffset = q.memOutPos q.memLength = q.memOutLen + let codeAddress = q.codeAddress + # EIP2929: This came before old gas calculator # because it will affect `c.gasMeter.gasRemaining` # and further `childGasLimit` - if FkBerlin <= c.fork: - q.gasCallEIPs = gasCallEIP2929(c, q.codeAddress) + q.gasCallEIP2929 = + proc(): GasInt = + if FkBerlin <= c.fork: + gasCallEIP2929(c, codeAddress) + else: + 0.GasInt + + q.gasCallDelegate = + proc(): GasInt = + if FkPrague <= c.fork: + let delegateTo = parseDelegationAddress(c.getCode(codeAddress)).valueOr: + return 0.GasInt + delegateResolutionCost(c, delegateTo) + else: + 0.GasInt - if FkPrague <= c.fork: - let delegateTo = parseDelegationAddress(c.getCode(q.codeAddress)) - if delegateTo.isSome: - q.gasCallEIPs += delegateResolutionCost(c, delegateTo[]) proc callParams(c: Computation): EvmResult[LocalParams] = ## Helper for callOp() @@ -202,17 +214,19 @@ proc callOp(cpt: VmCpt): EvmResultVoid = let p = ? cpt.callParams + isNewAccount = proc(): bool = not cpt.accountExists(p.contractAddress) (gasCost, childGasLimit) = ? cpt.gasCosts[Call].c_handler( p.value, GasParams( - kind: Call, - isNewAccount: not cpt.accountExists(p.contractAddress), - gasLeft: cpt.gasMeter.gasRemaining, - gasCallEIPs: p.gasCallEIPs, - contractGas: p.gas, - currentMemSize: cpt.memory.len, - memOffset: p.memOffset, - memLength: p.memLength)) + kind: Call, + isNewAccount: isNewAccount, + gasLeft: cpt.gasMeter.gasRemaining, + gasCallEIP2929: p.gasCallEIP2929, + gasCallDelegate: p.gasCallDelegate, + contractGas: p.gas, + currentMemSize: cpt.memory.len, + memOffset: p.memOffset, + memLength: p.memLength)) ? cpt.opcodeGasCost(Call, gasCost, reason = $Call) @@ -256,17 +270,19 @@ proc callCodeOp(cpt: VmCpt): EvmResultVoid = ## 0xf2, Message-call into this account with an alternative account's code. let p = ? cpt.callCodeParams + isNewAccount = proc(): bool = not cpt.accountExists(p.contractAddress) (gasCost, childGasLimit) = ? cpt.gasCosts[CallCode].c_handler( p.value, GasParams( - kind: CallCode, - isNewAccount: not cpt.accountExists(p.contractAddress), - gasLeft: cpt.gasMeter.gasRemaining, - gasCallEIPs: p.gasCallEIPs, - contractGas: p.gas, - currentMemSize: cpt.memory.len, - memOffset: p.memOffset, - memLength: p.memLength)) + kind: CallCode, + isNewAccount: isNewAccount, + gasLeft: cpt.gasMeter.gasRemaining, + gasCallEIP2929: p.gasCallEIP2929, + gasCallDelegate: p.gasCallDelegate, + contractGas: p.gas, + currentMemSize: cpt.memory.len, + memOffset: p.memOffset, + memLength: p.memLength)) ? cpt.opcodeGasCost(CallCode, gasCost, reason = $CallCode) @@ -311,17 +327,19 @@ proc delegateCallOp(cpt: VmCpt): EvmResultVoid = ## code, but persisting the current values for sender and value. let p = ? cpt.delegateCallParams + isNewAccount = proc(): bool = not cpt.accountExists(p.contractAddress) (gasCost, childGasLimit) = ? cpt.gasCosts[DelegateCall].c_handler( p.value, GasParams( - kind: DelegateCall, - isNewAccount: not cpt.accountExists(p.contractAddress), - gasLeft: cpt.gasMeter.gasRemaining, - gasCallEIPs: p.gasCallEIPs, - contractGas: p.gas, - currentMemSize: cpt.memory.len, - memOffset: p.memOffset, - memLength: p.memLength)) + kind: DelegateCall, + isNewAccount: isNewAccount, + gasLeft: cpt.gasMeter.gasRemaining, + gasCallEIP2929: p.gasCallEIP2929, + gasCallDelegate: p.gasCallDelegate, + contractGas: p.gas, + currentMemSize: cpt.memory.len, + memOffset: p.memOffset, + memLength: p.memLength)) ? cpt.opcodeGasCost(DelegateCall, gasCost, reason = $DelegateCall) @@ -360,17 +378,19 @@ proc staticCallOp(cpt: VmCpt): EvmResultVoid = let p = ? cpt.staticCallParams + isNewAccount = proc(): bool = not cpt.accountExists(p.contractAddress) (gasCost, childGasLimit) = ? cpt.gasCosts[StaticCall].c_handler( p.value, GasParams( - kind: StaticCall, - isNewAccount: not cpt.accountExists(p.contractAddress), - gasLeft: cpt.gasMeter.gasRemaining, - gasCallEIPs: p.gasCallEIPs, - contractGas: p.gas, - currentMemSize: cpt.memory.len, - memOffset: p.memOffset, - memLength: p.memLength)) + kind: StaticCall, + isNewAccount: isNewAccount, + gasLeft: cpt.gasMeter.gasRemaining, + gasCallEIP2929: p.gasCallEIP2929, + gasCallDelegate: p.gasCallDelegate, + contractGas: p.gas, + currentMemSize: cpt.memory.len, + memOffset: p.memOffset, + memLength: p.memLength)) ? cpt.opcodeGasCost(StaticCall, gasCost, reason = $StaticCall) 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/op_handlers/oph_sysops.nim b/execution_chain/evm/interpreter/op_handlers/oph_sysops.nim index 9fbc9d1184..04891b85f4 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_sysops.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_sysops.nim @@ -122,17 +122,28 @@ proc selfDestructEIP2929Op(cpt: VmCpt): EvmResultVoid = ## selfDestructEIP2929 (auto generated comment) ? cpt.checkInStaticContext() + let beneficiary = ? cpt.stack.popAddress() + + var beneficiaryIsCold = false + cpt.vmState.mutateLedger: + if not db.inAccessList(beneficiary): + beneficiaryIsCold = true + + var staticGasCosts = cpt.gasCosts[SelfDestruct].sc_handler(false) + if beneficiaryIsCold: + staticGasCosts += ColdAccountAccessCost + if staticGasCosts > cpt.gasMeter.gasRemaining: + return EvmResultVoid.err(gasErr(OutOfGas)) + let - beneficiary = ? cpt.stack.popAddress() isDead = not cpt.accountExists(beneficiary) balance = cpt.getBalance(cpt.msg.contractAddress) condition = isDead and not balance.isZero - var - gasCost = cpt.gasCosts[SelfDestruct].sc_handler(condition) + var gasCost = cpt.gasCosts[SelfDestruct].sc_handler(condition) cpt.vmState.mutateLedger: - if not db.inAccessList(beneficiary): + if beneficiaryIsCold: db.accessList(beneficiary) gasCost = gasCost + ColdAccountAccessCost diff --git a/execution_chain/evm/interpreter_dispatch.nim b/execution_chain/evm/interpreter_dispatch.nim index e8a78e06bf..3d33ddf25d 100644 --- a/execution_chain/evm/interpreter_dispatch.nim +++ b/execution_chain/evm/interpreter_dispatch.nim @@ -70,10 +70,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 @@ -97,6 +103,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. @@ -107,19 +115,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/debug.nim b/execution_chain/rpc/debug.nim index cbd585ca52..ee27c3a3db 100644 --- a/execution_chain/rpc/debug.nim +++ b/execution_chain/rpc/debug.nim @@ -13,7 +13,8 @@ import # std/json, stew/byteutils, json_rpc/rpcserver, - # ./rpc_utils, + web3/[eth_api_types, conversions], + ./rpc_utils, ./rpc_types, #../tracer, #../evm/types, @@ -21,10 +22,28 @@ import ../beacon/web3_eth_conv, ../core/tx_pool, ../core/chain/forked_chain, - ../stateless/witness_types, - web3/conversions + ../stateless/witness_types -# type +type + BadBlock = object + `block`: BlockObject + generatedBlockAccessList: Opt[BlockAccessList] + hash: Hash32 + rlp: seq[byte] + +BadBlock.useDefaultSerializationIn JrpcConv + +ExecutionWitness.useDefaultSerializationIn JrpcConv + +# Block access list json serialization +AccountChanges.useDefaultSerializationIn JrpcConv +SlotChanges.useDefaultSerializationIn JrpcConv +StorageChange.useDefaultSerializationIn JrpcConv +BalanceChange.useDefaultSerializationIn JrpcConv +NonceChange.useDefaultSerializationIn JrpcConv +CodeChange.useDefaultSerializationIn JrpcConv + +#type # TraceOptions = object # disableStorage: Opt[bool] # disableMemory: Opt[bool] @@ -33,7 +52,6 @@ import # disableStateDiff: Opt[bool] # TraceOptions.useDefaultSerializationIn JrpcConv -ExecutionWitness.useDefaultSerializationIn JrpcConv # proc isTrue(x: Opt[bool]): bool = # result = x.isSome and x.get() == true @@ -84,6 +102,30 @@ proc getExecutionWitness*(chain: ForkedChainRef, blockHash: Hash32): Result[Exec ok(executionWitness) +proc getBlockAccessList*( + chain: ForkedChainRef, + blockHash: Hash32): Result[BlockAccessList, string] = + + let txFrame = chain.txFrame(blockHash).txFrameBegin() + defer: + txFrame.dispose() + + let bal = (?txFrame.getBlockAccessList(blockHash)).valueOr: + return err("Block access list not found") + + ok(bal) + +proc getTotalDifficulty(chain: ForkedChainRef, blockHash: Hash32): UInt256 = + + let txFrame = chain.txFrame(blockHash).txFrameBegin() + defer: + txFrame.dispose() + + let totalDifficulty = txFrame.getScore(blockHash).valueOr: + return chain.baseTxFrame().headTotalDifficulty() + + return totalDifficulty + proc setupDebugRpc*(com: CommonRef, txPool: TxPoolRef, server: RpcServer) = let # chainDB = com.db @@ -232,3 +274,36 @@ proc setupDebugRpc*(com: CommonRef, txPool: TxPoolRef, server: RpcServer) = raise newException(ValueError, error) rlp.encode(header).to0xHex() + + server.rpc("debug_getBlockAccessList") do(quantityTag: BlockTag) -> BlockAccessList: + ## Returns a block access list for the given block number. + let header = chain.headerFromTag(quantityTag).valueOr: + raise newException(ValueError, "Header not found") + + chain.getBlockAccessList(header.computeBlockHash()).valueOr: + raise newException(ValueError, error) + + server.rpc("debug_getBlockAccessListByBlockHash") do(blockHash: Hash32) -> BlockAccessList: + ## Returns a block access list for the given block hash. + + chain.getBlockAccessList(blockHash).valueOr: + raise newException(ValueError, error) + + server.rpc("debug_getBadBlocks") do() -> seq[BadBlock]: + ## Returns a list of the most recently processed bad blocks. + var badBlocks: seq[BadBlock] + + let blks = chain.getBadBlocks() + for b in blks: + let + (blk, bal) = b + blkHash = blk.header.computeBlockHash() + + badBlocks.add BadBlock( + `block`: populateBlockObject( + blkHash, blk, chain.getTotalDifficulty(blkHash), fullTx = true), + generatedBlockAccessList: bal.map(proc (bal: auto): auto = bal[]), + hash: blkHash, + rlp: rlp.encode(blk)) + + badBlocks 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 3ca809780b..fea51ff0a5 100644 --- a/execution_chain/transaction/call_common.nim +++ b/execution_chain/transaction/call_common.nim @@ -44,6 +44,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[]) @@ -67,6 +69,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 @@ -87,6 +91,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): @@ -101,12 +107,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 @@ -186,12 +198,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 = @@ -226,6 +242,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..0ee58069e0 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="v2.0.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..a7cb4d2153 100644 --- a/tests/eest/eest_blockchain_test.nim +++ b/tests/eest/eest_blockchain_test.nim @@ -18,11 +18,12 @@ const eestType = "blockchain_tests" eestReleases = [ "eest_develop", - "eest_devnet" + "eest_devnet", + "eest_bal" ] const skipFiles = [ - "" + "" ] runEESTSuite( diff --git a/tests/eest/eest_engine_test.nim b/tests/eest/eest_engine_test.nim index 04bc8803fd..a5af8c7c7e 100644 --- a/tests/eest/eest_engine_test.nim +++ b/tests/eest/eest_engine_test.nim @@ -20,7 +20,8 @@ const eestType = "blockchain_tests_engine" eestReleases = [ "eest_develop", - "eest_devnet" + "eest_devnet", + "eest_bal" ] const skipFiles = [ diff --git a/tests/test_block_access_list_builder.nim b/tests/test_block_access_list_builder.nim index e3119e8531..a6d0f5e4cb 100644 --- a/tests/test_block_access_list_builder.nim +++ b/tests/test_block_access_list_builder.nim @@ -32,7 +32,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 @@ -55,7 +55,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 @@ -74,7 +74,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 @@ -91,7 +91,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 @@ -108,7 +108,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 @@ -124,7 +124,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 @@ -168,7 +168,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 7f6d41d474..94effc4f81 100644 --- a/tests/test_block_access_list_validation.nim +++ b/tests/test_block_access_list_validation.nim @@ -28,7 +28,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() @@ -69,7 +69,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": @@ -77,8 +77,9 @@ 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] + check bal.validate(bal.computeBlockAccessListHash()).isErr() test "Account changes out of order should fail validation": @@ -86,7 +87,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() @@ -96,7 +97,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() @@ -106,7 +107,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() @@ -116,7 +117,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() @@ -126,7 +127,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() @@ -136,7 +137,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() @@ -146,7 +147,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()