이더리움 DeepDive

[이더리움 프로토콜 DeepDive] 합의레이어 - 합의를 위한 준비과정

dxlixi 2025. 11. 27. 19:40

Disclaimer : 아주대학교 블록체인 학회 LayerA EPF Study 팀에서 이더리움 재단에서 진행하는 EPF(ethereum protocol Fellowship)을 기반으로 [이더리움 프로토콜 DeepDive]를 연재합니다. 본 글은 정보 전달을 목적으로 작성되었으며, 특정 프로젝트에 대한 투자 권고, 법률, 자문 등을 목적으로 하지 않습니다. 본문 내용을 통한 투자 의사결정은 지양하시길 바랍니다.



[이더리움 프로토콜 DeepDive]

  1. 합의레이어
    • 합의레이어 - 합의를 위한 준비과정 (Shuffling, Randao) ✅
    • 합의레이어 - 본격적인 합의 (Gasper)
  2. 실행레이어
  3. 상호작용

 

1. 이더리움 2.0

The Merge 업데이트 이전의 이더리움은 작업 증명 방식(PoW)을 사용한 모놀리틱 블록체인으로, 합의와 실행이 모두 한 클라이언트에서 이루어졌다. 따라서 채굴자들은 단일 클라이언트만 실행하면 누구나 이더리움 네트워크에 참여할 수 있었다.

하지만 이더리움은 2022년 merge 업데이트를 통해 합의 레이어를 추가하여 합의와 실행 계층이 분리된 형태의 모듈러 블록체인이 되었다. 따라서 네트워크 참여자들은 실행 클라이언트와 합의 클라이언트를 모두 실행해야만 작업 증명 방식에서 지분 증명 방식(PoS)으로 바뀐 이더리움 네트워크에 참여할 수 있게 되었다.

이 시리즈에서는 이더리움의 두 계층인 합의 레이어와 실행 레이어가 각각 어떤 작업을 하는지, 두 계층이 어떻게 통신하는지 알아보고자 한다.

1.1. 이더리움의 구성요소

이더리움은 위 그림과 같이 두 계층 합의 레이어(Consensus Layer)와 실행 레이어(Execution Layer)로 구분할 수 있으며 각 계층 안에는 이더리움을 구성하는 비콘 노드(Beacon Node), 실행 노드(Execution Node), 검증자(Validator)가 존재한다.

 

 

합의 레이어(Consensus Layer)

합의 레이어는 합의를 담당하는 계층이다. 해당 계층의 목적은 이더리움 체인을 하나의 표준체인(Canonical Chain)으로 유지하는 것이다. 합의 레이어의 구성은 다음과 같다.

  • Beacon Node: 이더리움 네트워크상에 존재하는 모든 노드가 같은 상태를 유지할 수 있도록 P2P 네트워크를 통해 정보를 공유하고 합의하는 노드
  • Validator: 32이더를 스테이킹 한 검증인의 키를 관리하면서 새로운 블록을 제안하고 투표를 하는 등 검증자의 의무를 수행하는 클라이언트

실행 레이어(Execution Layer)

실행 레이어는 실행을 담당하는 계층이다. 해당 레이어의 목적은 트랜잭션과 상태(state)등을 처리하는 것이다. 실행 레이어의 구성은 다음과 같다.

  • Execution Node: P2P 네트워크로 공유한 트랜잭션을 멤풀에 보관하며 이를 실행하고 상태를 관리하는 노드

실행 노드와 비콘 노드로 모두 참여하면 풀 노드로서 이더리움의 네트워크 안정성에 기여할 수 있으며, 나아가 32ETH를 스테이킹 해 Validator까지 실행한다면 검증자로서 블록 제안과 검증에 참여 할 기회를 가지게 된다.


2. 합의레이어

우리가 흔히 알고 있는 합의는 서로의 의견이 일치하는 것을 의미하는데, 블록체인에서의 합의도 이와 같은 맥락에서 이루어진다. 블록체인에서 합의는 다음 블록으로 어떤 블록을 인정할 것인지 의견을 일치시키는 과정을 뜻하며, 일련의 합의 과정을 통해 표준체인(Canonical Chain)을 정하고 유지해 해당 체인의 안전성과 보안성을 유지한다.

 

