Workspace IndexPoCs › DVT in the protocol

6DVT in the protocol PLANNED

Reading notes on absorbing distributed validators into the protocol — m-of-n without splitting keys, plus what it makes buildable.

Read-only design analysis — no wallet needed. Start with the two diagrams contrasting DVT today against the proposal.

Why

Reading a live protocol-design discussion closely enough to separate three things people usually blur: what the proposal actually changes, what it leaves unresolved, and which parts of the idea can be built one layer up without waiting for it. It is also a second instance of a pattern this site already documents elsewhere — middleware doing a job well until the protocol absorbs it, which is exactly what ERC-4337 bundlers face from native account abstraction.

How it works

Today's DVT (Obol, SSV) splits one validator key with Shamir sharing or threshold BLS and runs an off-chain consensus round to reassemble a signature each time; the protocol still sees a single validator, and all distribution lives in middleware. The proposal never splits the key: each participant registers their own (n ≤ 16), the protocol groups them m-of-n, and BLS aggregation plus a participation bitfield — the same grammar as today's attestation aggregation — decides whether enough took part. That removes both the per-signature consensus round and the DKG ceremony, and it is only possible because EIP-7251 raised the max effective balance so that 32·n ETH standing up n slots is arithmetic the protocol can do internally. The page is explicit that this is an ethresear.ch-stage discussion with no assigned EIP number, lists the four questions it leaves open (slashing attribution, latency budget, the m<n collusion trade-off, the n ≤ 16 rationale), and separates PoC candidates into those buildable today at the application layer and those that genuinely wait on adoption.

2 diagram(s) on the live page.

Related code

# DVT in the protocol — the proposal never splits one validator key (unlike today's
# Obol/SSV, which Shamir-splits a key and reassembles a signature via off-chain
# consensus). Instead each of n participants keeps their own key; the protocol groups
# them m-of-n and a participation bitfield (mocked BLS aggregation) decides if enough
# signed — the same grammar as today's attestation aggregation.

import hashlib
from dataclasses import dataclass


@dataclass
class Participant:
    operator_id: str
    private_key: str  # each operator's own key — never shared or split

    def sign(self, message: str) -> str:
        # Mock signature: a real BLS sig would be a curve point; a hash stands in.
        return hashlib.sha256((self.private_key + message).encode()).hexdigest()[:16]


def bls_aggregate(signatures: list[str]) -> str:
    """Mock aggregation: real BLS sums curve points into one constant-size signature."""
    combined = "".join(sorted(signatures))
    return hashlib.sha256(combined.encode()).hexdigest()[:16]


def protocol_check(n: int, m_threshold: int, signed_by: list[Participant], message: str) -> dict:
    """The protocol groups n registered operators m-of-n and checks the bitfield."""
    bitfield = [1 if p in signed_by else 0 for p in OPERATORS[:n]]
    aggregate_sig = bls_aggregate([p.sign(message) for p in signed_by])
    quorum_met = sum(bitfield) >= m_threshold
    return {
        "bitfield": bitfield,
        "participants_signed": sum(bitfield),
        "threshold": m_threshold,
        "quorum_met": quorum_met,
        "aggregate_signature": aggregate_sig if quorum_met else None,
    }


OPERATORS = [Participant(f"operator-{i}", private_key=f"sk-{i}") for i in range(1, 6)]  # n = 5

if __name__ == "__main__":
    print("DVT absorbed into the protocol — separate keys, m-of-n grouping, participation bitfield\n")

    n, m = 5, 3
    message = "attest(slot=1234)"

    print(f"n={n} registered operators, m={m} required to reach quorum\n")

    # Only 3 of 5 operators actually sign this round.
    signers = OPERATORS[:3]
    result = protocol_check(n, m, signers, message)
    print(f"Signers this round: {[p.operator_id for p in signers]}")
    print(f"Participation bitfield: {result['bitfield']}")
    print(f"Signed {result['participants_signed']}/{n}, threshold {result['threshold']} "
          f"-> quorum_met={result['quorum_met']}")
    print(f"Aggregate signature: {result['aggregate_signature']}\n")

    # Below threshold: only 2 sign.
    signers = OPERATORS[:2]
    result = protocol_check(n, m, signers, message)
    print(f"Signers this round: {[p.operator_id for p in signers]}")
    print(f"Participation bitfield: {result['bitfield']}")
    print(f"Signed {result['participants_signed']}/{n}, threshold {result['threshold']} "
          f"-> quorum_met={result['quorum_met']}")
    print("No aggregate signature: quorum not reached.")

Open on jaylabs.xyz →


한국어

6프로토콜에 흡수된 DVT PLANNED

분산 밸리데이터를 프로토콜이 직접 다루자는 제안 정독 노트 — 키를 쪼개지 않는 m-of-n, 그리고 그것이 만들어내는 것들.

읽기 전용 설계 분석 — 지갑 불필요. 오늘의 DVT와 제안을 대비시킨 다이어그램 두 장부터 보세요.

진행 중인 프로토콜 설계 논의를, 사람들이 흔히 뭉뚱그리는 세 가지를 분리할 만큼 자세히 읽는 작업입니다: 제안이 실제로 바꾸는 것, 미해결로 남긴 것, 그리고 제안을 기다리지 않고 한 층 위에서 지금 만들 수 있는 부분. 이 사이트가 이미 다른 곳에서 기록하고 있는 패턴의 두 번째 사례이기도 합니다 — 미들웨어가 어떤 일을 잘 해내다가 프로토콜에 흡수되는 흐름으로, ERC-4337 번들러가 네이티브 계정 추상화 앞에서 맞고 있는 상황과 같습니다.

동작 방식

