|
10 | 10 |
|
11 | 11 | import pytest |
12 | 12 | from execution_testing import ( |
| 13 | + AccessList, |
| 14 | + Account, |
13 | 15 | Alloc, |
14 | 16 | BenchmarkTestFiller, |
15 | 17 | Block, |
16 | 18 | Bytecode, |
17 | 19 | Environment, |
18 | 20 | Fork, |
| 21 | + Hash, |
19 | 22 | JumpLoopGenerator, |
20 | 23 | Op, |
| 24 | + Storage, |
21 | 25 | TestPhaseManager, |
22 | 26 | Transaction, |
23 | 27 | While, |
@@ -364,3 +368,191 @@ def test_storage_access_warm( |
364 | 368 | blocks.append(Block(txs=[op_tx])) |
365 | 369 |
|
366 | 370 | benchmark_test(blocks=blocks) |
| 371 | + |
| 372 | + |
| 373 | +def storage_contract(sloads_before_sstore: bool) -> Bytecode: |
| 374 | + """ |
| 375 | + Storage contract for benchmark slot access. |
| 376 | +
|
| 377 | + # Calldata Layout: |
| 378 | + # - CALLDATA[0..31]: Number of slots to access |
| 379 | + # - CALLDATA[32..63]: Starting slot index |
| 380 | + # - CALLDATA[64..95]: Value to write |
| 381 | + """ |
| 382 | + setup = Bytecode() |
| 383 | + loop = Bytecode() |
| 384 | + cleanup = Bytecode() |
| 385 | + |
| 386 | + start_marker = 10 |
| 387 | + end_marker = 30 + (2 if sloads_before_sstore else 0) |
| 388 | + |
| 389 | + setup += ( |
| 390 | + Op.CALLDATALOAD(0) # num_slots |
| 391 | + + Op.CALLDATALOAD(32) # start_slot |
| 392 | + + Op.CALLDATALOAD(64) # value |
| 393 | + ) |
| 394 | + |
| 395 | + setup += Op.PUSH0 # Counter |
| 396 | + setup += Op.JUMPDEST |
| 397 | + # [counter, value, start_slot, num_slots] |
| 398 | + |
| 399 | + # Loop Condition: Counter < Num Slots |
| 400 | + loop += Op.DUP4 |
| 401 | + loop += Op.DUP2 |
| 402 | + loop += Op.LT |
| 403 | + loop += Op.ISZERO |
| 404 | + loop += Op.PUSH1(end_marker) |
| 405 | + loop += Op.JUMPI |
| 406 | + # [counter, value, start_slot, num_slots] |
| 407 | + |
| 408 | + # Loop Body: Store Value at Start Slot + Counter |
| 409 | + loop += Op.DUP1 |
| 410 | + loop += Op.DUP4 |
| 411 | + loop += Op.ADD |
| 412 | + loop += Op.DUP3 |
| 413 | + # [value, start_slot+counter, counter, value, start_slot, num_slots] |
| 414 | + |
| 415 | + if sloads_before_sstore: |
| 416 | + loop += Op.DUP2 |
| 417 | + loop += Op.SSTORE |
| 418 | + loop += Op.SLOAD |
| 419 | + loop += Op.POP |
| 420 | + else: |
| 421 | + loop += Op.SWAP1 |
| 422 | + loop += Op.SSTORE # STORAGE[start_slot + counter] = value |
| 423 | + # [counter, value, start_slot, num_slots] |
| 424 | + |
| 425 | + # Loop Post: Increment Counter |
| 426 | + loop += Op.PUSH1(1) |
| 427 | + loop += Op.ADD |
| 428 | + loop += Op.PUSH1(start_marker) |
| 429 | + loop += Op.JUMP |
| 430 | + # [counter + 1, value, start_slot, num_slots] |
| 431 | + |
| 432 | + # Cleanup: Stop |
| 433 | + cleanup += Op.JUMPDEST |
| 434 | + cleanup += Op.STOP |
| 435 | + |
| 436 | + assert len(setup) - 1 == start_marker |
| 437 | + assert len(setup) + len(loop) == end_marker |
| 438 | + print(f"setup: {len(setup)}, loop: {len(loop)}, cleanup: {len(cleanup)}") |
| 439 | + return setup + loop + cleanup |
| 440 | + |
| 441 | + |
| 442 | +@pytest.mark.parametrize("slot_count", [50, 100]) |
| 443 | +@pytest.mark.parametrize("use_access_list", [True, False]) |
| 444 | +@pytest.mark.parametrize( |
| 445 | + "contract_size", |
| 446 | + [ |
| 447 | + pytest.param(0, id="just_created"), |
| 448 | + pytest.param(1024, id="small"), |
| 449 | + pytest.param(12 * 1024, id="medium"), |
| 450 | + pytest.param(24 * 1024, id="xen"), |
| 451 | + ], |
| 452 | +) |
| 453 | +@pytest.mark.parametrize("sloads_before_sstore", [True, False]) |
| 454 | +@pytest.mark.parametrize("num_contracts", [1, 5, 10]) |
| 455 | +@pytest.mark.parametrize( |
| 456 | + "initial_value,write_value", |
| 457 | + [ |
| 458 | + pytest.param(0, 0, id="zero_to_zero"), |
| 459 | + pytest.param(0, 0xDEADBEEF, id="zero_to_nonzero"), |
| 460 | + pytest.param(0xDEADBEEF, 0, id="nonzero_to_zero"), |
| 461 | + pytest.param(0xDEADBEEF, 0xBEEFBEEF, id="nonzero_to_nonzero"), |
| 462 | + ], |
| 463 | +) |
| 464 | +def test_sstore_variants( |
| 465 | + benchmark_test: BenchmarkTestFiller, |
| 466 | + pre: Alloc, |
| 467 | + gas_benchmark_value: int, |
| 468 | + slot_count: int, |
| 469 | + use_access_list: bool, |
| 470 | + contract_size: int, |
| 471 | + sloads_before_sstore: bool, |
| 472 | + num_contracts: int, |
| 473 | + initial_value: int, |
| 474 | + write_value: int, |
| 475 | +) -> None: |
| 476 | + """ |
| 477 | + Benchmark SSTORE instruction with various configurations. |
| 478 | +
|
| 479 | + Variants: |
| 480 | + - use_access_list: Warm storage slots via access list |
| 481 | + - contract_size: Contract code size |
| 482 | + (just_created=0, small=1KB, medium=12KB, xen=24KB) |
| 483 | + - sloads_before_sstore: Number of SLOADs per slot before SSTORE |
| 484 | + - num_contracts: Number of contract instances (cold storage writes) |
| 485 | + - initial_value/write_value: Storage transitions |
| 486 | + (zero_to_zero, zero_to_nonzero, nonzero_to_zero, nonzero_to_nonzero) |
| 487 | + """ |
| 488 | + base_contract = storage_contract(sloads_before_sstore) |
| 489 | + padded_contract = base_contract |
| 490 | + |
| 491 | + if len(base_contract) < contract_size: |
| 492 | + padded_contract += Op.INVALID * (contract_size - len(base_contract)) |
| 493 | + |
| 494 | + slots_per_contract = slot_count // num_contracts |
| 495 | + |
| 496 | + txs = [] |
| 497 | + post = {} |
| 498 | + |
| 499 | + base_gas_per_contract = gas_benchmark_value // num_contracts |
| 500 | + gas_remainder = gas_benchmark_value % num_contracts |
| 501 | + |
| 502 | + for contract_idx in range(num_contracts): |
| 503 | + initial_storage = Storage() |
| 504 | + |
| 505 | + start_slot = contract_idx * slot_count |
| 506 | + for i in range(slots_per_contract): |
| 507 | + initial_storage[start_slot + i] = initial_value |
| 508 | + |
| 509 | + contract_addr = pre.deploy_contract( |
| 510 | + code=padded_contract, |
| 511 | + storage=initial_storage, |
| 512 | + ) |
| 513 | + |
| 514 | + calldata = ( |
| 515 | + slots_per_contract.to_bytes(32, "big") |
| 516 | + + start_slot.to_bytes(32, "big") |
| 517 | + + write_value.to_bytes(32, "big") |
| 518 | + ) |
| 519 | + |
| 520 | + access_list = None |
| 521 | + if use_access_list: |
| 522 | + storage_keys = [ |
| 523 | + Hash(start_slot + i) for i in range(slots_per_contract) |
| 524 | + ] |
| 525 | + access_list = [ |
| 526 | + AccessList( |
| 527 | + address=contract_addr, |
| 528 | + storage_keys=storage_keys, |
| 529 | + ) |
| 530 | + ] |
| 531 | + |
| 532 | + contract_gas_limit = base_gas_per_contract |
| 533 | + if contract_idx == 0: |
| 534 | + contract_gas_limit += gas_remainder |
| 535 | + |
| 536 | + tx = Transaction( |
| 537 | + to=contract_addr, |
| 538 | + data=calldata, |
| 539 | + gas_limit=contract_gas_limit, |
| 540 | + sender=pre.fund_eoa(), |
| 541 | + access_list=access_list, |
| 542 | + ) |
| 543 | + txs.append(tx) |
| 544 | + |
| 545 | + expected_storage = Storage() |
| 546 | + for i in range(slots_per_contract): |
| 547 | + expected_storage[start_slot + i] = write_value |
| 548 | + |
| 549 | + post[contract_addr] = Account( |
| 550 | + code=padded_contract, |
| 551 | + storage=expected_storage, |
| 552 | + ) |
| 553 | + |
| 554 | + benchmark_test( |
| 555 | + blocks=[Block(txs=txs)], |
| 556 | + post=post, |
| 557 | + skip_gas_used_validation=True, |
| 558 | + ) |
0 commit comments