본문으로 건너뛰기
Change page

단순 직렬화

단순 직렬화(SSZ)는 비콘 체인에서 사용되는 직렬화 방법입니다. 이 방법은 피어 디스커버리 프로토콜을 제외한 합의 레이어 전체에서 실행 계층에 사용되는 RLP 직렬화를 대체합니다. RLP 직렬화에 대해 자세히 알아보려면 RLP(Recursive-length prefix)를 참조하세요. SSZ는 결정론적이며 효율적으로 머클화(Merkleize)되도록 설계되었습니다. SSZ는 직렬화 체계와 직렬화된 데이터 구조에서 효율적으로 작동하도록 설계된 머클화 체계라는 두 가지 구성 요소를 가지고 있다고 볼 수 있습니다.

SSZ는 어떻게 작동하나요?

직렬화

SSZ는 자체 설명적(self-describing)이지 않은 직렬화 체계로, 사전에 알려져야 하는 스키마에 의존합니다. SSZ 직렬화의 목표는 임의의 복잡성을 가진 객체를 바이트 문자열로 표현하는 것입니다. "기본 유형(basic types)"의 경우 이 과정은 매우 간단합니다. 요소는 단순히 16진수 바이트로 변환됩니다. 기본 유형은 다음과 같습니다.

  • 부호 없는 정수(unsigned integers)
  • 부울(Booleans)

복잡한 "복합(composite)" 유형의 경우, 복합 유형에는 유형이나 크기 또는 둘 다 다를 수 있는 여러 요소가 포함되어 있기 때문에 직렬화가 더 복잡합니다. 이러한 객체가 모두 고정된 길이를 갖는 경우(즉, 요소의 크기가 실제 값과 관계없이 항상 일정한 경우), 직렬화는 단순히 복합 유형의 각 요소를 리틀 엔디안(little-endian) 바이트 문자열로 순서대로 변환하는 것입니다. 이러한 바이트 문자열들은 서로 결합됩니다. 직렬화된 객체는 역직렬화된 객체에 나타나는 것과 동일한 순서로 고정 길이 요소의 바이트 목록 표현을 갖습니다.

가변 길이를 가진 유형의 경우, 직렬화된 객체 내 해당 요소의 위치에서 실제 데이터가 "오프셋(offset)" 값으로 대체됩니다. 실제 데이터는 직렬화된 객체의 끝에 있는 힙(heap)에 추가됩니다. 오프셋 값은 힙에 있는 실제 데이터 시작 부분의 인덱스이며, 관련 바이트를 가리키는 포인터 역할을 합니다.

아래 예시는 고정 길이 요소와 가변 길이 요소가 모두 있는 컨테이너에서 오프셋이 어떻게 작동하는지 보여줍니다.

serialized는 다음과 같은 구조를 갖습니다(여기서는 4비트로만 패딩되었지만 실제로는 32비트로 패딩되며, 명확성을 위해 int 표현을 유지합니다).

[37, 0, 0, 0, 55, 0, 0, 0, 16, 0, 0, 0, 22, 0, 0, 0, 1, 2, 3, 4]
------------  -----------  -----------  -----------  ----------
      |             |            |           |            |
   number1       number2    vector의      number 3     vector의
                             오프셋                       값

명확성을 위해 줄을 나누어 표현하면 다음과 같습니다.

[
  37, 0, 0, 0,  # `number1`의 리틀 엔디안 인코딩.
  55, 0, 0, 0,  # `number2`의 리틀 엔디안 인코딩.
  16, 0, 0, 0,  # `vector`의 값이 시작되는 위치를 나타내는 "오프셋"(리틀 엔디안 16).
  22, 0, 0, 0,  # `number3`의 리틀 엔디안 인코딩.
  1, 2, 3, 4,   # `vector`의 실제 값.
]

이것은 여전히 단순화된 형태입니다. 위 도식의 정수와 0은 실제로는 다음과 같이 바이트 목록으로 저장됩니다.

[
  10100101000000000000000000000000  # `number1`의 리틀 엔디안 인코딩
  10110111000000000000000000000000  # `number2`의 리틀 엔디안 인코딩.
  10010000000000000000000000000000  # `vector`의 값이 시작되는 위치를 나타내는 "오프셋"(리틀 엔디안 16).
  10010110000000000000000000000000  # `number3`의 리틀 엔디안 인코딩.
  10000001100000101000001110000100   # `bytes` 필드의 실제 값.
]

따라서 가변 길이 유형의 실제 값은 직렬화된 객체 끝의 힙에 저장되며, 해당 오프셋은 정렬된 필드 목록의 올바른 위치에 저장됩니다.