오늘의 DVT(Obol·SSV)는 하나의 밸리데이터 키를 샤미르 분할이나 임계 BLS로 쪼갠 뒤, 서명이 필요할 때마다 오프체인 합의 라운드로 재조립합니다 — 프로토콜은 여전히 밸리데이터 하나만 보고, 분산은 전부 미들웨어에 삽니다. 제안은 키를 쪼개지 않습니다: 각 참여자가 자기 키를 등록하고(n ≤ 16), 프로토콜이 m-of-n으로 묶으며, BLS 집계와 참여 비트필드(오늘날 attestation 집계와 같은 문법)로 충분한 인원이 참여했는지 판정합니다. 이로써 서명마다의 합의 라운드와 DKG 세리머니가 함께 사라지고, 이것이 가능한 이유는 EIP-7251이 유효 잔고 상한을 올려 32·n ETH가 n개 슬롯을 세운다는 산수를 프로토콜이 내부적으로 할 수 있게 되었기 때문입니다. 페이지는 이것이 EIP 번호가 없는 ethresear.ch 단계의 논의임을 명시하고, 미해결로 남은 질문 넷(슬래싱 귀속, 지연 예산, m<n 공모 트레이드오프, n ≤ 16의 근거)을 나열하며, PoC 후보를 애플리케이션 계층에서 지금 만들 수 있는 것과 실제로 채택을 기다려야 하는 것으로 구분합니다.

2 diagram(s) on the live page.

관련 코드

# DVT in the protocol — the proposal never splits one validator key (unlike today's
# Obol/SSV, which Shamir-splits a key and reassembles a signature via off-chain
# consensus). Instead each of n participants keeps their own key; the protocol groups
# them m-of-n and a participation bitfield (mocked BLS aggregation) decides if enough
# signed — the same grammar as today's attestation aggregation.

import hashlib
from dataclasses import dataclass


@dataclass
class Participant:
    operator_id: str
    private_key: str  # each operator's own key — never shared or split

    def sign(self, message: str) -> str:
        # Mock signature: a real BLS sig would be a curve point; a hash stands in.
        return hashlib.sha256((self.private_key + message).encode()).hexdigest()[:16]


def bls_aggregate(signatures: list[str]) -> str:
    """Mock aggregation: real BLS sums curve points into one constant-size signature."""
    combined = "".join(sorted(signatures))
    return hashlib.sha256(combined.encode()).hexdigest()[:16]


def protocol_check(n: int, m_threshold: int, signed_by: list[Participant], message: str) -> dict:
    """The protocol groups n registered operators m-of-n and checks the bitfield."""
    bitfield = [1 if p in signed_by else 0 for p in OPERATORS[:n]]
    aggregate_sig = bls_aggregate([p.sign(message) for p in signed_by])
    quorum_met = sum(bitfield) >= m_threshold
    return {
        "bitfield": bitfield,
        "participants_signed": sum(bitfield),
        "threshold": m_threshold,
        "quorum_met": quorum_met,
        "aggregate_signature": aggregate_sig if quorum_met else None,
    }


OPERATORS = [Participant(f"operator-{i}", private_key=f"sk-{i}") for i in range(1, 6)]  # n = 5

if __name__ == "__main__":
    print("DVT absorbed into the protocol — separate keys, m-of-n grouping, participation bitfield\n")

    n, m = 5, 3
    message = "attest(slot=1234)"

    print(f"n={n} registered operators, m={m} required to reach quorum\n")

    # Only 3 of 5 operators actually sign this round.
    signers = OPERATORS[:3]
    result = protocol_check(n, m, signers, message)
    print(f"Signers this round: {[p.operator_id for p in signers]}")
    print(f"Participation bitfield: {result['bitfield']}")
    print(f"Signed {result['participants_signed']}/{n}, threshold {result['threshold']} "
          f"-> quorum_met={result['quorum_met']}")
    print(f"Aggregate signature: {result['aggregate_signature']}\n")

    # Below threshold: only 2 sign.
    signers = OPERATORS[:2]
    result = protocol_check(n, m, signers, message)
    print(f"Signers this round: {[p.operator_id for p in signers]}")
    print(f"Participation bitfield: {result['bitfield']}")
    print(f"Signed {result['participants_signed']}/{n}, threshold {result['threshold']} "
          f"-> quorum_met={result['quorum_met']}")
    print("No aggregate signature: quorum not reached.")

Open on jaylabs.xyz →


Deep-dive note · thought-experiment session, 2026-08-13

DVT reading notes — from attestation basics to four fault lines

The long note behind the card summary above. Starts with what a validator actually does (§1), why DVT exists (§2), the protocol-absorption proposal (§3), and the four fault lines the thought experiment surfaced (§4–6).

Hand-written, so the generator leaves it alone — the card’s docsHref points here as the canonical page.

1. What a validator actually does

Understanding DVT requires knowing a validator’s duties and timing precisely. This section is that foundation, and it keeps the exact points that tripped me up during the session.

1.1 Slots and epochs

slot  = 12 seconds
epoch = 32 slots = 6.4 minutes

A validator does two things — attesting (once per epoch, 99% of the work) and proposing a block (once every few months at a million validators).

❓ The confusion — “doesn’t a validator attest every block?”

At the network level attestations do pour in every slot. At the individual level it is exactly once per epoch. Both are true; they are different layers of the same picture.

The reason is that validators are split into committees. Each epoch the whole validator set is shuffled and divided across the 32 slots:

epoch N (32 slots = 6.4 min)
├ slot 0  → committee A (~30k) attests
├ slot 1  → committee B (~30k) attests
│  ⋮
└ slot 31 → committee Z (~30k) attests

my validator: assigned to exactly one of these → once per epoch

Within a slot the set is subdivided again, and that is about p2p subnets: one committee maps to one subnet with its own aggregators, so 30,000 signatures get collected hierarchically rather than all in one place.

1.2 One attestation carries two votes

Attestation {
  head:   "the current head of the chain is this block"   ← fork choice (LMD-GHOST)
  source: "the last justified checkpoint"                 ┐
  target: "this epoch’s boundary block"                   ┘ ← finality vote (Casper FFG)
}

❓ The confusion — “do you sign once for the slot and once for the epoch?”

No. One signature, two votes inside it — like putting two ballots in one envelope. Two different consensus algorithms (fork choice and finality) ride on a single message.

1.3 Nobody signs anything at “epoch finalization”

epoch N begins
├ slot 0  committee A signs ── target: epoch N boundary block
├ slot 1  committee B signs ── target: same
│  ⋮
└ slot 31 committee Z signs ── target: same
│
▼ end of epoch N ← no signing. the protocol just counts what already arrived.
   did ≥2/3 of stake vote for that target? → justified

   two consecutive justified epochs → the earlier one is finalized   ≈ 12.8 min

❓ The confusion — “does everyone sign together at the epoch decision point?”

