~/blockchain-dev.md
BLOCKCHAIN_DEV

CLZ opcode는 왜 작은 수학 함수가 아니라 EVM 비용 모델의 변화일까: EIP-7939로 보는 비트 연산 기본기

10분 읽기·2026.07.15·출처 5·00
오늘의 질문

count leading zeros 같은 낮은 수준의 비트 연산 하나가 왜 스마트컨트랙트 가스, ZK 증명 비용, bytecode 크기까지 바꿀까?

blockchain-dev.md
EIP-7939 CLZ

CLZ opcode는 왜 작은 수학 함수가 아니라 EVM 비용 모델의 변화일까: EIP-7939로 보는 비트 연산 기본기

1. 왜 지금 봐야 하나

최근 Ethereum Fusaka/Osaka 계열 EIP를 보면 큰 주제만 보이기 쉽다. blob, PeerDAS, gas limit, block size, precompile repricing 같은 것들이다. 그런데 EIP-7939는 훨씬 작아 보인다. 새 opcode 하나, CLZ를 추가한다. CLZ(x)는 256-bit word에서 가장 앞쪽의 0 bit가 몇 개인지 세고, x == 0이면 256을 반환한다.

작아 보여도 개발자에게 중요한 이유가 있다. 스마트컨트랙트에서 비트 연산은 “마이크로 최적화”가 아니라 다음 세 영역의 공통 기반이다.

  1. 고정소수점 수학: sqrt, cbrt, lnWad, powWad 같은 함수는 숫자의 크기 범위를 빠르게 알아야 한다.
  2. 데이터 구조: bitmap에서 다음 set bit/unset bit를 찾을 때 leading/trailing zero 계산이 핵심이다.
  3. 증명 비용: zkVM이나 ZK 회로 안에서 동적 shift를 많이 쓰는 코드는 실행 가스보다 proving cost가 더 크게 문제가 될 수 있다.

EIP-7939는 이 공통 연산을 Solidity 라이브러리 트릭에서 EVM 기본 명령어로 내린다. 즉 오늘의 포인트는 “새 opcode가 생겼다”가 아니라, 자주 쓰이는 추상 연산을 VM primitive로 승격할 때 비용 모델과 감사 포인트가 어떻게 바뀌는가다.

2. 핵심 개념

CLZ는 count leading zeros의 약자다. 256-bit EVM word를 왼쪽, 즉 most significant bit 쪽에서부터 보면서 1이 나오기 전까지의 0 개수를 센다.

예를 들어:

  • 0x8000...0000은 맨 앞 bit가 1이므로 CLZ = 0
  • 0x4000...0000은 앞에 0이 하나 있고 그다음 bit가 1이므로 CLZ = 1
  • 0x0000...0001은 마지막 bit만 1이므로 CLZ = 255
  • 0x0000...0000은 1이 전혀 없으므로 특수 케이스로 CLZ = 256

EIP-7939의 specification은 새 opcode를 0x1e로 정의하고, stack에서 값 하나를 pop한 뒤 결과 하나를 push한다고 설명한다. execution-spec-tests의 spec.py도 같은 상수를 둔다.

zsh — 생존확인.sh
CLZ = 0x1E
CLZ_GAS_COST = 5

def calculate_clz(value: int) -> int:
    if value == 0:
        return 256
    return 256 - value.bit_length()

개발자가 눈여겨볼 숫자는 gas cost 5다. EIP 본문은 원래 빠른 Solidity 구현이 약 184 gas를 사용한다고 예시를 든다. 반면 opcode는 5 gas다. PR #9987은 CLZ gas cost를 3에서 5로 올렸는데, 이유를 “under-pricing DoS risk를 피하기 위한 보수적 상한”이라고 설명한다. 여기서 중요한 설계 감각은 이것이다.

VM primitive는 너무 비싸면 아무도 안 쓰고, 너무 싸면 DoS 표면이 된다. 그래서 “빠른가?”만큼 “최악 비용이 상수로 안전한가?”가 중요하다.

3. 최신 이슈와 연결

EIP-7939는 상태가 Final이고, EIP 저장소 PR #9701로 2025년 4월 29일 병합되었다. 이후 PR #9987에서 gas cost가 3에서 5로 조정되었다. execution-spec-tests는 tests/osaka/eip7939_count_leading_zeros 아래에 CLZ 상수, reference spec commit, edge case 테스트를 포함한다.

테스트가 흥미로운 이유는 단순 happy path만 보지 않기 때문이다. test_count_leading_zeros.py는 다음 케이스를 생성한다.

  • 전체 0: 결과 256
  • leading zeros가 0부터 256까지 변하는 값
  • single bit만 켜진 값
  • 임의의 큰 256-bit 값

이 테스트 목록은 개발자에게 좋은 힌트를 준다. CLZ 같은 연산은 “대충 숫자 크기 세는 함수”로 보면 안 된다. 경계값이 곧 consensus semantics다. 특히 0 -> 256은 일반적인 8-bit/32-bit 감각과 다르게 보일 수 있으므로 컨트랙트 wrapper와 라이브러리에서 명확히 다뤄야 한다.

4. 개발자 관점 해석

4.1 수학 라이브러리: 분기와 lookup table을 줄일 수 있다

