시계가 없는 비트코인은 어떻게 1년 뒤를 약속하는가
평범한 송금에도 이미 들어 있는 nLockTime부터, 시계를 보지 않는 CLTV와 CSV의 이중 설계, 채굴자의 시간 조작을 막는 중앙값 시간까지. 라이트닝 네트워크와 상속 계획이 전부 이 위에 서 있다.
블록 탐색기에서 아무 거래나 하나 열어 보자. 아래쪽에 locktime이라는 필드가 있고, 값이 0이 아닌 경우가 많다. 요즘 지갑이 만드는 평범한 송금 상당수가 그렇다. 특별한 설정을 한 적이 없는데도 거래에는 이미 시간 조건이 들어 있는 것이다. 왜 그런지는 글 중간에 답이 나온다.
타임락은 비트코인을 특정 시점 이전에는 쓸 수 없게 잠그는 장치다. 상속 계획, 에스크로, 그리고 라이트닝 네트워크 전체가 이 장치 위에 서 있다. 그런데 구현을 들여다보면 이상한 점이 하나 있다. 시간을 강제하는 기능인데, 그 어디에도 시계를 읽는 코드가 없다.
비트코인에는 시계가 없다
분산 시스템에서 "지금 몇 시인가"는 보기보다 어려운 질문이다. 노드마다 시계가 조금씩 다르고, 블록에 찍히는 타임스탬프는 채굴자가 적어 넣는 값이라 합의 규칙이 느슨하게만 제약한다. 이전 블록보다 빠른 시각을 적을 수도 있고, 미래로 어느 정도 당겨 적을 수도 있다.
문제는 돈에 관한 약속이 시간을 필요로 한다는 것이다. "이 자금은 1년 뒤부터 쓸 수 있다" 같은 조건을 강제하려면 시스템 어딘가에 모두가 동의하는 시간이 있어야 한다. 비트코인의 답은 벽시계가 아니라 체인 자체다. 블록 높이는 누구도 부정할 수 없이 단조롭게 증가하고, 타임스탬프도 뒤에서 볼 안전장치를 거치면 쓸 만한 시간이 된다. 타임락 설계 전체가 이 전제 위에 있다.
nLockTime: 이 거래는 아직 유효하지 않다
가장 오래된 타임락은 거래 자체에 붙는다. 모든 비트코인 거래에는 nLockTime이라는 4바이트 필드가 있고, 이 값이 설정된 거래는 그 시점이 오기 전까지 블록에 들어갈 수 없다. 값의 해석은 크기로 갈린다. 500,000,000 미만이면 블록 높이로 읽고, 그 이상이면 유닉스 타임스탬프로 읽는다. 5억 번째 블록은 수천 년 뒤에나 나오므로 두 해석이 충돌할 일은 없다.
단서가 하나 있다. 모든 입력의 nSequence가 최댓값(0xFFFFFFFF)이면 nLockTime은 무시된다. 이것은 사토시 나카모토의 원래 설계가 남긴 흔적이다. 초기 구현에는 확정 전 거래를 더 높은 nSequence의 새 버전으로 갈아치우는 교체 메커니즘이 있었고, nLockTime은 그 교섭이 끝나기 전에 거래가 확정되는 것을 막는 마감 시한이었다. 이 메커니즘은 수수료 없이 무한히 교체를 반복하는 DoS 공격에 쓰일 수 있어 일찍 비활성화됐지만, 필드와 규칙은 남아서 오늘의 RBF와 타임락으로 이어졌다.
글 첫머리의 수수께끼도 여기서 풀린다. 비트코인 코어를 비롯한 여러 지갑은 평범한 송금에도 nLockTime을 현재 블록 높이로 채운다. 수수료 스나이핑, 즉 체인이 재편성될 때 채굴자가 남의 확정 거래를 과거 블록으로 끌어와 다시 채굴하며 수수료를 쓸어 가는 행동을 막기 위해서다. 방금 만든 거래는 현재 높이 이전의 블록에는 들어갈 수 없으므로, 그 거래를 노리고 체인을 되감을 유인이 사라진다. 당신의 송금에 찍혀 있던 locktime 값은 그 흔적이다.
nLockTime의 한계는 잠그는 대상이 코인이 아니라 거래라는 점이다. 상속 계획에서 쓰는 방법처럼 1년 뒤에야 유효해지는 거래를 미리 서명해 둘 수는 있다. 그러나 코인 자체는 자유로우므로, 마음이 바뀌면 그 전에 다른 거래로 같은 코인을 써 버리면 된다. 갱신 가능한 약속으로는 장점이지만, 코인에 조건을 묶으려면 도구가 하나 더 필요하다.
스크립트는 시계를 보지 않는다: CLTV
그 도구가 2014년 피터 토드(Peter Todd)가 제안한 BIP-65, OP_CHECKLOCKTIMEVERIFY다. 줄여서 CLTV라 부른다. 이 옵코드를 스크립트에 넣으면 조건이 거래가 아니라 코인에 붙는다. 그 출력을 받는 순간부터, 지정한 시점 이전에는 누구도 쓸 수 없다.
<만기> OP_CHECKLOCKTIMEVERIFY OP_DROP <공개키> OP_CHECKSIG
흥미로운 것은 검증 방식이다. CLTV는 현재 블록 높이나 시각을 확인하지 않는다. 대신 이 코인을 쓰려는 거래의 nLockTime 필드가 스크립트에 적힌 만기 이상인지를 확인한다. 스택이 비어 있거나, 값이 음수거나, 한쪽은 블록 높이인데 다른 쪽은 타임스탬프라 단위가 어긋나거나, nLockTime이 만기에 못 미치거나, 해당 입력의 nSequence가 최댓값이라 nLockTime 자체가 꺼져 있으면 스크립트는 실패한다.
즉 잠금은 두 단계로 강제된다. 스크립트는 "쓰는 거래가 만기 이상의 nLockTime을 달고 있는가"만 보고, 그 nLockTime이 실제로 지났는지는 합의 규칙이 따로 확인한다. 언뜻 우회처럼 보이지만 이유가 있는 설계다. 스크립트 검증 결과는 어떤 블록에서 실행해도 같아야 한다. 만약 스크립트가 체인의 현재 상태를 직접 읽는다면 오늘 유효했던 거래가 재편성 뒤에 무효가 될 수 있고, 그 순간 검증의 결정성이 깨진다. 시간 확인을 거래의 필드로 한 번 감싸서, 스크립트는 결정적으로 유지하고 시간과의 대조는 합의 계층에 맡긴 것이다.
CLTV에는 배치의 묘도 있다. 새 옵코드를 만든 것이 아니라 아무 일도 하지 않던 예약 옵코드 OP_NOP2에 의미를 부여했다. 업그레이드하지 않은 옛 노드는 이 자리를 여전히 "아무것도 안 함"으로 읽고 통과시키므로, 새 규칙은 옛 규칙의 부분집합이 되어 소프트포크로 배포할 수 있었다. 2015년 말에 활성화됐다.
확정된 지 얼마나 지났는가: BIP-68과 CSV
절대 시점만으로는 부족한 경우가 곧 드러났다. 결제 채널에서 필요한 조건은 "2027년 3월 1일부터"가 아니라 "이 출력이 확정되고 나서 144블록 뒤부터"다. 채널이 언제 닫힐지 미리 알 수 없으니, 기준점이 달력이 아니라 그 출력의 확정 시점이어야 한다. 이것이 상대 타임락이다.
2016년에 함께 활성화된 세 BIP가 이것을 만들었다. BIP-68은 교체 메커니즘이 비활성화된 뒤 놀고 있던 nSequence 필드를 재활용한다. 버전 2 이상의 거래에서 이 필드의 최상위 비트(비활성 플래그)가 꺼져 있으면, 하위 16비트가 상대 잠금 기간이 된다. 22번 비트가 단위를 정한다. 꺼져 있으면 블록 수, 켜져 있으면 512초 단위 시간이다. 512는 2의 9제곱이라 비트 연산이 싸고, 평균 블록 간격 600초와 비슷해서 두 단위의 스케일이 맞는다. 16비트로는 최대 65,535블록, 약 1.25년까지 잠글 수 있다.
BIP-112의 OP_CHECKSEQUENCEVERIFY, 줄여서 CSV는 이것의 스크립트 짝이다. CLTV와 완전히 같은 간접 구조를 쓴다. 현재 체인을 보는 대신, 쓰는 거래의 해당 입력에 적힌 nSequence가 스크립트의 요구값 이상인지 확인하고, 그 nSequence가 실제로 충족됐는지는 BIP-68의 합의 규칙이 확인한다. 이번에 재정의된 예약 옵코드는 OP_NOP3다.
채굴자가 시간을 속이면: BIP-113
시간 단위 타임락에는 남은 구멍이 하나 있었다. 만기 판정에 블록의 타임스탬프를 쓴다면, 타임스탬프를 적는 사람이 채굴자라는 사실이 문제가 된다. 아직 만기가 안 된 거래들이 수수료를 달고 멤풀에 쌓여 있을 때, 채굴자에게는 블록 시각을 앞당겨 적어서 그 거래들을 먼저 쓸어 담을 유인이 있다. BIP-113의 표현을 빌리면 블록 시각에 대해 거짓말을 할 비뚤어진 유인이다.
BIP-113은 만기 비교의 기준을 블록 자신의 타임스탬프에서 직전 11개 블록 타임스탬프의 중앙값, 이른바 중앙값 시간(median time-past)으로 바꿨다. 중앙값을 움직이려면 블록 하나가 아니라 여럿의 시각을 꾸준히 조작해야 하므로 개별 채굴자의 거짓말이 힘을 잃고, 이 값은 항상 단조롭게 증가한다. 부작용은 사소하다. 중앙값은 실제 시각보다 대략 한 시간쯤 뒤처지므로, 시간 기준 타임락은 벽시계보다 그만큼 늦게 풀린다.
이 부품들로 굴러가는 것들
라이트닝 네트워크는 타임락의 최대 소비자다. 결제를 홉마다 이어 주는 HTLC는 해시 조건과 CLTV 만기를 결합해서, 중계자가 응답이 없으면 일정 시점 뒤 자금을 회수할 수 있게 한다. 채널을 일방적으로 닫을 때 자기 몫이 일정 기간 잠기는 것은 CSV다. 그 지연 기간이 상대방에게 옛 채널 상태로 사기를 치지 않았는지 검사하고 이의를 제기할 시간을 준다. 채널이 수십만 개 열려 있어도 정직하게 닫히는 한 타임락은 발동조차 하지 않는다. 평소에는 보이지 않다가 분쟁이 생기는 순간에만 작동하는 안전망이다.
상속 계획에서는 두 방식이 각자 다르게 쓰인다. nLockTime으로 미리 서명해 둔 거래는 주기적으로 갱신하는 유언장이 되고, CLTV를 넣은 스크립트는 "내가 5만 블록 동안 코인을 움직이지 않으면 상속인 키로도 열린다" 같은 조건을 코인 자체에 새긴다. 코버넌트 논의의 볼트 설계도 출금에 시간 지연을 강제하는 데 타임락을 쓴다. 키가 도둑맞아도 잠금 기간 동안 복구 키로 자금을 되돌릴 기회가 생긴다.
타임락이 못 하는 것
방향에 주의할 필요가 있다. 타임락은 언제나 한쪽으로만 잠근다. "이 시점 이전에는 못 쓴다"는 가능하지만 "이 시점 이후에는 못 쓴다", 즉 코인의 만료는 불가능하다. 우연이 아니라 의도된 제약이다. 한번 유효해진 지출 조건이 시간이 지나 무효로 바뀔 수 있다면, 재편성 때 이미 확정됐던 거래가 소급해서 무효가 되는 사태가 생긴다. 검증이 단조로워야, 즉 유효한 것은 계속 유효해야 체인이 안전하게 재조립될 수 있다.
그래서 만료가 필요한 곳에서는 잠금 대신 갈림길을 만든다. 만기 전에는 A의 키로만 열리고, 만기 뒤에는 B의 키로도 열리는 두 갈래 스크립트다. A의 권리는 사라지지 않고 B의 경로가 추가될 뿐이므로 단조성이 지켜진다. 에스크로의 환불 경로도, 위에서 본 상속 스크립트도, HTLC의 회수 경로도 전부 이 패턴이다.
시계도 없고 관리자도 없는 시스템이 시간이 걸린 약속을 강제한다. 거래에 붙는 nLockTime, 코인에 붙는 CLTV와 CSV, 채굴자의 거짓말을 걸러내는 중앙값 시간. 부품은 이 넷이 전부고, 하나하나는 필드 하나를 비교하는 수준으로 단순하다. 그 단순한 부품이 결정성을 깨지 않도록 신중하게 배치된 결과가, 라이트닝부터 상속까지 시간 위에 세워진 응용 전부다.