It is like collecting votes over 32 days and counting them on the 33rd. Nobody votes on counting day. The epoch boundary block is fixed the moment the epoch starts, so the slot-0 committee and the slot-31 committee can both name the same target.

Two things to add. Splitting across slots divides the work, not the judgement — every committee in the epoch votes for the same target. And the 2/3 threshold is stake, not headcount, which matters more now that EIP-7251 (MaxEB) lets balances differ.

1.4 The 512-member sync committee is a separate track

Attestation committeeSync committee
Whoeveryone, spread over 32 slots512 selected
How oftenonce per epoch eachevery slot
Termreshuffled every epoch~27 hours (256 epochs)
Signshead + source + targetthe block header
Purposeconsensuslight clients

Sync-committee signatures do not affect consensus. Where the chain goes and what finalizes is decided entirely by attestations. The sync committee is a convenience layer: a phone wallet cannot verify a million attestations, so it only has to check that 512 named validators signed a header. That said, it does mean being online every slot — one more reason to want DVT.

1.5 Why a million signers doesn’t collapse the chain — BLS aggregation

~1M validators × once per epoch ÷ 6.4 min
  ≈ 30k per slot ≈ 2,600 per second
  → at 96 bytes each that is ~3MB per slot. Stored raw, the chain dies.

This is why Ethereum uses BLS rather than ECDSA. BLS sums n signatures over the same message into one 96-byte signature, and the public keys aggregate too, so verification happens once. What is lost is “who signed” — which is why a participation bitfield is carried alongside.

Without aggregationAggregated
500 signatures48,000 bytes159 bytes (96 + 63-byte bitfield)
Verifications5001

And there is a quiet elegance here: target stays constant across the whole epoch, which is not a coincidence. That is precisely what lets a million signatures scattered across 32 slots aggregate into one. Putting the finality vote on the epoch boundary rather than on every slot is a design shaped to fit what BLS aggregation can do.

2. Why DVT exists

2.1 The asymmetry between the two failures

FailureCauseCost
Offlineserver or network dieslose roughly what you would have earned — small
Double signingtwo conflicting messages, same slotpenalty + correlation penalty + forced exit — catastrophic

Operational reality compounds it: you never know which slot you are assigned, so you must be always on; you have four seconds to run fork choice and sign; and because you must sign, the private key sits hot on an internet-connected server, 24/7.

2.2 Redundancy is the slashing condition

If you are worried the server will die, the obvious answer is redundancy. Except:

slot 17 (my assigned slot)
├ server A: sees block X as head → signs attest(head=X)
└ server B: never received X     → signs attest(head=W)
                                     ↑ double vote = slashing

The requirements collide head-on:

DVT resolves the contradiction with t-of-n. At 4-of-6, two machines can die and it keeps running, while no single machine can do anything alone.

2.3 Today’s DVT — two pieces

① The key (threshold BLS). Either a DKG ceremony, where the whole key is never computed at all, or Shamir-splitting an existing key. Either way, the key is never reassembled during operation — shares produce partial signatures that combine via Lagrange interpolation. (Same property as the MPC vs. SSS distinction in custody design.) The finished signature is bit-for-bit indistinguishable from a single-key signature, so Ethereum has no idea this validator is run by six parties.

② Off-chain consensus. BLS only combines partials over the same message, and independently-run fork choice can diverge. So the cluster must agree on what to sign before signing — QBFT in Obol, its own protocol in SSV. This is DVT’s real cost: one consensus round per signature.

2.4 The four-second budget

t=0s   ─ slot begins
t~1s   ─ block arrives; six nodes each run fork choice
t~2s   ─ ⚙️ QBFT round ("the data we will sign is this")
t~3s   ─ each signs with its key share → exchange partials
t~3.5s ─ whoever collects 4 reconstructs the full signature
t=4s   ─ ⏰ deadline — broadcast

A solo validator would be done around t~1s; DVT inserts a consensus round and fills the budget. Being late is not slashable but reduces rewards, and if consensus fails outright no attestation goes out at all. Bought availability, gained a new failure mode.

2.5 Where it is used — and “machines” vs. “parties”

The largest consumer is Lido. It began with the Simple DVT Module and now runs through CSM (Community Staking Module) v3’s “Identified DVT Clusters” via Obol and SSV, with bonds as low as 0.5 ETH per key. Elsewhere: institutional staking risk reduction, home-staker clusters, restaking operators.

My n machinesn independent parties
Solvesavailabilityavailability + decentralization
Trustonly myselfmutually distrusting
If slashedall my money anywayattribution is everything

Lido invests in the right-hand column — DVT is sold as a decentralization tool, not an uptime tool. And only the right-hand column produces hard questions: what does a non-participant earn? can you punish only the guilty? can I exit alone? Today’s DVT answers these outside the protocol — private agreements, reward-splitter contracts, and expensive eviction that requires re-running DKG. The protocol cannot help, because it does not know who signed.

3. The proposal — the protocol handles m-of-n directly

The premise: the only reason for the key-splitting acrobatics is that the protocol can see just one validator. So teach it to see several.

Today’s DVT (Obol / SSV)The proposal
Keyone key split (DKG / Shamir)each registers their own (n ≤ 16)
Pre-signing consensusQBFT round per dutynone
Participation— (protocol is blind)BLS aggregate + bitfield
Where distribution livesmiddlewarethe protocol

The elegance is that nothing new is invented. The protocol already runs “many participants → aggregate signature + bitfield → threshold check” at a scale of a million. An m-of-n group is that same grammar as a miniature committee of n ≤ 16.

And EIP-7251 (MaxEB) is what makes it possible now. With the balance ceiling raised from 32 to 2048, 32·n fits in one record — and only then can the protocol express “n people put in 32 each and made one validator” as arithmetic.

📍 Status (checked 2026-08-13)

It is not an EIP. It is an ethresear.ch post with no EIP number assigned. The pipeline is ① ethresear.ch → ② EIP draft → ③ All Core Devs → ④ CFI → ⑤ SFI → ⑥ devnet/mainnet, and this sits at . Being a consensus-layer change, even if it advances it is a multi-year path.

Do not confuse it with “DVT-lite.” That is a later, separate proposal about operational practice — simplifying setups with existing tools, no fork required — and the Ethereum Foundation is actually staking 72,000 ETH that way. Native DVT (this card’s subject) needs a hard fork; DVT-lite works today. Both come out of Vitalik’s 2026 “simplification” push.

