ZYCORD 문서
한국어
Zycord문서와이어 프로토콜

와이어 프로토콜

Zycord 노드들이 서로 주고받는 말, 그리고 피어 프로토콜이 증폭기가 되지 않게 막는 규칙들. 규범적 명세를 대신하는 것이 아니라 그 명세로 가는 안내서입니다.

이 페이지는 안내서이지 명세가 아닙니다

명세는 spec/입니다. 파라미터 파일과 벡터 말뭉치가 그것입니다. 이 페이지와 트리가 어긋나면 옳은 쪽은 트리입니다 — 어떤 구현이든 제네시스 id를 재현하고 벡터를 통과하면 적합하며, 이 페이지의 어떤 내용도 그 사실을 바꾸지 않습니다. 아래는 요구 사항이 어디에 있는지를 보여 주는 지도입니다.

전송#

TCP를 먼저, 그다음 TLS 1.3. 피어 신원은 양쪽이 함께 제시하는 자체 서명 X.509 인증서에 담긴 Ed25519 키입니다. 인증서 체인은 검증하지 않습니다 — 인증 기관도 없고 인증 기관이 보증할 대상도 없기 때문입니다 — 따라서 검증은 인증서가 정확히 하나일 것, 그 공개 키가 Ed25519일 것을 요구하며, 그 키를 피어의 신원으로 추출합니다.

여기서 얻는 성질은 인가가 아니라 키에 대한 채널 바인딩입니다. TLS는 핸드셰이크를 완료한 쪽이 제시된 신원의 개인 키를 갖고 있다는 것, 그리고 스트림이 전송 중에 읽히거나 변조될 수 없다는 것을 보장합니다. 그 피어가 정직한지에 대해서는 아무 말도 하지 않습니다. 이 프로토콜의 어떤 부분도 신원을 근거로 권한을 부여하지 않습니다.

인증서의 유효 기간은 현재 시각을 둘러싼 창이 아니라 고정된 상수이며, 이는 의도된 것입니다. 상대적인 창을 쓰면 인증서 수용 여부가 시계 일치에 의존하게 되고, 시계 오차를 따라 분할되는 네트워크는 합의와 무관한 이유로 분할되는 네트워크입니다.

구현은 다른 어떤 계층의 키도 피어 신원으로 재사용해서는 안 됩니다. 노드는 시작할 때마다 새 키를 생성하며 디스크에 결코 기록하지 않습니다.

프레이밍#

offset  size  field
0       4     length   uint32, little-endian - payload length, excluding this header
4       1     kind     uint8  - see below
5       n     payload
  • length는 어떤 할당보다도 먼저 MaxMessageBytes = 8 MiB에 대조해 검사해야 합니다. 주장된 길이만 보고 할당하는 수신자는 그 뒤에 무엇이 오든 원격 메모리 고갈 버그를 갖고 있는 것입니다.
  • kind는 [1, 9] 범위 안에 있어야 합니다. 0과 알려진 최고 종류보다 큰 값은 알 수 없는 확장이 아니라 프로토콜 위반입니다. 프로토콜 1에는 상위 호환을 위한 탈출구가 없습니다. 핸드셰이크에서 버전 필드를 검사하므로 그런 것이 필요 없기 때문입니다.

메시지 종류#

값이름방향페이로드
1hello양쪽, 최초핸드셰이크
2certificate가십ssz(Certificate)
3block-announce가십헤더와 인증서(certificate) id들
4get-block요청32바이트 블록 id ‖ u32 청크 인덱스
5block응답ssz(Block)의 청크 하나
6get-headers요청로케이터
7headers응답헤더 구간
8get-peers요청비어 있음
9peers응답주소 목록

합의 객체를 네트워크에 특화된 방식으로 인코딩하는 일은 없으며, 그렇기 때문에 "내가 받은 것의 id"가 확인 가능한 진술이 됩니다.

모든 인바운드 메시지는 보낸 쪽에 비용을 물립니다#

이 부분은 명세에서 전문을 읽을 가치가 가장 큰 대목입니다. 피어 프로토콜이 대개 새는 곳이 바로 여기이기 때문입니다. 두 가지 규칙이 이를 다스립니다.

비용 순서 규칙#

