├── LICENSE ├── README.md └── sporalyzer.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Shigeo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sporalyzer🍄 2 | EVM contract size analyzer 3 | 4 | ## Background 5 | 6 | This is a small script created to analyze the sources of contract size in contracts built by `solc`. The methodology is described in [this post](https://blog.fungify.it/p/introducing-sporalyzer-a-tool-for), which also has a worked example, and sample output. 7 | 8 | ## Usage 9 | 10 | At the moment we are still working on integrations with frameworks like Hardhat and Foundry, but you can download the current code and run it manually via Node.js. 11 | The script takes the following command-line arguments: 12 | - `buildInfoPath`: The filesystem path to the `build-info` JSON file generated by solc at compilation time. This is usually under the `artifacts/build-info` directory. 13 | - `mode`: This is either `bytecode` or `deployedBytecode` depending on whether you want to analyze the code run at contract creation time (e.g. constructors), or the steady-state code on-chain (i.e. what we usually think of as the main contract code). 14 | - `outputType`: This can be either `listing` or `script`, to set whether you want a simple listing of functions by size, or an 010 Editor script that annotates the bytecode with function names and pretty colors to make it easier to inspect. 15 | 16 | ### Notes 17 | - If a function size is listed as `?`, that means that no bytecode was attributed to that function by the source map, meaning it effectively had a size of `0`. However, what this usually actually means in practice is that the function was deduplicated, inlined, or otherwise somehow not attributable as a separate unit, making the `?` size designation a bit more accurate. 18 | - `#utility.yul` is a file generated by `solc` at compile-time with lots of little shims or helper functions. In the example from the blog post, you can see that it can even contain things like the actual data for error strings. In many ways it is a reflection of the contents of your contracts. If you want to inspect it, you can find it in the `build-info` JSON file, under the `generatedSources` key. Note there there will be two versions: one for `bytecode` and one for `deployedBytecode`, following the distinction outlined above. 19 | -------------------------------------------------------------------------------- /sporalyzer.js: -------------------------------------------------------------------------------- 1 | console.log( 2 | ` 3 | __.....__ 4 | .'" "\`. 5 | .' \`. 6 | . . 7 | . __...__ . 8 | . _.--""" """--._ . 9 | :" "; 10 | \`-.__ : : __.-' 11 | """-: :-""" 12 | J L 13 | : : 14 | J L 15 | : : 16 | \`._____.' 17 | Sporalyzer 18 | ` 19 | ); 20 | 21 | const {JSONPath} = require('jsonpath-plus'); 22 | 23 | let args = require('minimist')(process.argv.slice(2)); 24 | 25 | const buildInfo = require(args.buildInfoPath); 26 | 27 | // Get AST nodes of build info into a map by file ID 28 | 29 | let files = []; 30 | 31 | const inputFileSources = JSONPath({ json: buildInfo, path: "$.output.sources.*", resultType: "all"}); 32 | 33 | inputFileSources.forEach( 34 | (s) => { s.value.name = s.parentProperty; files[s.value.id] = s.value; } 35 | ); 36 | 37 | const generatedSources = JSONPath({ json: buildInfo, path: `$.output.contracts..${args.mode}.generatedSources[*]`}); 38 | 39 | generatedSources.forEach( 40 | (s) => { files[s.id] = s; } 41 | ); 42 | 43 | // Set up File -1 entry 44 | 45 | files[-1] = { ast: {src: "-1:-2:-1", name: "(no function name)"}, name: "(File -1)", id: -1 }; 46 | 47 | // Get all source map and bytecode nodes 48 | 49 | const bytecode = JSONPath({ json: buildInfo, path: `$.output.contracts..${args.mode}`})[0]; 50 | 51 | // Normalize the opcode listing so that PUSH instructions are single tokens, and trim excess data at the end 52 | 53 | const opcodes = bytecode.opcodes.replace(/(PUSH\d+)\s+(\w+)/g, "$1_$2").replace(/INVALID.*$/, '').split(/\s/); 54 | 55 | // Iterate through the source map entries and annotate AST nodes 56 | 57 | function enumPathToLeaf(root, childEnum, childPredicate, mutator) 58 | { 59 | childEnum(root).forEach((c) => { 60 | if ( childPredicate(c) ) enumPathToLeaf(c, childEnum, childPredicate, mutator); 61 | }); 62 | mutator(root); 63 | } 64 | 65 | const sourceMapEntries = bytecode.sourceMap.split(';'); 66 | 67 | let entry = [ 0, 0, -1, '-', 0 ]; // Setting up in this scope so we can carry entry data forward between entries 68 | 69 | sourceMapEntries.forEach((e, i) => { 70 | e.split(':').forEach((v, i) => { entry[i] = (v || entry[i]); }); 71 | 72 | const opcodeByteSize = ((opcodes[i].match(/^PUSH(\d+)/) || [, "0"])[1] | 0) + 1; 73 | 74 | enumPathToLeaf( 75 | files[entry[2]].ast, 76 | (n) => { return n.statements || n.nodes || (n.body ? [n.body] : undefined) || []; }, 77 | (c) => { 78 | const src = c.src.split(':'); 79 | return (entry[0] | 0) >= (src[0] | 0) && ((entry[0] | 0) + (entry[1] | 0)) <= ((src[0] | 0) + (src[1] | 0)); 80 | }, 81 | (n) => { 82 | if ( n.byteSize === undefined ) n.byteSize = 0; 83 | n.byteSize += opcodeByteSize; 84 | if ( n.opCount === undefined ) n.opCount = 0; 85 | n.opCount++; 86 | } 87 | ); 88 | }); 89 | 90 | // Get all function definition AST nodes 91 | 92 | let funcDefs = []; 93 | 94 | files.forEach((f) => { 95 | const funcNodes = JSONPath({ 96 | json: f.ast, 97 | path: `$..*[?(@.nodeType === "FunctionDefinition" || @.nodeType === "ModifierDefinition" || @.nodeType === "YulFunctionDefinition")]` 98 | }); 99 | 100 | funcDefs.push(...funcNodes); 101 | }); 102 | 103 | funcDefs.push(files[-1].ast); 104 | 105 | // Sort by code size 106 | 107 | funcDefs = funcDefs.sort((a, b) => { 108 | return (a.byteSize | 0) - (b.byteSize | 0); 109 | }); 110 | 111 | // Output as a list 112 | 113 | if ( args.outputType === "listing" ) 114 | { 115 | funcDefs.forEach((fd) => { 116 | console.log(`${fd.byteSize ? fd.byteSize : "?"}\t\t${files[fd.src.split(':')[2]].name}:${fd.name != "" ? fd.name : "(constructor)"}`); 117 | }); 118 | } 119 | 120 | // Output 010 script 121 | 122 | if ( args.outputType == "script" ) 123 | { 124 | let sha3 = require("js-sha3"); 125 | 126 | function nameToColorHexString(name) 127 | { 128 | let colorHash = sha3.sha3_512.array(name); 129 | return `0x${colorHash[0]}${colorHash[1]}${colorHash[2]}`; 130 | } 131 | 132 | const scriptPreamble = `typedef struct { 133 | enum EvmOpcode { 134 | STOP = 0x00, 135 | ADD = 0x01, 136 | MUL = 0x02, 137 | SUB = 0x03, 138 | DIV = 0x04, 139 | SDIV = 0x05, 140 | MOD = 0x06, 141 | SMOD = 0x07, 142 | ADDMOD = 0x08, 143 | MULMOD = 0x09, 144 | EXP = 0x0A, 145 | SIGNEXTEND = 0x0B, 146 | INVALID_0C = 0x0C, 147 | INVALID_0D = 0x0D, 148 | INVALID_0E = 0x0E, 149 | INVALID_0F = 0x0F, 150 | LT = 0x10, 151 | GT = 0x11, 152 | SLT = 0x12, 153 | SGT = 0x13, 154 | EQ = 0x14, 155 | ISZERO = 0x15, 156 | AND = 0x16, 157 | OR = 0x17, 158 | XOR = 0x18, 159 | NOT = 0x19, 160 | _BYTE = 0x1A, 161 | SHL = 0x1B, 162 | SHR = 0x1C, 163 | SAR = 0x1D, 164 | INVALID_1E = 0x1E, 165 | INVALID_1F = 0x1F, 166 | SHA3 = 0x20, 167 | INVALID_21 = 0x21, 168 | INVALID_22 = 0x22, 169 | INVALID_23 = 0x23, 170 | INVALID_24 = 0x24, 171 | INVALID_25 = 0x25, 172 | INVALID_26 = 0x26, 173 | INVALID_27 = 0x27, 174 | INVALID_28 = 0x28, 175 | INVALID_29 = 0x29, 176 | INVALID_2A = 0x2A, 177 | INVALID_2B = 0x2B, 178 | INVALID_2C = 0x2C, 179 | INVALID_2D = 0x2D, 180 | INVALID_2E = 0x2E, 181 | INVALID_2F = 0x2F, 182 | ADDRESS = 0x30, 183 | BALANCE = 0x31, 184 | ORIGIN = 0x32, 185 | CALLER = 0x33, 186 | CALLVALUE = 0x34, 187 | CALLDATALOAD = 0x35, 188 | CALLDATASIZE = 0x36, 189 | CALLDATACOPY = 0x37, 190 | CODESIZE = 0x38, 191 | CODECOPY = 0x39, 192 | GASPRICE = 0x3A, 193 | EXTCODESIZE = 0x3B, 194 | EXTCODECOPY = 0x3C, 195 | RETURNDATASIZE = 0x3D, 196 | RETURNDATACOPY = 0x3E, 197 | EXTCODEHASH = 0x3F, 198 | BLOCKHASH = 0x40, 199 | COINBASE = 0x41, 200 | TIMESTAMP = 0x42, 201 | NUMBER = 0x43, 202 | DIFFICULTY = 0x44, 203 | GASLIMIT = 0x45, 204 | CHAINID = 0x46, 205 | SELFBALANCE = 0x47, 206 | BASEFEE = 0x48, 207 | INVALID_49 = 0x49, 208 | INVALID_4A = 0x4A, 209 | INVALID_4B = 0x4B, 210 | INVALID_4C = 0x4C, 211 | INVALID_4D = 0x4D, 212 | INVALID_4E = 0x4E, 213 | INVALID_4F = 0x4F, 214 | POP = 0x50, 215 | MLOAD = 0x51, 216 | MSTORE = 0x52, 217 | MSTORE8 = 0x53, 218 | SLOAD = 0x54, 219 | SSTORE = 0x55, 220 | JUMP = 0x56, 221 | JUMPI = 0x57, 222 | PC = 0x58, 223 | MSIZE = 0x59, 224 | GAS = 0x5A, 225 | JUMPDEST = 0x5B, 226 | INVALID_5C = 0x5C, 227 | INVALID_5D = 0x5D, 228 | INVALID_5E = 0x5E, 229 | INVALID_5F = 0x5F, 230 | PUSH1 = 0x60, 231 | PUSH2 = 0x61, 232 | PUSH3 = 0x62, 233 | PUSH4 = 0x63, 234 | PUSH5 = 0x64, 235 | PUSH6 = 0x65, 236 | PUSH7 = 0x66, 237 | PUSH8 = 0x67, 238 | PUSH9 = 0x68, 239 | PUSH10 = 0x69, 240 | PUSH11 = 0x6A, 241 | PUSH12 = 0x6B, 242 | PUSH13 = 0x6C, 243 | PUSH14 = 0x6D, 244 | PUSH15 = 0x6E, 245 | PUSH16 = 0x6F, 246 | PUSH17 = 0x70, 247 | PUSH18 = 0x71, 248 | PUSH19 = 0x72, 249 | PUSH20 = 0x73, 250 | PUSH21 = 0x74, 251 | PUSH22 = 0x75, 252 | PUSH23 = 0x76, 253 | PUSH24 = 0x77, 254 | PUSH25 = 0x78, 255 | PUSH26 = 0x79, 256 | PUSH27 = 0x7A, 257 | PUSH28 = 0x7B, 258 | PUSH29 = 0x7C, 259 | PUSH30 = 0x7D, 260 | PUSH31 = 0x7E, 261 | PUSH32 = 0x7F, 262 | DUP1 = 0x80, 263 | DUP2 = 0x81, 264 | DUP3 = 0x82, 265 | DUP4 = 0x83, 266 | DUP5 = 0x84, 267 | DUP6 = 0x85, 268 | DUP7 = 0x86, 269 | DUP8 = 0x87, 270 | DUP9 = 0x88, 271 | DUP10 = 0x89, 272 | DUP11 = 0x8A, 273 | DUP12 = 0x8B, 274 | DUP13 = 0x8C, 275 | DUP14 = 0x8D, 276 | DUP15 = 0x8E, 277 | DUP16 = 0x8F, 278 | SWAP1 = 0x90, 279 | SWAP2 = 0x91, 280 | SWAP3 = 0x92, 281 | SWAP4 = 0x93, 282 | SWAP5 = 0x94, 283 | SWAP6 = 0x95, 284 | SWAP7 = 0x96, 285 | SWAP8 = 0x97, 286 | SWAP9 = 0x98, 287 | SWAP10 = 0x99, 288 | SWAP11 = 0x9A, 289 | SWAP12 = 0x9B, 290 | SWAP13 = 0x9C, 291 | SWAP14 = 0x9D, 292 | SWAP15 = 0x9E, 293 | SWAP16 = 0x9F, 294 | LOG0 = 0xA0, 295 | LOG1 = 0xA1, 296 | LOG2 = 0xA2, 297 | LOG3 = 0xA3, 298 | LOG4 = 0xA4, 299 | INVALID_A5 = 0xA5, 300 | INVALID_A6 = 0xA6, 301 | INVALID_A7 = 0xA7, 302 | INVALID_A8 = 0xA8, 303 | INVALID_A9 = 0xA9, 304 | INVALID_AA = 0xAA, 305 | INVALID_AB = 0xAB, 306 | INVALID_AC = 0xAC, 307 | INVALID_AD = 0xAD, 308 | INVALID_AE = 0xAE, 309 | INVALID_AF = 0xAF, 310 | INVALID_B0 = 0xB0, 311 | INVALID_B1 = 0xB1, 312 | INVALID_B2 = 0xB2, 313 | INVALID_B3 = 0xB3, 314 | INVALID_B4 = 0xB4, 315 | INVALID_B5 = 0xB5, 316 | INVALID_B6 = 0xB6, 317 | INVALID_B7 = 0xB7, 318 | INVALID_B8 = 0xB8, 319 | INVALID_B9 = 0xB9, 320 | INVALID_BA = 0xBA, 321 | INVALID_BB = 0xBB, 322 | INVALID_BC = 0xBC, 323 | INVALID_BD = 0xBD, 324 | INVALID_BE = 0xBE, 325 | INVALID_BF = 0xBF, 326 | INVALID_C0 = 0xC0, 327 | INVALID_C1 = 0xC1, 328 | INVALID_C2 = 0xC2, 329 | INVALID_C3 = 0xC3, 330 | INVALID_C4 = 0xC4, 331 | INVALID_C5 = 0xC5, 332 | INVALID_C6 = 0xC6, 333 | INVALID_C7 = 0xC7, 334 | INVALID_C8 = 0xC8, 335 | INVALID_C9 = 0xC9, 336 | INVALID_CA = 0xCA, 337 | INVALID_CB = 0xCB, 338 | INVALID_CC = 0xCC, 339 | INVALID_CD = 0xCD, 340 | INVALID_CE = 0xCE, 341 | INVALID_CF = 0xCF, 342 | INVALID_D0 = 0xD0, 343 | INVALID_D1 = 0xD1, 344 | INVALID_D2 = 0xD2, 345 | INVALID_D3 = 0xD3, 346 | INVALID_D4 = 0xD4, 347 | INVALID_D5 = 0xD5, 348 | INVALID_D6 = 0xD6, 349 | INVALID_D7 = 0xD7, 350 | INVALID_D8 = 0xD8, 351 | INVALID_D9 = 0xD9, 352 | INVALID_DA = 0xDA, 353 | INVALID_DB = 0xDB, 354 | INVALID_DC = 0xDC, 355 | INVALID_DD = 0xDD, 356 | INVALID_DE = 0xDE, 357 | INVALID_DF = 0xDF, 358 | INVALID_E0 = 0xE0, 359 | INVALID_E1 = 0xE1, 360 | INVALID_E2 = 0xE2, 361 | INVALID_E3 = 0xE3, 362 | INVALID_E4 = 0xE4, 363 | INVALID_E5 = 0xE5, 364 | INVALID_E6 = 0xE6, 365 | INVALID_E7 = 0xE7, 366 | INVALID_E8 = 0xE8, 367 | INVALID_E9 = 0xE9, 368 | INVALID_EA = 0xEA, 369 | INVALID_EB = 0xEB, 370 | INVALID_EC = 0xEC, 371 | INVALID_ED = 0xED, 372 | INVALID_EE = 0xEE, 373 | INVALID_EF = 0xEF, 374 | CREATE = 0xF0, 375 | CALL = 0xF1, 376 | CALLCODE = 0xF2, 377 | RETURN = 0xF3, 378 | DELEGATECALL = 0xF4, 379 | CREATE2 = 0xF5, 380 | INVALID_F6 = 0xF6, 381 | INVALID_F7 = 0xF7, 382 | INVALID_F8 = 0xF8, 383 | INVALID_F9 = 0xF9, 384 | STATICCALL = 0xFA, 385 | INVALID_FB = 0xFB, 386 | INVALID_FC = 0xFC, 387 | REVERT = 0xFD, 388 | INVALID = 0xFE, 389 | SELFDESTRUCT = 0xFF 390 | }; 391 | 392 | enum OpClass { Normal, Push, Metadata }; 393 | 394 | local OpClass opClass = Normal; 395 | 396 | if ( ReadUByte() == LOG2 && ReadUInt(FTell() + 1) == 0x66706964 ) 397 | { 398 | opClass = Metadata; 399 | ubyte metadataBlob[FileSize() - FTell()]; 400 | break; 401 | } 402 | 403 | EvmOpcode op; 404 | 405 | if ( op >= PUSH1 && op <= PUSH32 ) 406 | { 407 | opClass = Push; 408 | local uint immediateLength = op - (PUSH1 - 1); 409 | ubyte immediate[immediateLength]; 410 | } 411 | 412 | } EvmOp ; 413 | 414 | string EvmOpToString(EvmOp& evmOp) 415 | { 416 | switch ( evmOp.opClass ) 417 | { 418 | case Normal: 419 | return EnumToString(evmOp.op); 420 | 421 | case Push: 422 | local string immediateStr; 423 | local uint i; 424 | 425 | for ( i = 0; i < evmOp.immediateLength; i++ ) 426 | { 427 | immediateStr += Str("%02x", evmOp.immediate[i]); 428 | } 429 | return Str("%s <%s>", EnumToString(evmOp.op), immediateStr); 430 | 431 | case Metadata: 432 | return "Metadata blob"; 433 | } 434 | 435 | return "Unknown op class"; 436 | }`; 437 | 438 | entry = [ 0, 0, -1, '-', 0 ]; 439 | 440 | let entryFuncInfo = []; 441 | let defaultFuncNode = { 442 | name: "(no function name)", 443 | src: "-1:-1:-1", 444 | opCount: 1 445 | }; 446 | 447 | sourceMapEntries.forEach((e, i) => { 448 | e.split(':').forEach((v, i) => { entry[i] = (v || entry[i]); }); 449 | 450 | let entryFuncNode = defaultFuncNode; 451 | 452 | enumPathToLeaf( 453 | files[entry[2]].ast, 454 | (n) => { return n.statements || n.nodes || (n.body ? [n.body] : undefined) || []; }, 455 | (c) => { 456 | const src = c.src.split(':'); 457 | return (entry[0] | 0) >= (src[0] | 0) && ((entry[0] | 0) + (entry[1] | 0)) <= ((src[0] | 0) + (src[1] | 0)); 458 | }, 459 | (n) => { 460 | if ( n.nodeType === "FunctionDefinition" || n.nodeType === "ModifierDefinition" || n.nodeType === "YulFunctionDefinition" ) 461 | { 462 | entryFuncNode = n; 463 | } 464 | } 465 | ); 466 | 467 | entryFuncInfo.push(entryFuncNode); 468 | }); 469 | 470 | console.log("010 Editor script for coloring by function:\n\n"); 471 | 472 | console.log(scriptPreamble); 473 | entryFuncInfo.forEach((e, i) => { console.log(`EvmOp entry;`)}); 474 | } 475 | --------------------------------------------------------------------------------