使用 Go 實作區塊鏈(四):建立交易機制

前言

本文為「Building a Blockchain in Golang」教學影片的學習筆記。

實作

blockchain 資料夾裡建立一個 transaction.go 檔。

1
touch blockchain/transaction.go

建立一個 Transaction 結構體,代表一個交易紀錄。建立一個 TxInput 結構體,代表一個轉入紀錄。建立一個 TxOutput 結構體,代表一個對應轉入紀錄的轉出紀錄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Transaction struct {
ID []byte
Inputs []TxInput
Outputs []TxOutput
}

type TxInput struct {
ID []byte
Out int // 對應的 TxOutput 紀錄
Sig string // 簽名
}

type TxOutput struct {
Value int // 金額
PubKey string // 公鑰
}

建立一個 CoinbaseTx 方法,用來建立系統的第一筆交易,並且會生成初始代幣。

1
2
3
4
5
6
7
8
9
10
func CoinbaseTx(to, data string) *Transaction {
if data == "" {
data = fmt.Sprintf("Coins to %s", to)
}
txIn := TxInput{[]byte{}, -1, data} // 初始輸入不會對應到任何 TxOutput 紀錄
txOut := TxOutput{100, to} // 初始代幣設為 100 顆
tx := &Transaction{nil, []TxInput{txIn}, []TxOutput{txOut}}
tx.SetID()
return tx
}

Transaction 結構體建立一個 SetID 方法,用來產生一個唯一的交易 ID。

1
2
3
4
5
6
7
8
9
10
func (tx *Transaction) SetID() {
var encoded bytes.Buffer
var hash [32]byte
encoder := gob.NewEncoder(&encoded)
if err := encoder.Encode(tx); err != nil {
log.Fatalln(err)
}
hash = sha256.Sum256(encoded.Bytes())
tx.ID = hash[:]
}

Transaction 結構體建立一個 IsCoinbase 方法,判斷交易紀錄是否為系統所生成的代幣。

1
2
3
func (tx *Transaction) IsCoinbase() bool {
return len(tx.Inputs) == 1 && len(tx.Inputs[0].ID) == 0 && tx.Inputs[0].Out == -1
}

TxInput 結構體建立一個 CanUnlock 方法,判斷用戶是否擁有存取交易中轉入紀錄的權限。

1
2
3
func (in *TxInput) CanUnlock(data string) bool {
return in.Sig == data
}

TxOutput 結構體建立一個 CanBeUnlocked 方法,判斷用戶是否擁有存取交易中轉出紀錄的權限。

1
2
3
func (out *TxOutput) CanBeUnlocked(data string) bool {
return out.PubKey == data
}

重構 block.go 檔中的 Block 結構體。

1
2
3
4
5
6
type Block struct {
Hash []byte
Transactions []*Transaction
PrevHash []byte
Nonce int
}

重構 block.go 檔中的 CreateBlock 方法。

1
2
3
4
5
6
7
8
func CreateBlock(txs []*Transaction, prevHash []byte) *Block {
block := &Block{[]byte{}, txs, prevHash, 0}
pow := NewProof(block)
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}

重構 block.go 檔中的 Genesis 方法。

1
2
3
func Genesis(coinbase *Transaction) *Block {
return CreateBlock([]*Transaction{coinbase}, []byte{})
}

Block 結構體建立一個 HashTransactions 方法,使用區塊中的所有交易 ID 來為區塊建立一個唯一的雜湊值。

1
2
3
4
5
6
7
8
9
func (b *Block) HashTransactions() []byte {
var txHashIDs [][]byte
var txHash [32]byte
for _, tx := range b.Transactions {
txHashIDs = append(txHashIDs, tx.ID)
}
txHash = sha256.Sum256(bytes.Join(txHashIDs, []byte{}))
return txHash[:]
}

重構 proof.go 檔中的 InitData 方法。