4. Thought experiment — four findings

That the spec is unsettled is what makes the exercise worth doing: instead of reading and implementing, you have to make the design decisions yourself — and doing so surfaced fault lines not among the card’s original four questions.

Finding ① — the state model

Participation attribution is free; economic attribution has to be paid for in state.

The natural state model is one index, one balance (32n), n keys. Because duty assignment keys off the index, “always the same duties” follows automatically — and that is beneficial. If the index were split into n, each would land in a different slot and they would never sign the same thing; the merge is required.

But the same decision hurts on the other side. One balance means n people’s money is commingled:

bitfield: [1,1,1,1,1,1,1,1,0,0]   ← the protocol knows who idled
balance:  320 ETH (single number)  ← but cannot burn selectively

Separating it requires balances: [Gwei; n] and withdrawal_credentials: [_; n], inflating the validator record up to 16× — and beacon state is data every node must hold.

→ Hypothesis: n ≤ 16 is not “16 is decentralized enough” but “16 is what the state budget can carry.” A provisional answer to the card’s fourth open question.

Finding ② — not in the card: correlated liveness failure

Grouping converts graceful degradation into a cliff.

n independent validatorsm-of-n group
1 down32 ETH goes quietno effect (with headroom)
n−m+1 downonly that much goes quietall 32n goes quiet

Quorum is a binary test: clear it and the entire balance counts, miss it and zero does. It is worse at network scale — if many groups share the same client or the same cloud, one outage pushes several groups below m simultaneously, each dropping 32n at once, potentially stalling finality by missing the 2/3 threshold.

The paradox: DVT exists for resilience through diversity, yet absorbed into the protocol and widely adopted it could make failures correlate instead.

Finding ③ — not in the card: free-riding and mutual insurance are the same thing

Turn participation attribution on, and you turn off the reason the group exists.

With a bitfield, “no work, no pay” looks possible — and looks like the cleanest answer. But do the arithmetic:

participant with 99% uptime

"no work, no pay":
  solo  99% uptime → 99% of rewards
  group 99% uptime → 99% of rewards      ← gains exactly nothing

pooled rewards:
  10 members at 99% each → P(quorum miss) ≈ 0
  → group earns 100% → my share exceeds my solo take

The group exists to provide mutual insurance — pooling 99% uptimes into 100%. And yet:

FramingSame phenomenon
Charitable“if I go down, others cover me” = insurance
Uncharitable“if I idle, I still get paid” = free-riding

This is the classic insurance problem: eliminate moral hazard completely and the insurance itself ceases to exist. Nor is it new — the problem already exists in today’s DVT, handled by monitoring, social sanction, and (expensive) DKG re-runs. Lido calling them “Identified” Clusters is exactly this hole being plugged with identity-based trust in place of anonymous trust.

→ The proposal’s contribution is not solving this but making it solvable. The information is now available; the rule for using it is unwritten — and with a pooled balance, knowing does not help (Finding ①).

Finding ④ — why the consensus round can disappear

Byzantine consensus is not solved but avoided, by restating the problem as a monotone predicate.

Consensus (today’s DVT)Aggregation (the proposal)
Question“what shall we sign?”“who signed?”
Ranking of answersno objective orderingmore is plainly better and verifiable
Honest fraction needed≥ 2/3a single honest party
Round tripsrequirednone
operator 3 publishes: [1,1,1,0,1,1,1,1,0,0]  ← 8 bits (excludes me)
operator 7 publishes: [1,1,1,1,1,1,1,1,0,0]  ← 9 bits ← this one wins

→ censoring me requires unanimity. One honest party defeats it.

It is not free. Nothing forces agreement, so views can split:

during a reorg / late block:
  operators 1–6:  head = X → aggregate X (6 bits)
  operators 7–10: head = Y → aggregate Y (4 bits)
  m = 8 → neither reaches quorum ❌

Though natural convergence can be more forgiving than forced agreement — split 6:4 with m=6, the majority still passes, whereas DVT would have lost the whole attestation to a failed consensus round.

And the safety threshold is still m. Censorship resistance is a strong 1-of-n, but m colluding members can sign a slashable message. For BFT-grade safety, set m > 2n/3 (n=10 → m ≥ 7; 8/10 has headroom, 6/10 does not).

Side findings — two ways an honest participant gets wronged

① Aggregator griefing. A zero bit has two possible causes — you really did not sign, or you did and the aggregator did not wait. Harmless under pooled rewards; the moment you switch to per-participant accounting it becomes a free way to harm someone. (Mitigation: the “fuller aggregate wins” rule above.)

② Geographic bias. In a Europe-centred cluster, an Asian participant’s partials are structurally late. That is bias, not noise, so averaging does not wash it out. DVT’s deepest tension lives here — geographic spread is the goal, spread means latency, and latency distorts the participation verdict.

For what it is worth, Ethereum already carries buffers against the analogous problem: multiple aggregators per committee, an inclusion window of up to a full epoch, and rewards split into timely_source / timely_target / timely_head so that missing head still earns the rest. It is not all-or-nothing.

5. Provisional design conclusions

  1. Pool the rewards, individualize the slashing. Split the axes. What needs insuring is availability, not honesty — small lapses get covered, only real betrayal is charged personally. This matches the asymmetry from §2.1. Side benefit: the balance need not be fully separated, saving state and loosening the n ≤ 16 ceiling.
  2. Never judge on a single epoch — use a rolling window. One epoch’s bitfield is noise; cumulative participation is signal. Random inclusion failures wash out; chronic free-riding shows up statistically.
  3. A deductible curve. Make 97% and 100% nearly indistinguishable in payout, and make it bite sharply below 60%. Honest operators can ignore the noise; only free-riders feel it.
  4. Do not designate an aggregator. Anyone may publish, and the aggregate with more bits always wins — censorship then requires unanimity.
  5. m > 2n/3. Offline losses are cheap and a low collusion threshold is expensive, so err high on m.
  6. Exploit that eviction is now cheap. With no key splitting, replacing a participant is deregistration rather than a DKG ceremony — making automatic ejection a realistic option that today’s DVT cannot afford.

6. Open questions

