Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0x20 Introduced: Frontier (EVM genesis) SHA3/KECCAK256 computes the Keccak-256 cryptographic hash of a memory region. Despite its name referencing SHA3, this opcode implements the original Keccak-256 algorithm, not the NIST-standardized SHA3. This operation is essential for:
  • Computing function selectors (first 4 bytes of keccak256(signature))
  • Hashing event data and topics
  • Generating storage keys
  • Implementing authentication schemes

Specification

Stack Input:
offset (top - memory byte offset)
size (number of bytes to hash)
Stack Output:
hash (256-bit Keccak-256 digest as uint256)
Gas Cost:
30 (base) + 6 * ceil(size / 32) (word cost) + memory_expansion_cost
Operation:
data = memory[offset : offset + size]
hash = keccak256(data)  // Actual Keccak-256, not NIST SHA3-256
push hash to stack

Behavior

SHA3 reads a variable-length byte sequence from memory, computes its Keccak-256 hash, and pushes the result to the stack:
  1. Pop operands: Remove offset and size from stack (in that order)
  2. Validate: Ensure offset/size fit in u32 range; calculate gas costs
  3. Charge gas: Base (30) + per-word (6 * ceil(size/32)) + memory expansion
  4. Expand memory: If accessing memory beyond current size, allocate word-aligned pages
  5. Read data: Copy bytes [offset, offset+size) from memory
  6. Hash: Compute Keccak-256 digest (32 bytes)
  7. Push result: Convert hash to u256 (big-endian) and push to stack
  8. Increment PC: Move to next instruction
Special case: If size=0, return cached hash of empty data (0xc5d2460186f7…) without memory access.

Examples

Computing a Function Selector

import { sha3 } from '@tevm/voltaire/evm/instructions/keccak';

// Hash "transfer(address,uint256)" to get selector
const frame = createFrame();

// Write signature to memory
const sig = "transfer(address,uint256)";
const bytes = new TextEncoder().encode(sig);
for (let i = 0; i < bytes.length; i++) {
  frame.memory.set(i, bytes[i]);
}

// Push offset=0, size=25
frame.stack.push(0n);      // offset
frame.stack.push(25n);     // size

// Execute SHA3
sha3(frame);

// Result: 0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
// Selector: 0xa9059cbb (first 4 bytes)

Event Topic Hash

// Computing event signature hash
event Transfer(address indexed from, address indexed to, uint256 value);

// Solidity computes this during compilation
bytes32 eventSig = keccak256("Transfer(address,address,uint256)");
// = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef

Storage Key Generation

// Mapping storage key: keccak256(abi.encode(key, slot))
mapping(address => uint256) balances;  // slot 0

// Key for balances[0x123...] is:
// keccak256(abi.encodePacked(0x123..., 0))

// In EVM assembly:
// PUSH20 0x123...        // address
// PUSH1 0                // slot
// MSTORE                 // store address at mem[0:20]
// MSTORE                 // store slot at mem[20:32]
// PUSH1 32               // size = 32 bytes (address + slot)
// PUSH0                  // offset = 0
// SHA3                   // compute keccak256(addr || slot)

Gas Cost Calculation

Base Gas

All SHA3 operations cost minimum 30 gas (GasKeccak256Base).

Per-Word Gas

Additional 6 gas per 32-byte word (rounded up):
function wordCount(bytes) {
  return Math.ceil(bytes / 32);
}

function sha3Gas(size) {
  const baseGas = 30;
  const wordGas = 6 * wordCount(size);
  return baseGas + wordGas;
}

// Examples:
sha3Gas(0)    // 30 + 0 = 30
sha3Gas(1)    // 30 + 6 = 36 (rounds up to 1 word)
sha3Gas(32)   // 30 + 6 = 36 (exactly 1 word)
sha3Gas(33)   // 30 + 12 = 42 (rounds up to 2 words)
sha3Gas(64)   // 30 + 12 = 42 (exactly 2 words)
sha3Gas(65)   // 30 + 18 = 48 (rounds up to 3 words)

Memory Expansion Cost