1
2
3
4
5
6
7
8
9
func (pow *ProofOfWork) InitData(nonce int) []byte {
return bytes.Join(
[][]byte{
pow.Block.PrevHash,
pow.Block.HashTransactions(),
ToHex(int64(nonce)),
ToHex(int64(Difficulty)),
}, []byte{})
}

新增一些常數。

1
2
3
4
5
const (
dbPath = "./tmp/blocks"
dbFile = "./tmp/blocks/MANIFEST" // 用來確認 DB 是否已經被建立
genesisData = "First Transaction from Genesis" // 初始代幣交易的資料
)

blockchain.go 檔中建立一個 DatabaseExists 方法,用來判斷資料庫是否已被建立。

1
2
3
4
5
6
func DatabaseExists() bool {
if _, err := os.Stat(dbFile); os.IsNotExist(err) {
return false
}
return true
}

重構 InitBlockChain 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func InitBlockChain(address string) *BlockChain {
var lastHash []byte
if DatabaseExists() {
fmt.Println("Blockchain already exists")
runtime.Goexit()
}
opts := badger.DefaultOptions(dbPath)
opts.Logger = nil
db, err := badger.Open(opts)
if err != nil {
log.Fatalln(err)
}
err = db.Update(func(txn *badger.Txn) error {
cbTx := CoinbaseTx(address, genesisData)
genesis := Genesis(cbTx)
fmt.Println("Genesis proved")
if err = txn.Set(genesis.Hash, genesis.Serialize()); err != nil {
log.Fatalln(err)
}
err = txn.Set([]byte("lh"), genesis.Hash)
lastHash = genesis.Hash
return err
})
if err != nil {
log.Fatalln(err)
}
return &BlockChain{lastHash, db}
}

blockchain.go 檔中建立一個 ContinueBlockChain 方法,用來取得當前的區塊鏈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func ContinueBlockChain(address string) *BlockChain {
if !DatabaseExists() {
fmt.Println("No existing blockchain found, create one!")
}
var lastHash []byte
opts := badger.DefaultOptions(dbPath)
opts.Logger = nil
db, err := badger.Open(opts)
err = db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte("lh"))
if err != nil {
log.Fatalln(err)
}
lastHash, err = item.ValueCopy(nil)
return err
})
if err != nil {
log.Fatalln(err)
}
return &BlockChain{lastHash, db}
}

BlockChain 結構體建立一個 FindUnspentTransactions 方法,找出未花費的交易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func (chain *BlockChain) FindUnspentTransactions(address string) []Transaction {
var unspentTxs []Transaction
spentTxOutputs := make(map[string][]int)
iter := chain.Iterator()
for {
block := iter.Next()
for _, tx := range block.Transactions {
txID := hex.EncodeToString(tx.ID)
Outputs:
for outIdx, out := range tx.Outputs {
if spentTxOutputs[txID] != nil {
for _, spentOut := range spentTxOutputs[txID] {
if spentOut == outIdx {
continue Outputs
}
}
}
if out.CanBeUnlocked(address) {
unspentTxs = append(unspentTxs, *tx)
}
}
if !tx.IsCoinbase() {
for _, in := range tx.Inputs {
if in.CanUnlock(address) {
inTxID := hex.EncodeToString(in.ID)
spentTxOutputs[inTxID] = append(spentTxOutputs[inTxID], in.Out)
}
}
}
}
if len(block.PrevHash) == 0 {
break
}
}
return unspentTxs
}

BlockChain 結構體建立一個 FindUnspentTxOutputs 方法,找出用戶未花費的輸出紀錄。

1
2
3
4
5
6
7
8
9
10
11
12
func (chain *BlockChain) FindUnspentTxOutputs(address string) []TxOutput {
var unspentTxOutputs []TxOutput
unspentTransactions := chain.FindUnspentTransactions(address)
for _, tx := range unspentTransactions {
for _, out := range tx.Outputs {
if out.CanBeUnlocked(address) {
unspentTxOutputs = append(unspentTxOutputs, out)
}
}
}
return unspentTxOutputs
}