작업은 비용이 커지는 순서로 수행되며, 메시지는 그 뒤에 오는 비싼 단계보다 먼저 청구됩니다. 먼저 검증하고 나중에 점수를 매기는 수신자는 증폭기를 만든 것입니다. 보내기에 값싼 것이 검사하기에 비싼 것이 되기 때문입니다.

모든 결과에 값이 매겨져 있습니다#

이름 없는 Free는 없습니다. 모든 메시지 종류가 그것이 만들어 낼 수 있는 모든 결과와 교차되어, 점수가 붙은 표에 나타납니다. 청구되지 않은 채 표를 빠져나가는 결과가 바로 이 규칙이 막으려고 존재하는 결함입니다 — 그리고 값이 매겨지지 않은 행보다 더 좁은 경로로 실제로 그런 일이 일어난 적이 있습니다. 잘못된 형식의 프레임을 유일한 점수 매김 지점에서 비켜 가게 한 어떤 구현은, 어떤 표에도 행을 추가하지 않은 채, 요청이 미처리 상태로 남아 있는 동안 그 프레임을 공짜로 만들어 버렸습니다.

제공하는 쪽도 계량됩니다. 블록 바이트는 그것을 요청하는 메시지보다 몇 자릿수나 큰 유일한 응답이므로, 요청 횟수에 더해 자기만의 바이트 예산을 함께 지닙니다.

연결 관리, 그리고 이클립스 방어#

이 절은 다른 어떤 절보다 측정된 실패를 많이 담고 있으며, 아래의 각 요구사항은 그것이 없는 구현이 측정 과정에서 이클립스 공격을 당했기 때문에 존재합니다.

  • 아웃바운드 대상은 주소 다양성을 지켜 선택되어야 합니다. 그래야 하나의 호스팅 대역이 노드의 아웃바운드 슬롯을 다 채울 수 없습니다.
  • 아웃바운드 대상은 가십 출처별로도 상한이 있어야 합니다. 주소 다양성만으로는 충분하지 않은데, 이는 판단이 아니라 산수의 문제입니다. 주소 그룹은 공격자가 소유한 주소의 성질이지만, 피어가 주장하는 주소는 그가 지어낸 바이트일 뿐이고, 임의의 네 바이트는 유효한 IPv4 호스트입니다. 주소를 하나도 갖고 있지 않은 공격자도 프레임 하나 값에 문자열마다 새로운 다양성 그룹을 찍어 냅니다. 공격자가 지어낼 수 없는 것은 그 주장이 도착한 연결입니다.
  • 인바운드 연결이 도착한 소켓은 아웃바운드 대상이 되어서는 안 됩니다. 그것은 피어의 출발지 주소 — 그 운영체제가 고른 임시 포트이지 피어가 수신 대기하는 곳이 아닙니다 — 따라서 그 주소로 접속을 시도하는 데 쓴 슬롯은 응답할 수 없는 주소에 쓴 슬롯입니다.
  • 두 상한 모두 한 번의 선택 호출이 아니라 노드가 보유한 연결에 대해 세어야 합니다. 그렇게 하지 않으면 이미 연결된 피어를 제외하는 접속 루프가 매 라운드마다 두 예산을 가득 찬 상태로 되돌리고, 상한은 공격자를 제한하는 대신 허용치마다 한 라운드씩 늦출 뿐입니다. 측정된 결과: 어떤 알림 노드 하나가 네 라운드에 걸쳐 아웃바운드 슬롯 8개 중 2개, 4개, 6개, 8개를 차지했습니다.
  • 피어 저장소는 영속화되어야 하고, 상한이 있어야 합니다. 재시작할 때마다 텅 빈 상태로 시작하는 노드는 재시작할 때마다 공격자에게 새 기회를 건네줍니다. 공격자의 목록을 그대로 갖고 시작하는 노드는 같은 기회를 영구히 건네줍니다.
  • 상한이 있는 저장소는 가득 찼다는 이유로 올바른 형식의 주소를 거부해서는 안 되며, 그 주소를 위해 다른 항목을 밀어내야 합니다. 한 번도 접속해 본 적 없는 정직한 주소가 한 번도 접속해 본 적 없는 지어낸 주소보다 나을 것은 없으므로, 저장소를 먼저 채운 공격자는 그 뒤에 제안되는 모든 것에 대해 저장소를 걸어 잠그게 됩니다 — 운영자 자신의 부트스트랩 목록까지 포함해서 말입니다.
  • 밀어내기가 무엇을 고르느냐가 밀어내기가 일어난다는 사실보다 더 중요합니다. "가장 큰 집단에서 희생자를 고른다"는 홍수가 스스로를 밀어낸다는 뜻으로 읽히지만, 그것은 홍수가 저장소에서 가장 큰 것일 동안에만 그렇습니다. 친절한 피어 하나에서 부트스트랩한 노드에서는 가장 큰 집단이 바로 그 피어의 주소록입니다. 그런 순서를 쓰는 구현에서 측정된 결과: 한 출처에서 온 지어낸 주소 200개가 정직한 주소 200개를 밀어냈고, 공격자에게는 아무 비용도 들지 않았습니다.
  • 구별할 수 없는 항목들 사이에서 최종 동점 처리는 가십을 보낸 피어가 고르는 어떤 것도 — 주소를 포함해서 — 기준으로 삼아서는 안 되며, 이는 밀어내기에 적용되는 것과 똑같이 선택에도 적용됩니다. 선택기가 결국 주소 문자열에 의존한 구현에서 측정된 결과: 정직한 주소 8개와 지어낸 주소 8개를 놓고 8개 모두 지어낸 주소가 반환되었고, 열 라운드 동안 정직한 아웃바운드 연결은 하나도 없었습니다.