직렬화 중에 길이 제한(length cap)을 추가하고 역직렬화 중에 제거해야 하는 BitList 유형과 같이 특별한 처리가 필요한 몇 가지 특수한 경우도 있습니다. 자세한 내용은 SSZ 사양 (opens in a new tab)에서 확인할 수 있습니다.

역직렬화

이 객체를 역직렬화하려면 스키마(schema)가 필요합니다. 스키마는 직렬화된 데이터의 정확한 레이아웃을 정의하여, 각 특정 요소가 바이트 블롭에서 올바른 유형, 값, 크기 및 위치를 가진 의미 있는 객체로 역직렬화될 수 있도록 합니다. 역직렬화기(deserializer)에게 어떤 값이 실제 값이고 어떤 값이 오프셋인지 알려주는 것이 바로 스키마입니다. 객체가 직렬화될 때 모든 필드 이름은 사라지지만, 스키마에 따라 역직렬화 시 다시 인스턴스화됩니다.

이에 대한 대화형 설명은 ssz.dev (opens in a new tab)를 참조하세요.

머클화(Merkleization)

이 SSZ 직렬화된 객체는 머클화될 수 있습니다. 즉, 동일한 데이터의 머클 트리 표현으로 변환될 수 있습니다. 먼저, 직렬화된 객체에서 32바이트 청크(chunk)의 수를 결정합니다. 이것들이 트리의 "리프(leaves)"가 됩니다. 리프들을 함께 해싱하여 최종적으로 단일 해시 트리 루트(hash-tree-root)를 생성할 수 있도록 전체 리프 수는 2의 거듭제곱이어야 합니다. 자연스럽게 이렇게 되지 않는 경우, 32바이트의 0을 포함하는 추가 리프가 더해집니다. 도식으로 나타내면 다음과 같습니다.

위의 예시처럼 트리의 리프가 자연스럽게 고르게 분포하지 않는 경우도 있습니다. 예를 들어, 리프 4는 머클 트리에 추가적인 "깊이(depth)"를 요구하는 여러 요소를 가진 컨테이너일 수 있으며, 이로 인해 불균형한 트리가 생성될 수 있습니다.

이러한 트리 요소를 리프 X, 노드 X 등으로 부르는 대신, 루트 = 1로 시작하여 각 레벨을 따라 왼쪽에서 오른쪽으로 세는 일반화된 인덱스(generalized indices)를 부여할 수 있습니다. 이것이 위에서 설명한 일반화된 인덱스입니다. 직렬화된 목록의 각 요소는 2**depth + idx와 같은 일반화된 인덱스를 갖습니다. 여기서 idx는 직렬화된 객체에서 0부터 시작하는 위치(zero-indexed position)이고, 깊이(depth)는 머클 트리의 레벨 수이며, 이는 요소(리프) 수의 밑이 2인 로그로 결정될 수 있습니다.

일반화된 인덱스

일반화된 인덱스는 이진 머클 트리에서 노드를 나타내는 정수이며, 각 노드는 2 ** depth + index in row라는 일반화된 인덱스를 갖습니다.

1           --깊이 = 0  2**0 + 0 = 1
    2       3       --깊이 = 1  2**1 + 0 = 2, 2**1+1 = 3
  4   5   6   7     --깊이 = 2  2**2 + 0 = 4, 2**2 + 1 = 5...

이러한 표현은 머클 트리의 각 데이터 조각에 대한 노드 인덱스를 생성합니다.

다중 증명(Multiproofs)

특정 요소를 나타내는 일반화된 인덱스 목록을 제공하면 해시 트리 루트에 대해 해당 요소를 검증할 수 있습니다. 이 루트는 우리가 인정하는 현실의 버전입니다. 우리에게 제공된 모든 데이터는 머클 트리의 올바른 위치(일반화된 인덱스에 의해 결정됨)에 삽입하고 루트가 일정하게 유지되는지 관찰함으로써 그 현실에 대해 검증될 수 있습니다. 특정 일반화된 인덱스 집합의 내용을 검증하는 데 필요한 최소 노드 집합을 계산하는 방법을 보여주는 사양의 함수는 여기 (opens in a new tab)에 있습니다.

예를 들어, 아래 트리의 인덱스 9에 있는 데이터를 검증하려면 인덱스 8, 9, 5, 3, 1에 있는 데이터의 해시가 필요합니다. (8,9)의 해시는 해시 (4)와 같아야 하며, 이는 5와 해싱되어 2를 생성하고, 2는 3과 해싱되어 트리 루트 1을 생성합니다. 9에 대해 잘못된 데이터가 제공되면 루트가 변경됩니다. 우리는 이를 감지하고 해당 브랜치의 검증에 실패하게 됩니다.

* = 증명을 생성하는 데 필요한 데이터

                    1*
          2                      3*
    4          5*          6          7
8*     9*   10    11   12    13    14    15

더 읽어보기