Workspace IndexKnowledge Notes › Merkle vs Verkle

#42PoC

Merkle vs Verkle

Why proof size, not hashing speed, is what decides whether stateless clients are possible.

Reference — docs/knowledge/merkle-vs-verkle.html.

Why

The state-bloat problem this catalogue keeps running into from the application side, looked at from the protocol side. Every card here that writes a storage slot — an enforcer's spent counter, a token balance — adds to state that every node keeps live forever. Verkle trees do not delete any of it; they change what a node must carry to prove a piece of it, which is the difference between "state is too big" and "state is too big to sync".

How it works

Reading note, not a demo: how a Merkle proof's size grows with tree width (you must supply every sibling at every level), why vector commitments collapse that to a constant-size proof regardless of width, and what Ethereum's Verge roadmap intends to buy with the swap — stateless clients that validate without holding the state. Also what it costs: heavier cryptography, and a migration of the entire state trie.

Review clarification

Proof size, not proof time

This is the distinction the review kept returning to. A Verkle tree shrinks the proof's size (bytes to transmit), not the time to prove or verify. Its cryptography is heavier per operation — vector commitments over elliptic curves instead of plain SHA-256 hashing — so end-to-end compute goes up, not down. That is the card's subtitle made literal: hashing was never the bottleneck, bandwidth was.

One opening per level, regardless of width

Merkle: at each level you must supply every other child in the group — branching − 1 siblings, which grows with width. Verkle: a vector commitment collapses each level to one constant-size opening, whatever the width. So total proof ≈ (constant per level) × (number of levels). Numbers from the Related code, proving leaf #5 of 64:

branching depth Merkle openings Verkle openings
2 6 6 6
16 2 30 2
256 1 255 1

Why Verkle deliberately goes wide

Because width is free for proof size, Verkle designers make nodes wide — Ethereum's design is 256-ary — which makes the tree shallow, so the proof is tiny on two counts at once: constant per level and fewer levels. Merkle cannot do this: every extra child costs one more sibling in every proof, which is why real Merkle trees stay binary.

The cost moved; it did not vanish

Verkle trades many cheap hash siblings for fewer, bigger, cryptographically heavier openings. Size down, crypto compute up. That trade is what buys stateless clients — a node validates a block without holding the whole state, because the witness it ships each block is finally small enough to move around the network.

What the Related code does — and does not

The Merkle half is real (SHA-256, counts siblings). The Verkle half mirrors it line-for-line but models proof size only: its commitment is still a hash, so the file runs with no libraries and is not cryptographically sound. A production Verkle needs an IPA/KZG vector commitment (elliptic-curve math) to make one O(1) opening actually prove a child at any position.

Related code

"""Merkle vs Verkle PoC -- proof size grows with tree width, not with hashing speed.
Illustrates the core mechanism: a Merkle proof needs one sibling hash per level, so
wider trees (more children per node) need more siblings per level to prove membership.

The Verkle half mirrors the Merkle half line-for-line -- same tree, same leaf. The ONLY
thing that changes is how many openings a proof carries per level:
  Merkle: (children in the group - 1) siblings per level  -> grows with width
  Verkle: exactly ONE opening         per level           -> constant, any width

NOTE: a production Verkle uses an IPA/KZG *vector commitment* (elliptic-curve math) so
that one O(1) opening proves a child at any position. The Verkle commitment below is
still a hash, so the file runs with no libraries -- it only MODELS the proof-*size*
property, and is NOT cryptographically sound. The point is size, not crypto.
"""

import hashlib


def h(*parts: str) -> str:
    return hashlib.sha256("|".join(parts).encode()).hexdigest()[:12]


# ── Merkle ────────────────────────────────────────────────────────────────────
def build_tree(leaves: list[str], branching: int) -> list[list[str]]:
    """Builds a Merkle tree with `branching` children per node; returns levels bottom-up."""
    levels = [leaves]
    while len(levels[-1]) > 1:
        cur = levels[-1]
        nxt = []
        for i in range(0, len(cur), branching):
            group = cur[i:i + branching]
            nxt.append(h(*group))
        levels.append(nxt)
    return levels


def proof_size(levels: list[list[str]], leaf_index: int, branching: int) -> int:
    """Count sibling hashes needed to prove one leaf's membership -- (branching - 1) per level."""
    siblings = 0
    idx = leaf_index
    for level in levels[:-1]:
        siblings += branching - 1  # every level, you must supply all other children in the group
        idx //= branching
    return siblings