BlockChain 結構體建立一個 FindSpendableOutputs 方法,找出用戶可花費的交易餘額。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (chain *BlockChain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
unspentOutputs := make(map[string][]int)
unspentTxs := chain.FindUnspentTransactions(address)
accumulated := 0
Work:
for _, tx := range unspentTxs {
txID := hex.EncodeToString(tx.ID)
for outIdx, out := range tx.Outputs {
if out.CanBeUnlocked(address) && accumulated < amount {
accumulated += out.Value
unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
if accumulated >= amount {
break Work
}
}
}
}
return accumulated, unspentOutputs
}

重構 BlockChain 結構體的 AddBlock 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (chain *BlockChain) AddBlock(transactions []*Transaction) {
var lastHash []byte
err := chain.Database.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte("lh"))
if err != nil {
log.Fatalln(err)
}
lastHash, err = item.ValueCopy(nil)
return err
})
if err != nil {
log.Fatalln(err)
}
newBlock := CreateBlock(transactions, lastHash)
err = chain.Database.Update(func(txn *badger.Txn) error {
if err := txn.Set(newBlock.Hash, newBlock.Serialize()); err != nil {
log.Fatalln(err)
}
err = txn.Set([]byte("lh"), newBlock.Hash)
chain.LastHash = newBlock.Hash
return err
})
if err != nil {
log.Fatalln(err)
}
}

transaction.go 檔建立一個 NewTransaction 方法,用來為用戶建立一筆新的交易紀錄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func NewTransaction(from, to string, amount int, chain *BlockChain) *Transaction {
var inputs []TxInput
var outputs []TxOutput
acc, validOutputs := chain.FindSpendableOutputs(from, amount)
if acc < amount {
log.Fatalln("Not enough funds")
}
for txIdx, outs := range validOutputs {
txID, err := hex.DecodeString(txIdx)
if err != nil {
log.Fatalln(err)
}
for _, out := range outs {
input := TxInput{txID, out, from}
inputs = append(inputs, input)
}
}
// 將金額轉給對方的輸出紀錄
outputs = append(outputs, TxOutput{amount, to})
if acc > amount {
// 將餘額轉給自己的輸出紀錄
outputs = append(outputs, TxOutput{acc - amount, from})
}
tx := &Transaction{nil, inputs, outputs}
tx.SetID()
return tx
}

重構有關 CommandLine 結構體的相關方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
type CommandLine struct{}

func (cli *CommandLine) printUsage() {
fmt.Println("Usage:")
fmt.Println(" get-balance -address ADDRESS - gets the balance for the address")
fmt.Println(" create-blockchain -address ADDRESS - creates a blockchain and sends genesis reward to address")
fmt.Println(" print-chain - prints the blocks in the chain")
fmt.Println(" send -from FROM -to TO -amount AMOUNT - sends amount of coins")
}

func (cli *CommandLine) validateArgs() {
if len(os.Args) < 2 {
cli.printUsage()
runtime.Goexit()
}
}

func (cli *CommandLine) printChain() {
chain := blockchain.ContinueBlockChain("")
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()))
fmt.Println()
if len(block.PrevHash) == 0 {
break
}
}
}

func (cli *CommandLine) createBlockChain(address string) {
chain := blockchain.InitBlockChain(address)
chain.Database.Close()
fmt.Println("Finished!")
}

func (cli *CommandLine) getBalance(address string) {
chain := blockchain.ContinueBlockChain(address)
defer chain.Database.Close()
balance := 0
unspentTxOutputs := chain.FindUnspentTxOutputs(address)
for _, out := range unspentTxOutputs {
balance += out.Value
}
fmt.Printf("Balance of %s: %d\n", address, balance)
}

func (cli *CommandLine) send(from, to string, amount int) {
chain := blockchain.ContinueBlockChain(from)
defer chain.Database.Close()
tx := blockchain.NewTransaction(from, to, amount, chain)
chain.AddBlock([]*blockchain.Transaction{tx})
fmt.Println("Success!")
}

