Blockchain

Go로 만드는 블록체인 part 5 - Wallet

hou27 2022. 1. 29. 19:29

지금까지는 사용자를 특정 짓는 데에 단순한 문자열을 사용하였지만

이제는 지갑을 사용해보도록 하겠다.

 

Bitcoin에서의 지갑이란 무엇일까?

A Bitcoin address is a 160-bit hash of the public portion of a public/private ECDSA keypair. Using public-key cryptography, you can "sign" data with your private key and anyone who knows your public key can verify that the signature is valid.


거래를 위해서는 사용자를 식별할 수 고유한 무언가가 필요하다. 은행의 경우 계좌번호로 생각해도 무방하다.

추가로 자신의 코인을 사용하기 위해서는 스스로가 정말 본인이 맞는지 '인증'할 수 있어야 한다.

Bitcoin은 이러한 수단으로 공개/비공개ECDSA 키 쌍의 공개 부분에 대한 160bit 해시를 주소로 사용한다.

공개 키 암호화를 사용하면 개인 키 데이터에 "서명"할 수 있으며 공개 키를 아는 사람은 누구나 서명이 유효한지 확인할 수 있게 된다.

즉, Public key가 계좌번호와 같은 역할을 하며 나만의 인증 수단으로 Private key를 사용하게 되는 것이라고 말할 수 있다.

 

암호화폐에는 "마스터 주소"란 존재하지 않는다.

Bitcoin 주소에는 체크 코드가 내장되어 있으므로 일반적으로 비트코인을 잘못 입력된 주소로 보내는 것은 불가능하다.

주의할 점은 본인의 지갑 주소를 잃게 된다면 영원히 자신의 코인은 찾을 수 없다는 것이다.

 

특정 사용자를 나타내는 주소를 '지갑'이라고 부르기 때문에

대부분 지갑에 돈이 보관되는 것이라고 착각하기 쉽다. 여기서의 지갑은 보관의 역할이 아니라, 스스로를 인증하고 코인을 사용할 수 있게 해주는 수단이다. 이전 포스트에서도 설명했지만 잔고는 utxo를 통해 계산하는 이유가 조금이라도 더 이해가 되길 바란다.

충돌

Bitcoin 주소는 극히 드물게 다른 사용자가 동일한 주소를 생성하는 것이 이론적으로 가능하다. 이러한 상황을 '충돌'이라고 하는데, 주소의 원래 소유자와 충돌하는 소유자 모두가 해당 주소로 보내진 코인을 사용할 수 있게 된다.

이렇게 말하면 걱정될 수 있겠지만 가능한 주소의 영역이 너무나 크기 때문에 다음 천년에 충돌이 발생하는 것보다 다음 5초 안에 지구가 파괴될 가능성이 더 높다고 Bitcoin-wiki에 명시되어있다.

 

이제 본격적으로 구현을 시작해보겠다.

Struct Wallet, Wallets

// Wallet
type Wallet struct {
	PrivateKey ecdsa.PrivateKey
	PublicKey  []byte
}

// Wallets stores bunch of wallets
type Wallets struct {
	Wallets map[string]*Wallet
}

지갑은 공개 키, 개인 키가 쌍을 이룬 것이다.

Wallet 구조체는 공개 키와 개인 키를 가지며, Wallets 구조체는 발행된 여러 주소들을 보관하기 위함이다.


주소의 발행

이제 실제 Bitcoin 주소가 어떤 과정을 통해 발행되는지 살펴보도록 하겠다.

Public key를 먼저 SHA-256으로 해싱하고, 이 결과를 RIPEMD-160으로 또다시 해싱한다.

마지막으로, version을 가리키는 bytes와 checksum을 양쪽에 붙이게 된다.

Function NewWallet

// Generate New Wallet
func NewWallet() *Wallet {
	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		log.Panic(err)
	}

	publicKey := append(privateKey.PublicKey.X.Bytes(), privateKey.PublicKey.Y.Bytes()...)

	return &Wallet{*privateKey, publicKey}
}

1 - 공개 키와 개인 키 쌍을 생성