고정소수점 수학 라이브러리는 입력 숫자의 magnitude를 빠르게 파악해야 한다. 지금까지는 여러 번의 SHR, 비교, magic constant, lookup table을 조합했다. EIP 본문도 빠른 Solidity 구현이 여러 dynamic bitwise right shift와 큰 constant를 사용한다고 설명한다.

CLZ가 있으면 다음과 같은 코드가 단순해진다.

zsh — 생존확인.sh
// 개념 예시: 실제 사용은 fork 지원 여부와 compiler 지원을 확인해야 한다.
uint256 msbIndex = 255 - clz(x); // x != 0일 때

이 변화는 가스만 줄이는 게 아니다. 리뷰어가 “이 magic constant가 맞나?”를 검토하던 일을 “CLZ 결과를 어떤 경계 조건으로 쓰는가?”로 바꾼다.

4.2 bitmap: next bit 탐색 비용이 예측 가능해진다

DEX tick bitmap, NFT/slot allocation bitmap, permission bitmap처럼 bitset을 쓰는 컨트랙트는 다음 set bit를 찾는 로직을 자주 가진다. CLZ는 leading direction 탐색을 자연스럽게 만든다. trailing zero가 필요하면 EIP rationale처럼 가장 낮은 set bit를 x & -x로 isolate한 뒤 CLZ로 위치를 계산할 수 있다.

핵심은 loop를 줄이고 worst-case를 상수화하는 것이다. 블록체인에서는 평균 성능보다 최악 입력에서 gas가 폭발하지 않는지가 더 중요하다.

4.3 ZK/rollup: 실행 가스와 proving cost는 다르다

EIP-7939의 motivation은 ZK proving cost를 직접 언급한다. 빠른 Solidity 구현은 dynamic shr를 여러 번 쓰는데, 특정 zkVM에서는 shift가 multiplication보다 더 비쌀 수 있다고 설명한다. 이 말은 블록체인 개발자가 점점 더 자주 마주치는 구분을 보여준다.

  • L1 gas: EVM에서 한 트랜잭션이 내는 비용
  • prover cost: 같은 계산을 증명으로 만들 때의 비용
  • bytecode size: 배포와 코드 로딩, 감사 난이도에 영향을 주는 비용

CLZ는 이 셋을 동시에 줄일 수 있는 primitive다. 그래서 “opcode 하나”가 rollup prover, on-chain verifier, math-heavy DeFi library의 공통 관심사가 된다.

5. 내 프로젝트에 적용할 체크포인트

  1. fork 지원 여부를 분리하라

    • 새 opcode는 해당 fork 이후 체인에서만 유효하다.
    • L1, L2, local anvil/hardhat, forked network가 같은 opcode table을 쓰는지 확인한다.
  2. x == 0 정책을 명시하라

    • CLZ는 0에 대해 256을 반환한다.
    • msb = 255 - clz(x) 같은 식은 x == 0에서 underflow/의미 오류가 날 수 있다.
    • wrapper에서 require(x != 0) 또는 별도 branch를 둔다.
  3. 기존 assembly 최적화를 바로 삭제하지 말라

    • compiler가 CLZ intrinsic을 언제 지원하는지 확인해야 한다.
    • inline assembly로 opcode를 직접 쓰면 chain compatibility와 테스트 환경이 더 중요해진다.
  4. 가스 최적화와 감사 가능성을 같이 보라

    • magic constant 기반 구현은 빠르지만 리뷰 난이도가 높다.
    • CLZ 도입 후에는 “상수 검증”보다 “경계값과 fork guard”가 감사 포인트가 된다.
  5. ZK 프로젝트라면 proving benchmark를 따로 재라

    • EVM gas가 낮아져도 특정 proving backend에서 효과가 얼마인지는 별도 측정이 필요하다.
    • 특히 bitmap, compression, hash-to-field 전처리, PQ signature 관련 코드는 후보가 된다.

6. 오늘 10분 액션

아래처럼 내 코드베이스에서 CLZ 후보를 찾아보자.

zsh — 생존확인.sh
# Solidity/Yul에서 비트 크기 계산, shift-heavy 코드 찾기
rg "shr\(|shl\(|bitLength|mostSignificantBit|msb|leading|bitmap|ctz|clz" contracts test

그리고 후보마다 표를 만든다.

위치현재 구현0 입력 처리loop/shift 수CLZ로 바꿀 수 있나fork guard 필요
Math.sqrtassembly msbrevert여러 SHR가능
TickBitmap.nextmask + scanskipbranch 많음일부 가능
Compression.decodebyte scan32 byte skiploop가능성 검토

마지막으로 테스트 케이스에 최소한 아래 값을 넣는다.

zsh — 생존확인.sh
0
1
2**255
2**255 - 1
2**128
2**128 + 1
2**256 - 1

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase의 최근 blockchain_dev 글을 확인했다. 최근 글은 EIP-7907 contract code size gas, Solana Alpenglow finality, Taiko bridge origin verification, OP Stack fault proof soundness, ERC-4626 NAV manipulation, P-256/passkey precompile, MODEXP repricing, EIP-7702 delegation, PeerDAS/BPO/blob fee reserve 등을 다뤘다. 이번 글은 DA, bridge, finality, precompile repricing이 아니라 EIP-7939 CLZ opcode를 통해 EVM primitive 하나가 math library, bitmap, bytecode size, ZK proving cost, fork compatibility에 주는 영향을 다뤄 중복을 피했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)