의도적으로 빠져 있는 것#

없음이유
NAT 통과그 대가는 명시되어 있고, 결정을 다시 열 조건은 가정되지 않고 측정됩니다. 공개 testnet에서 외부 접속이 가능한 노드의 비율이 이 결정을 다시 열 첫 번째 조건입니다.
압축공격자가 통제하는 스트림 위의 압축기는 공격 표면이며, 아무도 필요성을 측정해 본 적 없는 대역폭 절약을 위한 것입니다.
메시지 수준 서명전송 계층이 채널을 인증하고, 합의 객체는 자기 서명을 갖고 다닙니다. 세 번째 계층은 중계자를 인증하게 되는데, 그것은 어떤 결정도 의존하지 않는 대상입니다.
요청 id요청과 응답은 연결과 순서로 짝지어지므로, 동기화는 자기만의 연결에서 실행됩니다 — 상태 기계 하나가 사라지는 셈입니다. 피어가 외부에서 접속할 수 없는 경우 동기화는 기존 가십 연결 위에서 실행될 수 있으며, 그때는 응답을 내용으로 짝지어야 합니다.
상위 호환 확장 메커니즘프로토콜 1은 핸드셰이크에서 버전을 검사하고 어긋나면 연결을 끊습니다. 확장은 프로토콜 2로 도착합니다.

인증서(certificate) id가 포괄하지 않는 것#

중계 규칙 하나를 결정하기 때문에 여기서 다시 짚어 둘 가치가 있습니다. 인증서(certificate)의 id는 그것이 인가하는 것에만 구속되고, 단지 보여 주는 것에는 결코 구속되지 않습니다. 그래서 서명은 id의 원상 바깥에 놓입니다. 그 결과 증거는 전송 중인 누구든 바꿔치기할 수 있는 것이 됩니다. 인증서를 하나 가져다가 서명 자리에 쓰레기를 넣고 퍼뜨리면 — id는 그대로인데 실체(exemplar)는 이제 유효하지 않습니다.

두 가지 규칙이 이 틈을 막습니다. 블록은 자신이 담고 있는 증거에 구속되며, 이는 id가 아니라 실체(exemplar) 해시 위의 루트를 통해 이루어집니다. 그리고 중계는 id가 아니라 실체(exemplar)를 다룹니다. 증거 검증에 실패한 실체를 받은 노드는 id에는 아무런 불이익을 주지 않고 그 실체만 폐기합니다 — id는 표시되지도, 유효하지 않은 것으로 캐시되지도 않으며, 나중에 검증을 통과하는 실체가 오면 평소대로 중계됩니다. 훼손된 사본은 그것을 훼손한 쪽에게 그것을 보내는 대역폭만큼의 비용을 물리고, 인증서에는 아무 비용도 물리지 않습니다.

이것을 처음부터 구현하기#

골든 벡터를 통과하는 독립 구현은 포크가 아니라 피어입니다 — 이런 방식으로 명세를 쓰는 이유가 바로 그것입니다. 합의 객체는 spec/README.md부터 시작하고, 그런 다음 zcd vectors로 결과를 확인하세요.