func GenerateKey

func GenerateKey(c elliptic.Curve, rand io.Reader) (*PrivateKey, error)
  • GenerateKey generates a public and private key pair.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)

elliptic.P256()secp256r1 또는 Prime256v1이라고도 하는 NIST P-256(FIPS 186-3, 섹션 D.2.3)을 구현하는 곡선을 반환한다.

해당 곡선을 통해 개인 키를 생성하고, 개인 키를 통해 공개 키가 생성된다.

ECDSA의 키는 개인 키(private key)와 공개 키(public key)를 쌍으로 생성하는데, 개인 키(d)는 (도메인 파라미터 n을 사용하여) 1 ~ n-1까지의 정수 중 하나를 랜덤 하게 선택하면 된다.
ECDSA의 공개키는 개인키를 생성자(G) 파라미터에 곱한 Q = d*G 를 통해 구하게 된다.

 

우선 반환 값은 *PrivateKey와 error이다.

type PrivateKey struct {
    PublicKey
    D *big.Int
}

PrivateKey 내에는 PublicKey가 존재한다.

publicKey := append(privateKey.PublicKey.X.Bytes(), privateKey.PublicKey.Y.Bytes()...)

return &Wallet{*privateKey, publicKey}

ECDSA는 타원 곡선 기반 알고리즘인데, 이 알고리즘에서 공개 키들은 곡선 위의 점들을 가리킨다.

Bitcoin에서 이 점들을 이어주면 공개 키를 형성하게 된다.

 

2 - Public key를 먼저 SHA-256으로 해싱하고, 이 결과를 RIPEMD-160으로 또다시 해싱

Function HashPublicKey

// Hash public key
func HashPublicKey(pubKey []byte) []byte {
	publicSHA256 := sha256.Sum256(pubKey) // Public key를 SHA-256으로 해싱

	// RIPEMD-160으로 다시 해싱
	RIPEMD160Hasher := ripemd160.New()
	_, err := RIPEMD160Hasher.Write(publicSHA256[:])
	if err != nil {
		log.Panic(err)
	}

	publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
	return publicRIPEMD160
}

3 - version을 가리키는 bytes와 checksum을 양쪽에 추가

Function GetAddress

// Get wallet address
func (w Wallet) GetAddress() string {
	publicKeyHash := HashPublicKey(w.PublicKey)

	return base58.CheckEncode(publicKeyHash, version)
}

func CheckEncode

func CheckEncode(input []byte, version byte) string

CheckEncode는 버전 byte를 앞에 추가하고 4byte 체크섬을 추가하여 Base58로 해싱한다.

 

INFO

- Public key를 주소로 사용하지 않고 해싱하는 이유

 

1. 더 짧은 주소를 얻기 위함

 

Key를 해싱하면 20 bytes로, 해싱하지 않고 트랜잭션에 포함하는 것보다 훨씬 효율적이다.

+ 20 bytes의 주소를 사용함으로써 QR code로도 변환할 수 있다고 한다.

 

2. Elliptic Curve Cryptography에는 근본적인 취약점이 존재

 

modified Shor’s algorithm가 타원곡선의 이산 로그 문제를 풀 수 있기 때문에

(양자 컴퓨터가 나왔을 때의 이야기이긴 하지만, 어쨌든 이를 통해 Public key로부터 Private key를 유추할 수 있는 가능성이 있다.)

Public key를 숨길 필요가 있다고 한다.

 

 + 자신의 Address에 존재하는 BTC를 소비하게 되면, Transaction의 Unlock Script 안에 자신의 Public key와 Signature 등이 포함되게 된다. 즉, 정확히는, Public key Hash를 사용하더라도 Public key는 이것을 사용하기 전까지만 숨겨진다는 것이다. 그래서 같은 Address를 계속해서 재사용하지 말라고 한다.

 

이제 지갑을 만들 수 있게 되었다.

Function createWallet

func (cli *Cli) createWallet() {
	wallets, _ := NewWallets()
	address := wallets.CreateWallet()
	wallets.SaveToFile()
	
	fmt.Printf("Your new address: %s\n", address)
}