The four the source left open:

  1. Slashing attribution — the bitfield knows, the pooled balance blocks it (Finding ①)
  2. Latency budget — the consensus round goes away, view divergence arrives (Finding ④)
  3. The m<n collusion trade-off — availability and collusion threshold move in exact opposition
  4. The rationale for n ≤ 16 — provisional answer: the state budget (Finding ①)

Added by this session:

  1. Correlated liveness failure — the cliff effect, plus client/cloud monoculture risk (Finding ②)
  2. Reward attribution / free-riding — insurance and moral hazard are one phenomenon (Finding ③)
  3. Aggregator griefing — the costless attack vector that per-participant accounting opens
  4. Geographic bias — the goal of dispersion fights the fairness of the verdict

🔗 Where this connects to other items on this site

  • Middleware absorbed by the protocol — the same pattern ERC-4337 bundlers face from native account abstraction. The narrative already in the card’s “Why”.
  • Correlated failure — a quorum of N nodes running identical code is one judgement copied N times. A recurring trap in bridge and oracle design.
  • MPC vs. SSS — whether the key is reassembled during operation is the same axis as custody design.

심화 노트 · 2026-08-13 사고 실험 세션

DVT 정독 노트 — 어테스테이션 기초부터 네 개의 균열까지

위 카드 요약의 배경이 되는 긴 노트. 밸리데이터가 실제로 무엇을 하는지(1장)부터 시작해, DVT가 왜 필요한지(2장), 프로토콜 흡수 제안(3장), 그리고 사고 실험으로 찾아낸 네 개의 균열(4~6장)까지.

이 절만 손으로 쓴 것이라 생성기가 덮어쓰지 않는다 — 카드의 docsHref 가 이 파일을 정본으로 가리킨다.

1. 밸리데이터는 실제로 무엇을 하는가

DVT를 이해하려면 밸리데이터의 의무와 타이밍을 먼저 정확히 알아야 한다. 이 장은 그 기초이고, 세션에서 실제로 헷갈렸던 지점들을 그대로 담았다.

1.1 슬롯과 에폭

슬롯 = 12초
에폭 = 32슬롯 = 6.4분

밸리데이터는 두 가지 일을 한다 — 어테스테이션(매 에폭, 일의 99%)과 블록 제안(백만 밸리데이터 기준 수개월에 한 번).

❓ 헷갈렸던 것 — "밸리데이터는 매 블록마다 어테스트하지 않나?"

네트워크 전체로 보면 매 슬롯 어테스테이션이 쏟아지는 게 맞다. 개별 밸리데이터로 보면 에폭당 딱 한 번이다. 둘 다 맞는 말이고, 보는 층위가 다를 뿐이다.

이유는 밸리데이터를 위원회로 나누기 때문이다. 매 에폭마다 전체 밸리데이터 집합을 섞어 32개 슬롯에 배분한다:

에폭 N (32슬롯 = 6.4분)
├ 슬롯 0  → 위원회 A (약 3만 명) 어테스트
├ 슬롯 1  → 위원회 B (약 3만 명) 어테스트
│  ⋮
└ 슬롯 31 → 위원회 Z (약 3만 명) 어테스트

내 밸리데이터: 이 중 한 슬롯에만 배정 → 에폭당 1회

슬롯 안에서 다시 여러 위원회로 쪼개는데, 이건 p2p 서브넷 때문이다. 위원회 하나가 서브넷 하나에 대응하고 각 서브넷에 집계자가 있어서, 3만 명의 서명을 계층적으로 모은다.

1.2 어테스테이션 하나에 투표가 두 개 들어있다

Attestation {
  head:   "지금 체인 머리는 이 블록이다"   ← 포크 초이스 (LMD-GHOST)
  source: "직전에 정당화된 체크포인트"     ┐
  target: "이번 에폭 경계 블록"            ┘ ← 확정 투표 (Casper FFG)
}

❓ 헷갈렸던 것 — "슬롯에도 서명하고 에폭에도 서명하나?"

아니다. 서명은 한 번, 그 안에 투표가 두 개다. 봉투 하나에 투표용지 두 장을 넣는 것과 같다. 두 개의 다른 합의 알고리즘(포크 초이스 + 확정)이 하나의 메시지에 얹혀 있다.

1.3 "에폭 확정 시점"에는 아무도 서명하지 않는다

에폭 N 시작
├ 슬롯 0  위원회A 서명 ── target: 에폭N 경계블록
├ 슬롯 1  위원회B 서명 ── target: 같음
│  ⋮
└ 슬롯 31 위원회Z 서명 ── target: 같음
│
▼ 에폭 N 끝 ← 서명 없음. 이미 모인 표를 세기만 함.
   같은 target에 투표한 지분이 2/3 이상? → 정당화(justified)

   연속 두 에폭이 정당화되면 → 앞 에폭 확정(finalized)   ≈ 12.8분

❓ 헷갈렸던 것 — "에폭 결정 시점에 다 같이 서명하는 건가?"

투표를 32일에 나눠 받고 33일째에 개표하는 것과 같다. 개표일에 새로 투표하는 사람은 없다. 에폭 경계 블록은 에폭이 시작하자마자 정해지므로, 슬롯 0의 위원회도 슬롯 31의 위원회도 같은 target을 찍을 수 있다.

두 가지를 덧붙인다. 슬롯 분산은 "일을 나눈 것"이지 "판단을 나눈 것"이 아니다 — 모든 슬롯의 위원회가 같은 target에 투표한다. 그리고 2/3은 머릿수가 아니라 지분이다. EIP-7251(MaxEB)로 잔고가 제각각인 지금은 이 구분이 더 중요해졌다.

1.4 싱크 위원회 512명은 별개 트랙

어테스테이션 위원회싱크 위원회
누가전체를 32슬롯에 배분512명만 선발
빈도각자 에폭당 1회매 슬롯
임기매 에폭 재배정약 27시간 (256 에폭)
서명 대상head + source + target블록 헤더
목적합의라이트 클라이언트

싱크 위원회 서명은 합의에 영향을 주지 않는다. 체인이 어디로 갈지, 뭐가 확정될지는 전부 어테스테이션이 정한다. 싱크 위원회는 휴대폰 지갑이 100만 개의 어테스테이션을 검증할 수 없으니, "512명이 이 헤더에 서명했다"만 확인하면 되게 만든 편의 장치다. 다만 매 슬롯 온라인이어야 한다는 뜻이라, DVT를 쓸 이유가 하나 더 생기는 지점이기도 하다.