# ── Verkle (same tree shape; only the proof model differs) ──────────────────────
def verkle_build_tree(leaves: list[str], branching: int) -> list[list[str]]:
    """Same shape as build_tree; commit stands in for a vector commitment over the children."""
    levels = [leaves]
    while len(levels[-1]) > 1:
        cur = levels[-1]
        nxt = []
        for i in range(0, len(cur), branching):
            group = cur[i:i + branching]
            nxt.append(h("vc", *group))  # a real Verkle uses an IPA/KZG commitment here
        levels.append(nxt)
    return levels


def verkle_proof_size(levels: list[list[str]], leaf_index: int, branching: int) -> int:
    """ONE constant-size opening per level, regardless of width -- what a vector commitment buys."""
    return len(levels) - 1


if __name__ == "__main__":
    leaves = [f"leaf{i}" for i in range(64)]

    # bytes per opening: a 32B sibling hash (Merkle) vs one 48B EC opening (Verkle, BLS12-381 G1)
    MB, VB = 32, 48
    print(f"{'branch':>6}{'depth':>7}{'merkle_open':>13}{'verkle_open':>13}{'merkle_B':>10}{'verkle_B':>10}")
    for branching in (2, 4, 8, 16):
        mt = build_tree(leaves, branching)
        vt = verkle_build_tree(leaves, branching)
        mo = proof_size(mt, leaf_index=5, branching=branching)
        vo = verkle_proof_size(vt, leaf_index=5, branching=branching)
        print(f"{branching:>6}{len(mt) - 1:>7}{mo:>13}{vo:>13}{mo * MB:>10}{vo * VB:>10}")

    # why Verkle deliberately picks a WIDE node (Ethereum's design is 256-ary):
    # width is free for proof SIZE, so go wide -> shallow tree -> tiny proof.
    print("\n--- wide node: free for Verkle, ruinous for Merkle ---")
    for branching in (2, 16, 256):
        mt = build_tree(leaves, branching)
        vt = verkle_build_tree(leaves, branching)
        print(f"branch={branching:>3}  depth={len(mt) - 1}  "
              f"merkle openings={proof_size(mt, 5, branching):>3}   "
              f"verkle openings={verkle_proof_size(vt, 5, branching)}")

    print("\nMerkle: wider = bigger proof (siblings pile up), so real trees stay binary.")
    print("Verkle: a vector commitment collapses each level's siblings to a constant-size")
    print("opening regardless of width -- so go wide, shallow, tiny. The cost did not vanish;")
    print("it moved into heavier cryptography (per opening), not into more bytes. That")
    print("constant-size property is what makes stateless clients (validate without holding")
    print("the whole state) practical.")

Where it lands in Jayverse

  • Devnet: track proof size, not hashing speed, for any future L2. When Devnet moves from an Anvil fork toward an OP-Stack L2, treat proof-size growth as the metric that decides whether a stateless or light client is feasible, not raw hash throughput.
  • Auditor: flag storage growth in every contract that writes persistent state. Verex order state, Bridge mint counters and Rabbit's session-key mandates all add to state every node keeps forever; treat that growth as a first-class cost the way Ethereum's Verge roadmap does.
  • gitboard: add a state-size metric per service. Track storage growth for Verex and Bridge so the "how big does this get" question is visible before it becomes a syncing problem.

Key expressions

Words and phrases from this page worth keeping, with the Korean meaning and the sentence they come from.

