Skip to main content

Overview

Opcode: 0x40 Introduced: Frontier (EVM genesis) BLOCKHASH retrieves the keccak256 hash of a specified block number. It only returns hashes for the 256 most recent complete blocks. For blocks outside this range or future blocks, it returns zero. This instruction enables contracts to reference historical blockchain state for verification, commitment schemes, and deterministic randomness.

Specification

Stack Input:
block_number (top)
Stack Output:
hash (or 0 if unavailable)
Gas Cost: 20 (GasExtStep) Behavior:
  • Returns block hash if current_block - 256 < block_number < current_block
  • Returns 0x0000...0000 if block is too old (> 256 blocks ago)
  • Returns 0x0000...0000 if block_number >= current_block
  • Returns 0x0000...0000 if block hash not available in context

Behavior

Valid Range Window

BLOCKHASH maintains a sliding 256-block window:
Current Block: 18,000,000

Valid Range: 17,999,744 to 17,999,999 (256 blocks)
├─ 17,999,744 (oldest available)
├─ 17,999,745
│  ...
├─ 17,999,999 (most recent complete)
└─ 18,000,000 (current - unavailable)

Returns 0:
├─ <= 17,999,743 (too old)
└─ >= 18,000,000 (current or future)

Hash Availability

The EVM maintains an internal block_hashes array indexed with negative offsets:
block_hashes[-(current_block - block_number)]

Examples

Recent Block Hash

import { blockhash } from '@tevm/voltaire/evm/block';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// Query hash of block 17,999,999 (current: 18,000,000)
const frame = createFrame({
  stack: [17_999_999n],
  blockContext: {
    block_number: 18_000_000n,
    block_hashes: [
      Bytes32().fill(0xaa), // block 17,999,999
      // ... more hashes
    ]
  }
});

const err = blockhash(frame);
console.log(frame.stack); // [hash as u256]
console.log(frame.gasRemaining); // Original - 20

Block Too Old

// Query block from 300 blocks ago (outside 256 window)
const frame = createFrame({
  stack: [17_999_700n],
  blockContext: {
    block_number: 18_000_000n,
    block_hashes: [/* recent 256 hashes */]
  }
});

const err = blockhash(frame);
console.log(frame.stack); // [0n] - Too old

Current or Future Block

// Query current block (not yet complete)
const frame = createFrame({
  stack: [18_000_000n],
  blockContext: {
    block_number: 18_000_000n,
    block_hashes: [/* hashes */]
  }
});

const err = blockhash(frame);
console.log(frame.stack); // [0n] - Current block unavailable

// Query future block
const frame2 = createFrame({
  stack: [18_000_001n],
  blockContext: { block_number: 18_000_000n }
});

blockhash(frame2);
console.log(frame2.stack); // [0n] - Future block

Full 256-Block Range

// Iterate through valid range
const currentBlock = 18_000_000n;

for (let i = 1; i <= 256; i++) {
  const queryBlock = currentBlock - BigInt(i);
  const frame = createFrame({
    stack: [queryBlock],
    blockContext: {
      block_number: currentBlock,
      block_hashes: blockHashesArray
    }
  });

  blockhash(frame);
  const hash = frame.stack[0];

  if (hash !== 0n) {
    console.log(`Block ${queryBlock}: ${hash.toString(16)}`);
  }
}

Gas Cost

Cost: 20 gas (GasExtStep) BLOCKHASH is more expensive than simple context queries (2 gas) because it requires:
  • Range validation
  • Array index calculation
  • Hash retrieval from storage
  • 32-byte hash conversion to u256
Comparison:
  • BLOCKHASH: 20 gas
  • NUMBER, TIMESTAMP, GASLIMIT: 2 gas
  • SLOAD (cold): 2100 gas
  • BALANCE (cold): 2600 gas
Despite the 20 gas cost, BLOCKHASH is efficient compared to storage operations.

Common Usage

Commit-Reveal Schemes

contract CommitReveal {
    mapping(address => bytes32) public commits;
    mapping(address => uint256) public commitBlocks;

    function commit(bytes32 hash) external {
        commits[msg.sender] = hash;
        commitBlocks[msg.sender] = block.number;
    }

    function reveal(uint256 secret) external {
        uint256 commitBlock = commitBlocks[msg.sender];
        require(block.number - commitBlock <= 256, "Commitment expired");

        bytes32 blockHash = blockhash(commitBlock);
        bytes32 expectedCommit = keccak256(abi.encodePacked(secret, blockHash));
        require(commits[msg.sender] == expectedCommit, "Invalid reveal");

        // Process reveal
    }
}

Block Hash Verification

contract BlockVerifier {
    function verifyBlockHash(
        uint256 blockNumber,
        bytes32 expectedHash
    ) external view returns (bool) {
        require(blockNumber < block.number, "Future block");
        require(block.number - blockNumber <= 256, "Block too old");

        return blockhash(blockNumber) == expectedHash;
    }
}

Historical Data Anchoring

contract DataAnchor {
    struct Anchor {
        bytes32 dataHash;
        uint256 blockNumber;
        bytes32 blockHash;
    }

    mapping(bytes32 => Anchor) public anchors;

    function anchor(bytes32 dataHash) external {
        uint256 anchorBlock = block.number - 1;
        bytes32 blockHash = blockhash(anchorBlock);

        require(blockHash != bytes32(0), "Block hash unavailable");

        anchors[dataHash] = Anchor({
            dataHash: dataHash,
            blockNumber: anchorBlock,
            blockHash: blockHash
        });
    }

    function verify(bytes32 dataHash) external view returns (bool) {
        Anchor memory a = anchors[dataHash];
        if (a.blockNumber == 0) return false;

        // Can only verify if block is still in 256-block window
        if (block.number - a.blockNumber > 256) return false;

        return blockhash(a.blockNumber) == a.blockHash;
    }
}

