Really interesting proposal! My first-pass thoughts:
I think the most pratical way to do this would be via access lists. So any transaction would have an (EIP-2930-style, but adapted to the new address scheme) list of state locations that would be part of the signed tx data. The witness itself (meaning the state at the specified locations, and inclusion / exclusion proofs) would not be signed over. Transactions could include witness data during propagation, but would only be included into blocks in their raw form. The block itself would then include one aggregated witness created by the block producer, so that:
- If a transaction tries to access old state (meaning from a previous epoch and not in the latest tree) that is provided by the witness, the state access always succeeds (even if the location was not in the tx’s access list) and tx execution continues.
- If a transaction tries to access old state not provided by the witness and not present in the access list, transaction execution fails (and the whole tx is reverted, not only the current level), but the tx sender is still charged.
- If a transaction tries to access old state not provided by the witness, but present in the access list, the block is considered invalid.
In addition (less confident on these):
- Access lists are fully charged (for state refresh cost, except for those locations that are already part of the latest tree, where only calldata price is charged) at the beginning of a tx. At the end of the tx, all locations not touched during execution are refunded (up to calldata cost).
- A block with unused witness parts is considered invalid, even if these locations were included in access lists of the block’s txs.
- There could be a new tx type for explicitly refreshing locations (i.e. moving them into the latest tree), without any execution attached. For those the block witness would be required to include all such locations.
I think this would likely have to be enshrined into protocol, meaning that the rules listed above would be modified with:
- If a transaction tries to access old state not provided by the witness and not present in the access list, but where the witness includes an exclusion proof against S_{e-1}, transaction execution fails (and the whole tx is reverted, not only the current level), but the tx sender is still charged.
- If a transaction tries to access old state not provided by the witness and not present in the access list, and if the witness does not include an exclusion proof against S_{e-1}, the block is considered invalid.
Without this modification, users would have to add all accessed S_{e-1} locations to a tx’s access list, which for many applications would require full execution of the transaction, which would in turn require the user to hold the relevant S_{e-1} state and make this "block producers keep S_{e-1}" rule mostly useless. Note also that for any such transactions there are new UX challenges regardless, as without the necessary state users cannot easily assess the tx’s likely outcome (including its gas consumption).
It seems to me that users would have to pay the refresh cost even for access to state from the S_{e-1} tree. In addition, if the transaction is sent via the “vanilla” transaction pool, it would likely have to come with a witness for the sender account regardless, to allow propagating nodes to assess its validity.
Transaction pool propagation rules would likely also require transactions to come with full witnesses attached for their access lists (with the discussed S_{e-1} exceptions), although state providers that voluntarily store more than the latest two state trees could offer ways to send txs without those witnesses directly.
edit:
I just realized that in the updated version of this proposal here all nodes are required to also hold S_{e-1}. That makes a lot of sense to me and simplifies these S_{e-1} special case rules I described above. Accessing state from S_{e-1} would presumably still have to be more expensive to account for moving the state to S_e.