이더리움에서의 합의는 총 두가지 과정으로 나누어 볼 수있다.

  1. 합의를 위한 준비 과정 : 블록 생성시기에 맞춰 제안자 밎 검증자 위원회 선출
  2. 본격적인 합의 : 선출된 제안자가 규칙에 맞춰 블록을 제안한 후, 다른 검증자들이 블록을 검증하고 블록을 동기화하여 표준체인을 유지

두 과정 통해 합의 레이어는 새로운 블록이 제안되면 해당 블록을 표준체인(Canonical Chain)에 연결하고, 모든 이더리움 검증자 노드들이 같은 상태를 유지할 수 있도록 하는 것을 목표로 한다.

2.1. 합의의 주체: 검증자 위원회와 제안자

해당 파트에서는 앞서 살펴본 두 과정 중 첫 번째 단계, [합의를 위한 준비 과정 : 블록 생성시기에 맞춰 제안자 밎 검증자 위원회 선출] 부분을 다룬다. 이더리움에서 사용하는 시간 단위와 블록 제안자, 검증자 위원회의 구성 방법에 대해 알아보자.

 

PoS 방식을 사용하는 이더리움은 특정한 규칙으로 검증자 위원회를 구성하는데, 이 과정에서 블록체인에 참여하는 검증자는 블록 제안자(block proposer), 검증자 위원회(validator committee) 등 여러 주체로 나뉜다.

검증자는 블록체인이 유지될 수 있도록 네트워크에 참여해 기여하며 블록 제안자는 검증자 중 무작위로 선택되어 블록을 제안하는 구성원이다. 이더리움은 블록 제안자가 블록을 제안하면 해당 블록을 대상으로 검증자 위원회의 합의 과정을 거쳐 표준 체인에 연결한다.

2.2. 이더리움 블록 주기: 슬롯(slot)과 에포크(epoch)

이더리움의 비콘 체인은 슬롯과 에포크라는 시간 단위로 움직인다. 슬롯은 12초의 주기이며 1 슬롯 당 하나의 블록이 생성될 수 있다. 에포크는 32개의 슬롯을 의미하며 시간은 6.4분(12sec*32=6.4min)이다.

슬롯과 에포크는 일정하게 움직이게 하는 시간 단위일 뿐 매 슬롯마다 반드시 블록이 생성되는 것은 아니다. 만약 이더리움이 최적으로 실행 되고 있다면 12초마다 1개의 블록이 생성된다.


2.3. 검증자 위원회의 구성

이더리움에서는 매 에포크가 시작될 때 활성 검증자들을 각 슬롯에 고르게 나누어 위원회로 할당하는데, 모든 검증자는 한 에포크에 한 번만 위원회로 참여할 수 있다. 슬롯마다 할당된 제안자와 검증자 위원회는 각각 블록을 제안하고 검증하는 역할을 수행하고 그에 맞는 보상을 얻는다.

이 때, 블록 제안자와 검증자 위원회의 구성을 예측할 수 있다면 일부 악의적 노드로부터 공격 당할 여지가 더 많아진다. 따라서 이러한 문제를 방지하기 위해 비콘 체인은 매 에포크가 시작 될 때 마다 각 슬롯별 블록 제안자와 검증자 위원회를 무작위로 선출하는 방법을 필요로 한다.

이를 위해 이더리움 2.0에서는 검증자 리스트를 무작위로 섞는 셔플링(Shuffling)을 수행하며, 그 중에서도 Swap-or-not 셔플링 방법을 채택하고 있다. 이 방법에 대해서 구체적으로 알아보도록 하자.


2.3.1. Shuffling (Swap-or-not)

셔플링(Shuffling)은 검증자를 무작위로 섞고 위원회로 할당하기 위해 검증자 리스트를 섞고 배정하는 작업이다. 이더리움은 에포크 시작지점에서 셔플링을 통해 활성 검증자 리스트를 섞는다. 위에서 언급한 것과 같이 이더리움은 Swap-or-not 셔플링 방법을 사용하며, 이름 그대로 특정 값을 기준으로 대칭 위치에 있는 두 검증자를 서로 바꿀 것인지 말 것인지를 결정하는 방식으로 셔플링이 진행된다.