func (cli *CommandLine) run() {
cli.validateArgs()
getBalanceCmd := flag.NewFlagSet("get-balance", flag.ExitOnError)
createBlockchainCmd := flag.NewFlagSet("create-blockchain", flag.ExitOnError)
sendCmd := flag.NewFlagSet("send", flag.ExitOnError)
printChainCmd := flag.NewFlagSet("print-chain", flag.ExitOnError)
getBalanceAddress := getBalanceCmd.String("address", "", "The address to get balance for")
createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send genesis block reward to")
sendFrom := sendCmd.String("from", "", "Source wallet address")
sendTo := sendCmd.String("to", "", "Destination wallet address")
sendAmount := sendCmd.Int("amount", 0, "Amount to send")
switch os.Args[1] {
case "get-balance":
err := getBalanceCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "create-blockchain":
err := createBlockchainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "print-chain":
err := printChainCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
case "send":
err := sendCmd.Parse(os.Args[2:])
if err != nil {
log.Panic(err)
}
default:
cli.printUsage()
runtime.Goexit()
}
if getBalanceCmd.Parsed() {
if *getBalanceAddress == "" {
getBalanceCmd.Usage()
runtime.Goexit()
}
cli.getBalance(*getBalanceAddress)
}
if createBlockchainCmd.Parsed() {
if *createBlockchainAddress == "" {
createBlockchainCmd.Usage()
runtime.Goexit()
}
cli.createBlockChain(*createBlockchainAddress)
}
if printChainCmd.Parsed() {
cli.printChain()
}
if sendCmd.Parsed() {
if *sendFrom == "" || *sendTo == "" || *sendAmount <= 0 {
sendCmd.Usage()
runtime.Goexit()
}
cli.send(*sendFrom, *sendTo, *sendAmount)
}
}

修改 main.go 檔,處理命令列介面的執行。

1
2
3
4
5
func main() {
defer os.Exit(0)
cli := CommandLine{}
cli.run()
}

執行 create-blockchain 指令,創建一個新的區塊鏈。

1
go run main.go create-blockchain -address "Memo Chou"

結果顯示如下。

1
2
3
00065a1879cf312d59dd2e8dd5e82d6db266b5a3beb0ed43a98067f8db6a6688
Genesis proved
Finished!

執行 print-chain 指令,將區塊鏈印出。

1
go run main.go print-chain

結果顯示如下。

1
2
3
Previous Hash: 
Hash: 00065a1879cf312d59dd2e8dd5e82d6db266b5a3beb0ed43a98067f8db6a6688
Pow: true

執行 get-balance 指令,取得 Memo Chou 用戶的餘額。

1
go run main.go get-balance -address "Memo Chou"

結果顯示如下。

1
Balance of Memo: 100

執行 send 指令,將 Memo Chou 用戶的 60 個代幣轉給 Tensor 用戶。

1
go run main.go send -from "Memo Chou" -to "Tensor" -amount 60

結果顯示如下。

1
2
000717be0cf982076997a54cbd6988055c097065e03e5b0dc32ad03d72257fec
Success!

執行 print-chain 指令,將區塊鏈印出。

1
go run main.go print-chain

結果顯示如下,多了一個新的區塊。

1
2
3
4
5
6
7
Previous Hash: 00065a1879cf312d59dd2e8dd5e82d6db266b5a3beb0ed43a98067f8db6a6688
Hash: 000717be0cf982076997a54cbd6988055c097065e03e5b0dc32ad03d72257fec
Pow: true

Previous Hash:
Hash: 00065a1879cf312d59dd2e8dd5e82d6db266b5a3beb0ed43a98067f8db6a6688
Pow: true

執行 get-balance 指令,取得 Tensor 用戶的餘額。

1
go run main.go get-balance -address "Tensor"

結果顯示如下。

1
Balance of Tensor: 60

執行 get-balance 指令,取得 Memo Chou 用戶的餘額。

1
go run main.go get-balance -address "Memo Chou"

結果顯示如下。

1
Balance of Tensor: 40

程式碼

參考資料