사슬이 둘로 갈라진 날: 2013년 3월, 아무도 정한 적 없는 규칙

한 줄로 오던 블록 사슬이 두 갈래로 갈라진다. 위 갈래는 노란 큰 블록으로 시작해 점선으로 흐려지고, 아래 갈래만 계속 이어진다.

앞 글에서 네트워크의 컴퓨터 하나하나(노드)는 전해 받은 블록을 저마다 검사한다고 했습니다. 그러면 같은 블록을 놓고 노드들의 답이 갈리면 어떻게 될까요? 2013년 3월 11일 비트코인에서 실제로 그런 일이 있었습니다 [S1] [S3].

한 블록, 두 가지 답

그날 225,430번 블록이 만들어져 네트워크에 퍼졌습니다 [S3]. 블록에 든 거래의 입력을 모두 합친 수가 그때까지 나온 어느 블록보다 많았습니다 [S1]. 보고서를 따라 이 블록을 «큰 블록» 이라고 부르겠습니다.

비트코인 프로그램 0.8 버전을 돌리던 노드는 큰 블록을 검사해 유효하다고 받아들였습니다. 0.8 이전 버전을 돌리던 노드 가운데 일부는 받아들이지 않았습니다 [S1]. 답이 둘 나왔습니다(그림 1).

같은 블록을 전해 받은 두 노드의 서로 다른 답 왼쪽의 큰 블록 하나가 오른쪽 두 노드에 전해진다. 0.8 버전을 돌리는 위 노드는 받아들이고, 0.8 이전 버전을 돌리는 아래 노드는 받아들이지 않는다. 큰 블록 노드 · 0.8 버전 ✓ 받아들임 노드 · 0.8 이전 버전 (일부) ✗ 받아들이지 않음
그림 1. 같은 블록이 두 노드에 전해졌지만, 돌리는 프로그램의 버전에 따라 답이 달랐습니다 [S1]. 아래에서는 큰 블록을 받아들인 0.8 노드를 새 프로그램 노드, 받아들이지 않은 0.8 이전 노드를 옛 프로그램 노드라고 부릅니다.

노드는 저마다 검증합니다

노드의 사슬에는 그 노드가 검증한 블록만 들어갑니다. 노드들이 같은 블록들을 갖게 하려고 함께 따르는 검증 규칙을 합의 규칙이라고 합니다 [S2].

큰 블록을 받아들이지 않은 노드의 사슬에는 그 블록이 없습니다. 그 블록 위에 이어진 블록도 붙일 자리가 없습니다. 그래서 사슬이 둘로 갈라졌습니다(그림 2) [S1]. 사슬이 이렇게 갈라지는 것을 포크라고 합니다 [S2].

큰 블록에서 둘로 갈라진 사슬과 노드들이 따르는 쪽 왼쪽에서 한 줄로 오던 블록이 둘로 갈라진다. 위 사슬은 큰 블록으로 시작하고 새 프로그램 노드가 따른다. 아래 사슬에는 큰 블록이 없고 옛 프로그램 노드가 따른다. 옛 프로그램 노드는 위 사슬을 받아들이지 못한다. 갈라지기 전 새 프로그램 노드가 따르는 사슬 큰 블록 ↑ 옛 프로그램 노드는 ✗ 받아들이지 못함 옛 프로그램 노드가 따르는 사슬 새 프로그램 노드도 받아들일 수는 있음 ✓
그림 2. 큰 블록에서 갈라진 두 사슬입니다. 화살표는 «앞 블록의 해시 값» 이 가리키는 쪽입니다. 블록 수는 보기로 그린 것이고, 실제로 몇 개씩 쌓였는지는 보고서에 없습니다.

이중 지불 글에서 본 포크는 다음 블록이 나오면 긴 쪽으로 모였습니다. 이번에는 큰 블록이 든 사슬 쪽에 계산력의 약 60% 가 있었습니다. 보고서는 그래서 포크가 저절로 풀리지 않았다고 적습니다 [S1]. 노드는 유효한 블록만 든 사슬 가운데서 고르는데 [S2], 그 사슬은 아무리 길어져도 옛 프로그램 노드에게는 받아들일 수 없는 사슬이기 때문입니다.

하나가 되는 길은 반대쪽에 있었습니다. 큰 블록이 없는 사슬은 새 프로그램 노드도 받아들일 수 있습니다. 그쪽이 더 길어지면(쌓인 작업이 더 많아지면) 새 프로그램 노드가 그 사슬로 옮겨 옵니다 [S1]. 그런데 계산력이 40% 인 쪽은 블록을 더 느리게 쌓습니다.

계산력을 옮겨 보기

계산력을 어느 쪽에 얼마나 두어야 할까요? 먼저 60% 로 진행해 보고, 그날처럼 과반을 반대쪽으로 옮겨 이어서 해 보세요.

시뮬레이터 ① · 질문: 일부 노드가 받아들이지 못하는 블록이 나오면, 계산력을 어느 쪽에 얼마나 두어야 다시 하나가 될까요?

여기서 견줍니다: 2가 1보다 긴가?

