EIP-7906: Transaction Assertions via State Diff Opcode

looks ok.
regarding the entries offset (and order): TXTRACE implicitly defines few tables - balances, slots, deployed, events.
The opcode should clearly define the ordering or entries of each of those tables.
ordering can either by by action time or sorted by address+slot.
note that balances and slots can be modified multiple times, so using the action time is tricky (should it be the first change or the last?)
Also, we can’t keep the relative ordering of different events (did an event happened before/after the slot change or contract deploy)
My proposal: sort balances table by address, and slots table by (address, slot).
events should keep their order - which indeed means you can’t easily find all events of a single contract (which is less important: I want all event of my account, not all even of a specific token..)

1 Like

Due to a mishap, I accidentally deleted that draft. However, the core idea is that you pre-declare specific locations (such as accounts or storage slots), and the sub-frames are restricted to modifying only those areas. This saves gas because it avoids post-execution state checks, instead verifying constraints directly during the execution of state-mutating opcodes (e.g., SSTORE, CALL). I don’t think it needs to be a separate draft; it could simply be integrated into your existing proposal as an additional mode.

While full state access is beneficial, in most cases, we only need to control specific state points to save on costs—rather than iterating through a list of completed state changes. Consequently, adding lightweight state immutability mechanisms ensures safety while offering greater flexibility in contract design. For instance, if you wish to transfer USDC, simply restricting the scope of changes to the balance slots prior to the transaction suffices, eliminating the need for further state access to perform checks.

Yes, that model can do SOME of the work of assertion - namely, prevent specific storage changes. However, you can’t implement relaxed policies (e.g, limit total transferred amount)
The TXDIFF now allows checking modifications to specific contract without iteration, and thus is only slightly less gas-efficient, while being much more powerful.

It seems you have misunderstood my point. I want to allow changes only at specific positions, while the remaining positions must remain unchanged. You are certainly free to monitor the variations at those specific positions, but doing so is not a mandatory requirement.

I do not think EIP-7906 as it currently stands would benefit from adding an additional “pre-declaration” mechanism, but restoring your draft proposal may still be helpful to inform the discussion around transaction assertions.

Right, unfortunately there is no *reasonable* mechanism that would provide information in the form of “slot A changed after event Y was emitted” or so. However, I don’t think such assertions would be a good idea anyways, so I am okay with the proposed sorting of slots and balances, while events remain sequential.

Please note that gas prices are not set for EIP-7906 and my expectation is that they will be very cheap as the opcodes only provide the “hot” data of the EVM during a transaction.

Yes, I admit your idea is more robust; the original purpose of that draft was to patch the gaps in checks that the InvariantGuard library couldn’t cover. InvariantGuard checks for changes at specific locations but cannot prevent state changes occurring outside its monitored scope—which is why that EIP was proposed. However, your ideas seem to have met those expectations. Upon reflection, the only remaining advantage might be the gas cost. If you have a set of declared allowed positions and a set of runtime state changes, you face O(n^2) lookup complexity—or you would need to use persistent storage to leverage randomness, which would incur higher costs. Furthermore, you would have to verify that no unintended changes occurred, whereas preventing them before the transaction begins eliminates the need for such post-execution checks.

1 Like

I’ll do a brain dump here while reading this EIP :smiley: :+1:

  • POST_TX frames must form a contiguous trailing suffix of tx.frames: once any frame has mode POST_TX, every subsequent frame in the transaction must also have mode POST_TX. A frame transaction violating this is invalid.

This feels rather restrictive. I get the idea, but any other features we might want to add at some point will likely change this.
Why don’t we group POST_TX after atomic batches? These apply to the previous batch. This is also not what we want in some situations though, in some cases you want the scope of the entire transaction.

I could also imagine that there are signers (esp. fee sponsors) who care about specific parts of the transaction, for instance they care that X happened, but they dont care if Y happened. It feels like they should be able to sign something which guarantees that. Maybe this can already be expressed as EVM code and that means we dont have to spec it because this the responsibility of the app layer.

  • A POST_TX frame is executed as a STATICCALL, disallowing all state manipulation. A POST_TX frame has no valid reason to call APPROVE, and its usage is forbidden.

What is meant by forbidden? Likely: consume all gas and exit. What happens if they use APPROVE though. Is the transaction invalid? Does it revert all changes? This is unclear and ambiguous, please fix.

Transaction Trace Opcode

the total number of changed X

What does this mean? The total number of changes? Or the total unique accounts/slots changed? In case of contract deployments, are these successful deployments? So if a creation reverts, this is not part of the total number of deployments?

For the first part of the trace opcode, it feels like this should be an inspection of the EIP-7928: Block-Level Access Lists block level access list (BAL) up to that point. This also makes the implementation much easier, since we already have to track the BAL. We can directly read these storage/address/contract values from there.

State Difference Semantics

The before values

This is a nasty one, because it means we have to journal the “before” values. This is NOT part of the BAL, which means we have to start explicitly journaling the “before” values of these accounts/storage keys. This might incur some performance problems for some clients. For storage slots, we only care if the slot existed for the gas calculation (creating a slot is more expensive than changing an existing one, w.r.t. pre-tx state). For the balance we need the original value beacuse we need to deduct/add it, so this seems fine. Thinking about it, it does not make sense if a client has a db purely to store if a key exists or not, if you can also directly store the value. But maybe there are use cases for such DB layout.

In general

