Chapter 7. 트랜잭션 검증과 생성

진수복
Programming Bitcoin
10 min readMay 31, 2020

아래 글에서는 “밑바닥부터 시작하는 비트코인(Programming Bitcoin) -한빛미디어(2019)”의 Chapter 7. 트랜잭션 검증과 생성에 대한 내용을 다룬다.
해당 글에서는 트랜잭션의 생성 및 검증 절차에 초점을 맞추어 설명하며, 절차에 집중하여 설명하기 위해 locktime과 같은 트랜잭션의 일부 field는 생략했다.

Programming Bitcoin Series

Chapter 1. 유한체
Chapter 2. 타원곡선
Chapter 3. 타원곡선 암호
Chapter 4. 직렬화
Chapter 5. 트랜잭션
Chapter 6. 스크립트
Chapter 7. 트랜잭션 검증과 생성
Chapter 8. p2sh 스크립트
Chapter 9. 블록
Chapter 10. 네트워킹
Chapter 11. 단순 지급 검증
Chapter 12. 블룸 필터
Chapter 13. 세그윗

Overview

철수가 영희에게 1.25 비트코인을 전달하는 트랜잭션을 만든다고 가정했을 때 어떠한 순서로 트랜잭션을 생성하는지 설명하고자 한다.

철수가 영희에게 비트코인을 전달하기 위해서는 전달하는 내용이 담긴 트랜잭션이 채굴자들로부터 유효성을 검증받아야 한다.

여기서 유효한 트랜잭션이란 비트코인 네트워크에서 정의한 규칙을 모두 만족하는 트랜잭션을 의미한다. 다음은 트랜잭션 유효성 검증 규칙의 일부이다.

  • 거래의 구문과 데이터 구조가 정확해야 함
  • 각각의 입력값에 대해 참조 출력값은 존재해야 하며, 소비되지 않은 상태여야 함(UTXO)
  • 입력값 액수가 출력값 총액보다 작은 경우 해당 거래는 거절됨
  • 각 입력값에 대한 해제 스크립트는 그에 해당하는 출력값 잠금 스크립트에 대해 검증해야 함

가장 기본적인 input, output 값, data format부터 시작하여 전자서명을 통한 신원 증명까지, 이 모든 조건을 만족시키는 트랜잭션을 생성하여 영희에게 실제로 비트코인을 전달해 보자.

transaction 생성

유효한 트랜잭션은 크게 다음과 같은 절차를 거쳐 생성된다.

  1. 전자서명을 제외한 입력값 (input data) 추가
  2. 출력값 (output data) 추가
  3. version number, lock time 추가
  4. 각 input data의 해제 스크립트 추가

실제로는 version number를 먼저 기록할 수 있으나 이 순서는 크게 중요하지 않으므로 설명의 편의를 위해 입, 출력 값 이후 기록하는 순서로 정의한다.

1. 입력 (Input) & 출력 (Output) 값 추가

첫 번째로 진행할 행위는 입력 데이터의 추가이다. 입력 값은 크게 UTXO에 대한 정보 데이터와 (UTXO가 위치한 트랜잭션 해시값 및 트랜잭션 내부의 인덱스 정보) 해제 스크립트로 구성된다. (자세한 설명은 Chap 5 참조)

입력값에 철수의 UTXO를 기록하기 위해 철수는 자신의 UTXO를 확인한다. 아래는 철수가 가진 2개의 UTXO이다. 이를 활용하여 영희에게 1.25 비트코인을 전송한다.

* 철수가 가진 UTXO. 설명 편의상 TX hash를 tx01, tx02로 표기했다

철수는 위 두개의 UTXO를 토대로 입력 값을 작성해 나간다.

각 UTXO에 대한 트랜잭션 ID, 인덱스, sequence 등을 기록한다. 여기서 해제 스크립트는 우선 비워둔다.

여기서 철수가 이미 사용한 UTXO 를 이용하여 input을 생성하면 유효성 검증을 통과하지 못한다. 각 노드는 비트코인 네트워크에 존재하는 모든 UTXO set을 기록하고 있어 철수의 UTXO가 이미 사용되었는지를 파악할 수 있기 때문이다.