1.5 100만 명이 다 하는데 왜 안 터지나 — BLS 집계

밸리데이터 약 100만 × 에폭당 1회 ÷ 6.4분
  ≈ 슬롯당 3만 건 ≈ 초당 2,600건
  → 서명 96바이트면 슬롯당 3MB. 그대로 저장하면 체인이 죽는다.

이더리움이 ECDSA 대신 BLS를 쓰는 이유가 이것이다. BLS는 같은 메시지에 대한 서명 n개를 하나의 96바이트 서명으로 합칠 수 있고, 공개키도 같이 합쳐 검증을 한 번만 하면 된다. 대신 "누가 서명했는지"가 사라지므로 참여 비트필드로 따로 기록한다.

집계 없이집계 후
500명 서명48,000 바이트159 바이트 (96 + 비트필드 63)
검증 연산500회1회

그리고 여기 설계의 아름다움이 하나 숨어 있다 — target이 에폭 내내 같은 값이라는 것이 우연이 아니다. 그래야 32개 슬롯에 흩어져 서명한 100만 개를 하나로 뭉칠 수 있다. 확정 투표를 "매 슬롯"이 아니라 "에폭 경계"에 걸어둔 설계가 정확히 BLS 집계가 통하는 형태로 맞춰진 것이다.

2. DVT가 왜 필요한가

2.1 두 실패의 비대칭

실패원인대가
오프라인서버·네트워크 장애벌었을 만큼 잃음 — 작음
이중 서명같은 슬롯에 상충하는 두 메시지몰수 + 상관 페널티 + 강제 퇴출 — 파멸적

여기에 운영 현실이 겹친다. 어느 슬롯에 배정될지 모르니 항상 켜져 있어야 하고, 4초 안에 포크 초이스를 돌려 서명해야 하며, 서명을 해야 하므로 개인키가 인터넷에 연결된 서버 위에 24시간 뜨겁게 올라가 있어야 한다.

2.2 이중화가 곧 슬래싱 조건이다

서버가 죽을까 걱정되면 상식적인 해법은 이중화다. 그런데:

슬롯 17 (내 배정 슬롯)
├ 서버 A: 블록 X를 머리로 봄 → attest(head=X) 서명
└ 서버 B: 블록 X를 못 받음  → attest(head=W) 서명
                                  ↑ double vote = 슬래싱

요구사항이 정면충돌한다:

DVT는 t-of-n으로 이 모순을 푼다. 4-of-6이면 두 대가 죽어도 돌아가고, 어떤 한 대도 혼자서는 아무것도 못 한다.

2.3 오늘의 DVT — 두 조각

① 키 (임계 BLS)DKG(분산 키 생성)로 온전한 키를 한 번도 만들지 않거나, 기존 키를 샤미르 분할한다. 어느 경로든 운영 중에는 키를 재조립하지 않는다 — 조각들이 부분 서명을 만들고 그것이 라그랑주 보간으로 합쳐진다. (커스터디에서의 MPC vs SSS 구분과 같은 성질이다.) 완성된 서명은 단일 키 서명과 비트 단위로 구별되지 않으므로, 이더리움은 이 밸리데이터가 분산 운영 중임을 전혀 모른다.

② 오프체인 합의 — BLS는 같은 메시지에 대한 부분 서명만 합쳐진다. 각 노드가 독립적으로 포크 초이스를 돌리면 결론이 갈릴 수 있으므로, 서명 전에 "무엇을 서명할지" 합의해야 한다. Obol은 QBFT, SSV는 자체 프로토콜. 이것이 DVT의 진짜 비용이다 — 서명 한 번마다 합의 라운드 한 번.

2.4 4초 예산

t=0s  ─ 슬롯 시작
t~1s  ─ 블록 도착, 6개 노드가 각자 포크 초이스
t~2s  ─ ⚙️ QBFT 합의 라운드 ("우리가 서명할 데이터는 이것")
t~3s  ─ 각자 키 조각으로 부분 서명 → 교환
t~3.5s─ 4개 모은 노드가 완성 서명 재조립
t=4s  ─ ⏰ 마감 — 브로드캐스트

솔로라면 t~1s에 끝날 일을 DVT는 합의 라운드를 끼워 넣어 4초를 꽉 채운다. 늦으면 슬래싱은 아니지만 보상이 깎이고, 합의가 실패하면 어테스테이션이 아예 안 나간다. 가용성을 사려고 도입했는데 새로운 실패 모드(합의 실패)를 하나 얻은 셈이다.

2.5 어디에 쓰이나 — 그리고 "머신"과 "주체"의 구분

최대 사용처는 Lido다. Simple DVT Module로 시작해 지금은 CSM(Community Staking Module) v3의 "Identified DVT Clusters"로 이어졌고, Obol·SSV를 통해 키당 본드 0.5 ETH까지 낮아졌다. 그 외 기관 스테이킹의 운영 리스크 완화, 홈 스테이커 클러스터, 리스테이킹 오퍼레이터.

내 머신 n대독립 운영자 n명
푸는 문제가용성가용성 + 탈중앙화
신뢰나 자신만서로 안 믿음
슬래싱되면어차피 내 돈누구 탓인지가 전부

Lido가 DVT에 투자하는 이유는 오른쪽이다 — 가용성 도구가 아니라 탈중앙화 도구로 팔린다. 그리고 오른쪽일 때만 어려운 질문들이 생긴다: 논 사람의 보상은? 악행한 사람만 벌할 수 있나? 나만 빠져나올 수 있나? 오늘의 DVT는 이 질문들에 프로토콜 밖에서 답한다 — 사적 계약, 리워드 스플리터 컨트랙트, 그리고 DKG 재실행이 필요한 값비싼 추방. 프로토콜은 누가 서명했는지 모르니 도와줄 수가 없다.

3. 제안 — 프로토콜이 직접 m-of-n을 다룬다

발상은 이렇다: 키를 쪼개는 곡예를 하는 이유는 프로토콜이 밸리데이터 하나만 볼 줄 알기 때문이다. 그럼 여러 개를 볼 줄 알게 만들면 되지 않나?