Expression뜻 · 쓰이는 자리
keep running into계속 맞닥뜨리다 · 반복해서 같은 문제에 부딪힐 때. "the state-bloat problem this catalogue keeps running into"
looked at from ... side~쪽에서 바라본 · 같은 문제를 다른 관점에서 볼 때. "looked at from the protocol side"
collapse to~으로 축소되다, 하나로 합쳐지다 · 여러 항목이 하나의 값으로 줄어들 때. "collapses each level to one constant-size opening"
buy (achieve)대가를 치르고 얻어내다 · 비용을 들여 특정 이익을 확보할 때. "what Ethereum's Verge roadmap intends to buy"
trade X for YX를 내주고 Y를 얻다 · 하나를 포기하고 다른 것을 얻는 교환. "Verkle trades many cheap hash siblings for fewer"
line-for-line한 줄 한 줄 그대로 · 구조를 거의 똑같이 따라할 때. "mirrors it line-for-line but models proof size only"
made literal말 그대로 실현된, 문자 그대로가 된 · 비유적 표현이 실제 사실로 확인될 때. "the card's subtitle made literal"
go wide설계를 폭넓게 가다, 너비를 늘리다 · 트리 구조 등에서 분기 수를 늘리는 선택. "Why Verkle deliberately goes wide"
IPA/KZG내적 논증/케이트 공약(Inner Product Argument / Kate-Zaverucha-Goldberg commitment) · 실제 Verkle 트리에 쓰이는 벡터 공약 방식들. "A production Verkle needs an IPA/KZG vector commitment"
vector commitment벡터 공약(여러 값을 하나의 짧은 증거로 압축하는 암호 기법) · Verkle 증명 크기를 일정하게 만드는 핵심 도구. "vector commitments over elliptic curves instead of plain SHA-256"
witness블록 검증에 필요한 압축 증거 데이터 · 상태 전체 없이도 유효성을 증명하게 해주는 증거. "the witness it ships each block is finally small"

← All Knowledge Notes · Workspace Index · Top ↑

Merkle vs Verkle

무상태 클라이언트의 가능 여부를 가르는 건 해싱 속도가 아니라 증명 크기라는 것.

참조 — docs/knowledge/merkle-vs-verkle.html.

이 카탈로그가 애플리케이션 쪽에서 계속 부딪히는 상태 팽창 문제를, 프로토콜 쪽에서 본 것입니다. 스토리지 슬롯을 쓰는 여기 모든 카드 — 강제기의 지출 카운터, 토큰 잔고 — 가 모든 노드가 영구히 살려 두는 상태에 더해집니다. Verkle 트리는 그걸 지우지 않습니다. 노드가 그중 한 조각을 증명하기 위해 들고 다녀야 하는 양을 바꿀 뿐이고, 그게 "상태가 너무 크다"와 "상태가 너무 커서 동기화가 안 된다" 사이의 차이입니다.

동작 방식

데모가 아니라 정독 노트입니다: 머클 증명의 크기가 트리 폭에 따라 어떻게 늘어나는지(각 레벨의 형제 노드를 전부 제출해야 한다), 벡터 커밋먼트가 어떻게 폭과 무관한 상수 크기 증명으로 그것을 접는지, 그리고 이더리움 Verge 로드맵이 그 교체로 사려는 것 — 상태를 들고 있지 않고도 검증하는 무상태 클라이언트. 대가도 함께: 더 무거운 암호학, 그리고 상태 트라이 전체의 마이그레이션.

검토 후 보완

증명 크기지, 증명 시간이 아니다

검토 중 계속 돌아온 구분입니다. Verkle 트리는 증명의 크기(전송할 바이트)를 줄이지, 증명·검증 시간을 줄이는 게 아닙니다. 암호학은 연산당 오히려 더 무겁습니다 — 단순 SHA-256 해싱이 아니라 타원곡선 위의 벡터 커밋먼트라서, 전체 계산량은 줄지 않고 늘어납니다. 카드 부제를 문자 그대로 옮긴 것입니다: 병목은 애초에 해싱이 아니라 대역폭이었습니다.

폭과 무관하게, 레벨당 오프닝 하나

머클: 각 레벨에서 그룹의 나머지 자식을 전부 제출해야 합니다 — branching − 1개 형제, 폭에 따라 증가. Verkle: 벡터 커밋먼트가 각 레벨을 폭과 무관한 상수 크기 오프닝 하나로 접습니다. 그래서 전체 증명 ≈ (레벨당 상수) × (레벨 수). Related code에서 64개 중 리프 #5를 증명한 수치:

branching depth 머클 오프닝 Verkle 오프닝
2 6 6 6
16 2 30 2
256 1 255 1

Verkle이 일부러 넓게 가는 이유

폭이 증명 크기 면에서 공짜이기 때문에, Verkle 설계자는 노드를 넓게 만듭니다 — 이더리움 설계는 256분기 — 그러면 트리가 얕아져서 증명이 두 가지로 동시에 작아집니다: 레벨당 상수 그리고 더 적은 레벨. 머클은 이걸 못 합니다: 자식 하나가 늘 때마다 모든 증명에 형제가 하나씩 더 붙으므로, 실제 머클 트리는 이진을 유지합니다.