다음은 출력값을 추가할 차례이다. 출력 값 영역에는 영희만 사용할 수 있도록하는 영희의 UTXO를 작성한다. 해당 트랜잭션이 블록에 담기면 비로소 영희가 사용할 수 있는 UTXO가 된다.

해당 트랜잭션을 풀이하면 ‘철수의 UTXO를 사용 (input) 하여 영희가 쓸 수 있는 UTXO를 생성 (output) 한다’ 라고 볼 수 있다.

2. 해제 스크립트 추가

해제 스크립트는 일반적으로 전자서명과 공개키로 구성되며 이를 추가할 필요가 있다.

그런데 앞서 Input을 입력할 때 해제 스크립트 부분을 비워 두었다. 왜 해제 스크립트는 비워두고 다른 데이터부터 입력했을까? 이를 이해하기 위해서는 전자 서명의 생성 과정을 복기할 필요가 있다.

전자서명을 생성할 때 트랜잭션을 직렬화한 데이터를 이용하는 것을 기억할 것이다. 이유는 트랜잭션 데이터의 일부가 변경되었을 때 전자서명 검증에 실패하도록 하여 서명자가 생성했던 트랜잭션의 무결성을 보장하기 위함이었다.

즉 전자서명을 생성하기 위해선 직렬화된 트랜잭션 데이터가 필요하다. 여기서 문제가 발생한다. 트랜잭션에는 전자서명이 포함되고, 전자서명을 생성하기 위해서는 트랜잭션 데이터가 필요하여 무한 재귀에 빠지게 된다.

이 문제를 해결하기 위해서 전자서명에 사용되는 트랜잭션 직렬화 데이터를 생성할 때 전자서명 부분을 제외한 체 직렬화한다. 제외 한 부분에는 참조하는 UTXO의 잠금 스크립트를 대신 채워 넣는다.

아래 그림은 첫 번째 입력의 해제 스크립트 부분에 tx01에 해당하는 잠금 스크립트를 추가한 모습이다.

2.1 transaction hash에 포함되는 SIGHASH 정보

각 트랜잭션의 입력값에 대한 해제 스크립트를 생성할 때 해시 유형을 설정할 수 있으며, 해시 유형에 따라 전자 서명의 위, 변조에 대한 보증 범위(scope)가 달라진다. 예를 들어 ‘SIGHASH_ALL’ 유형으로 서명을 생성할 경우 해당 서명은 트랜잭션의 모든 입력 및 출력이 변경되지 않음을 보장한다. 각 유형에 대한 설명은 다음과 같다.

  • SIGHASH_ALL: 어떠한 input, output도 수정될 수 없다. (0x01)
  • SIGHASH_NONE: 모든 input은 수정 불가능, output은 수정 가능 (0x02)
  • SIGHASH_SINGLE: 모든 input은 수정 불가능, output은 서명과 동일한 index의 output 만 수정되지 않음을 보증 (0x03)
  • SIGHASH_ANYONECANPAY: 위 해시 유형과 조합을 통해 여러 보증 범위를 결정할 수 있다.

SIGHASH_ALL로 설정하면 모든 입력 및 출력 데이터를 트랜잭션 직렬화에 포함한다.

이렇게 직력화한 트랜잭션 데이터를 해시 연산하여 전자서명에 필요한 Z값을 얻어내고, Z와 철수의 개인키를 이용해 전자 서명을 생성한다.

이렇게 생성된 전자서명을 공개키와 함께 기존에 비워두었던 해제 스크립트 부분에 채워 넣는다. 그리고 해시 유형 데이터를 전자서명 뒤에 추가한다. 검증인이 해당 전자서명의 Z를 생성할 때 어떤 해시 유형으로 Z를 생성했는지 알 수 있어야 서명자와 같은 방식으로 Z를 생성할 수 있기 때문이다.

* 그림상은 전자서명으로 표기했지만 실제는 공개키와 전자서명 그리고 SIGHASH가 해제 스크립트 부분에 자리하게 된다.