Reading memory beyond current size triggers expansion cost:
function memoryExpansionCost(currentSize, accessEnd) {
  const newSize = Math.ceil(accessEnd / 32) * 32;  // Word-align
  if (newSize <= currentSize) return 0;

  // Quadratic cost: (newWords^2 - oldWords^2) / 512 + (newWords - oldWords) * 3
  const oldWords = currentSize / 32;
  const newWords = newSize / 32;
  return (newWords * newWords - oldWords * oldWords) / 512
         + (newWords - oldWords) * 3;
}

// Example: Read 10 bytes at offset 0 (requires 1 word = 32 bytes)
memoryExpansionCost(0, 10)  // (1 - 0)/512 + (1 - 0)*3 = 3

// Example: Read 1 byte at offset 1000000 (requires 31251 words)
// Cost is thousands of gas due to quadratic growth

Total Cost

totalGas = baseGas + wordGas + memoryExpansionCost

Common Usage

Event Signatures

event Transfer(address indexed from, address indexed to, uint256 value);
// Topic 0 = keccak256("Transfer(address,address,uint256)")

event Approval(address indexed owner, address indexed spender, uint256 value);
// Topic 0 = keccak256("Approval(address,address,uint256)")

Function Selectors

interface ERC20 {
  // Selector = keccak256("transfer(address,uint256)")[0:4]
  function transfer(address to, uint256 amount) external returns (bool);

  // Selector = keccak256("approve(address,uint256)")[0:4]
  function approve(address spender, uint256 amount) external returns (bool);
}

State Root Hashing

Merkle tree construction hashes account storage:
hash(account) = keccak256(nonce || balance || storageRoot || codeHash)

Commit-Reveal Pattern

Prevents transaction front-running:
// Phase 1: Commit
bytes32 commitment = keccak256(abi.encode(secret, value));

// Phase 2: Reveal (in later block)
require(keccak256(abi.encode(secret, value)) == commitment, "Invalid reveal");

Access Control (Legacy)

// Mapping role -> account -> bool
mapping(bytes32 => mapping(address => bool)) roles;

// ADMIN_ROLE = keccak256("ADMIN_ROLE")
bytes32 constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

// Check admin
require(roles[ADMIN_ROLE][msg.sender], "Not admin");

Implementation

/**
 * SHA3/KECCAK256 opcode (0x20) - Hash memory region
 */
export function sha3(frame: FrameType): EvmError | null {
  // Pop offset and size from stack
  const offsetResult = popStack(frame);
  if (offsetResult.error) return offsetResult.error;
  const offset = offsetResult.value;

  const lengthResult = popStack(frame);
  if (lengthResult.error) return lengthResult.error;
  const length = lengthResult.value;

  // Validate fit in safe integer range
  if (offset > Number.MAX_SAFE_INTEGER) {
    return { type: "OutOfBounds" };
  }
  if (length > Number.MAX_SAFE_INTEGER) {
    return { type: "OutOfBounds" };
  }

  const off = Number(offset);
  const len = Number(length);

  // Calculate gas: base (30) + word_count * per_word (6)
  const wordCount = len === 0 ? 0n : BigInt(Math.ceil(len / 32));
  const dynamicGas = 30n + 6n * wordCount;

  // Charge gas
  const gasErr = consumeGas(frame, dynamicGas);
  if (gasErr) return gasErr;

  // Charge memory expansion
  if (len > 0) {
    const endBytes = off + len;
    const memCost = memoryExpansionCost(frame, endBytes);
    const memGasErr = consumeGas(frame, memCost);
    if (memGasErr) return memGasErr;

    // Update memory size
    const alignedSize = Math.ceil(endBytes / 32) * 32;
    if (alignedSize > frame.memorySize) {
      frame.memorySize = alignedSize;
    }
  }

  // Handle empty data
  if (len === 0) {
    const emptyHash = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470n;
    const pushErr = pushStack(frame, emptyHash);
    if (pushErr) return pushErr;
    frame.pc += 1;
    return null;
  }

  // Read data from memory
  const data = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    data[i] = readMemory(frame, off + i);
  }

  // Compute Keccak-256 hash
  const hashBytes = hash(data);

  // Convert to u256 (big-endian)
  let hashValue = 0n;
  for (let i = 0; i < 32; i++) {
    hashValue = (hashValue << 8n) | BigInt(hashBytes[i]);
  }

  // Push result
  const pushErr = pushStack(frame, hashValue);
  if (pushErr) return pushErr;

  frame.pc += 1;
  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { sha3 } from './0x20_SHA3.js';