1큰 블록이 든 사슬 포크 뒤 블록 1개 따르는 노드 새 프로그램

2큰 블록이 없는 사슬 포크 뒤 블록 0개 따르는 노드 옛 프로그램

포크 전 블록 큰 블록 포크 뒤 블록

블록을 어느 쪽이 만드는지는 계산력 비율대로 난수로 정합니다. 60% 여도 포크 직후에는 운으로 옛 쪽이 앞지르는 판이 있습니다 (이 모형의 계산으로 블록 열 개 안에 약 34%). «그날처럼 과반을 옮기기» 는 비율을 30% 로 내립니다. 보고서에는 과반이 넘어갔다는 것까지만 있고, 30% 는 이 모형이 정한 값입니다. 60% 로 열 개를 진행하고도 둘로 남은 판은, 30% 로 내려 서른 개를 더 진행하면 약 94% 가 하나로 모입니다.

그림 3. 두 사슬을 왼쪽 끝을 맞춰 위아래로 놓았습니다. 큰 블록이 없는 사슬이 더 길어질 때에만 새 프로그램 노드가 옮겨 와 하나가 됩니다.

사람이 나서서 다시 하나로

실제로도 그렇게 풀렸습니다. 포크는 금방 알려졌고, 필요한 사람들과 바로 연락이 닿았습니다 [S1]. 블록을 만드는 쪽 가운데 큰 두 곳(BTCGuild 와 Slush)이 자기 노드를 0.8 에서 0.7 로 내렸습니다. 자기들도 큰 블록을 받아들이지 않게 한 것입니다 [S1]. 계산력의 과반이 큰 블록이 없는 사슬로 넘어갔고, 결국 0.8 노드들도 그 사슬로 옮겨 왔습니다1 [S1]. 보고서는 두 곳이 이 일로 적지 않은 돈을 포기했다고 적습니다 [S1].

그동안 큰 거래소들은 입금을 잠시 멈췄습니다 [S1]. 그사이 큰 이중 지불이 적어도 한 번 있었습니다2. 보고서에 따르면 되는지 실험해 본 사람이 한 일이고, 악의는 없었습니다 [S1].

아무도 정한 적 없는 규칙

옛 프로그램은 왜 큰 블록을 받아들이지 못했을까요? 0.8 이전 버전은 Berkeley DB 라는 데이터베이스를 썼습니다. 이 데이터베이스는 쓸 수 있는 잠금(lock)의 수를 미리 설정해 두어야 하는데, 그 수가 «크지만 그 밖에는 유효한» 블록을 처리하기에 모자랐습니다 [S1]. 0.8 은 데이터베이스를 LevelDB 로 바꿨고, 여기에는 같은 한도가 없었습니다 [S1]. 걸린 것은 블록의 크기가 아니라 블록을 처리하는 데 드는 잠금 수였습니다(표 1). 입력이 많은 블록이 왜 잠금을 더 쓰는지는 보고서가 풀어 적지 않습니다. 잠금이 디스크의 쪽(page)마다 잡힌다는 것만 적습니다 [S1].

표 1. 같은 큰 블록을 전해 받은 두 프로그램 [S1]
0.8 이전 (옛 프로그램)0.8 (새 프로그램)
데이터베이스Berkeley DBLevelDB
잠금 수의 한도미리 설정한 수까지같은 한도 없음
큰 블록일부 노드가 처리하지 못해 받아들이지 않음받아들임

«잠금이 모자라면 그 블록은 받아들이지 않는다» 는 누가 정한 규칙이 아닙니다. 그런데 보고서는 이 설정이 «암묵적으로 네트워크의 합의 규칙이 되어 있었다» 고 적습니다 [S1]. 일관된 규칙도 아니었습니다. 필요한 잠금 수는 노드마다 디스크에 놓인 데이터의 배치에 따라 달랐습니다. 그래서 모든 노드가 같은 0.7.2 버전이었어도, 이론으로는 한 노드가 만든 블록을 다른 노드가 검증하지 못할 수 있었습니다3 [S1].

숨은 규칙을 드러내 고치기

개발자들은 숨어 있던 규칙을 드러내 적었습니다. 곧 나온 0.8.1 은 두 달 동안, 잠금을 1만 개 넘게 쓸 것 같은 블록을 받아들이지 않고 50만 바이트가 넘는 블록을 만들지 않았습니다4 [S1]. 2013년 8월 16일에는 252,451번 블록이 네트워크에 받아들여졌고, 고치지 않은 노드들은 네트워크에서 떨어져 나갔습니다 [S1].

개발자 안내서는 이 일을 두 번의 하드 포크로 적습니다5. 하나는 뜻하지 않은 하드 포크로, 업그레이드한 노드의 능력을 잠시 낮춰서 풀었습니다. 다른 하나는 그 임시 조치를 걷어 낼 때의, 의도한 하드 포크입니다 [S2].

같은 장부는 같은 규칙에서