이와 같은 방법으로 모든 입력에 대한 해제 스크립트를 채우고나면 비로소 유효한 트랜잭션이 생성된다.

해당 트랜잭션이 블록에 담기게 되면, 철수의 UTXO는 더 이상 사용하지 못하며, 영희에게는 사용 가능한 새로운 UTXO가 생긴다.

유효한 트랜잭션을 생성했으니, 해당 트랜잭션의 검증 절차를 알아보자.

transaction 검증

transaction 검증 과정의 핵심은 해제 스크립트 검증이다

UTXO의 유효성, output 크기, locktime 등 기본적인 검증이 끝나면 잠금, 해제 스크립트를 결합하여 실행한다. 일반적으로 잠금, 해제 스크립트에는 전자서명의 검증 절차가 포함되어 있다.

스크립트에 대한 자세한 내용은 글의 범위를 넘어서므로 스크립트에서 핵심 오퍼레이션인 전자서명 검증에 대해 중점적으로 설명하겠다.

철수가 만든 트랜잭션의 경우 입력값이 2개이므로 검증인 (노드)는 두 입력값의 해제 스크립트를 각각 실행 및 검증한다. 입력값 중 하나라도 유효하지 않으면 트랜잭션은 블록에 담기지 못한다.

해제 스크립트를 실행하여 전자서명의 유효성을 검증할 때 검증인은 전자서명에 사용되는 트랜잭션 해시값을 직접 생성한다.

그 이유는 네트워크 전파 과정에서 트랜잭션에 위, 변조가 생길 수 있으며 이를 검증할 필요가 있기 때문이다. 위, 변조 여부를 파악하기 위해서 외부에서 제공되는 해시값이 아닌 트랜잭션을 직접 직렬화하여 에서 직접 z 값을 축출, 전자 서명 검증에 활용한다.

z값을 직접 추출하기 위해서 우선 input data의 끝에 기록된 SIGHASH를 확인하여 직렬화에 포함될 데이터를 파악한다.

여기선 철수가 SIGHASH_ALL을 설정했기 때문에 트랜잭션 내용 전체를 직렬화한다.

직렬화 방법은 트랜잭션 생성 절차와 같은 방식으로 진행한다. 입력에 있던 해제 스크립트 (일반적으로 전자서명 + 공개키) 를 제거한 후 그 자리에 철수가 사용한 UTXO의 잠금 스크립트를 넣고 마지막에 SIGHASH를 덧붙인다. 정확히 트랜잭션을 생성하는 방식과 동일한 방식으로 트랜잭션 해시를 만드는 것이 핵심이다.

이렇게 만들어진 트랜잭션 해시 정보 (Z) 와 트랜잭션에 포함된 해제 스크립트 (전자서명 + 공개키)를 이용하여 최종적인 검증을 진행한다.

이 모든 검증 절차가 끝나면 비로소 트랜잭션의 유효성이 검증되고 블록에 담기게 됩니다. 이렇게 영희는 철수로부터 1.25 비트를 전송 받게 된다.

Transaction Malleability

transaction malleability는 같은 처리를 수행하는 트랜잭션에 대하여 여러 트랜잭션 ID (i.e. transaction hash)가 생성될 수 있는 특성을 의미한다.

발생 원인은 signature, script 크게 두 가지로 나뉘는 데, 여기서는 후자를 설명하겠다.

signature를 구성하는 요소인 ‘message’ (i.e. Z)를 생성할 때 해제 script가 포함되지 않아 해제 script를 수정하더라도 signature의 유효성은 유지된다. 즉 해제 script를 일부 수정하더라도 원본 트랜잭션과 동일하게 정상 처리되는 트랜잭션을 생성할 수 있는데, 이때 변경된 해제 script로 인해서 트랜잭션의 ID가 변경된다.

이러한 특성으로 인해 트랜잭션 ID가 unique identifier의 역할을 완벽히 수행하지 못하는 문제가 발생한다. 실질적으로 같은 처리를 하는 트랜잭션에서 대한 식별자가 복수 개가 있을 수 있기 때문이다. 이러한 문제를 비트코인 코어에선 세그윗 (Segwit) 을 도입하여 해결한다.

--

--