func NewWallets() 

: 기존의 주소들을 저장한 목록을 불러오는 함수이다.

 (없다면 빈 값을 리턴)

func wallets.SaveToFile()

: 주소 목록을 파일에 저장하는 함수이다.

 

테스트를 진행해보자.

 

실행 시 모습 :

$ go run . createwallet
Your new address: 1159YQDpHy97WrXacYjFg3ET84A8qi1B3b

 


Implement Signature

열심히 지갑을 구현했는데 아무나 자신의 코인을 사용할 수 있다면 억울하지 않을까?

그래서 모든 트랜잭션은 '서명'되어야만 한다.

 

여기서 말하는 서명이란,

간단히 말해 네트워크에서 신원을 증명하는 방법이다.

 

공개 키는 소유자의 신원을 나타내며 개인 키는 공개 키는 개인 키를 가지고 있다는 것을 증명한다.

 

Bob과 Alice가 서로 메시지를 주고받는다고 가정하자.

 

 

Alice는 Bob이 메시지를 전송하는 과정에서,

중간에 변조되었는지 Bob이 보낸 것은 맞는지 알 수 없다.

그렇기 때문에 Bob은 자신만이 알고 있는 Private Key로 메시지를 암호화하여 서명을 생성하여 전송한다.

 

Alice는 Bob의 Public Key를 통해 서명을 복호화한 값과 수신한 메시지를 해싱한 값을 비교하고,

두 값이 같을 때 수신한 메시지가 Bob이 보낸 것이며 변조되지 않았음을 신뢰할 수 있다.

 

이제 구현을 시작해보도록 하겠다.

 

Structure

// Coin transaction
type Transaction struct {
	ID	[]byte
	Vin	[]TXInput
	Vout	[]TXOutput
}

// Unlock script
type ScriptSig struct {
	Signature	[]byte
	PublicKey	[]byte
}

// Transaction input
type TXInput struct {
	Txid		[]byte
	TxoutIdx	int
	ScriptSig	*ScriptSig
}

// Transaction output
type TXOutput struct {
	Value        int
	ScriptPubKey []byte
}

Transaction을 구현할 때 대부분 살펴보았었지만

Type ScriptSig

type ScriptSig struct {
	Signature	[]byte
	PublicKey	[]byte
}

이 친구는 살펴보지 않았었다.

ScriptSig는 서명 정보와 공개 키를 보관한다.

Function Sign

// Signs each input of a Transaction
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
	if tx.IsCoinbase() {
		return
	}

	...

	abbreviatedTx := tx.AbbreviatedCopy()

	for inId, vin := range abbreviatedTx.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]

		abbreviatedTx.Vin[inId].ScriptSig = &ScriptSig{}
		abbreviatedTx.Vin[inId].ScriptSig.PublicKey = prevTx.Vout[vin.TxoutIdx].ScriptPubKey

		// Use ECDSA(not RSA)
		r, s, err := ecdsa.Sign(rand.Reader, &privKey, abbreviatedTx.ID)
		if err != nil {
			log.Panic(err)
		}
		signature := append(r.Bytes(), s.Bytes()...)

		tx.Vin[inId].ScriptSig.Signature = signature
		abbreviatedTx.Vin[inId].ScriptSig.PublicKey = nil
	}
}

coinbase라면 입력이 없기 때문에 서명하지 않고 넘어간다.

 

트랜잭션을 map형태로 모아 넘겨받아 하나하나 순회하며 서명한다.

 

트랜잭션의 입력이 참조한 이전 Transaction.

즉, 잠금이 해제된 출력의 ScriptPubKey에는 해시된 공개 키가 담겨있는데,

이는 송신자를 나타내 주며,

현재 잠겨있는 출력에 담겨있는 해시된 공개 키는 코인을 전송하는 대상을 나타내므로

수신자를 나타낸다.

 

즉, 이들을 서명하여 서명 정보를 생성할 것이다.

 

여기서 AbbreviatedCopy()가 뭘 하는 건지 궁금할 것이다.

Function AbbreviatedCopy

