#6 Transaction Advanced
Transaction에 서명을 추가하여 보안을 강화한다.




blockchain/tx.go
blockchain/transaction.go
blockchain/blockchain.go
cli/cli.go
Testing




Last updated
Transaction에 서명을 추가하여 보안을 강화한다.








Last updated
// tx.go
package blockchain
import (
"bytes"
"github.com/siisee11/golang-blockchain/wallet"
)
type TxOutput struct {
Value int // 잔액
// address를 decode하여 얻을 수 있는 값입니다.
// 좀더 raw한 형태의 주소라고 생각하면됩니다.
// 자세한 내용은 wallet문서를 참조하세요.
PubKeyHash []byte
}
// Input으로 사용하고자 하는 UTXO를 가르킵니다.
type TxInput struct {
ID []byte // UTXO가 생성된 트랜잭션의 ID
Out int // 그 트랜잭션에서 몇번째 UTXO였는 지
Signature []byte // UTXO를 사용하려는 사람의 서명
PubKey []byte // UTXO에 적혀있는 PublicKeyHash 값
}
// {value}와 {address}를 사용해 TXO를 만듭니다.
func NewTXOutput(value int, address string) *TxOutput {
txo := &TxOutput{value, nil}
txo.Lock([]byte(address))
return txo
}
// Pubkey를 이용해 소유권 판별.
func (in *TxInput) UsesKey(pubKeyHash []byte) bool {
lockingHash := wallet.PublicKeyHash(in.PubKey)
return bytes.Equal(lockingHash, pubKeyHash)
}
// {address}를 통해 pubKeyHash를 구해 TXO에 적습니다.
func (out *TxOutput) Lock(address []byte) {
// Base58 Decode를 하고
pubKeyHash := wallet.Base58Decode(address)
// version byte와 checksum byte를 뺍니다.
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
out.PubKeyHash = pubKeyHash
}
// TXO의 pubKeyHash를 보고 소유권을 판단합니다.
func (out *TxOutput) IsLockedWithKey(pubKeyHash []byte) bool {
// 인자로 받은 pubKeyHash와 TXO의 pubKeyHash를 비교합니다.
return bytes.Equal(out.PubKeyHash, pubKeyHash)
}package blockchain
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"fmt"
"log"
"math/big"
"strings"
"github.com/siisee11/golang-blockchain/wallet"
)
// Transaction은 Input, Output으로 구성되어있습니다.
type Transaction struct {
ID []byte
Inputs []TxInput
Outputs []TxOutput
}
// Transaction structure를 []byte로 인코딩하는 함수
func (tx Transaction) Serialize() []byte {
var encoded bytes.Buffer
enc := gob.NewEncoder(&encoded)
err := enc.Encode(tx)
if err != nil {
log.Panic(err)
}
return encoded.Bytes()
}
// Serialized Transaction을 이용해서 Hash값 계산
func (tx *Transaction) Hash() []byte {
var hash [32]byte
txCopy := *tx
txCopy.ID = []byte{}
hash = sha256.Sum256(txCopy.Serialize())
return hash[:]
}
// mining하면 to에게 코인을 보상으로 주는 Coinbase Transaction.
func CoinbaseTx(to, data string) *Transaction {
if data == "" {
data = fmt.Sprintf("Coins to %s", to)
}
txin := TxInput{[]byte{}, -1, nil, []byte(data)}
txout := NewTXOutput(100, to)
tx := Transaction{nil, []TxInput{txin}, []TxOutput{*txout}}
tx.ID = tx.Hash()
return &tx
}
// Transaction을 만드는 함수 입니다.
func NewTransaction(from, to string, amount int, chain *BlockChain) *Transaction {
var inputs []TxInput
var outputs []TxOutput
// Wallets에서 {from}의 address갖는 wallet을 가져옵니다.
wallets, err := wallet.CreateWallets()
Handle(err)
w := wallets.GetWallet(from)
// Public key로 부터 publicKeyHash를 생성합니다.
pubKeyHash := wallet.PublicKeyHash(w.PublicKey)
// {from}이 {amount}를 지불하기 위해 필요한 {from}소유의 UTXO를 가지고 옵니다.
// {from}의 공개키 해시 {pubKeyHash}를 이용합니다.
acc, validOutputs := chain.FindSpendableOutputs(pubKeyHash, amount)
// UTXO를 다 모았는데 amount보다 작다면 잔액 부족입니다.
if acc < amount {
log.Panic("Error: not enough funds")
}
// 모아온 UTXOs에 대해 for loop
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
Handle(err)
for _, out := range outs {
// {txID}를 가지는 트랜잭션의 {out}번째 {from}소유의 아웃풋이 인풋이 됩니다.
// 4번째 인자로 {from}의 PublicKey가 추가됩니다.
input := TxInput{txID, out, nil, w.PublicKey}
inputs = append(inputs, input)
}
}
// 이제 트랜잭션의 인풋으로 사용될 UTXO를 모두 모았습니다.
// 그리고 그 가치의 합은 acc가 될 것입니다.
// 트랜잭션의 아웃풋은 전송될 TXO, 잔금(반환될) TXO
// 항상 두개로 이루어집니다.
// 전송될 TXO을 만듭니다.
outputs = append(outputs, *NewTXOutput(amount, to))
// 반환될 TXO
if acc > amount {
outputs = append(outputs, TxOutput{acc - amount, pubKeyHash})
}
// 인풋과 아웃풋을 바탕으로 Transaction이 생성됩니다.
tx := Transaction{nil, inputs, outputs}
tx.ID = tx.Hash()
chain.SignTransaction(&tx, w.PrivateKey)
return &tx
}
// 해당 트랜잭션이 Coinbase 인가?
func (tx *Transaction) IsCoinbase() bool {
return len(tx.Inputs) == 1 && len(tx.Inputs[0].ID) == 0 && tx.Inputs[0].Out == -1
}
// 소비하려는 UTXO(UTXO-IN)가 자신의 소유임을 Sign하는 과정
// 이는 Verity함수에서 Validator에 의해 검증된다.
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
if tx.IsCoinbase() {
return
}
for _, in := range tx.Inputs {
if prevTXs[hex.EncodeToString(in.ID)].ID == nil {
log.Panic("ERROR: Previous transaction is not corret")
}
}
// Signature와 PubKey field가 비어있는 트랜잭션을 복사해 만든다.
txCopy := tx.TrimmedCopy()
// 트랜잭션의 각 인풋에 서명함.
// 각 인풋에 대해 아래 과정을 거침.
// 1. 인풋의 이전 트랜잭션을 확인하여 PubKeyHash를 가져와 저장.
// 2. 트랜잭션과 private key를 이용해 signature를 구함.
// 3. Signature를 트랜잭션에 추가함.
for inId, in := range txCopy.Inputs {
prevTX := prevTXs[hex.EncodeToString(in.ID)]
txCopy.Inputs[inId].Signature = nil
txCopy.Inputs[inId].PubKey = prevTX.Outputs[in.Out].PubKeyHash
dataToSign := fmt.Sprintf("%x\n", txCopy)
r, s, err := ecdsa.Sign(rand.Reader, &privKey, []byte(dataToSign))
Handle(err)
signature := append(r.Bytes(), s.Bytes()...)
tx.Inputs[inId].Signature = signature
txCopy.Inputs[inId].PubKey = nil
}
// 모든 UTXO-IN에 서명을 완료 후 종료.
}
// 해당 트랜잭션이 유효한 트랜잭션인지 확인하는 함수.
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
if tx.IsCoinbase() {
return true
}
for _, in := range tx.Inputs {
if prevTXs[hex.EncodeToString(in.ID)].ID == nil {
log.Panic("ERROR: Previous transaction is not corret")
}
}
// Signature와 PubKey field가 비어있는 트랜잭션을 복사해 만든다.
txCopy := tx.TrimmedCopy()
// P-256(secp256r1) elliptic curve를 사용. (비트코인에서는 secp256k1을 사용한다.)
curve := elliptic.P256()
// Public key로 각 Input에 포함되어있는 Signature가 유효한지 판별.
for inId, in := range tx.Inputs {
// Sign 할때와 같은 Hash값을 얻기 위해서 똑같은 과정을 거친다.
prevTx := prevTXs[hex.EncodeToString(in.ID)]
txCopy.Inputs[inId].Signature = nil
txCopy.Inputs[inId].PubKey = prevTx.Outputs[in.Out].PubKeyHash
// signature는 S값과 R값으로 나뉘어진다.
r := big.Int{}
s := big.Int{}
sigLen := len(in.Signature)
r.SetBytes(in.Signature[:(sigLen / 2)])
s.SetBytes(in.Signature[(sigLen / 2):])
x := big.Int{}
y := big.Int{}
KeyLen := len(in.PubKey)
x.SetBytes(in.PubKey[:(KeyLen / 2)])
y.SetBytes(in.PubKey[(KeyLen / 2):])
dataToVerify := fmt.Sprintf("%x\n", txCopy)
rawPubKey := ecdsa.PublicKey{Curve: curve, X: &x, Y: &y}
// Public Key와 트랜잭션의 해시값, Signature(R,S)를 가지고 유효성을 판별한다.
if ecdsa.Verify(&rawPubKey, []byte(dataToVerify), &r, &s) == false {
return false
}
txCopy.Inputs[inId].PubKey = nil
}
// 모든 인풋(UTXO-IN)에 대해 검사를 통과하였으면 검증 성공
return true
}
// TxInput의 pubkey와 signature를 nil로 초기화하며 복사
func (tx *Transaction) TrimmedCopy() Transaction {
var inputs []TxInput
var outputs []TxOutput
for _, in := range tx.Inputs {
inputs = append(inputs, TxInput{in.ID, in.Out, nil, nil})
}
for _, out := range tx.Outputs {
outputs = append(outputs, TxOutput{out.Value, out.PubKeyHash})
}
txCopy := Transaction{tx.ID, inputs, outputs}
return txCopy
}
// Transaction information을 출력할 때 사용하는 함수.
func (tx Transaction) String() string {
var lines []string
lines = append(lines, fmt.Sprintf("+- Transaction %x", tx.ID))
lines = append(lines, fmt.Sprintf("|"))
for i, input := range tx.Inputs {
lines = append(lines, fmt.Sprintf("+--- Input %d:", i))
lines = append(lines, fmt.Sprintf("| +-- TXID: %x", input.ID))
lines = append(lines, fmt.Sprintf("| +-- Out: %d", input.Out))
lines = append(lines, fmt.Sprintf("| +-- Signature: %x", input.Signature))
lines = append(lines, fmt.Sprintf("| +-- PubKey: %x", input.PubKey))
}
for i, output := range tx.Outputs {
if i == len(tx.Outputs)-1 {
lines = append(lines, fmt.Sprintf("|"))
lines = append(lines, fmt.Sprintf("+--- Output %d:", i))
lines = append(lines, fmt.Sprintf(" +- Value: %d", output.Value))
lines = append(lines, fmt.Sprintf(" +- Script: %x", output.PubKeyHash))
break
}
lines = append(lines, fmt.Sprintf("|"))
lines = append(lines, fmt.Sprintf("+--- Output %d:", i))
lines = append(lines, fmt.Sprintf("| +- Value: %d", output.Value))
lines = append(lines, fmt.Sprintf("| +- Script: %x", output.PubKeyHash))
}
return strings.Join(lines, "\n")
}// Block을 순회하면서 Transaction ID를 가진 Transaction을 검색합니다.
func (chain *BlockChain) FindTransaction(ID []byte) (Transaction, error) {
iter := chain.Iterator()
for {
block := iter.Next()
for _, tx := range block.Transactions {
if bytes.Compare(tx.ID, ID) == 0 {
return *tx, nil
}
}
if len(block.PrevHash) == 0 {
break
}
}
return Transaction{}, errors.New("Transaction does not exist")
}
// 트랜잭션을 Private Key를 이용해 Sign합니다.
func (chain *BlockChain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
prevTXs := make(map[string]Transaction)
// 트랜잭션의 인풋에 대하여 for loop
for _, in := range tx.Inputs {
// input에 적힌 정보로 해당 UTXO의 이전 거래를 검색한다.
prevTX, err := chain.FindTransaction(in.ID)
Handle(err)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
// 이전 거래 기록과 Private Key를 이용해 서명합니다.
tx.Sign(privKey, prevTXs)
}
// 트랜잭션을 검증합니다.
func (chain *BlockChain) VerifyTransaction(tx *Transaction) bool {
prevTXs := make(map[string]Transaction)
for _, in := range tx.Inputs {
prevTX, err := chain.FindTransaction(in.ID)
Handle(err)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
}
// 이전 거래 기록을 이용해서 검증합니다.
return tx.Verify(prevTXs)
}// Chain을 순회하며 블록을 출력합니다.
func (cli *CommandLine) printChain() {
chain := blockchain.ContinueBlockChain("") // blockchain을 DB로 부터 받아온다.
defer chain.Database.Close()
iter := chain.Iterator()
for {
block := iter.Next()
fmt.Printf("Previous Hash: %x\n", block.PrevHash)
fmt.Printf("Hash: %x\n", block.Hash)
pow := blockchain.NewProof(block)
fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate()))
for _, tx := range block.Transactions {
fmt.Println(tx)
if !tx.IsCoinbase() {
fmt.Printf("Transcation verification: %s\n", strconv.FormatBool(chain.VerifyTransaction(tx)))
}
}
fmt.Println()
// if Genesis
if len(block.PrevHash) == 0 {
break
}
}
}
func (cli *CommandLine) getBalance(address string) {
if !wallet.ValidateAddress(address) {
log.Panic("Address is not Valid")
}
chain := blockchain.ContinueBlockChain("") // blockchain을 DB로 부터 받아온다.
defer chain.Database.Close()
balance := 0
// Human readable Address를 PubKeyHash로 다시 변환.
pubKeyHash := wallet.Base58Decode([]byte(address))
pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
UTXOs := chain.FindUTXO(pubKeyHash)
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of %s: %d\n", address, balance)
}rm tmp/wallet.data
rm tmp/blocks/*go run main.go createblockchain -address <A1>go run main.go printchaingo run main.go send -from <A1> -to <A2> -amount 30
go run main.go printchain