Simple Randomness (Not Secure)

// WARNING: Not secure for production
contract BasicLottery {
    function drawWinner(address[] memory participants) external view returns (address) {
        bytes32 blockHash = blockhash(block.number - 1);
        uint256 randomIndex = uint256(blockHash) % participants.length;
        return participants[randomIndex];
    }
}

Security Considerations

Not Suitable for High-Stakes Randomness

Block hashes are predictable by miners and can be manipulated:
// VULNERABLE: Miner can influence outcome
function lottery() external {
    bytes32 hash = blockhash(block.number - 1);
    address winner = participants[uint256(hash) % participants.length];
    payable(winner).transfer(jackpot);
}
Attack Vector:
  • Miner sees they won’t win
  • Miner withholds block to try different nonce
  • Profitability: If jackpot > block reward, rational to try
Mitigation: Use Chainlink VRF or commit-reveal with multiple participants.

256-Block Expiration

Commitments using BLOCKHASH expire after 256 blocks:
contract SecureCommit {
    uint256 constant MAX_BLOCK_AGE = 240; // Safety margin

    function reveal(uint256 secret) external {
        uint256 commitBlock = commitBlocks[msg.sender];

        // Use safety margin to account for reveal tx delays
        require(
            block.number - commitBlock <= MAX_BLOCK_AGE,
            "Commitment expired - please recommit"
        );

        bytes32 blockHash = blockhash(commitBlock);
        require(blockHash != bytes32(0), "Block hash unavailable");

        // Verify commitment
    }
}

Zero Hash Ambiguity

Zero hash can mean multiple things:
function safeBlockHash(uint256 blockNum) internal view returns (bytes32) {
    require(blockNum < block.number, "Future block");
    require(block.number - blockNum <= 256, "Block too old");

    bytes32 hash = blockhash(blockNum);
    require(hash != bytes32(0), "Block hash unavailable");

    return hash;
}

Current Block Unavailability

The current block hash is never available within the block:
// ALWAYS returns 0
bytes32 currentHash = blockhash(block.number);

// CORRECT: Query previous block
bytes32 previousHash = blockhash(block.number - 1);

Implementation

/**
 * BLOCKHASH opcode (0x40) - Get hash of recent block
 */
export function blockhash(frame: FrameType): EvmError | null {
  // Consume gas (GasExtStep = 20)
  frame.gasRemaining -= 20n;
  if (frame.gasRemaining < 0n) {
    frame.gasRemaining = 0n;
    return { type: "OutOfGas" };
  }

  // Pop block number
  if (frame.stack.length < 1) return { type: "StackUnderflow" };
  const blockNumber = frame.stack.pop();

  const currentBlock = frame.evm.blockContext.block_number;

  // Check if block is in valid range
  if (blockNumber >= currentBlock || currentBlock - blockNumber > 256n) {
    // Out of range - return zero
    if (frame.stack.length >= 1024) return { type: "StackOverflow" };
    frame.stack.push(0n);
  } else {
    // In range - get hash
    const index = Number(currentBlock - blockNumber);
    const blockHashes = frame.evm.blockContext.block_hashes;

    if (index > 0 && index <= blockHashes.length) {
      const actualIndex = blockHashes.length - index;
      const blockHash = blockHashes[actualIndex];

      // Convert 32-byte hash to u256
      let hashValue = 0n;
      for (const byte of blockHash) {
        hashValue = (hashValue << 8n) | BigInt(byte);
      }

      if (frame.stack.length >= 1024) return { type: "StackOverflow" };
      frame.stack.push(hashValue);
    } else {
      // Hash not available - return zero
      if (frame.stack.length >= 1024) return { type: "StackOverflow" };
      frame.stack.push(0n);
    }
  }

  frame.pc += 1;
  return null;
}

Edge Cases

Exactly 256 Blocks Ago

// Block exactly at boundary (oldest available)
const frame = createFrame({
  stack: [currentBlock - 256n],
  blockContext: {
    block_number: currentBlock,
    block_hashes: hashes256
  }
});

blockhash(frame);
// Returns hash if available in array

257 Blocks Ago

// One block past the boundary
const frame = createFrame({
  stack: [currentBlock - 257n],
  blockContext: { block_number: currentBlock }
});

blockhash(frame);
console.log(frame.stack); // [0n] - Too old

Genesis Block Query

// Query block 0 from block 1000
const frame = createFrame({
  stack: [0n],
  blockContext: { block_number: 1000n }
});

blockhash(frame);
console.log(frame.stack); // [0n] - Too old (> 256 blocks)

Empty Block Hashes Array

// No hashes available in context
const frame = createFrame({
  stack: [currentBlock - 10n],
  blockContext: {
    block_number: currentBlock,
    block_hashes: []
  }
});

blockhash(frame);
console.log(frame.stack); // [0n] - No hashes available

Benchmarks

Performance characteristics:
  • Array index calculation: O(1)
  • Hash retrieval: O(1)
  • Conversion to u256: O(32) - iterate 32 bytes
Gas efficiency:
  • 20 gas per query
  • ~50,000 queries per million gas
  • More efficient than equivalent storage reads (2100 gas cold)

References