Skip to main content

Try it Live

Run Bytecode examples in the interactive playground

Complex CFG Analysis

TypeScript implementation only. Control flow graph analysis with block detection is complex and not yet ported to C.
// TypeScript implementation only
// CFG analysis not yet available in C

BasicBlock Type

interface BasicBlock {
  /** Block index (0-based) */
  index: number

  /** Starting program counter */
  startPc: number

  /** Ending program counter (exclusive) */
  endPc: number

  /** Number of instructions in block */
  instructionCount: number

  /** Total static gas cost */
  gasEstimate: number

  /** Minimum stack items required to enter */
  minStack: number

  /** Maximum stack depth reached */
  maxStack: number

  /** Net stack effect (exit - entry) */
  stackEffect: number

  /** Block terminator type */
  terminator: TerminatorType

  /** Jump target PC (if terminator is JUMP/JUMPI) */
  target?: number

  /** Whether block is reachable from entry */
  isReachable: boolean

  /** Successor block indices */
  successors: number[]

  /** Predecessor block indices */
  predecessors: number[]
}
See analyzeBlocks for full type details.

Usage Patterns

Debug Specific Instruction

const code = Bytecode(contractBytecode);

// Find which block contains an instruction
const targetPc = 142;
const block = code.getBlock(targetPc);

if (block) {
  console.log(`PC ${targetPc} is in block ${block.index}`);
  console.log(`Block spans PC ${block.startPc} to ${block.endPc}`);
  console.log(`Gas cost for block: ${block.gasEstimate}`);
} else {
  console.warn(`PC ${targetPc} not found in any block`);
}

Trace Execution Path

const code = Bytecode(bytecode);

// Trace execution path from entry
function tracePath(startPc: number, maxSteps: number = 100): number[] {
  const path: number[] = [];
  let currentPc = startPc;

  for (let i = 0; i < maxSteps; i++) {
    const block = code.getBlock(currentPc);
    if (!block) break;

    path.push(block.index);

    // Follow first successor
    if (block.successors.length > 0) {
      const nextBlock = blocks[block.successors[0]];
      currentPc = nextBlock.startPc;
    } else {
      break; // No successors (exit block)
    }
  }

  return path;
}

const executionPath = tracePath(0);
console.log(`Execution path: ${executionPath.join(' → ')}`);

Jump Target Validation

// Verify jump target is valid (starts a basic block)
function isValidJumpTarget(code: BrandedBytecode, targetPc: number): boolean {
  const block = code.getBlock(targetPc);

  // Valid if target is exactly at block start (JUMPDEST)
  return block !== undefined && block.startPc === targetPc;
}

const jumpTarget = 0x42;
if (isValidJumpTarget(code, jumpTarget)) {
  console.log(`${jumpTarget} is a valid jump target`);
} else {
  console.error(`${jumpTarget} is NOT a valid jump target`);
}

Gas Profiling by Block

const code = Bytecode(bytecode);

// Profile gas costs of blocks containing specific opcodes
const blocks = code.analyzeBlocks();
const instructions = code.parseInstructions();

const sstoreBlocks = new Set<number>();

instructions.forEach(inst => {
  if (inst.opcode === 'SSTORE') {
    const block = code.getBlock(inst.position);
    if (block) {
      sstoreBlocks.add(block.index);
    }
  }
});

console.log(`Blocks with SSTORE operations:`);
sstoreBlocks.forEach(blockIdx => {
  const block = blocks[blockIdx];
  console.log(`  Block ${blockIdx}: ${block.gasEstimate} gas`);
});

Coverage Analysis

// Track which blocks were executed
const executedPcs = new Set<number>([0, 5, 23, 45, 67]);
const executedBlocks = new Set<number>();

executedPcs.forEach(pc => {
  const block = code.getBlock(pc);
  if (block) {
    executedBlocks.add(block.index);
  }
});

const blocks = code.analyzeBlocks();
const coverage = (executedBlocks.size / blocks.length) * 100;

console.log(`Block coverage: ${coverage.toFixed(1)}%`);
console.log(`Executed ${executedBlocks.size} of ${blocks.length} blocks`);

Performance

getBlock() internally caches block analysis results:
  • First call: O(n) to analyze all blocks
  • Subsequent calls: O(log n) binary search through cached blocks
Block analysis results are cached per bytecode instance. Multiple getBlock() calls on the same bytecode are efficient.

Edge Cases

PC Not in Any Block

const block = code.getBlock(9999);
if (!block) {
  console.log('PC out of bounds or in padding/metadata');
}

PC in Middle of PUSH Data

// PUSH2 0x1234 at PC 0 means:
//   PC 0: PUSH2 opcode
//   PC 1-2: immediate data (0x1234)

const block1 = code.getBlock(0);  // Valid: block start
const block2 = code.getBlock(1);  // Same block (inside PUSH data)
const block3 = code.getBlock(2);  // Same block (inside PUSH data)

// All three return the same block
console.log(block1.index === block2?.index); // true

Invalid Bytecode

const invalid = Bytecode("0xFF"); // INVALID opcode

const block = code.getBlock(0);
if (block) {
  console.log(`Terminator: ${block.terminator}`); // 'invalid'
}

See Also