Swap-or-not 셔플링은 라운드를 기준으로 진행되며, 여러 라운드를 거치고 나면 검증자 리스트가 무작위로 섞이게 된다. 한번의 셔플당 90번의 라운드가 수행되며, 지금부터 각 라운드별로 반복되는 구체적인 절차에 대해 알아보도록 하겠다.

 

1. pivot이 선택된다.

한 라운드가 시작되면 시드값과 라운드 번호를 활용해 pivot을 선택한다. pivot은 활성 검증자 리스트 내에서 무작위로 선택된 하나의 인덱스(Index: 검증자 리스트 내에서 각 검증자의 위치를 나타내는 값)로, 셔플링 과정에서 기준점 역할을 한다.

2. 첫번째 mirror index가 결정되고, 인덱스 스왑 여부가 결정된다.

pivot 값이 결정되면 0과 pivot 가운데에 위치한 값이 mirror index(m1) 값이 된다. 쉽게 말해 0과 pivot 인덱스가 대칭이 될 수 있도록 하는 지점이 m1이 된다.

첫번째 mirror index가 결정되고 나면 m1과 pivot 사이의 모든 인덱스는 m1을 기준으로 대칭인 지점의 인덱스와 교환될지 말지 결정된다.

이 때 스왑 여부는 랜덤 시드, 라운드 넘버, position 데이터를 해시한 값에 의해 결정되는데, 이에 대한 구체적인 설명은 생략하겠다.

 

3. 두번째 mirror index가 선택되고, 인덱스 스왑 여부가 결정된다.

m1과 pivot 사이 모든 검증자 인덱스가 스왑될지 말지 결정되고 나면, 두번째 mirror index가 결정된다. 마찬가지로 pivot 인덱스와 n-1이 대칭이 될 수 있도록 하는 지점이 두번째 mirror index(m2)가 된다. 이후 pivot 인덱스와 m2값 사이의 모든 인덱스가 m2를 기준으로 대칭인 지점의 인덱스와 교환될 지 말 지 결정된다.

 

 

4. 새로운 라운드를 시작하기 위해 pivot이 선택된다.

3번 과정까지 거치고 나면 m1부터 m2까지(전체 인덱스의 절반) 모든 인덱스는 해당 인덱스로부터 대칭에 있는 인덱스와의 스왑 여부가 결정된다. 이는 전체 인덱스가 각각 정확히 한번씩 고려되었음을 의미한다.

위 과정이 모두 실행되고 나면 다음 라운드를 시작하기 위해 새로운 pivot 값이 선택된다.

 

위 과정을 90번, 즉 90라운드 반복하고 나면 활성 검증자 리스트 인덱스는 위와 같이 거의 무작위로 섞이게 된다.

 

검증자 리스트를 무작위로 섞기 위해 사용되는 피봇(Pivot)값은 셔플링 과정의 각 라운드마다 기준점 역할을 하며, 이 값을 기준으로 리스트가 재배열되기 때문에 이를 무작위로 선택하기 위한 시드값이 중요하다. 이 때 시드값은 RANDAO라는 난수 알고리즘을 통해 생성된 값이 된다. RANDAO는 블록이 생성될 때 마다 시드값을 업데이트하는 중요한 특징을 가지고 있는데, 지금부터 RANDAO 알고리즘에 대해 알아보도록 하겠다.

 

2.3.2. RANDAO 알고리즘

이더리움에서는 프로토콜의 무작위성을 유지하기 위해 RANDAO라고 불리는 의사 난수 알고리즘을 사용한다. RANDAO 알고리즘은 다수의 개인으로부터 무작위성(Randomness)을 점진적으로 축적하는 방식으로 작동한다. 비유하자면, 테이블을 돌며 모든 참여자가 차례로 카드덱을 섞는 게임과 같다고 할 수 있다. RANDAO 알고리즘의 작동 원리를 알기 위해 비콘 체인 beacon state 두 가지 필드의 값에 대해 알아보자.

 

- randao_reveal

randao_reveal 필드에는 각 블록의 제안자가 에포크 번호를 자신의 secret key로 서명하여 제출한 값이 저장된다. 누구나 검증할 수 있어 악의적 의도에 의한 위변조가 어렵다.

- randao_mixes

randao_mixes 값에는 블록 제안자와 검증자 위원회를 랜덤하게 배정할 수 있게 해주는 시드 값이 저장된다. randao_mixes 값은 다음 에포크의 블록 제안자 및 위원회를 구성하는 함수인 셔플링의 시드값으로 활용된다. 프로토콜의 무작위성을 유지할 수 있게 해주는 핵심적인 값이다.

 

