[밑바닥비트코인] 8. 트랜잭션 검증과 생성

2021. 8. 9. 22:43Blockchain

저번 장까지 트랜잭션을 직렬화하고 파싱하는 방법과 

 

트랜잭션을 검증할 수 있는 방법인 스크립트에 대해서 살펴봤으니 

 

이번 장에서는 스크립트를 이용하여 트랜잭션을 검증하고 생성하는 방법에 대해서 알아보자. 

 

 

 

트랜잭션 검증 


비트코인을 소비하고자 하는 사람 A는 트랜잭션을 생성하고, 이 트랜잭션을 다른 노드에 전파하면 

 

다른 노드들은 이 트랜잭션이 타당한지 검증하는 절차를 수행한다. 

 

다른 노드들에 의해 받아들여지려면 다음 조건을 만족해야한다. 

1. 트랜잭션 입력이 가리키는 비트코인이 존재하고 사용가능한가?
2. 입력 비트코인의 합은 출력 비트코인의 합보다 크거나 같은가?
3. 입력의 해제 스크립트는 이전 트랜잭션 출력의 잠금 스크립트를 올바르게 해제하는가?

 

1번 항목은 이중지불 방지를 위함이고 2번 항목은 새로운 비트코인의 생성을 막기 위함이다. 

 

트랜잭션이 가르키는 비트코인이 존재하고 사용가능한지 여부를 따지기 위해서는 

 

UXTO 집합 전체를 뒤져보는 수밖에 없다. 트랜잭션이 검증되면 해당되는 비트코인을 UXTO 집합에서 제거한다. 

 

2번 항목에서 트랜잭션 입력의 총합은 트랜잭션 출력의 총합보다 더 크거나 같아야하는데 이는 수수료로 인한 것이다.

class Tx:
     def fee(self):
        input_sum, output_sum = 0, 0
        for tx_in in self.tx_ins:
            input_sum += tx_in.value(self.testnet)
        for tx_out in self.tx_outs:
            output_sum += tx_out.amount
        return input_sum - output_sum

트랜잭션 클래스에서 이를 검증하는 fee 메소드를 구현하였다. 

 

다음으로 서명 z를 생성하고 이를 검증하는 방법을 알아보자. 

class Tx:
    def sig_hash(self, input_idx):
        s = int_to_little_endian(self.version, 4)
        s += encode_varint(len(self.tx_ins))
        for i, tx_in in enumerate(self.tx_ins):
            if i == input_idx:
                s += TxIn(
                    prev_tx = tx_in.prev_tx,
                    prev_idx = tx_in.prev_idx,
                    script_sig = tx_in.script_pubkey(self.testnet),
                    sequence=tx_in.sequence,
                ).serialize()
            else:
                s+=TxIn(
                    prev_tx=tx_in.prev_tx,
                    prev_idx=tx_in.prev_idx,
                    sequence=tx_in.sequence,
                ).serialize()
            s+=encode_varint(len(self.tx_outs))
            for tx_out in self.tx_outs:
                s+=tx_out.serialize()
            s+= int_to_little_endian(self.locktime, 4)
            s += int_to_little_endian(SIGHASH_ALL, 4)
            h256 = hash256(s)
            return int.from_bytes(h256, 'big')

서명해시 z를 생성하는 sig_hash 함수는 위와 같다. 

def sig_hash(self, input_idx):

parameter로 input_idx를 받는데, 사용하고자 하는 입력(input)을 나타낸다. 

        s = int_to_little_endian(self.version, 4)
        s += encode_varint(len(self.tx_ins))

먼저 version 정보를 4바이트 리틀엔디언 정수로 변환해준다. 

 

그리고 총 입력(input) 개수를 덧붙인다. 

        for i, tx_in in enumerate(self.tx_ins):
            if i == input_idx:
                s += TxIn(
                    prev_tx = tx_in.prev_tx,
                    prev_idx = tx_in.prev_idx,
                    script_sig = tx_in.script_pubkey(self.testnet),
                    sequence=tx_in.sequence,
                ).serialize()

입력들을 순회하다가 소비하고자 하는 트랜잭션 입력에 해당된다면 이 입력의 잠금 스크립트를 이전 트랜잭션의 

 

잠금 스크립트로 대체한 TxIn 객체를 직렬화하여 덧붙인다. 이렇게 이전 트랜잭션 출력의 잠금스크립트로 대체하는 

 

이유는 이미 해제 스크립트에 서명이 포함되기 때문 에 모순을 피하기 위해서이다. 

class TxIn:
    def script_pubkey(self, testnet=False):
        tx = self.fetch_tx(testnet=testnet) 
        ##fetch_tx 함수는 입력이 가르키는 이전 트랜잭션을 fetch해온다.
        return tx.tx_outs[self.prev_idx].script_pubkey

TxIn 클래스에서 script_pubkey 함수는 이전 트랜잭션을 가져와서 이 트랜잭션의 (내 트랜잭션의 입력에 대응하는)

 

출력의 잠금 스크립트를 가져온다. 

            else:
                s+=TxIn(
                    prev_tx=tx_in.prev_tx,
                    prev_idx=tx_in.prev_idx,
                    sequence=tx_in.sequence,
                ).serialize()

사용하고자 하는 input에 해당하지 않는 입력들은 스크립트를 제외하고 직렬화하여 추가해준다. 

            s+=encode_varint(len(self.tx_outs))
            for tx_out in self.tx_outs:
                s+=tx_out.serialize()