// Creates a abbreviated copy of Transaction to use in sign
func (tx *Transaction) AbbreviatedCopy() Transaction {
	var inputs []TXInput

	// The public key stored in the input doesn't need to be signed.
	for _, vin := range tx.Vin {
		inputs = append(inputs, TXInput{vin.Txid, vin.TxoutIdx, nil})
	}

	abbreviatedTx := Transaction{tx.ID, inputs, tx.Vout}

	return abbreviatedTx
}

조금 전 알아보았듯이 서명을 진행할 때 입력에 있는 ScriptSig는 서명할 데이터가 아니므로

해당 데이터를 제외한 축약된 버전의 Transaction을 만들어주는 함수이다.

 

그렇게 축약된 Transaction의 입력의 공개 키에는 

abbreviatedTx.Vin[inId].ScriptSig.PublicKey = prevTx.Vout[vin.TxoutIdx].ScriptPubKey

이전 Transaction에 있는 해시된 공개 키를 담아주어 송신자 정보를 추가하고,


func ecdsa.Sign

func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error)
  • Sign은 개인 키 priv를 사용하여 해시에 서명합니다. 해시가 개인 키 곡선 순서의 비트 길이보다 길면 해시가 해당 길이로 잘립니다. 서명을 정수 쌍으로 반환합니다.

// Use ECDSA(not RSA)
r, s, err := ecdsa.Sign(rand.Reader, &privKey, abbreviatedTx.ID)
if err != nil {
    log.Panic(err)
}
signature := append(r.Bytes(), s.Bytes()...)

tx.Vin[inId].ScriptSig.Signature = signature
abbreviatedTx.Vin[inId].ScriptSig.PublicKey = nil

 ECDSA에서 제공하는 서명 함수를 이용하여 개인 키를 사용하여 서명하고 정수 쌍을 이어붙여 서명정보를 생성해준 후

Signature에 저장한다.

 

그 후 다음 순회에 영향이 없도록 PublicKey를 nil로 초기화해주었다.

 

다음으로 검증 메서드이다.

Function Verify

// Verifies signatures of Transaction inputs
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	if tx.IsCoinbase() {
		return true
	}

	for _, vin := range tx.Vin {
		if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
			log.Panic("Error with Previous transaction")
		}
	}

	abbreviatedTx := tx.AbbreviatedCopy()
	curve := elliptic.P256() // The same curve used to generate key pairs.

	for inId, vin := range tx.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]

		abbreviatedTx.Vin[inId].ScriptSig = &ScriptSig{}
		abbreviatedTx.Vin[inId].ScriptSig.PublicKey = prevTx.Vout[vin.TxoutIdx].ScriptPubKey

		sigLen := len(vin.ScriptSig.Signature)
		keyLen := len(vin.ScriptSig.PublicKey)

		var r, s big.Int
		var x, y big.Int

		// Signature is a pair of numbers.
		r.SetBytes(vin.ScriptSig.Signature[:(sigLen / 2)])
		s.SetBytes(vin.ScriptSig.Signature[(sigLen / 2):])

		// PublicKey is a pair of coordinates.
		x.SetBytes(vin.ScriptSig.PublicKey[:(keyLen / 2)])
		y.SetBytes(vin.ScriptSig.PublicKey[(keyLen / 2):])

		rawPublicKey := &ecdsa.PublicKey{Curve: curve, X: &x, Y: &y}
		if !ecdsa.Verify(rawPublicKey, abbreviatedTx.ID, &r, &s) {
			return false
		}
		abbreviatedTx.Vin[inId].ScriptSig.PublicKey = nil
	}
	return true
}

검증할 때도 마찬가지로,

 

coinbase라면 넘어가고, 아니라면 축약된 버전의 복사본을 생성한다.


func ecdsa.Verify

func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool
  • 공개 키를 사용하여 해시의 r, s에 있는 서명을 확인합니다. 반환 값은 서명이 유효한지 여부를 기록합니다.

if !ecdsa.Verify(rawPublicKey, abbreviatedTx.ID, &r, &s) {
    return false
}