앞서 RANDAO 알고리즘을 차례로 카드덱을 섞는 게임으로 비유했는데, randao_mixes는 카드덱으로, randao_reveal은 카드덱을 섞는 참여자로 비유된다.

그렇다면 RANDAO 알고리즘은 어떤 방식으로 작동할까?

def process_randao(state: BeaconState, body: BeaconBlockBody) -> None:
    epoch = get_current_epoch(state)
    
    # randao_reveal값 검증
    proposer = state.validators[get_beacon_proposer_index(state)]
    signing_root = compute_signing_root(epoch, get_domain(state, DOMAIN_RANDAO))
    assert bls.Verify(proposer.pubkey, signing_root, body.randao_reveal)
    
    # xor연산을 통해 randao값에 randao_reveal 섞기
    mix = xor(get_randao_mix(state, epoch), hash(body.randao_reveal))
    state.randao_mixes[epoch % EPOCHS_PER_HISTORICAL_VECTOR] = mix

먼저 블록 제안자가 제출한 서명, 즉 randao_reveal 값을 검증하는 과정을 거친다. 제안자가 올바르지 않은 randao_reveal 값을 제출하여 검증에 실패하거나 아무것도 제출하지 않는 경우 해당 블록은 유효하지 않게 된다. 따라서 대부분의 경우 블록 제안자는 올바른 서명을 제출해 randao_mixes 값이 업데이트될 수 있도록 기여한다.

 

randao_reveal 값이 검증되고 나면 randao_reveal 값은 해시되어 XOR 연산을 통해 randao_mixes 값을 업데이트한다. XOR 연산을 하면 공격에 대한 저항성이 조금이나마 더 높아지기 때문에 XOR 연산을 사용한다.

 

위 과정을 수학적으로 표현하면 다음과 같다. 𝑅𝑛은 n번째 기여 후 randao_mixes 필드에 저장된 값이고, 𝑟𝑛은 n번째 검증자가 제출한 서명(randao_reveal)이다. 이를 그림으로 간단히 나타내면 다음과 같다.

randao_reveal 값과 XOR 연산 되어 업데이트된 randao_mixes 값은 활성 검증자를 섞어 블록 제안자와 위원회로 구성하는 데에 사용된다. 위에서 설명한 Swap-or-not 셔플링 방법에서 검증자 인덱스를 섞는 기준점이 되는 피봇값이 바로 randao_mixes 값으로부터 선택되며, 현재 에포크의 randao_mixes 값이 아닌 이전 에포크가 시작될 때의 randao_mixes 값을 시드 값으로 활용한다.

2.3.3 Assignment

지금까지 검증자 위원회와 제안자를 무작위로 선출하기 위해 RANDAO와 셔플링을 살펴보았다. 셔플링은 무작위성을 위해 검증자들을 섞는 과정이었다면, 실제로 해당 검증자들을 각 위원회로 배치(Assignment)하는 방법에 대해 간단히 알아보도록 하자.

 

셔플링 파트에서도 언급했듯이 검증자들을 검증자 위원회와 제안자로 선출하는 과정은 에포크 시작 경계에서 발생한다. 실제 합의 클라이언트를 살펴보면 아래 보이는 함수 이름 그대로 에포크 경계에서 검증 위원회와 제안자를 업데이트 하는 함수를 확인할 수 있다.

func (s *Service) updateEpochBoundaryCaches(ctx context.Context, st state.BeaconState) error {
	//...//
	// 검증자 위원회 선출 함수
	if err := helpers.UpdateCommitteeCache(ctx, st, e); err != nil {
		return errors.Wrap(err, "could not update committee cache")
	}
	// 제안자 선출 함수
	if err := helpers.UpdateProposerIndicesInCache(ctx, st, e); err != nil {
		return errors.Wrap(err, "could not update proposer index cache")
	}
	//...//
}

이때 검증자 위원회를 선출하는 방법과 제안자를 선출하는 방법에 조금의 차이가 있다. 검증자 위원회는 모든 활성 검증자를 셔플링하여 선출하지만 제안자는 하나의 인덱스만을 셔플링해서 선출한다. 이는 네트워크의 과부화를 줄이기 위해 상황에 맞는 방법을 채택했다고 볼 수 있다.

 