My feeling is that this txtrace should be changed to directly check and enumerate the BAL. By doing this you can see all the exact changes done so far, it is already part of EVM, but moreover, you can also see all the side effects. So you can also confirm that something did NOT happen, for instance, there were only two balances changed in this ERC20 contract (there was not a hidden extra transfer for instance). We can then check the BAL to see the final value.
I do realize that for verification we likely also want the prestate value, for instance to see that the balance has gone up by at least X of an ERC20 token for instance.

I also feel that this should not be a new POST_TX frame but rather a SENDER frame which is part of an atomic batch operation. We batch this as the final atomic batch. We can even use a specific verify contract (deployed via EIP-7997 factory) to ensure this is at the same address for all chains. This does the state changes verification, and if something is wrong (this obv. depends on the contract and the calldata and whatever we want to check) then this reverts, thus cancelling the entire atomic batch. I don’t see why we should introduce this extra frame, and these extra rules imposed on it seems unnecessarily restrictive.

Let me know your thoughts and if anything is unclear, happy to discuss :smile: :+1:

1 Like

Hello @jochem-brouwer

Thank you for this review. It touches a number of very important topics that should be thoroughly discussed, so I will go over all of them and highlight the choices we have at each step.

  1. Transaction diff assertions vs. batch diff assertions

I agree that being able to define a POST_TX assertion frame per atomic batch is a powerful feature and I would gladly add it. It is not currently in the EIP because I am worried that it would add some complexity to the EIP while it is criticised for being too complex.

Technically, the main question here is which state does the POST_TX assertion should see as the ‘original’ one.

If a POST_TX frame is scoped to an atomic batch, the ‘original’ value it sees must be the state outcome of a previous batch - and not the state the user could observe before signing a transaction.
(For example, pre-tx state is 0, batch A sets it to 1, batch B sets it to 2, a POST_TX frame from batch B sees ‘before’ as 1 and ‘after’ as 2, but the user saw 0 before the transaction.)
I am concerned this might become pretty hard for the wallets to track.

We will also need to be careful with what reverting an atomic batch does with the outcome state - the transaction would probably still need a combined, transaction-scoped assertion frame to make sure the aggregate state of all executed and reverted atomic batches does not lead to an invalid state.

  1. Explicit diff snapshots

Regarding the “signers who care about specific parts of the transaction”, it is actually pretty easy to express the “X happened, but I don’t care if Y happened” rule with the EVM assertion as long as X & Y are clearly understood and scoped - the assertion can just ignore everything that is not X.

But I am wondering if these cases could benefit from POST_TX frames scoped to explicit snapshots, instead of an atomic batch and an explicit split between X and Y. The verifying contract could specify where the part that interests them begins, and then observe only the relevant diffs with their POST_TX, and we could even have multiple snapshots running simultaneously for different scopes. This all can be achieved with a relatively simple addition to the opcodes, but it would be great to hear from wallet & paymaster developers first.

  1. Following BAL shape vs. defining a custom assertion-specific structure

The BALs contain the majority of parameters that we might want to expose for Transaction Assertions; however, they were never explicitly designed to serve this use case, and are missing things we might need - “before” values, events, codehashes, gas charges, etc. So by attaching the assertions opcodes API to the shape defined by BALs, we make an implicit decision to abandon everything that cannot be read from the BAL - regardless of how useful it might be for Transaction Assertions.

Alternatively, we could take into account the added cost of tracking and make sure we don’t cause any performance problems for any client. For what it’s worth, the current EVM already requires tracking the ‘original’ value of each modified storage slot before calculating the fair gas cost of the SSTORE according to EIP-2200 etc., so there should not be much overhead in exposing these values back to the EVM. The same likely applies to balances, nonces, and code, as EVM clients are probably already retaining their pre-transaction values via the ordinary call-revert journal, since a transaction/batch is always “revertible” back to the original state. All information we need is out there, even if it lives outside of the BAL shape and is not easily accessible, and if it is useful for assertions - hiding it should be an explicit decision for the Transaction Assertions EIP.

  1. A special SENDER frame vs. a new POST_TX frame

One reason for introducing the POST_TX frame was to allow the revert caused by a violated assertion to take effect across the entire transaction, ignoring the lines between atomic batches. This way we provide users with a bulletproof guarantee - if they see a POST_TX frame in their wallet, it’s rules are enforced.

With a simple SENDER frame, this is only possible for a frame transaction with a single atomic batch, and users must be aware of that - which is a UX issue in my opinion.

Another reason for a POST_TX frame was to ensure there is no mechanism for an assertion script to be used to leak a ‘private’ contract state into the current transaction’s execution - which could be used to violate other contracts’ security assumptions, potentially extract aggressive MEV, create entangled state and so on.

We can certainly achieve a similar set of rules with a SENDER frame and a specific canonical “verify contract”, but if we ever allow an arbitrary contract to access the diff opcodes, it may be challenging to restrict a SENDER frame.

Also, I have been following some of the recent discussions around EIP-8141 and I became convinced that the “guarantor” frames might be the common ground between an explicit POST_TX and an unrestricted TXDIFF opcode.

  1. Clarity and fixes

Thanks for brigning those up - I will go over the specification again to make sure to address those!

I will be happy to hear you feedback on these topics and would be happy to discuss it in detail. Thanks again for the help!

With the Ethrex implementation and EIP-7906 testnet progressing, is a separate independent security review planned for the POST_TX, TXTRACE, EVENTDATACOPY and TXDIFF execution boundary?

If so, who is coordinating the review scope and provider decision?

Reuben Kassongo
Arctek Audits