Skip to main content

Overview

Opcode: 0x16 Introduced: Frontier (EVM genesis) AND performs bitwise AND on two 256-bit unsigned integers. Each bit in the result is 1 only if the corresponding bits in both operands are 1. This operation is fundamental for bit masking, extracting specific bit ranges, and checking flags. Primary uses: extracting addresses from uint256, applying bit masks, checking flag combinations.

Specification

Stack Input:
a (top)
b
Stack Output:
a & b
Gas Cost: 3 (GasFastestStep) Truth Table (per bit):
a | b | a & b
--|---|------
0 | 0 |   0
0 | 1 |   0
1 | 0 |   0
1 | 1 |   1

Behavior

AND pops two values from the stack, performs bitwise AND on each corresponding bit pair, and pushes the result. The operation is:
  • Commutative: a & b = b & a
  • Associative: (a & b) & c = a & (b & c)
  • Identity element: a & MAX_UINT256 = a
  • Null element: a & 0 = 0

Examples

Basic Masking

import { and } from '@tevm/voltaire/evm/bitwise';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// Extract lower 8 bits (least significant byte)
const value = 0x123456789ABCDEFn;
const mask = 0xFFn;
const frame = createFrame({ stack: [value, mask] });
const err = and(frame);

console.log(frame.stack); // [0xEFn]

Extract Address from uint256

// Extract 160-bit address from packed uint256
const packed = 0x000000000000000000000000dEaDbEeFcAfE1234567890abcdef1234567890ABn;
const addressMask = (1n << 160n) - 1n;  // 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
const frame = createFrame({ stack: [packed, addressMask] });
and(frame);

console.log(frame.stack[0].toString(16));
// 'deadbeefcafe1234567890abcdef1234567890ab'

Check Multiple Flags

// Check if both FLAG_A and FLAG_B are set
const FLAGS_AB = (1n << 0n) | (1n << 1n);  // 0b11
const value = 0b1011n;  // Has flags 0, 1, 3
const frame = createFrame({ stack: [value, FLAGS_AB] });
and(frame);

const hasBoth = frame.stack[0] === FLAGS_AB;
console.log(hasBoth);  // true (bits 0 and 1 are both set)

Isolate Specific Bytes

// Extract bytes 12-15 (middle 4 bytes of address-like value)
const value = 0x1122334455667788990011223344556677889900n;
const mask = 0xFFFFFFFF00000000000000000000000000000000n;
const frame = createFrame({ stack: [value, mask] });
and(frame);

const extracted = frame.stack[0] >> 160n;
console.log(extracted.toString(16));  // '11223344'

Commutative Property

// a & b = b & a
const a = 0xAAAAAAAAn;
const b = 0x55555555n;

const frame1 = createFrame({ stack: [a, b] });
and(frame1);

const frame2 = createFrame({ stack: [b, a] });
and(frame2);

console.log(frame1.stack[0] === frame2.stack[0]);  // true (both = 0)

Gas Cost

Cost: 3 gas (GasFastestStep) AND shares the lowest gas tier with:
  • OR (0x17), XOR (0x18), NOT (0x19)
  • BYTE (0x1a)
  • SHL (0x1b), SHR (0x1c), SAR (0x1d)
  • ADD (0x01), SUB (0x03)
  • Comparison operations (LT, GT, EQ, etc.)

Edge Cases

Identity Element

// AND with all ones
const MAX = (1n << 256n) - 1n;
const value = 0x123456n;
const frame = createFrame({ stack: [value, MAX] });
and(frame);

console.log(frame.stack[0] === value);  // true (identity)

Null Element

// AND with zero
const value = 0x123456n;
const frame = createFrame({ stack: [value, 0n] });
and(frame);

console.log(frame.stack[0]);  // 0n

Maximum Values

// MAX & MAX = MAX
const MAX = (1n << 256n) - 1n;
const frame = createFrame({ stack: [MAX, MAX] });
and(frame);

console.log(frame.stack[0] === MAX);  // true

Alternating Bits

// Alternating bit patterns
const pattern1 = 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAn;
const pattern2 = 0x5555555555555555555555555555555555555555555555555555555555555555n;
const frame = createFrame({ stack: [pattern1, pattern2] });
and(frame);

console.log(frame.stack[0]);  // 0n (no overlapping bits)

Stack Underflow

// Insufficient stack items
const frame = createFrame({ stack: [0x123n] });
const err = and(frame);

console.log(err);  // { type: "StackUnderflow" }

Out of Gas

// Insufficient gas
const frame = createFrame({ stack: [0x123n, 0x456n], gasRemaining: 2n });
const err = and(frame);

console.log(err);  // { type: "OutOfGas" }
console.log(frame.gasRemaining);  // 0n

Common Usage

Extract Address from Storage Value

// Storage packs address (160 bits) + flags (96 bits)
assembly {
    let packed := sload(slot)
    let addr := and(packed, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)
    let flags := shr(160, packed)
}

Check Permission Flags

// Check if user has both READ and WRITE permissions
uint256 constant READ = 1 << 0;
uint256 constant WRITE = 1 << 1;
uint256 constant EXECUTE = 1 << 2;

function hasReadWrite(uint256 permissions) pure returns (bool) {
    return (permissions & (READ | WRITE)) == (READ | WRITE);
}

Align to Boundary

// Align pointer to 32-byte boundary (clear lower 5 bits)
function alignTo32(uint256 ptr) pure returns (uint256) {
    return ptr & ~uint256(0x1F);  // Clear bits 0-4
}

Extract Nibble (4 bits)

