前言 本文為「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 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} txOut := TxOutput{100 , to} 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" 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"
結果顯示如下。
執行 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"
結果顯示如下。
執行 get-balance
指令,取得 Memo Chou
用戶的餘額。
1 go run main.go get-balance -address "Memo Chou"
結果顯示如下。
程式碼
參考資料