그리고 검증 시에는 서명할 때와 반대로 공개 키를 사용하여 확인하게 된다.

 

천천히 살펴보면 충분히 이해할 수 있을 것이다.

Function SignTransaction

// Signs inputs of a Transaction
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.GetTransaction(vin.Txid)
		if err != nil {
			log.Panic(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	tx.Sign(privKey, prevTXs)
}

필요한 Transaction들을 모아 prevTXs에 담은 후, Sign메서드로 넘겨주는 역할을 하는 친구이다.

Function GetTransction

// Get transaction
func (bc *Blockchain) GetTransaction(id []byte) (Transaction, error) {
	bcI := bc.Iterator()
	for {
		block := bcI.getNextBlock()
		for _, tx := range block.Transactions {
			if bytes.Equal(tx.ID, id) {
					return *tx, nil
			}
		}
		if len(block.PrevHash) == 0 {
			break
		}
	}
	return Transaction{}, errors.New("Transaction not found")
}

위에서 Transaction들을 모을 때 이 친구가 도와준다.

Transaction의 ID값을 통해서 찾아준다.

Function VerifyTransaction

// Verifies transaction input signatures
func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
	if tx.IsCoinbase() {
		return true
	}

	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.GetTransaction(vin.Txid)
		if err != nil {
			log.Panic(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	return tx.Verify(prevTXs)
}

SignTransaction과 같은 친구인데, 다른 점은 서명이 아닌 검증을 한다는 것 뿐이다.


이제 Sign, Verify를 적용해보도록 하겠다.

// Creates a new transaction
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	...
	tx.SetID()
	bc.SignTransaction(&tx, wallet.PrivateKey)
	
	return &tx
}

이제 NewUTXOTransaction 함수에서 ID를 설정한 후에 작업이 하나 늘었다.

 

func (bc *Blockchain) AddBlock(transactions []*Transaction) {
	var lastHash []byte

	for _, tx := range transactions {
		if !bc.VerifyTransaction(tx) {
			log.Panic("!!Invalid transaction!!")
		}
	}

	...
}

또한 블록 추가 전에 검증하는 작업이 추가되었다.

func (cli *Cli) send(from, to string, amount int) {
	bc := GetBlockchain()
	defer bc.db.Close()
	tx := NewUTXOTransaction(from, to, amount, bc)
	rwTx := NewCoinbaseTX(from, "Mining reward") // 요기 ^^
	bc.AddBlock([]*Transaction{rwTx, tx})
	fmt.Println("Send Complete!!")
}

 

변경사항이 하나 더 있다.

바로 거래가 발생할 때마다 채굴 보상을 지급하도록

rwTx := NewCoinbaseTX(from, "Mining reward")

보상 트랜잭션을 추가해주었다.

 

실행 시 모습 :

$ go run . getbalance -address 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U
Balance of '1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U': 10

$ go run . send -from 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U -to 1159YQDpHy97WrXacYjFg3ET84A8qi1B3b -amount 7
0217170d37432ca230ff29f6df56930f0f0189d686f18593d9b3d4c9701ae6d5
Successfully Added
Send Complete!!

$ go run . getbalance -address 1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U
Balance of '1HkPzYQgyBy3wkCqwN8bAwWE4vx1d6hv9U': 13

7 coin을 다른 주소로 전송하여 3 coin이 남아있어야했지만,

채굴 보상을 지급받아 잔고에 13 coin이 있는 것을 확인할 수 있다.

 

 

자세한 코드의 변경사항이 궁금하거나 직접 테스트해보고 싶다면 아래의 주소에서 확인할 수 있을 것이다.

https://github.com/hou27/blockchain_go/tree/part5

 

GitHub - hou27/blockchain_go: Making a Cryptocurrency with GO.

Making a Cryptocurrency with GO. . Contribute to hou27/blockchain_go development by creating an account on GitHub.

github.com


참고자료

 

bitcoin wallet

Why we use publicKeyHash

Technical background of version 1 Bitcoin addresses

bitcoin developer guide

Digital Signatures in Blockchains

Digital signatures and certificates