비용은 사라진 게 아니라 옮겨갔다

Verkle은 값싼 해시 형제 여럿을 더 적고 크고 암호적으로 무거운 오프닝과 맞바꿉니다. 크기 , 암호 계산 . 이 맞바꿈이 무상태 클라이언트를 사옵니다 — 노드가 상태 전체를 들고 있지 않고도 블록을 검증합니다. 매 블록 실어 보내는 witness가 드디어 네트워크로 나를 만큼 작아졌기 때문입니다.

Related code가 하는 일, 그리고 하지 않는 일

머클 절반은 진짜입니다(SHA-256, 형제 계수). Verkle 절반은 그것을 그대로 대응시키되 증명 크기만 모델링합니다: 커밋먼트가 여전히 해시라 라이브러리 없이 돌아가고, 암호적으로는 안전하지 않습니다. 실제 Verkle은 O(1) 오프닝 하나가 임의 위치의 자식을 실제로 증명하려면 IPA/KZG 벡터 커밋먼트(타원곡선 연산)가 필요합니다.

관련 코드

"""Merkle vs Verkle PoC -- proof size grows with tree width, not with hashing speed.
Illustrates the core mechanism: a Merkle proof needs one sibling hash per level, so
wider trees (more children per node) need more siblings per level to prove membership.

The Verkle half mirrors the Merkle half line-for-line -- same tree, same leaf. The ONLY
thing that changes is how many openings a proof carries per level:
  Merkle: (children in the group - 1) siblings per level  -> grows with width
  Verkle: exactly ONE opening         per level           -> constant, any width

NOTE: a production Verkle uses an IPA/KZG *vector commitment* (elliptic-curve math) so
that one O(1) opening proves a child at any position. The Verkle commitment below is
still a hash, so the file runs with no libraries -- it only MODELS the proof-*size*
property, and is NOT cryptographically sound. The point is size, not crypto.
"""

import hashlib


def h(*parts: str) -> str:
    return hashlib.sha256("|".join(parts).encode()).hexdigest()[:12]


# ── Merkle ────────────────────────────────────────────────────────────────────
def build_tree(leaves: list[str], branching: int) -> list[list[str]]:
    """Builds a Merkle tree with `branching` children per node; returns levels bottom-up."""
    levels = [leaves]
    while len(levels[-1]) > 1:
        cur = levels[-1]
        nxt = []
        for i in range(0, len(cur), branching):
            group = cur[i:i + branching]
            nxt.append(h(*group))
        levels.append(nxt)
    return levels


def proof_size(levels: list[list[str]], leaf_index: int, branching: int) -> int:
    """Count sibling hashes needed to prove one leaf's membership -- (branching - 1) per level."""
    siblings = 0
    idx = leaf_index
    for level in levels[:-1]:
        siblings += branching - 1  # every level, you must supply all other children in the group
        idx //= branching
    return siblings


# ── Verkle (same tree shape; only the proof model differs) ──────────────────────
def verkle_build_tree(leaves: list[str], branching: int) -> list[list[str]]:
    """Same shape as build_tree; commit stands in for a vector commitment over the children."""
    levels = [leaves]
    while len(levels[-1]) > 1:
        cur = levels[-1]
        nxt = []
        for i in range(0, len(cur), branching):
            group = cur[i:i + branching]
            nxt.append(h("vc", *group))  # a real Verkle uses an IPA/KZG commitment here
        levels.append(nxt)
    return levels


def verkle_proof_size(levels: list[list[str]], leaf_index: int, branching: int) -> int:
    """ONE constant-size opening per level, regardless of width -- what a vector commitment buys."""
    return len(levels) - 1