// Extract specific nibble from bytes32
function getNibble(bytes32 data, uint256 index) pure returns (uint8) {
    require(index < 64, "index out of range");
    uint256 shift = (63 - index) * 4;
    return uint8((uint256(data) >> shift) & 0xF);
}

Color Channel Extraction

// Extract RGB channels from packed uint24 (0xRRGGBB)
function unpackRGB(uint24 color) pure returns (uint8 r, uint8 g, uint8 b) {
    r = uint8((color >> 16) & 0xFF);
    g = uint8((color >> 8) & 0xFF);
    b = uint8(color & 0xFF);
}

Implementation

/**
 * AND opcode (0x16) - Bitwise AND operation
 */
export function op_and(frame: FrameType): EvmError | null {
  // Consume gas (GasFastestStep = 3)
  frame.gasRemaining -= 3n;
  if (frame.gasRemaining < 0n) {
    frame.gasRemaining = 0n;
    return { type: "OutOfGas" };
  }

  // Pop operands
  if (frame.stack.length < 2) return { type: "StackUnderflow" };
  const a = frame.stack.pop();
  const b = frame.stack.pop();

  // Compute bitwise AND
  const result = a & b;

  // Push result
  if (frame.stack.length >= 1024) return { type: "StackOverflow" };
  frame.stack.push(result);

  // Increment PC
  frame.pc += 1;

  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { op_and } from './and.js';

describe('AND (0x16)', () => {
  it('performs basic AND', () => {
    const frame = createFrame({ stack: [0b1100n, 0b1010n] });
    expect(op_and(frame)).toBeNull();
    expect(frame.stack[0]).toBe(0b1000n);
  });

  it('extracts address mask', () => {
    const packed = 0x000000000000000000000000deadbeefn;
    const mask = (1n << 160n) - 1n;
    const frame = createFrame({ stack: [packed, mask] });
    expect(op_and(frame)).toBeNull();
    expect(frame.stack[0]).toBe(0xdeadbeefn);
  });

  it('handles identity (AND with all ones)', () => {
    const MAX = (1n << 256n) - 1n;
    const value = 0x123456n;
    const frame = createFrame({ stack: [value, MAX] });
    expect(op_and(frame)).toBeNull();
    expect(frame.stack[0]).toBe(value);
  });

  it('handles null element (AND with zero)', () => {
    const frame = createFrame({ stack: [0x123456n, 0n] });
    expect(op_and(frame)).toBeNull();
    expect(frame.stack[0]).toBe(0n);
  });

  it('is commutative', () => {
    const a = 0xAAAAn;
    const b = 0x5555n;
    const frame1 = createFrame({ stack: [a, b] });
    const frame2 = createFrame({ stack: [b, a] });
    op_and(frame1);
    op_and(frame2);
    expect(frame1.stack[0]).toBe(frame2.stack[0]);
  });

  it('returns StackUnderflow with insufficient stack', () => {
    const frame = createFrame({ stack: [0x123n] });
    expect(op_and(frame)).toEqual({ type: 'StackUnderflow' });
  });

  it('returns OutOfGas when insufficient gas', () => {
    const frame = createFrame({ stack: [0x123n, 0x456n], gasRemaining: 2n });
    expect(op_and(frame)).toEqual({ type: 'OutOfGas' });
  });
});

Edge Cases Tested

  • Basic AND operations (truth table)
  • Identity element (AND with MAX_UINT256)
  • Null element (AND with 0)
  • Address extraction from uint256
  • Flag checking
  • Commutative property
  • Alternating bit patterns
  • Stack underflow
  • Out of gas

Security

Incorrect Mask Size

// VULNERABLE: Mask doesn't cover full address
function extractAddress(uint256 packed) pure returns (address) {
    return address(uint160(packed & 0xFFFFFFFF));  // Only 32 bits!
}

// CORRECT: Full 160-bit mask
function extractAddress(uint256 packed) pure returns (address) {
    return address(uint160(packed & type(uint160).max));
}

Flag Checking Logic Errors

// WRONG: Checks if ANY flag is set (should be AND)
function hasPermission(uint256 perms, uint256 required) pure returns (bool) {
    return (perms | required) != 0;  // Using OR instead of AND
}

// CORRECT: Checks if ALL required flags are set
function hasPermission(uint256 perms, uint256 required) pure returns (bool) {
    return (perms & required) == required;
}

Off-by-One in Bit Positions

// WRONG: Flag indices off by one
uint256 constant FLAG_0 = 1 << 1;  // Should be 1 << 0
uint256 constant FLAG_1 = 1 << 2;  // Should be 1 << 1

// CORRECT: Proper flag definitions
uint256 constant FLAG_0 = 1 << 0;  // Bit 0
uint256 constant FLAG_1 = 1 << 1;  // Bit 1

Endianness Confusion

// Be aware: BYTE opcode uses big-endian (byte 0 = MSB)
// But bitwise operations are position-agnostic
bytes32 data = 0x0123456789ABCDEF;
assembly {
    // This extracts byte 0 (0x01), not byte 31!
    let b := and(shr(248, data), 0xFF)  // Correct
}

Benchmarks

AND is one of the fastest EVM operations: Execution time (relative):
  • AND: 1.0x (baseline, fastest tier)
  • OR/XOR: 1.0x (same tier)
  • ADD: 1.0x (same tier)
  • MUL: 1.2x
  • DIV: 2.5x
Gas efficiency:
  • 3 gas per 256-bit AND operation
  • ~333,333 AND operations per million gas
  • Native hardware instruction on all platforms

References