블록체인/메인넷

Go 언어로 블록체인 메인넷 만들기 - 디지털 서명(Signature)

Go 언어로 블록체인 메인넷 만들기 - 프로토타입

Go 언어로 블록체인 메인넷 만들기 - 작업증명(PoW)

Go 언어로 블록체인 메인넷 만들기 - 영속성

Go 언어로 블록체인 메인넷 만들기 - CLI(Command Line Interface)

Go 언어로 블록체인 메인넷 만들기 - 트랜잭션(Transaction)

Go 언어로 블록체인 메인넷 만들기 - 거래#1

Go 언어로 블록체인 메인넷 만들기 - 키와 주소, ―지갑

Go 언어로 블록체인 메인넷 만들기 - 거래#2

Go 언어로 블록체인 메인넷 만들기 - 디지털 서명(Signature)

서명

이전 포스트인 Go 언어로 블록체인 메인넷 만들기 - 거래#2 에서 서명 구현을 미룬 바 있는데, 이제 서명을 구현해보도록 하자. 일단 서명이 무엇인지부터 고민해야 하는데, 요즘엔 빈도가 적어졌지만 예전에는 실물 신용카드로 물건을 구입할 때 서명을 하는 경우가 많았다. 이러한 서명은 해당 카드는 내가 소유했거나, 또는 사회적 합의로 인한 정당한 조건으로 서명을 대신하여 지불할 권리가 있다는 것을 말한다.

 

블록체인에서 서명이란 A 가 B 에게 거래를 생성해서 보낼 때, A 의 서명을 입력값에 첨부하는데, A 의 입장에선 이 거래를 A 가 생성해서 보냈음을 증명하기 위한 것이며 B 의 입장에서는 A 가 거래를 생성했음을 검증할 수 있다.  이러한 거래가 검증되면 중간에 거래가 변경되지 않았고, 부인방지가 된다는 것을 말한다.

 

서명에는 두 가지 요소가 존재한다. 하나는 서명을 생성하기 위한 알고리즘이고, 또 한 가지는 서명을 검증하기 위한 알고리즘이다. 서명을 생성하는 것은 거래를 생성하는 A 가, 서명을 검증하는 것은 B, 또는 블록체인 어플리케이션이 처리하여 검증이 되지 않은 거래는 블록에 포함되지 않도록 조치한다.

type Transaction

트랜잭션에 일부 추가해야할 메서드가 있다. 위에서 언급한 서명을 생성하고, 검증하기 위한 메서드다.

func .Sign(*ecdsa.PrivateKey, map[string]*Transaction)

서명 생성은 도대체 어떻게 해야할까? 먼저 첫 번째로 간단하게 이야기하자면, 서명할 데이터의 해시에 개인키를 넣고 서명 알고리즘을 돌리면 서명이 생성된다. 간단하지 않은가.

Sig = F`sig(F`hash(M), k)

Sig = 서명, F`sig = 서명 알고리즘, F`hash = 해시함수, M = 서명할 데이터, k = 개인키

그럼 서명할 데이터를 선정하는 것이 큰 문제가 된다. 서명할 데이터에서는 송신자와 수신자의 식별정보가 필요한데 이러한 식별정보는 공개키 해시(Public Key Hash)로 표현될 수 있다.

 

서명하려는 거래의 입력값에서 참조하고 있는 이전 트랜잭션TXOutput.PubKeyHash 에는 거래를 생성하는 자의 공개키 해시가 담겨있으므로 이는 송신인을, 출력에는 이미 수신인의 공개키 해시가 담겨있기 때문에 이를 서명 대상 데이터로 지정할 수 있다. 거래에는 입력값과 출력값이 여럿 포함되어 있어서 이를 데이터로 사용할텐데, 거래를 바로 해싱하지 않을 것이고 거래를 복사한 뒤 값을 일부 수정하여 해싱할 것이다.

 

또한 서명하기 위한 알고리즘으로 ECDSA(Elliptic Curve Digital Signature Algorithm)이 사용되므로 계산과정을 거쳐 내부에서 반환되는 R, S 라는 값을 사용하게 될것이며 이 둘을 이어서 서명을 생성할 수 있다.