if __name__ == "__main__":
    leaves = [f"leaf{i}" for i in range(64)]

    # bytes per opening: a 32B sibling hash (Merkle) vs one 48B EC opening (Verkle, BLS12-381 G1)
    MB, VB = 32, 48
    print(f"{'branch':>6}{'depth':>7}{'merkle_open':>13}{'verkle_open':>13}{'merkle_B':>10}{'verkle_B':>10}")
    for branching in (2, 4, 8, 16):
        mt = build_tree(leaves, branching)
        vt = verkle_build_tree(leaves, branching)
        mo = proof_size(mt, leaf_index=5, branching=branching)
        vo = verkle_proof_size(vt, leaf_index=5, branching=branching)
        print(f"{branching:>6}{len(mt) - 1:>7}{mo:>13}{vo:>13}{mo * MB:>10}{vo * VB:>10}")

    # why Verkle deliberately picks a WIDE node (Ethereum's design is 256-ary):
    # width is free for proof SIZE, so go wide -> shallow tree -> tiny proof.
    print("\n--- wide node: free for Verkle, ruinous for Merkle ---")
    for branching in (2, 16, 256):
        mt = build_tree(leaves, branching)
        vt = verkle_build_tree(leaves, branching)
        print(f"branch={branching:>3}  depth={len(mt) - 1}  "
              f"merkle openings={proof_size(mt, 5, branching):>3}   "
              f"verkle openings={verkle_proof_size(vt, 5, branching)}")

    print("\nMerkle: wider = bigger proof (siblings pile up), so real trees stay binary.")
    print("Verkle: a vector commitment collapses each level's siblings to a constant-size")
    print("opening regardless of width -- so go wide, shallow, tiny. The cost did not vanish;")
    print("it moved into heavier cryptography (per opening), not into more bytes. That")
    print("constant-size property is what makes stateless clients (validate without holding")
    print("the whole state) practical.")

Jayverse에서의 위치

  • Devnet: 미래 L2를 위해 해싱 속도가 아니라 증명 크기를 추적한다. Devnet이 Anvil 포크에서 OP-Stack L2로 옮겨갈 때, 스테이트리스나 라이트 클라이언트가 가능한지를 결정하는 지표로 원시 해시 처리량이 아니라 증명 크기 증가를 본다.
  • Auditor: 영구 상태를 쓰는 모든 컨트랙트의 상태 증가를 표시한다. Verex 주문 상태, Bridge의 민팅 카운터, Rabbit의 세션키 매니데이트 모두 모든 노드가 영구히 들고 있어야 하는 상태를 늘린다. Ethereum의 Verge 로드맵처럼 이 증가를 1급 비용으로 취급한다.
  • gitboard: 서비스별 상태 크기 지표를 추가한다. Verex와 Bridge의 스토리지 증가를 추적해, "이게 얼마나 커지는가"라는 질문이 동기화 문제가 되기 전에 눈에 보이게 한다.

핵심 표현

이 페이지의 영어 본문에서 배울 만한 단어와 표현, 뜻과 나온 자리.

Expression뜻 · 쓰이는 자리
keep running into계속 맞닥뜨리다 · 반복해서 같은 문제에 부딪힐 때. "the state-bloat problem this catalogue keeps running into"
looked at from ... side~쪽에서 바라본 · 같은 문제를 다른 관점에서 볼 때. "looked at from the protocol side"
collapse to~으로 축소되다, 하나로 합쳐지다 · 여러 항목이 하나의 값으로 줄어들 때. "collapses each level to one constant-size opening"
buy (achieve)대가를 치르고 얻어내다 · 비용을 들여 특정 이익을 확보할 때. "what Ethereum's Verge roadmap intends to buy"
trade X for YX를 내주고 Y를 얻다 · 하나를 포기하고 다른 것을 얻는 교환. "Verkle trades many cheap hash siblings for fewer"
line-for-line한 줄 한 줄 그대로 · 구조를 거의 똑같이 따라할 때. "mirrors it line-for-line but models proof size only"
made literal말 그대로 실현된, 문자 그대로가 된 · 비유적 표현이 실제 사실로 확인될 때. "the card's subtitle made literal"
go wide설계를 폭넓게 가다, 너비를 늘리다 · 트리 구조 등에서 분기 수를 늘리는 선택. "Why Verkle deliberately goes wide"
IPA/KZG내적 논증/케이트 공약(Inner Product Argument / Kate-Zaverucha-Goldberg commitment) · 실제 Verkle 트리에 쓰이는 벡터 공약 방식들. "A production Verkle needs an IPA/KZG vector commitment"
vector commitment벡터 공약(여러 값을 하나의 짧은 증거로 압축하는 암호 기법) · Verkle 증명 크기를 일정하게 만드는 핵심 도구. "vector commitments over elliptic curves instead of plain SHA-256"
witness블록 검증에 필요한 압축 증거 데이터 · 상태 전체 없이도 유효성을 증명하게 해주는 증거. "the witness it ships each block is finally small"

← 전체 기술 노트 · 워크스페이스 인덱스 · 맨 위 ↑