또한 제안자를 단순히 셔플링만으로 선출하는 것이 아니라, 셔플링된 검증자가 가지고 있는 지분에 따라 선출 여부가 결정된다. 아래 코드를 보면, 먼저 셔플링을 통해 새로운 검증자를 선택하고, 그 후 해당 검증자가 가진 지분에 따라 제안자로 선출될 확률을 계산한다. 만약 확률에 의해 당첨되면 바로 제안자로 선출되며, 그렇지 않으면 다시 셔플링을 통해 검증자를 선택하고 이 과정을 반복하여 제안자가 선출될 때까지 계속 진행된다.

func ComputeProposerIndex(state state.ReadOnlyBeaconState, activeIndices []primitives.ValidatorIndex, seed [32]byte) (primitives.ValidatorIndex, error) {
    length := uint64(len(activeIndices))
    //...//
    // i는 계속 증가하면서 새로운 후보자 선택
    for i := uint64(0); ; i++ {
        // 1. 새로운 후보자 선택
        candidateIndex, err := ComputeShuffledIndex(primitives.ValidatorIndex(i%length), length, seed, true)
        candidateIndex = activeIndices[candidateIndex]
        
        // 2. 후보자의 지분 확인
        validator, err := state.ValidatorAtIndexReadOnly(candidateIndex)
        effectiveBal := validator.EffectiveBalance()
        
        // 3. 지분에 따른 당첨 여부 확인
        if effectiveBal*maxRandomByte >= maxEffectiveBalance*randomByte {
            return candidateIndex, nil  // 당첨되면 반환
        }
        // 당첨되지 않으면 다음 후보자로 계속
    }
    //...//
}

 

이렇게 선출한 제안자들은 해당 에포크의 슬롯에 부여된 슬롯 번호에 따라 배치되고, 검증자 위원회는 셔플링된 전체 리스트를 순서대로 슬라이싱되여 각 슬롯에 배치된다.

3. 마무리

이번 파트에서는 이더리움의 구성요소와 우리가 나눈 합의의 두 가지 단계 중 첫 번째 단계, [합의를 위한 준비 과정 : 블록 생성시기에 맞춰 제안자 밎 검증자 위원회 선출] 부분을 살펴보았다.

 

이더리움은 슬롯과 에포크라는 시간 단위로 움직이며 매 에포크가 시작될 때 셔플링을 통해 활성 검증자를 무작위로 섞는다. 악의적 노드가 그 구성을 예측할 수 없도록 RANDAO 알고리즘을 사용해 시드 값을 업데이트하며, 무작위로 섞인 검증자 리스트에서 블록 제안자를 선출하고 위원회를 구성한다.

 

다음 파트에서는 두 번째 단계, 검증자들이 블록을 검증하고 합의하여 표준체인(Canonical Chain)을 만들어 가는 과정을 살펴보도록 하겠다.

 

💡 요약

  • 이더리움은 합의 레이어실행 레이어로 나뉘는 모듈러 블록체인으로 바뀌었다.
  • 합의 레이어는 표준체인(Canonical Chain)을 유지하기 위해 합의를 한다.
  • 검증자는 제안자검증자 위원회로 나뉜다. 제안자는 블록을 제안하며, 검증자 위원회는 해당 블록을 검증하고 합의한다.
  • 이더리움은 슬롯에포크라는 시간 단위를 통해 작동한다.
  • 에포크 시작 지점에서 셔플링(Shuffing)을 통해 검증자를 무작위로 섞고 위원회를 구성한다.
  • 셔플링을 하기 위해 난수가 필요한데, 이는 RANDAO 알고리즘을 통해 생성한다.
  • RANDAO 값은 매 블록이 생성될 때마다 업데이트되며, 이를 통해 이더리움 프로토콜의 무작위성을 유지한다.
  • 제안자와 검증자 위원회 셔플링은 같은 swap or not 셔플링을 사용하지만 셔플링 범위에 따른 차이가 존재하며 이는 네트워크 과부화를 줄이기 위해서 이다.
  • 제안자는 소유하고 있는 지분 즉, 스테이킹 하고 있는 ETH의 양에 따라 선출 확률이 달라진다.