func (tx *Transaction) Sign(privKey *ecdsa.PrivateKey, prevTXs map[string]*Transaction) {
	if tx.IsCoinbase() {
		return
	}
	// 거래의 복사본 생성
	txCopy := tx.TrimmedCopy()

	for inID, in := range txCopy.Vin {
		// 서명 대상 데이터 구성 및 초기화, 데이터를 대상으로 해싱 .SetID()
		txCopy.Vin[inID].Signature = nil
		txCopy.Vin[inID].PubKey = prevTXs[hex.EncodeToString(in.Txid)].Vout[in.Vout].PubKeyHash
		txCopy.SetID()
		txCopy.Vin[inID].PubKey = nil

		// 서명 생성, 개인키와 서명한 데이터의 해시를 넣자.
		r, s, err := ecdsa.Sign(rand.Reader, privKey, txCopy.ID)
		if err != nil {
			log.Panic(err)
		}

		tx.Vin[inID].Signature = append(r.Bytes(), s.Bytes()...)
	}
}

복사된 거래의 TXInput.PubKey 에 송신자 식별을 위한 이전 트랜잭션의 TXOutput.PubKeyHash 를 넣자. 그 다음 .SetID() 를 통해 거래를 해싱하고, .ID 를 할당한다음, ecdsa.Sign() 으로 서명을 생성하면 된다. 타원곡선 암호화의 결과로 R, S 값이 생성되는데 이를 연결해주면 된다. R, S 값에 대한 자세한 사항은 타원곡선 암호화에 대한 깊은 이해가 필요할 것이다. 

func .TrimmedCopy()

대상 트랜잭션의 복사본을 생성한다. 다만 입력 값에 대해서는 TXInput.PubKey 는 비워두도록 하자.

func (tx *Transaction) TrimmedCopy() *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	for _, in := range tx.Vin {
		inputs = append(inputs, TXInput{in.Txid, in.Vout, nil, nil})
	}
	for _, out := range tx.Vout {
		outputs = append(outputs, TXOutput{out.Value, out.PubKeyHash})
	}

	return &Transaction{nil, inputs, outputs}
}

func .Verify(map[string]*Transaction)

이 다음에 해야 할 일은 서명을 검증하는 것이다. 서명을 검증하기 위해서는 해시된 데이터, 서명(R,S), 공개키(X,Y)가 필요하다. 그러나 파라매터로는 .Sign() 과 마찬가지로 이전 트랜잭션들을 받는다. 검증을 위해 서명에 사용된 데이터를 해시해서 비교할 것이기 때문이다.

func (tx *Transaction) Verify(prevTXs map[string]*Transaction) bool {
	txCopy := tx.TrimmedCopy()
	curve := elliptic.P256()

	for inID, in := range tx.Vin {
		// 서명에 사용할 데이터를 생성하고 해싱.
		// 여기서 생성된 해시는 검증을 위해 만든 것.
		txCopy.Vin[inID].Signature = nil
		txCopy.Vin[inID].PubKey = prevTXs[hex.EncodeToString(in.Txid)].Vout[in.Vout].PubKeyHash
		txCopy.SetID()
		txCopy.Vin[inID].PubKey = nil

		// 서명의 R, S 값 얻기
		var r, s big.Int

		sigLen := len(in.Signature)
		r.SetBytes(in.Signature[:sigLen/2])
		s.SetBytes(in.Signature[sigLen/2:])

		// 공개키의 X, Y 값 얻기
		var x, y big.Int

		keyLen := len(in.PubKey)
		x.SetBytes(in.PubKey[:keyLen/2])
		y.SetBytes(in.PubKey[keyLen/2:])

		// 공개키 생성
		pubKey := ecdsa.PublicKey{curve, &x, &y}

		// 검증
		if isVerified := ecdsa.Verify(&pubKey, txCopy.ID, &r, &s); !isVerified {
			return false
		}
	}

	return true
}

