Workspace IndexAlgorithms › Day 57

DAG-Based Consensus — The Mempool/Consensus Split in Narwhal and Bullshark TODO

Algorithms · Day 57 / 100 · D. Distributed Systems & Consensus (Day 52-68)

Concept

DAG-based consensus separates "spreading the data" from "deciding the order." Narwhal is the mempool layer: each validator builds transaction batches, collects other validators' signatures (a proof of availability) for them, and creates a vertex referencing the previous round's batches — the result is that all validators end up holding nearly the same DAG. Because this DAG already guarantees the data has been propagated and stored, the consensus layer only has to handle the DAG's metadata, not the actual transactions. An ordering protocol like Bullshark exchanges almost no extra messages: each validator interprets its own local DAG by a fixed deterministic rule and derives the same total order as everyone else. With this split, throughput scales with network bandwidth, and consensus latency becomes nearly independent of data size.

In classic BFT, where a single leader broadcasts every transaction, that leader's bandwidth becomes the system's throughput ceiling — this split is the core idea behind how modern high-throughput chain designs solve that bottleneck.

Code & Formula

# DAG 합의 — Narwhal(데이터 전파)과 Bullshark(순서화)처럼, 전파와 순서 결정을 분리한다.
# 각 정점은 이전 라운드의 과반 정점을 참조하고, 모든 노드가 같은 규칙으로 같은 전체 순서를 뽑는다.

import random

random.seed(9)

N_VALIDATORS = 4
QUORUM = N_VALIDATORS // 2 + 1  # 3

class Vertex:
    def __init__(self, round_no, validator, refs):
        self.round_no = round_no
        self.validator = validator
        self.refs = refs  # 참조하는 이전 라운드 정점들의 (round, validator) 목록
        self.id = (round_no, validator)

def build_dag(n_rounds):
    dag = {0: [Vertex(0, v, refs=[]) for v in range(N_VALIDATORS)]}
    for r in range(1, n_rounds):
        prev_vertices = dag[r - 1]
        dag[r] = []
        for v in range(N_VALIDATORS):
            # 이전 라운드 중 과반(QUORUM)개를 무작위로 참조 (가용성 증명을 흉내)
            refs = random.sample([pv.id for pv in prev_vertices], QUORUM)
            dag[r].append(Vertex(r, v, refs))
    return dag

def deterministic_order(dag, n_rounds):
    """각 라운드의 validator 0을 anchor로 삼아, anchor가 참조하는 조상들을 순서대로 나열 (Bullshark 축약판)"""
    order = []
    seen = set()
    for r in range(n_rounds - 1, -1, -1):
        anchor = next(v for v in dag[r] if v.validator == 0)
        stack = [anchor.id]
        local_order = []
        while stack:
            vid = stack.pop()
            if vid in seen:
                continue
            seen.add(vid)
            local_order.append(vid)
            rr, vv = vid
            vertex = next(v for v in dag[rr] if v.validator == vv)
            stack.extend(vertex.refs)
        order.extend(reversed(local_order))
    return order

dag = build_dag(n_rounds=3)
for r in sorted(dag):
    print(f"round {r}: {[v.id for v in dag[r]]}, 참조 예시(v0)={dag[r][0].refs}")

order_node_a = deterministic_order(dag, n_rounds=3)
order_node_b = deterministic_order(dag, n_rounds=3)  # 다른 노드가 같은 DAG로 동일하게 계산했다고 가정
print(f"\n전체 순서 (노드 A 계산): {order_node_a}")
print(f"전체 순서 (노드 B 계산): {order_node_b}")
print(f"두 노드의 순서 동일: {order_node_a == order_node_b} "
      f"(같은 DAG에 같은 규칙 -> 추가 통신 없이 결정적으로 동일한 전체 순서)")

Exercise

Build a simple round-based DAG in code where each vertex references a quorum's worth of the previous round's vertices, write a function that picks anchors by a fixed rule to derive a total order, and verify that different nodes produce identical results.

Practical Connection

This is directly relevant to understanding what determines the throughput and confirmation latency of the execution layer carrying Verex's orders and settlements, and what kind of ordering guarantee to expect when posting an off-chain matching result on-chain.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

DAG 합의 TODO

Algorithms · Day 57 / 100 · D. 분산시스템·합의 (Day 52–68)

Narwhal·Bullshark 계열의 분리(멤풀/합의)

개념