오늘의 DVT (Obol·SSV)제안
하나를 쪼갬 (DKG/샤미르)각자 자기 키 등록 (n ≤ 16)
서명 전 합의매 의무마다 QBFT 라운드없음
참여 판정—(프로토콜이 모름)BLS 집계 + 참여 비트필드
분산이 사는 곳미들웨어프로토콜

우아한 점은 새로 발명하는 것이 하나도 없다는 것이다. 프로토콜은 이미 100만 명 규모로 "여러 참여자 → 집계 서명 + 비트필드 → 임계값 판정"을 돌리고 있다. m-of-n 그룹은 그 문법의 n ≤ 16짜리 미니 위원회일 뿐이다.

그리고 지금 가능해진 이유가 EIP-7251(MaxEB)이다. 잔고 상한이 32에서 2048로 오르면서 32·n이 한 레코드에 담기게 됐고, 그 순간 "n명이 각자 32씩 넣어 하나를 만든다"는 산수를 프로토콜이 표현할 수 있게 됐다.

📍 현재 상태 (2026-08-13 확인)

EIP가 아니다. ethresear.ch 게시글 단계이고 EIP 번호가 배정된 적이 없다. 파이프라인은 ① ethresear.ch → ② EIP 초안 → ③ All Core Devs → ④ CFI → ⑤ SFI → ⑥ 데브넷·메인넷이고 지금 ①이다. 합의계층 변경이라 진행되더라도 연 단위다.

혼동 주의 — "DVT-lite"는 별개다. 비탈릭이 이후에 낸 운영 방식 제안으로, 프로토콜 변경 없이 기존 도구로 셋업을 단순화하자는 것이다. 이더리움 재단이 실제로 이 방식으로 72,000 ETH를 스테이킹 중이다. 네이티브 DVT(이 카드의 주제)는 하드포크가 필요하고, DVT-lite는 지금 된다. 둘 다 비탈릭의 2026년 "단순화" 캠페인에서 나왔다.

4. 사고 실험 — 네 개의 발견

스펙이 확정되지 않았다는 것이 오히려 이 실험의 값어치다. 읽고 구현하는 대신 설계 결정을 직접 내려봐야 하고, 그 과정에서 카드의 원래 네 질문에 없던 균열들이 드러났다.

발견 ① — 상태 모델

참여 귀속은 공짜로 얻지만, 경제적 귀속은 상태 비용을 내야 얻는다.

가장 자연스러운 상태 모델은 인덱스 하나 · 잔고 하나(32n) · 키 n개다. 의무 배정이 인덱스 기준이므로 "항상 같은 역할"이 자동으로 성립하고 — 이건 이롭게 작용한다. 인덱스가 n개로 흩어지면 각자 다른 슬롯에 배정되어 애초에 같이 서명할 일이 없어지니, 이 통합은 필수적이다.

그런데 같은 결정이 반대편에서는 해롭다. 잔고가 한 덩어리라 n명의 돈이 섞이고, 그러면:

비트필드: [1,1,1,1,1,1,1,1,0,0]   ← 누가 놀았는지 프로토콜이 안다
잔고:     320 ETH (한 덩어리)      ← 그런데 골라서 깎을 수가 없다

쪼개려면 balances: [Gwei; n]withdrawal_credentials: [_; n]이 필요하고, 밸리데이터 레코드가 최대 16배로 부푼다. 비콘 상태는 모든 노드가 들고 있어야 하는 데이터다.

→ 가설: n ≤ 16은 "16이면 충분히 분산됐다"는 판단이 아니라 "16까지가 상태를 감당할 수 있는 한계"다. 카드의 네 번째 미해결 질문에 대한 잠정 답.

발견 ② — 카드에 없던 질문: 상관된 가용성 실패

그룹화는 우아한 열화(graceful degradation)를 절벽(cliff)으로 바꾼다.

독립 밸리데이터 n개m-of-n 그룹
1명 장애32 ETH만 침묵영향 없음 (여유 있으면)
n−m+1명 장애그만큼만 침묵32n 전체 침묵

정족수는 이진 판정이라, 통과하면 잔고 전액이 실리고 미달하면 0이 실린다. 그리고 네트워크 차원에서 더 무섭다 — 여러 그룹이 같은 클라이언트나 같은 클라우드를 쓰면 그 하나가 죽을 때 여러 그룹이 동시에 m 미달이 되고, 각각 32n씩 빠지면서 2/3 문턱을 못 넘겨 확정이 멈출 수 있다.

역설: DVT의 목적은 "다양성을 통한 복원력"인데, 프로토콜에 흡수되어 대중화되면 오히려 실패를 뭉치게 만들 수 있다.

발견 ③ — 카드에 없던 질문: 무임승차와 상호 보험은 같은 것이다

참여 귀속을 켜는 순간, 그룹이 존재하는 이유를 끄게 된다.

비트필드가 있으니 "일 안 하면 안 준다"가 가능해 보인다. 가장 깔끔한 답 같지만, 계산해보면:

가동률 99%인 참여자 기준

"일 안 하면 안 준다":
  솔로 99% 가동 → 99% 보상
  그룹 99% 가동 → 99% 보상      ← 얻는 게 정확히 0

보상 통합:
  10명이 각자 99% → 정족수 미달 확률 ≈ 0
  → 그룹은 100% 획득 → 내 몫은 솔로보다 많음

그룹의 존재 이유는 상호 보험이다 — 각자 99%짜리 가동률을 모아 100%를 만드는 것. 그런데:

보는 각도같은 현상
좋게 보면"내가 죽어도 남들이 메워준다" = 보험
나쁘게 보면"내가 놀아도 보상은 나온다" = 무임승차

보험업의 고전적 문제 그대로다 — 도덕적 해이를 완전히 제거하면 보험 자체가 성립하지 않는다. 그리고 이 문제는 제안이 만든 게 아니라 오늘의 DVT에서 이미 존재한다. 지금은 모니터링 + 사회적 제재 + (비싼) DKG 재실행으로 처리하며, Lido가 "Identified" Clusters라고 이름 붙인 것도 익명 신뢰 대신 신원 기반 신뢰로 이 구멍을 메우기 때문이다.

→ 제안의 기여는 "푸는 것"이 아니라 "풀 수 있게 만드는 것"이다. 정보는 확보했지만 그것을 쓸 규칙은 아직 쓰이지 않았고, 잔고가 통합돼 있으면 알아도 못 쓴다(발견 ①).