서명의 r, s 와, 공개키의 x, y 는 생성시 모두 타원곡선 암호화를 사용하였고, 생성시 두 요소를 단순 연결시켜주는 방식을 사용했으므로 역으로 얻는 과정도 그렇게 어렵지는 않다. 공개키에서 얻은 x, y 값을 기반으로 ecdsa.PublicKey 를 생성하고 ecdsa.Verify() 의 서명 검증에 사용하는 것에 사용한다.

Type Blockchain

위의 메서드들에서 파라매터를 보면 이전 트랜잭션들을 받는 것을 알 수 있다. 따라서 우리는 이전 트랜잭션을 찾을 필요가 있는데, 이전 트랜잭션들은 대상 트랜잭션의 Transaction.TXInput.Txid 를 통해 얻어올 수 있다.

func .FindTransaction([]byte) *Transaction

이 메서드는 단순한 일을 한다. 블록체인에서 파라매터로 넘어온 txid 에 해당하는 트랜잭션을 얻어온다.

func (bc *Blockchain) FindTransaction(txid []byte) *Transaction {
	bci := NewBlockchainIterator(bc)
	for bci.HasNext() {
		block := bci.Next()
		for _, tx := range block.Transactions {
			if bytes.Compare(tx.ID, txid) == 0 {
				return tx
			}
		}
	}

	return nil
}

func .SignTransaction(*ecdsa.PrivateKey, *Transaction)

트랜잭션에 서명한다. 위에서 만든 Transaction.Sign() 을 사용할 것이고, 해당 트랜잭션의 TXInput 을 조사하여 이전 트랜잭션들을 얻어온다.

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

	for _, in := range tx.Vin {
		prevTXs[hex.EncodeToString(in.Txid)] = bc.FindTransaction(in.Txid)
	}

	tx.Sign(privKey, prevTXs)
}

func .VerifyTransaction(*Transaction) bool

해당 트랜잭션의 서명을 검증할 수 있다. Transaction.Verify() 를 사용하게 될 것이고, 마찬가지로 이전 트랜잭션들을 필요로 하기때문에 코드의 내용은 아주 유사하다.

func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
	prevTXs := make(map[string]*Transaction)

	for _, in := range tx.Vin {
		prevTXs[hex.EncodeToString(in.Txid)] = bc.FindTransaction(in.Txid)
	}

	return tx.Verify(prevTXs)
}

변경 사항

이제 몇가지 변경사항이 있는데, 별거 없으니 살펴보도록 하자.

func Blockchain.Send(string, string, uint64)

거래를 만들때 이제 우리는 서명을 할 수 있다.

func (bc *Blockchain) Send(value uint64, from, to string) *Transaction {
	// ...

	tx := NewTransaction(txin, txout)
	bc.SignTransaction(wallet.PrivKey, tx)

	return tx
}

func Blockchain.Addblock([]*Transaction)

블럭을 추가하기 이전에, 블록에 추가될 거래를 검증해야 한다.

func (bc *Blockchain) AddBlock(transactions []*Transaction) {
	for _, tx := range transactions {
		if isVerified := bc.VerifyTransaction(tx); !isVerified {
			log.Panic("ERROR: Invalid transaction")
		}
	}

	// ...
}

결론

이제 Go 언어로 블록체인 메인넷 만들기 - 거래#2 와 마찬가지로 똑같이 실험해볼 수 있다. 보기엔 하나도 변한게 없어보이지만 서명이라는 중요한 개념이 추가되었다.

# 지갑 만들기
$ ./bc newwallet
Address: 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM

$ ./bc newwallet
Address: 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB

# 블록체인 생성
$ ./bc new -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
0000184346f709a36306b255d6411f0b0e0413a3f5a8e3e4c3d278da1c5766bc

# 거래 전 잔액 확인하기
$ ./bc getbalance -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
Balance of '16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM': 10

$ ./bc getbalance -address 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB
Balance of '1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB': 0

# 거래 생성하기
$ ./bc send -from 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM -to 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB -value 8
000021449c934cec995d87279c01c5b2a7d9968a101608573df14c0d2a94c317

# 거래 후 잔액 확인하기
$ ./bc getbalance -address 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB
Balance of '1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB': 8

$ ./bc getbalance -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
Balance of '16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM': 2