DAG 기반 합의는 '데이터를 퍼뜨리는 일'과 '순서를 정하는 일'을 분리한다. Narwhal은 멤풀 계층으로, 각 검증자가 트랜잭션 배치를 만들고 다른 검증자들의 서명(가용성 증명)을 모아 이전 라운드 배치들을 참조하는 정점을 만들며, 그 결과 모든 검증자가 거의 같은 DAG를 갖게 된다. 이 DAG는 이미 데이터가 전파·저장되었음을 보장하므로, 합의 계층은 실제 트랜잭션이 아니라 DAG의 메타데이터만 다루면 된다. Bullshark 같은 순서화 프로토콜은 추가 메시지를 거의 주고받지 않고, 각 검증자가 자기 로컬 DAG를 정해진 규칙으로 해석해 결정적으로 같은 전체 순서를 뽑아낸다. 이렇게 분리하면 처리량은 네트워크 대역폭에 따라 스케일하고, 합의 지연은 데이터 크기와 거의 무관해진다.

리더 하나가 모든 트랜잭션을 브로드캐스트하는 고전 BFT는 리더의 대역폭이 곧 시스템 처리량 상한이 되는데, 이 분리 구조가 그 병목을 어떻게 푸는지가 최신 고성능 체인 설계의 핵심 아이디어다.

코드 · 수식

# DAG 합의 — Narwhal(데이터 전파)과 Bullshark(순서화)처럼, 전파와 순서 결정을 분리한다.
# 각 정점은 이전 라운드의 과반 정점을 참조하고, 모든 노드가 같은 규칙으로 같은 전체 순서를 뽑는다.

import random

random.seed(9)

N_VALIDATORS = 4
QUORUM = N_VALIDATORS // 2 + 1  # 3

class Vertex:
    def __init__(self, round_no, validator, refs):
        self.round_no = round_no
        self.validator = validator
        self.refs = refs  # 참조하는 이전 라운드 정점들의 (round, validator) 목록
        self.id = (round_no, validator)

def build_dag(n_rounds):
    dag = {0: [Vertex(0, v, refs=[]) for v in range(N_VALIDATORS)]}
    for r in range(1, n_rounds):
        prev_vertices = dag[r - 1]
        dag[r] = []
        for v in range(N_VALIDATORS):
            # 이전 라운드 중 과반(QUORUM)개를 무작위로 참조 (가용성 증명을 흉내)
            refs = random.sample([pv.id for pv in prev_vertices], QUORUM)
            dag[r].append(Vertex(r, v, refs))
    return dag

def deterministic_order(dag, n_rounds):
    """각 라운드의 validator 0을 anchor로 삼아, anchor가 참조하는 조상들을 순서대로 나열 (Bullshark 축약판)"""
    order = []
    seen = set()
    for r in range(n_rounds - 1, -1, -1):
        anchor = next(v for v in dag[r] if v.validator == 0)
        stack = [anchor.id]
        local_order = []
        while stack:
            vid = stack.pop()
            if vid in seen:
                continue
            seen.add(vid)
            local_order.append(vid)
            rr, vv = vid
            vertex = next(v for v in dag[rr] if v.validator == vv)
            stack.extend(vertex.refs)
        order.extend(reversed(local_order))
    return order

dag = build_dag(n_rounds=3)
for r in sorted(dag):
    print(f"round {r}: {[v.id for v in dag[r]]}, 참조 예시(v0)={dag[r][0].refs}")

order_node_a = deterministic_order(dag, n_rounds=3)
order_node_b = deterministic_order(dag, n_rounds=3)  # 다른 노드가 같은 DAG로 동일하게 계산했다고 가정
print(f"\n전체 순서 (노드 A 계산): {order_node_a}")
print(f"전체 순서 (노드 B 계산): {order_node_b}")
print(f"두 노드의 순서 동일: {order_node_a == order_node_b} "
      f"(같은 DAG에 같은 규칙 -> 추가 통신 없이 결정적으로 동일한 전체 순서)")

연습

정점이 이전 라운드의 정족수만큼을 참조하는 간단한 라운드 기반 DAG를 코드로 만들고, 정해진 규칙으로 앵커를 골라 전체 순서를 뽑는 함수를 짜서 서로 다른 노드의 결과가 동일한지 검증하라.

실무 · Verex 연결

Verex의 주문·정산이 올라가는 실행 계층의 처리량과 확정 지연이 어디서 결정되는지, 그리고 오프체인 매칭 결과를 온체인에 올릴 때 기대할 수 있는 순서 보장의 성격을 이해하는 데 직결된다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 56. 비잔틴 정족수(3f+1)와 PBFT58. 나카모토 합의의 확률적 최종성과 selfish mining →