발견 ④ — 왜 합의 라운드가 사라질 수 있는가

비잔틴 합의를 "해결"한 게 아니라 문제를 단조(monotone) 술어로 바꿔 "회피"했다.

합의 (오늘의 DVT)집계 (제안)
질문"우리가 뭘 서명할까?""누가 서명했나?"
답의 우열객관적 우열 없음많을수록 명백히 낫고 검증 가능
필요한 정직 비율2/3 이상1명이면 충분
왕복 라운드필요불필요
운영자3이 발행: [1,1,1,0,1,1,1,1,0,0]  ← 8개 (나를 뺌)
운영자7이 발행: [1,1,1,1,1,1,1,1,0,0]  ← 9개 ← 이게 이김

→ 나를 빼려면 전원이 공모해야 한다. 정직한 1명이면 검열이 무력화된다.

공짜는 아니다. 일치를 강제하지 않으므로 뷰가 갈릴 수 있다:

리오그 중 / 블록 지연:
  운영자 1~6: head = X → 집계본 X (6개)
  운영자 7~10: head = Y → 집계본 Y (4개)
  m = 8 → 어느 쪽도 미달 ❌

다만 강제 일치보다 자연 수렴이 오히려 관대할 수 있다 — 6:4로 갈렸는데 m=6이었다면 다수파로 통과된다. DVT였다면 합의 실패로 통째로 날아갔을 상황이다.

그리고 안전성 문턱은 결국 m이다. 집계의 검열 저항은 1-of-n으로 강하지만, m명이 공모하면 슬래싱 조건에 서명할 수 있다. BFT급 안전성을 원하면 m > 2n/3으로 잡아야 한다 (n=10이면 m ≥ 7; 8/10은 여유 있음, 6/10은 미달).

부수 발견 — 정직한 참여자가 억울해지는 두 경로

① 집계자 그리핑. 비트가 0으로 찍히는 이유가 두 가지다 — 정말 안 했거나, 했는데 집계자가 안 기다렸거나. 통합 정산이면 무해하지만 개별 정산으로 바꾸는 순간 공짜로 남을 해치는 벡터가 열린다. (완화책은 위의 "더 꽉 찬 집계가 이긴다".)

② 지리적 편향. 클러스터가 유럽 중심인데 나만 아시아에 있으면 부분 서명이 구조적으로 늦는다. 노이즈가 아니라 편향이라 평균으로도 씻기지 않는다. DVT의 근본 긴장이 여기 있다 — 지리적 분산이 목적인데, 분산이 곧 지연이고, 지연이 곧 참여 판정의 왜곡이다.

참고로 이더리움은 유사한 문제에 이미 완충장치를 갖고 있다: 위원회당 집계자가 여러 명이고, 포함 창이 최대 한 에폭이며, 보상이 timely_source / timely_target / timely_head로 쪼개져 head를 놓쳐도 나머지는 받는 부분 인정 구조다. 올오어낫싱이 아니다.

5. 잠정 설계 결론

  1. 보상은 통합, 슬래싱은 개별. 축을 나눈다. 보험이 필요한 것은 가용성이지 정직성이 아니다 — 작은 실수는 서로 커버하고, 큰 배신만 개별 책임. 앞서 본 비대칭(오프라인은 작고 슬래싱은 파멸적)과 결이 맞는다. 부수 효과: 잔고를 완전히 쪼갤 필요가 줄어 상태 비용이 절약되고 n ≤ 16 상한도 느슨해질 여지가 생긴다.
  2. 단일 에폭으로 판정하지 않는다 — 이동 창. 단일 에폭의 비트필드는 노이즈이고 누적 참여율은 신호다. 우연한 포함 실패는 평균에서 씻기고 만성 무임승차는 통계적으로 드러난다.
  3. 자기부담금 곡선. 참여율 97%와 100%의 실질 차이는 거의 없게, 60% 아래부터 급격히 아프게. 정상 운영자는 노이즈를 신경 쓰지 않아도 되고 무임승차자만 손해를 본다.
  4. 집계자를 지정하지 않는다. 아무나 발행 가능 + 비트가 더 많은 집계본이 무조건 우선. 검열에 전원 공모가 필요해진다.
  5. m > 2n/3. 오프라인 손실은 싸고 공모 문턱이 낮은 것은 비싸므로, m은 높게 잡는 쪽이 합리적이다.
  6. 퇴출이 싸다는 점을 활용한다. 키를 쪼개지 않으므로 DKG 재실행 없이 등록 해제만으로 참여자를 교체할 수 있다 — 오늘의 DVT가 못 하던 자동 제명이 현실적인 선택지가 된다.

6. 미해결 질문 정리

원문이 남긴 넷:

  1. 슬래싱 귀속 — 비트필드로 알지만 잔고 통합이 막는다 (발견 ①)
  2. 지연 예산 — 합의 라운드는 사라지지만 뷰 분기를 떠안는다 (발견 ④)
  3. m<n 공모 트레이드오프 — 가용성과 공모 문턱이 정확히 반비례
  4. n ≤ 16의 근거 — 잠정 답: 상태 비용의 한계 (발견 ①)

이번 세션에서 추가된 것:

  1. 상관된 가용성 실패 — 절벽 효과와 클라이언트·클라우드 단일화 위험 (발견 ②)
  2. 보상 귀속 / 무임승차 — 보험과 도덕적 해이가 같은 현상 (발견 ③)
  3. 집계자 그리핑 — 개별 정산이 여는 무비용 공격 벡터
  4. 지리적 편향 — 분산이라는 목적이 판정의 공정성과 충돌

🔗 이 사이트의 다른 항목과 이어지는 지점

  • 미들웨어의 프로토콜 흡수 — ERC-4337 번들러가 네이티브 AA 앞에서 맞고 있는 상황과 같은 패턴. 카드의 원래 "왜"에 적힌 서사.
  • 상관 실패 — 같은 코드를 돌리는 N개 노드의 정족수는 하나의 판단을 N번 복사한 것에 불과하다. 브릿지·오라클 설계에서 반복되는 함정.
  • MPC vs SSS — 운영 중 키가 재조립되는가의 구분은 커스터디 설계와 동일한 축이다.
← 5. OpenZeppelin Relayer & Monitor79. Institutional custody study →