«잠금이 모자라면 받아들이지 않는다» 는 누가 정한 적이 없지만, 옛 프로그램의 그 동작이 규칙 노릇을 했습니다 [S1]. 합의를 지키려면 모든 전체 노드가 같은 합의 규칙으로 블록을 검증해야 합니다 [S2]. 시뮬레이터 ① 의 «옛 프로그램도 큰 블록을 받아들인다면» 을 누르면, 두 프로그램의 답이 같을 때는 사슬이 처음부터 갈라지지 않습니다. «모두가 같은 장부를 갖는다» 는 «모두가 같은 규칙으로 검증한다» 에 기대고 있습니다.

이 글의 계산과 확인

사건의 사실은 사후 보고서(BIP 50) [S1] 와 당시 bitcoin.org 공지 [S3] 에서 왔습니다. 날짜(3월 11일)와 블록 번호(225,430)는 공지에, 원인과 뒤처리는 보고서에 있습니다. 포크 뒤 두 사슬에 블록이 몇 개씩 쌓였는지, 풀리기까지 걸린 시간, 이중 지불의 금액은 두 문서에 없어 쓰지 않았습니다.

시뮬레이터 ① 의 규칙은 둘입니다. 옛 프로그램 노드는 큰 블록과 그 위에 이어진 블록을 받아들이지 않습니다. 새 프로그램 노드는 두 사슬을 받아들일 수 있습니다. 더 긴 쪽을 따르되, 길이가 같으면 따르던 쪽에 남습니다. 블록마다 드는 작업이 같다고 두어 «더 긴 사슬» 이 곧 «작업이 더 많이 든 사슬» 입니다. 블록을 만드는 쪽은 자기 노드가 따르는 사슬 끝에 블록을 붙입니다. 큰 블록이 막 나온 때(한 개 앞선 때)에서 시작합니다.

«열 개 안에 약 34%» 와 «서른 개를 더 진행하면 약 94%» 는 이 모형에서 경우를 모두 따라가며 더한 값입니다. 따로 짠 계산과 난수로 20,000판씩 돌린 결과가 이 값과 맞는지 시험했습니다. 옛 프로그램 노드도 큰 블록을 받아들이게 하면 사슬이 갈라지지 않고, 진행해도 한 사슬만 자라는 것도 시험했습니다 (verify.mjs 121개 통과).

곁질문
  1. 보통 사용자는 무엇을 해야 했나요? 아무것도 하지 않아도 됐습니다. 당시 공지는 블록을 만들거나 결제를 받는 쪽이 아니면, 어느 버전을 쓰든 프로그램이 알아서 옳은 사슬로 옮겨 간다고 안내했습니다 [S3]. ↩
  2. 그 이중 지불은 어떻게 통했나요? 보고서는 두 사슬 쪽이 거래들을 같은 차례로 전해 받았는데도 이중 지불이 성공했다고 적습니다. 블록을 만드는 쪽이 노드의 버전을 내릴 때, 아직 블록에 들지 않은 거래를 모아 둔 곳(memory pool)이 비워졌기 때문일 가능성이 크다고 봅니다 [S1]. 금액은 보고서에 없습니다. ↩
  3. 왜 아무도 미리 몰랐나요? 허용되는 가장 큰 블록은 테스트넷(시험용 네트워크)에서 문제없이 처리됐습니다. 그래서 그보다 작은데도 잠금은 더 많이 쓰는 블록이 있을 수 있다는 생각을 아무도 하지 못했다고 보고서는 적습니다 [S1]. 0.7 이전에는 블록을 만드는 노드가 프로그램을 고쳐 쓰지 않는 한 블록 크기를 스스로 50만 바이트로 묶어 두었고, 사건 전 한 주 사이에 그 크기를 올리라는 권고가 있었습니다 [S1]. ↩
  4. 옛 버전은 어떻게 하기로 했나요? 보고서의 뒤처리 목록에는 옛 버전에 같은 규칙을 넣고 잠금 수를 53만 7천 개로 늘리는 패치를 내는 일이 있습니다. 업그레이드할 수 없는 사람이 설정에서 잠금 수를 직접 늘리는 방법을 bitcoin.org 에 안내하고, 옛 버전 사용자에게 두 달 동안 알림을 보내는 일도 함께 적혀 있습니다 [S1]. ↩
  5. 하드 포크와 소프트 포크는 어떻게 다른가요? 합의 규칙이 바뀔 때, 새 규칙을 따른 블록을 업그레이드하지 않은 노드가 받아들이지 못하면 두 쪽의 사슬이 영영 갈라질 수 있고, 이것이 하드 포크입니다. 거꾸로 업그레이드한 노드만 어떤 블록을 받아들이지 않는 경우에는, 그쪽 계산력이 과반이면 옛 노드도 받아들이는 더 강한 사슬을 만들어, 영영 갈라지는 것을 막을 수 있습니다. 이것이 소프트 포크입니다 [S2]. ↩

출처

  1. [S1] BIP 50 — March 2013 Chain Fork Post-Mortem (Gavin Andresen).
  2. [S2] Bitcoin Developer Guide — Block Chain, Block Chain Overview · Common And Uncommon Block Chain Forks · Consensus Rule Changes.
  3. [S3] bitcoin.org — 11/12 March 2013 Chain Fork Information (당시 공지).