describe('SHA3 (0x20)', () => {
  it('hashes empty data', () => {
    const frame = createFrame([0n, 0n]);
    expect(sha3(frame)).toBeNull();
    expect(frame.stack[0]).toBe(
      0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470n
    );
  });

  it('hashes "hello world"', () => {
    const frame = createFrame();
    const data = new TextEncoder().encode("hello world");
    for (let i = 0; i < data.length; i++) {
      frame.memory.set(i, data[i]);
    }
    frame.stack.push(11n, 0n);

    expect(sha3(frame)).toBeNull();
    expect(frame.stack[0]).toBe(
      0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fadn
    );
  });

  it('charges correct gas', () => {
    const frame = createFrame([32n, 0n], 100n);
    sha3(frame);
    expect(frame.gasRemaining).toBe(100n - 36n); // 30 + 6*1 word
  });

  it('expands memory correctly', () => {
    const frame = createFrame([10n, 0n]);
    expect(frame.memorySize).toBe(0);

    sha3(frame);
    expect(frame.memorySize).toBe(32); // 1 word
  });

  it('handles offset correctly', () => {
    const frame = createFrame();
    const data = new TextEncoder().encode("test");
    for (let i = 0; i < data.length; i++) {
      frame.memory.set(100 + i, data[i]);
    }
    frame.stack.push(4n, 100n);

    sha3(frame);
    expect(frame.stack[0]).toBe(
      0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658n
    );
  });
});

Security

Preimage Resistance

Keccak-256 is a cryptographic one-way function: finding input x given keccak256(x) = h requires ~2^256 operations. Used securely for:
  • Transaction hashing and signature verification
  • State root computation
  • Storage key generation

Collision Resistance

Finding two different inputs with the same hash requires ~2^128 operations (birthday paradox bound). This guarantees:
  • Merkle tree integrity for account storage
  • Uniqueness of function selectors (extremely unlikely to collide accidentally)

Domain Separation (Keccak vs NIST SHA3)

Critical: Ethereum uses Keccak-256, NOT NIST SHA3-256. Never assume compatibility:
// These are DIFFERENT:
const evmHash = keccak256("data");           // Ethereum opcode
const nistHash = sha3_256("data");           // NIST standard (with 0x06 padding)
// evmHash !== nistHash

Predictable Randomness Anti-Pattern

⚠️ NEVER use for randomness:
// WRONG: Predictable by miners/validators
bytes32 randomness = keccak256(abi.encodePacked(block.timestamp, msg.sender));

// Miners can choose when to include transaction, or validator can reorder

// CORRECT: Use Chainlink VRF or similar oracle

Edge Cases

Maximum Memory Access

// SHA3 with very large size triggers quadratic memory expansion cost
const frame = createFrame();
frame.stack.push(0x100000n);  // 1MB of data
frame.stack.push(0n);         // offset 0

// Gas cost is astronomical due to memory expansion
// Even with large gas limit, operation may fail

Uninitialized Memory Reads

// Reading from uninitialized memory returns zeros
bytes memory empty = new bytes(10);  // 10 zero bytes (not the same as "")
bytes32 hash1 = keccak256("");       // = 0xc5d2460186f7...
bytes32 hash2 = keccak256(empty);    // Different (hash of 10 zeros)
assert(hash1 != hash2);

Integer Overflow Prevention

Stack values are u256, memory offsets are u32. Validation ensures no overflow:
// If offset + size > 2^32, operation fails
// This prevents integer overflow in memory address calculation

Benchmarks

Gas costs reflect computational and memory expenses:
Input SizeWordsBase GasWord GasMemoryTotalPer Byte
0 bytes0300333-
1 byte130633939.0
32 bytes13063391.2
64 bytes230126480.75
256 bytes8304812900.35
1024 bytes3230192482700.26

References