트랜잭션 출력(output)들도 순회하여 직렬화하고 추가해준다.

            s+= int_to_little_endian(self.locktime, 4)
            s += int_to_little_endian(SIGHASH_ALL, 4)
            h256 = hash256(s)
            return int.from_bytes(h256, 'big')

locktime 역시 4바이트 리틀엔디언으로 변환하여 덧붙이고, 해시 유형을 추가한다. 여기서 SIGHASH_ALL 해시유형은 

 

입력과 출력 모두를 인증해준다는 뜻이다. 

 

이렇게 생성된 s의 hash256 값을 구하고 빅엔디언 정수로 바꿔주면 최종적으로 서명해시 z가 생성된다. 

class Verify:
    @classmethod
    def sig_verify(cls, sig, z, pubkey):
        s_inv = pow(sig.s, N - 2, N)
        u = z * s_inv % N
        v = sig.r * s_inv % N
        total = u * G + v * pubkey
        print('calculated r : ', hex(total.x.num))
        return total.x.num == sig.r

서명을 검증하는 Verify 클래스 내에 sig_verify 함수를 만들었다. 서명(sig)와 서명해시(z), 그리고 공개키(pubkey)를 

 

이용하여 서명을 검증한다. 

class Tx:
    def verify_input(self, input_idx):
        tx_in = self.tx_ins[input_idx]
        script_pubkey = tx_in.script_pubkey(testnet=self.testnet)
        z = self.sig_hash(input_idx)
        combined = tx_in.script_sig + script_pubkey
        return combined.evaluate(z)

 verify_input 함수는 소비하고자 하는 입력을 검증한다. input_idx에 해당하도록 서명해시 z를 생성하고, 

 

이전 트랜잭션의 잠금 스크립트를 가져와서 입력의 해제 스크립트로 올바르게 해제하는지 검증한다. 

class Tx:
    def verify(self):
        if self.fee() < 0:
            return False
        for i in range(len(self.tx_ins)):
            if not self.verify_input(i):
                return False
        return True

마지막으로 verify 함수는 fee 함수와 verify_input 함수를 이용하여 이 트랜잭션의 타당성 여부를 검증한다. 

 

 

 

실검증


    tx_hash = 'd5c528b713816f7dbd13347631852b2427e05c4d370eee96983ed444e7861e92'
    tx = TxFetcher.fetch(tx_hash, testnet=False)
    print(tx.verify())

위 트랜잭션 해시에 해당하는 트랜잭션을 검증해보았다. 

 

먼저 해시 d5c528b....의 트랜잭션의 정보는 다음과 같다. 

version: 2
tx_ins: 
2eab7cf416d430a1803145478894c4c62beeb0ae86f2fa574509118e023ca021:0
Script_sig : 3045022100d568721c034b416c240f1e9f3af7bb599c623fe46ea3d19fe622e147baaf460c022016122ca9ca63ec94b43c77e51d97d9e169a2df682ba408244cb1e0bf4591a2cc01 0332148da891685e898c1295a4a287f17bbfba2aef7ca4194578e0b3b4ef147b25
tx_outs: 
655555:OP_DUP OP_HASH160 133f6ffe7ce4464c6bde26f89b405ddac3b7985c OP_EQUALVERIFY OP_CHECKSIG
10305:OP_0 7ad6be856a7964ddfc420ff372780b7c2f06ab49
locktime: 0

그리고 이 트랜잭션이 참조하는 트랜잭션의 정보는 

version: 1
tx_ins: 
c75e79c7b68cab579c22ab6cdc2e159493e20817a9b43a3a5ad55b6fecd349eb:1
Script_sig : 
cf9f96b1dc2f96e1bc67390a6be40ae49dac8d905ffc06bb4e78e40228cc1602:1
Script_sig : 
tx_outs: 
683890:OP_DUP OP_HASH160 e36394a4401b859e03238cefcd2cebd182dba275 OP_EQUALVERIFY OP_CHECKSIG
217182:OP_0 f2ecef5cc035e8f1419367f95e70341954f74834
locktime: 0

위와 같으며 트랜잭션 출력이 총 2개인데 이 중 첫번째 출력이 검증대상이다. 

 

잠금 스크립트와 해제 스크립트를 시각적으로 나타낸 그림이다. 

 

검증을 위해서 잠금 스크립트와 해제 스크립트를 결합하였다.

 

검증 과정을 하나하나 살펴보자.

1) 먼저 스택에 서명과 공개키를 순서대로 쌓는다.

 

2) 그런 다음 OP_DUP에 의해 값의 복사가 일어난다. 

3) OP_hash160에 의해 스택 가장 위 원소(공개키)의 hash160값을 구한다.

4) 잠금 스크립트에 존재하던 해시값을 스택에 올린다.

 

5) OP_EQUALVERIFY에 의해 스택 위 두 값이 같음을 검증한다.

마지막으로 남은 두 원소(위: 공개키, 아래: 서명)을 OP_CHECKSIG 함수로 검증하면 유효한 것을 확인할 수 있다.

POP!!!
POP!!!
POP!!!
POP!!!
hashed data is  e36394a4401b859e03238cefcd2cebd182dba275
POP!!!
POP!!!
e1 is  e36394a4401b859e03238cefcd2cebd182dba275
e2 is  e36394a4401b859e03238cefcd2cebd182dba275
POP!!!
calculated r :  0xd568721c034b416c240f1e9f3af7bb599c623fe46ea3d19fe622e147baaf460c
True

유효한 결과를 얻었기 때문에 True를 반환하였다.