Skip to content

test_bal_intra_tx_round_trip_after_prior_tx_write()

Documentation for tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py::test_bal_intra_tx_round_trip_after_prior_tx_write@87aba1a3.

Generate fixtures for these test cases for Amsterdam with:

fill -v tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py::test_bal_intra_tx_round_trip_after_prior_tx_write --fork Amsterdam

Verify a per-tx no-op SSTORE round-trip is not recorded as a storage change when an earlier tx in the same block wrote the slot.

Per EIP-7928 §Storage, a write is compared against "the storage value as of immediately before the current block_access_index (i.e., the cumulative state from all prior indices, falling back to the pre-block state)", and "a no-op write MUST NOT remove storage_changes entries from earlier indices for the same slot".

Both txs call the same contract whose runtime SSTOREs 0xff then 0x42 to slot 1. Tx 1 changes slot 1 from 0 to 0x42 (real change). Tx 1's write becomes tx 2's baseline, so tx 2's 0x42 -> 0xff -> 0x42 nets to a no-op and only tx 1 appears in storage_changes (with tx 1's entry left intact).

Source code in tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_cross_index.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def test_bal_intra_tx_round_trip_after_prior_tx_write(
    pre: Alloc,
    blockchain_test: BlockchainTestFiller,
) -> None:
    """
    Verify a per-tx no-op SSTORE round-trip is not recorded as a storage
    change when an earlier tx in the same block wrote the slot.

    Per EIP-7928 §Storage, a write is compared against "the storage value
    as of immediately before the current `block_access_index` (i.e., the
    cumulative state from all prior indices, falling back to the pre-block
    state)", and "a no-op write MUST NOT remove `storage_changes` entries
    from earlier indices for the same slot".

    Both txs call the same contract whose runtime SSTOREs 0xff then 0x42
    to slot 1. Tx 1 changes slot 1 from 0 to 0x42 (real change). Tx 1's
    write becomes tx 2's baseline, so tx 2's 0x42 -> 0xff -> 0x42 nets to
    a no-op and only tx 1 appears in `storage_changes` (with tx 1's entry
    left intact).
    """
    # Runtime: write 0xff to slot 1, then write 0x42 to slot 1, STOP.
    # The two SSTOREs hit the journal at every call, but the net effect
    # on the slot is `pre_value -> 0x42` — a no-op when pre_value == 0x42.
    contract_code = Bytecode(Op.SSTORE(1, 0xFF) + Op.SSTORE(1, 0x42) + Op.STOP)
    contract = pre.deploy_contract(code=contract_code)

    sender_a = pre.fund_eoa()
    sender_b = pre.fund_eoa()

    # Both txs go into the same block; tx 1 makes the real 0 -> 0x42
    # change, tx 2 starts from 0x42 and ends at 0x42 (per-tx no-op).
    tx_1 = Transaction(sender=sender_a, to=contract)
    tx_2 = Transaction(sender=sender_b, to=contract)

    expected_block_access_list = BlockAccessListExpectation(
        account_expectations={
            contract: BalAccountExpectation(
                # Only tx 1's real change appears. Tx 2's same-value
                # round-trip MUST be classified as a read for tx 2.
                storage_changes=[
                    BalStorageSlot(
                        slot=1,
                        slot_changes=[
                            BalStorageChange(
                                block_access_index=1, post_value=0x42
                            ),
                        ],
                    ),
                ],
                # `storage_changes` is only verified as a sub-sequence
                # at fill time, so this additional check guards against a
                # reference regression that emits tx 2's no-op as a
                # spurious index-2 change.
                absent_values=BalAccountAbsentValues(
                    storage_changes=[
                        BalStorageSlot(
                            slot=1,
                            slot_changes=[
                                BalStorageChange(
                                    block_access_index=2, post_value=0x42
                                ),
                            ],
                        ),
                    ],
                ),
            ),
            sender_a: BalAccountExpectation(
                nonce_changes=[
                    BalNonceChange(block_access_index=1, post_nonce=1),
                ],
            ),
            sender_b: BalAccountExpectation(
                nonce_changes=[
                    BalNonceChange(block_access_index=2, post_nonce=1),
                ],
            ),
        }
    )

    blockchain_test(
        pre=pre,
        blocks=[
            Block(
                txs=[tx_1, tx_2],
                expected_block_access_list=expected_block_access_list,
            ),
        ],
        post={contract: Account(storage={1: 0x42})},
    )

Parametrized Test Cases

This test generates 1 parametrized test case across 1 fork.