一、意義:
ps:基於bolt的v3版本已上傳github,點擊鏈接下載源碼:
https://github.com/lsy-zhaoshuaiji/boltBlockChain.git
V4、V5版皆已上傳,點擊鏈接下載源碼(請注意分支):
https://github.com/lsy-zhaoshuaiji/pow.git
區塊鏈是什麼?(個人理解非百度..)
區塊鏈,就是一個分佈式的超級賬本。是比特幣的底層技術。在區塊鏈中每一個節點都是數據校驗者和守護者,數據的真實性,由所有節點共同守護,一旦出現了某個節點作弊的行爲,就會被踢出節點。而新數據驗證存儲的過程就叫做工作量證明(pow),也就是挖礦。說到分佈式,我們會想起以前比較火的p2p平臺,p2p也是分佈式,但缺少校驗機制,所以是缺乏第三方監管和擔保的點對點交易平臺,所以容易發生跑路現象。依賴中心數據庫來處理信息的平臺往往在安全性方面存在弊端。區塊鏈上的p2p因爲全網無特殊節點,每個節點都可以提供全網所需的全部服務,任何一個節點垮掉,都不會對整個網絡的穩定性構成威脅,所以是非常安全的。
區塊的構成?
區塊的構成分爲區塊頭和區塊體,區塊頭中記錄了版本號、上一個區塊的Hash地址、merkle根、區塊創建時間戳、區塊的工作量難度目標以及用於計算目標的參數值。區塊體中記錄了該區塊存儲的交易數量以及交易數據。
區塊頭:
字段 | 大小 | 描述 |
version | 4字節 | 版本號,⽤於跟蹤軟件/協議的更新 |
prevBlockHash | 32字節 | 上一個區塊的Hash地址 |
merkleRoot | 32字節 | 該區塊中交易的merkl e樹根的哈希值 |
time | 4字節 | 該區塊的創建時間戳 |
difficultyTarget | 4字節 | 該區塊鏈工作量證明難度目標(稍後講解工作量證明) |
nonce | 4字節 | 用於證明工作量的計算參數(挖礦要求出的值) |
區塊體:
字段 | 大小 | 描述 |
---|---|---|
numTransactionsBytes | 1字節 | 交易數量佔用的字節數 |
numTransactions | 0-8個字節 | 區塊內存儲的交易數量 |
transactions | 不確定 | 區塊內存的多個交易數據 |
二、最簡單的區塊鏈的創建:
通過上述描述,我們知道了區塊鏈是由區塊頭加區塊體組成,區塊體存放了各種其他數據,我們暫不做考慮。我們先從創建一個區塊頭開始學習。
1.首先我們定義一個結構體並起名爲block,裏面存放區塊頭字段,爲了簡單起見,字段中只包含前哈希,當前哈希和某些字符串數據(可以理解爲區塊鏈中的轉賬數據)。
2.我們創建一個newblock方法,此方法的作用是爲block結構體賦值,並返回區塊地址。
3.創建一個setHash方法,模擬挖礦生成當前哈希。
4.定義區塊鏈結構體,blockChain,存放區塊地址。
5.創建一個genesisBlock方法,此方法的目的是創建一個創世塊(第一個區塊),
5.創建一個newBlockChain方法,此方法的目的是將創世塊地址放入結構體變量blockChain.blockchain中,初始化區塊鏈。
6.創建一個AddBlockChain方法,此方法的目的是將新增區塊的地址,存放在blockChain.blockchain中,將新區塊連接到區塊鏈中。
ps:在區塊鏈中區塊生成當前哈希的過程就是pow(工作量證明/挖礦),爲了簡單起見,我們將pow放到後面詳細講,此部分我們利用前哈希和data拼接後的數據做sha256得到當前哈希。
程序分爲三個文件,分別是負責區塊的block.go文件,負責區塊鏈的blockchain模塊,負責程序運行的main.go文件
main.go:
package main
import (
"fmt"
)
/*
區塊的創建分爲7步
1.定義結構體
2.創建區塊
3.生成哈希(pow/工作量證明/)
4.定義區塊鏈結構體
5.生成區塊鏈並添加創世
6.生成創世塊塊
7.添加其他區塊
*/
func main(){
BC:=NewBlockChain()
BC.AddBlockChain("A向laughing轉賬580個BTC")
for i,block:=range BC.blockChain{
fmt.Printf("當前區塊高度:%d\n",i)
fmt.Printf("當前哈希:%x\n",block.Hash)
fmt.Printf("上一級哈希:%x\n",block.prevHash)
fmt.Printf("交易信息:%s\n",block.data)
}
}
block .go
package main
import "crypto/sha256"
//1.定義區塊結構體
type Block struct {
prevHash []byte
Hash []byte
data []byte
}
//2.創建區塊
func NewBlock(data string,prevHash []byte) *Block{
block:=Block{
prevHash:prevHash,
Hash:[]byte{},
data:[]byte(data),
}
block.SetHash()
return &block
}
//3.生成哈希
func (this *Block)SetHash (){
blockInfo:=append(this.prevHash,this.data...)
//func Sum256(data []byte) [Size]byte {
Hash:=sha256.Sum256(blockInfo)
this.Hash=Hash[:]
}
blockChain.go
package main
//4.定義區塊鏈結構體
type BlockChain struct {
blockChain []*Block
}
//5.生成創世塊
func GenesisBlock()*Block{
return NewBlock("laughing 轉賬給 fance 1BTC",[]byte{})
}
//6.生成區塊鏈
func NewBlockChain()*BlockChain{
genesisBlock:=GenesisBlock()
return &BlockChain{
blockChain:[]*Block{genesisBlock},
}
}
//添加其他區塊
func (this *BlockChain)AddBlockChain(data string){
//1.找到上一個區塊,並獲得其哈希
lastBlock:=this.blockChain[len(this.blockChain)-1]
prevHash:=lastBlock.Hash
//2.生成新的區塊
block:=NewBlock(data,prevHash)
//3.將新的區塊添加到區塊鏈中
this.blockChain=append(this.blockChain,block)
}
三、完整字段的區塊鏈的創建:
以上代碼,區塊頭中字段過於簡單。而在真實的區塊頭中,是存在很多字段的。所以現在我們將區塊頭完整字段加入到代碼中。修改如下:
1.修改了區塊頭block結構體,新增了version等區塊字段。
2.修改了哈希生成函數,利用bytes.join()方法,將區塊頭字段拼接起來。
ps:鏈上(blockchain)的代碼是沒有進行修改的。完整字段包含版本號、前哈希,markleRoot、時間戳、難度、目標值、當前哈希和某些字符串數據。 代碼如下:
package main
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"time"
)
//1.定義區塊結構體
type Block struct {
//這裏進行了修改,新增了變量
version uint64
prevHash []byte
markleRoot []byte
timeStamp uint64
diffculty uint64
nonce uint64
Hash []byte
data []byte
}
//2.創建區塊
func NewBlock(data string,prevHash []byte) *Block{
block:=Block{
version:00,
prevHash:prevHash,
markleRoot:[]byte{},
timeStamp:uint64(time.Now().Unix()),
diffculty:0,
nonce:0,
Hash:[]byte{},
data:[]byte(data),
}
block.SetHash()
return &block
}
func IntToByte(n uint64)[]byte{
x:=int64(n)
bytesBuffer:=bytes.NewBuffer([]byte{})
err:=binary.Write(bytesBuffer,binary.BigEndian,x)
if err!=nil{
fmt.Println(err)
return []byte{}
}
return bytesBuffer.Bytes()
}
//3.生成哈希
func (this *Block)SetHash (){
byteList:=[][]byte{
IntToByte(this.version),
this.prevHash,
this.markleRoot,
IntToByte(this.timeStamp),
IntToByte(this.diffculty),
IntToByte(this.nonce),
this.Hash,
this.data,
}
//利用bytes.join()方法將[]byte拼接起來,不會的同學請自行百度
blockInfo:=bytes.Join(byteList,[]byte{})
Hash:=sha256.Sum256(blockInfo)
this.Hash=Hash[:]
}
四、實現pow(工作量證明/挖礦):
通過上述的描述,相信大家也意識到了pow其實就是生成當前哈希的過程。我們將pow有關的信息存放到proofofwork.go文件中。文件中定義了pow的結構體,有一個創建pow的方法,和一個挖礦(工作量證明)的run方法。現在我們詳細分析pow的整個過程。如下:
1.創建pow結構體,並在結構體中定義變量--“挖礦難度值”(diffculty),由於挖礦競爭很大,int存儲可能不夠,所以我們使用big.int型作爲diffculty的基礎類型。
2.創建run方法,實現當前區塊的數據拼接(包括版本號、前哈希、難度值、一個隨機數(nonce)等....)
3.將拼接後的數據做哈希sha256,得到一個哈希值
4.將此哈希值轉化爲big.int類型
5.將轉換後的數據與diffculty進行比較,
5.1 若小於diffculty則,代表挖礦成功,繼而將此區塊添加進區塊鏈中,並返回隨機數nonce。
5.2若大於diffculty則表達,挖礦失敗,nonce++,繼續循環,直到滿足5.1條件。如圖:
實現代碼如下:
1.首先,我們創建一個proofofwork.go(pow.go)文件:
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"math/big"
)
type ProofOfWork struct {
block *Block
target *big.Int
}
func NewProofOfWork(block *Block)*ProofOfWork{
pow:=ProofOfWork{
block:block,
}
//挖礦難度值
diffculty:="0000100000000000000000000000000000000000000000000000000000000000"
tmp:=big.Int{}
tmp.SetString(diffculty,16)
pow.target=&tmp
pow.block.diffculty=pow.target.Uint64()
return &pow
}
func (this *ProofOfWork)run()([]byte,uint64){
var nonce uint64
var hash [32]byte
for {
//1.拼接區塊數據
byteList:=[][]byte{
IntToByte(this.block.version),
this.block.prevHash,
this.block.markleRoot,
IntToByte(this.block.timeStamp),
IntToByte(this.block.diffculty),
IntToByte(nonce),
this.block.data,
}
blockinfo:=bytes.Join(byteList,[]byte{})
//2.將拼接後的數據進行哈希256運算
hash=sha256.Sum256(blockinfo)
//3.將哈希數據轉爲big.int類型
tmp:=big.Int{}
tmp.SetBytes(hash[:])
//4.比較
status:=tmp.Cmp(this.target)
if status==-1{
fmt.Printf("挖礦成功,難度爲:%d, 當前哈希爲:%x, nonce爲:%d\n",this.block.diffculty,hash,nonce)
break;
}else {
//fmt.Println(nonce)
nonce++
}
}
return hash[:],nonce
}
2.修改block.go文件,將原生成哈希的方法修改爲pow的方式:
package main
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"time"
)
//1.定義區塊結構體
type Block struct {
version uint64
prevHash []byte
markleRoot []byte
timeStamp uint64
diffculty uint64
nonce uint64
Hash []byte
data []byte
}
//2.創建區塊
func NewBlock(data string,prevHash []byte) *Block{
block:=Block{
version:00,
prevHash:prevHash,
markleRoot:[]byte{},
timeStamp:uint64(time.Now().Unix()),
diffculty:0,
nonce:0,
Hash:[]byte{},
data:[]byte(data),
}
//這裏註釋了原生成哈希的方法,並通過NewProofOfWork方法創建了pow結構體,最後再調用pow的run方法,開啓挖礦,生成hash
//block.SetHash()
pow:=NewProofOfWork(&block)
hash,nonce:=pow.run()
block.Hash=hash
block.nonce=nonce
return &block
}
func IntToByte(n uint64)[]byte{
x:=int64(n)
bytesBuffer:=bytes.NewBuffer([]byte{})
err:=binary.Write(bytesBuffer,binary.BigEndian,x)
if err!=nil{
fmt.Println(err)
return []byte{}
}
return bytesBuffer.Bytes()
}
//3.生成哈希
func (this *Block)SetHash (){
byteList:=[][]byte{
IntToByte(this.version),
this.prevHash,
this.markleRoot,
IntToByte(this.timeStamp),
IntToByte(this.diffculty),
IntToByte(this.nonce),
this.Hash,
this.data,
}
//利用bytes.join()方法將[]byte拼接起來,不會的同學請自行百度
blockInfo:=bytes.Join(byteList,[]byte{})
Hash:=sha256.Sum256(blockInfo)
this.Hash=Hash[:]
}
blockchain和main文件爲進行修改,可以繼續使用。
五、基於Bolt數據庫實現可持續化的區塊鏈
上文中,我們已經可以實現一個最基本的區塊鏈結構了。但是我們在運行過程中發現,目前的程序數據都存在內存中,換句話來說就是,程序一關閉數據就不見了,所以我們需要引入數據庫。這時輕量級數據庫Bolt就映入眼簾了。
什麼是Bolt數據庫呢?
Bolt
是一個用Go
編寫的鍵值數據庫。其目標是爲了給程序提供一個簡單、快捷、穩定的數據庫。
安裝:
go get github.com/boltdb/bolt/...
基於BOLT的讀寫、
func main() {
//打開數據庫
// func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
db, err := bolt.Open("testDb.db", 0600, nil)//第二個參數爲權限
if err != nil {
fmt.Println(" bolt Open err :", err)
return
}
defer db.Close()
//創建bucket
//Updata參數爲一個函數類型,是一個事務
err = db.Update(func(tx *bolt.Tx) error {
//打開一個bucket
b1 := tx.Bucket([]byte("bucket1"))
//沒有這個bucket
if b1 == nil {
//創建一個bucket
b1, err = tx.CreateBucket([]byte("bucket1"))
if err != nil {
fmt.Printf("tx.CreateBucket err:", err)
return err
}
//寫入數據
b1.Put([]byte("key1"), []byte("hello"))
b1.Put([]byte("key2"), []byte("world"))
//讀取數據
v1 := b1.Get([]byte("key1"))
v2 := b1.Get([]byte("key2"))
v3 := b1.Get([]byte("key3"))
fmt.Printf("v1:%s\n", string(v1))
fmt.Printf("v2:%s\n", string(v2))
fmt.Printf("v3:%s\n", string(v3))
}
return nil
})
if err != nil {
fmt.Printf("db.Update err:", err)
}
return
}
只讀:
func main() {
//打開數據庫
// func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
db, err := bolt.Open("testDb.db", 0400, nil)//第二個參數爲權限
if err != nil {
fmt.Println(" bolt Open err :", err)
return
}
defer db.Close()
//創建bucket
//View參數爲一個函數類型,是一個事務
err = db.View(func(tx *bolt.Tx) error {
//打開一個bucket
b1 := tx.Bucket([]byte("bucket1"))
//沒有這個bucket
if b1 == nil {
return errors.New("bucket do not exist!")
}
v1 := b1.Get([]byte("key1"))
v2 := b1.Get([]byte("key2"))
v3 := b1.Get([]byte("key3"))
fmt.Printf("v1:%s\n", string(v1))
fmt.Printf("v2:%s\n", string(v2))
fmt.Printf("v3:%s\n", string(v3))
return nil
})
if err != nil {
fmt.Printf("db.View err:", err)
}
return
}
特別注意--血坑,博主在因爲此原因排查了一下午(~~):
切記結構體中的變量要大寫:!!!
結構體中參數首字母沒有大寫時,別的包雖然可以調用這個結構體,但是找不到這個結構體中沒有首字母大寫的參數。
附上block.go中修改結構體變量後的代碼:
package main
import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"log"
"time"
)
//1.定義區塊結構體
type Block struct {
//1.版本號
Version uint64
//2. 前區塊哈希
PrevHash []byte
//3. Merkel根(梅克爾根,這就是一個哈希值,我們先不管,我們後面v4再介紹)
MerkelRoot []byte
//4. 時間戳
TimeStamp uint64
//5. 難度值
Difficulty uint64
//6. 隨機數,也就是挖礦要找的數據
Nonce uint64
//a. 當前區塊哈希,正常比特幣區塊中沒有當前區塊的哈希,我們爲了是方便做了簡化!
Hash []byte
//b. 數據
Data []byte
}
//2.創建區塊
func NewBlock(data string, prevBlockHash []byte) *Block {
block := Block{
Version: 00,
PrevHash: prevBlockHash,
MerkelRoot: []byte{},
TimeStamp: uint64(time.Now().Unix()),
Difficulty: 0, //隨便填寫的無效值
Nonce: 0, //同上
Hash: []byte{},
Data: []byte(data),
}
//block.SetHash()
//創建一個pow對象
pow := NewProofOfWork(&block)
//查找隨機數,不停的進行哈希運算
hash, nonce := pow.run()
//根據挖礦結果對區塊數據進行更新(補充)
block.Hash = hash
block.Nonce = nonce
return &block
}
func IntToByte(n uint64)[]byte{
x:=int64(n)
bytesBuffer:=bytes.NewBuffer([]byte{})
err:=binary.Write(bytesBuffer,binary.BigEndian,x)
if err!=nil{
fmt.Println(err)
return []byte{}
}
return bytesBuffer.Bytes()
}
//3.生成哈希,目前不再使用此方法,而是使用pow
//序列化
func (block *Block) Serialize() []byte {
var buffer bytes.Buffer
//- 使用gob進行序列化(編碼)得到字節流
//1. 定義一個編碼器
//2. 使用編碼器進行編碼
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(&block)
if err != nil {
log.Panic("編碼出錯!")
}
//fmt.Printf("編碼後的小明:%v\n", buffer.Bytes())
return buffer.Bytes()
}
//反序列化
func Deserialize(data []byte) Block {
decoder := gob.NewDecoder(bytes.NewReader(data))
var block Block
//2. 使用解碼器進行解碼
err := decoder.Decode(&block)
if err != nil {
log.Panic("解碼出錯!")
}
return block
}
基於Bolt數據庫實現可持續化的區塊鏈
1.重構blockChain結構體,結構體變量爲:1. bolt數據庫對象db,2.存放最後一塊區塊的哈希tail。
type BlockChain struct {
db *bolt.DB
tail []byte
}
2.在block.go中實現能將結構體block系列化爲字節的Serialize方法,和將字節反序列化爲結構體的Deserialize方法。
//序列化
func (block *Block) Serialize() []byte {
var buffer bytes.Buffer
//- 使用gob進行序列化(編碼)得到字節流
//1. 定義一個編碼器
//2. 使用編碼器進行編碼
encoder := gob.NewEncoder(&buffer)
err := encoder.Encode(&block)
if err != nil {
log.Panic("編碼出錯!")
}
//fmt.Printf("編碼後的小明:%v\n", buffer.Bytes())
return buffer.Bytes()
}
//反序列化
func Deserialize(data []byte) Block {
decoder := gob.NewDecoder(bytes.NewReader(data))
var block Block
//2. 使用解碼器進行解碼
err := decoder.Decode(&block)
if err != nil {
log.Panic("解碼出錯!")
}
return block
}
2.重構newBlockChain方法,將創世塊的哈希和序列化後的結構體,存放到boltDB中。並返回db對象,和最後一塊區塊哈希。
const blockChainDb = "blockChain.db"
const blockBucket = "blockBucket"
//5. 定義一個區塊鏈
func NewBlockChain() *BlockChain {
//return &BlockChain{
// blocks: []*Block{genesisBlock},
//}
//最後一個區塊的哈希, 從數據庫中讀出來的
var lastHash []byte
//1. 打開數據庫
db, err := bolt.Open(blockChainDb, 0600, nil)
//defer db.Close()
if err != nil {
log.Panic("打開數據庫失敗!")
}
//將要操作數據庫(改寫)
_=db.Update(func(tx *bolt.Tx) error {
//2. 找到抽屜bucket(如果沒有,就創建)
bucket := tx.Bucket([]byte(blockBucket))
if bucket == nil {
//沒有抽屜,我們需要創建
bucket, err = tx.CreateBucket([]byte(blockBucket))
if err != nil {
log.Panic("創建bucket(b1)失敗")
}
//創建一個創世塊,並作爲第一個區塊添加到區塊鏈中
genesisBlock := GenesisBlock()
//3. 寫數據
//hash作爲key, block的字節流作爲value,尚未實現
_=bucket.Put(genesisBlock.Hash, genesisBlock.Serialize())
_=bucket.Put([]byte("LastHashKey"), genesisBlock.Hash)
lastHash = genesisBlock.Hash
////這是爲了讀數據測試,馬上刪掉,套路!
//blockBytes := bucket.Get(genesisBlock.Hash)
//block := Deserialize(blockBytes)
//fmt.Printf("block info : %s\n", block)
} else {
lastHash = bucket.Get([]byte("LastHashKey"))
}
return nil
})
return &BlockChain{db, lastHash}
}
3.重構blockChain結構體的AddBlock方法,獲取lastHash,然後在函數中執行Newblock方法,並將該block的hash和信息序列化到boltDB中。
func (bc *BlockChain) AddBlock(data string) {
//如何獲取前區塊的哈希呢??
db := bc.db //區塊鏈數據庫
lastHash := bc.tail //最後一個區塊的哈希
_=db.Update(func(tx *bolt.Tx) error {
//完成數據添加
bucket := tx.Bucket([]byte(blockBucket))
if bucket == nil {
log.Panic("bucket 不應該爲空,請檢查!")
}
//a. 創建新的區塊
block := NewBlock(data, lastHash)
//b. 添加到區塊鏈db中
//hash作爲key, block的字節流作爲value,尚未實現
_=bucket.Put(block.Hash, block.Serialize())
_=bucket.Put([]byte("LastHashKey"), block.Hash)
//c. 更新一下內存中的區塊鏈,指的是把最後的小尾巴tail更新一下
bc.tail = block.Hash
return nil
})
}
4.定義迭代器,方便後續遍歷。
package main
import (
"github.com/boltdb/bolt"
"log"
)
type BlockChainIterator struct {
db *bolt.DB
//遊標,用於不斷索引
currentHashPointer []byte
}
//func NewIterator(bc *BlockChain) {
//
//}
func (bc *BlockChain) NewIterator() *BlockChainIterator {
return &BlockChainIterator{
bc.db,
//最初指向區塊鏈的最後一個區塊,隨着Next的調用,不斷變化
bc.tail,
}
}
//迭代器是屬於區塊鏈的
//Next方式是屬於迭代器的
//1. 返回當前的區塊
//2. 指針前移
func (it *BlockChainIterator) Next() *Block {
var block Block
_=it.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blockBucket))
if bucket == nil {
log.Panic("迭代器遍歷時bucket不應該爲空,請檢查!")
}
blockTmp := bucket.Get(it.currentHashPointer)
//解碼動作
block = Deserialize(blockTmp)
//遊標哈希左移
it.currentHashPointer = block.PrevHash
return nil
})
return &block
}
func (it *BlockChainIterator)Restore(){
//用於將迭代器遊標移回初始值位置
_=it.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blockBucket))
if bucket == nil {
log.Panic("迭代器遍歷時bucket不應該爲空,請檢查!")
}
blockTmp := bucket.Get([]byte("LastHashKey"))
//遊標哈希左移
it.currentHashPointer = blockTmp
return nil
})
}
5.新建cli.go文件用於接收外部參數,執行命令。
package main
import (
"flag"
"fmt"
"os"
)
const Usage = `
AddBlock --data "add block to blockChain" example:./block AddBlock {DATA}
PrintBlockChain "print all blockChain data"
`
const AddBlockString ="AddBlock"
const PrintBlockString = "PrintBlockChain"
type Cli struct {
Bc *BlockChain
}
func PrintUsage (){
println(Usage)
}
func (cli *Cli)CheckInputLenth(){
if len(os.Args)<2{
fmt.Println("Invalid ARGS")
PrintUsage()
os.Exit(1)
}
}
func (this *Cli)Run(){
this.CheckInputLenth()
AddBlocker:=flag.NewFlagSet(AddBlockString,flag.ExitOnError)
PrintBlockChainer:=flag.NewFlagSet(PrintBlockString,flag.ExitOnError)
AddBlockerParam:=AddBlocker.String("data","","AddBlock {data}")
switch os.Args[1] {
case AddBlockString:
//AddBlock
err:=AddBlocker.Parse(os.Args[2:])
if err!=nil{fmt.Println(err)}
if AddBlocker.Parsed(){
if *AddBlockerParam==""{PrintUsage()}else {
this.AddBlock(*AddBlockerParam)
}
}
case PrintBlockString:
//PrintBlockChain
err:=PrintBlockChainer.Parse(os.Args[2:])
if err!=nil{fmt.Println(err)}
if PrintBlockChainer.Parsed(){
this.PrintBlockChain()
}
default:
fmt.Println("Invalid input ")
PrintUsage()
}
}
6.新建command.go文件,用於實現cli的方法。
package main
import "fmt"
func (this *Cli)AddBlock(data string){
this.Bc.AddBlock(data)
}
func (this *Cli)PrintBlockChain(){
//TODO
Iterator:=this.Bc.NewIterator()
HeightblockChain:=0
for{
HeightblockChain++
block:=Iterator.Next()
if len(block.PrevHash)==0{
Iterator.Restore()
break
}
}
for{
block:=Iterator.Next()
fmt.Printf("=======當前區塊高度:%d======\n",HeightblockChain)
fmt.Printf("當前哈希:%x\n",block.Hash)
fmt.Printf("上一級哈希:%x\n",block.PrevHash)
fmt.Printf("交易信息:%s\n",block.Data)
HeightblockChain--
if len(block.PrevHash)==0{
break
}
}
}
7.修改main函數,
package main
/*
區塊的創建分爲7步
1.定義結構體
2.創建區塊
3.生成哈希
4.定義區塊鏈結構體
5.生成區塊鏈並添加創世
6.生成創世塊塊
7.添加其他區塊
*/
func main(){
bc:=NewBlockChain()
cli:=Cli{bc}
cli.Run()
}
六、基於UTXO實現區塊鏈交易(V版本)
UTXO(Unspent Transaction Outputs)是未花費的交易輸出,它是比特幣交易生成及驗證的一個核心概念。交易構成了一組鏈式結構,所有合法的比特幣交易都可以追溯到前向一個或多個交易的輸出,這些鏈條的源頭都是挖礦獎勵,末尾則是當前未花費的交易輸出。
一、新建Trancastions.go文件,並定義結構體
type Transaction struct {
TXID []byte //交易ID
TXinputs []TXinput //交易輸入數組
TXoutputs []TXoutput //交易輸出數組
}
type TXinput struct {
TXid []byte//引用的交易ID
index int64//引用的output索引值
Sig string//解鎖腳本
}
type TXoutput struct {
value float64//轉賬金額
PubKeyHash string//鎖定腳本
}
//設置交易id
二、序列化Transcation結構體,並設置TXID
func (tx *Transaction)SetHash(){
var buffer bytes.Buffer
encoder:=gob.NewEncoder(&buffer)
err:=encoder.Encode(tx)
if err!=nil {
fmt.Println(err)
}
hash:=sha256.Sum256(buffer.Bytes())
tx.TXID=hash[:]
}
三、提供創建Transcation的挖礦方法
func NewCoinBaseTX(address string,data string) *Transaction{
//1.挖礦交易只有一個Input和一個output
//2.在input時,TXid爲空,index爲-1,解鎖腳本爲:礦池地址
input:=TXinput{[]byte{},-1,address}
//3.在output中,金額爲btc常量,reward{12.5},鎖定腳本爲address
output:=TXoutput{reward,address}
//4.創建Transcation交易,並設置TXid
tx:=Transaction{[]byte{},[]TXinput{input},[]TXoutput{output}}
//通過SetHash方法創建交易ID
tx.SetHash()
return &tx
}
四、修改block.go和blockChain.go(略,報錯的文件都需要修改,可以下載源碼等比)
五、修改cli.go新建getbalance命令
const AddBlockString ="addblock"
const PrintBlockString = "print"
const GetBlanceString = "getbalance"
type Cli struct {
Bc *BlockChain
}
func PrintUsage (){
println(Usage)
}
func (cli *Cli)CheckInputLenth(){
if len(os.Args)<2{
fmt.Println("Invalid ARGS")
PrintUsage()
os.Exit(1)
}
}
func (this *Cli)Run(){
this.CheckInputLenth()
AddBlocker:=flag.NewFlagSet(AddBlockString,flag.ExitOnError)
PrintBlockChainer:=flag.NewFlagSet(PrintBlockString,flag.ExitOnError)
getBalancer:=flag.NewFlagSet(GetBlanceString,flag.ExitOnError)
AddBlockerParam:=AddBlocker.String("data","","AddBlock {data}")
getBalancerParam:=getBalancer.String("address","","打印餘額")
switch os.Args[1] {
case AddBlockString:
//AddBlock
err:=AddBlocker.Parse(os.Args[2:])
if err!=nil{fmt.Println(err)}
if AddBlocker.Parsed(){
if *AddBlockerParam==""{PrintUsage()}else {
this.AddBlock(*AddBlockerParam)
}
}
case GetBlanceString:
err:=getBalancer.Parse(os.Args[2:])
if err!=nil{
PrintUsage()
log.Panic(err)
}
if getBalancer.Parsed(){
if *getBalancerParam==""{fmt.Println(PrintUsage)}else {
this.getBalance(*getBalancerParam)
}
}
六、修改command.go
func (this *Cli)getBalance(address string){
utxos:=this.Bc.FindUTXOs(address)
total:=0.0
for _,utxo:=range utxos{
total+=utxo.value
}
fmt.Printf("/%s/的餘額爲:%f\n",address,total)
}
七、在blockChain.go中新建FindCoinbaseUTXOs方法,獲取UTXOs
func (this *BlockChain)FindUTXOs(address string)[]TXoutput{
var UTXO []TXoutput
spentOutputs := make(map[string][]int64)
it := this.NewIterator()
for {
block := it.Next()
for _, tx := range block.Transactions {
OUTPUT:
for i, output := range tx.TXoutputs {
if spentOutputs[string(tx.TXID)] != nil {
fmt.Println(spentOutputs[string(tx.TXID)])
for _, j := range spentOutputs[string(tx.TXID)] {
if int64(i) == j {
continue OUTPUT
}
}
}
if output.PubKeyHash == address {
UTXO=append(UTXO, output)
}
}
if !tx.IsCoinBaseTX(tx) {
for _, input := range tx.TXinputs {
if input.Sig == address {
spentOutputs[string(input.TXid)] = append(spentOutputs[string(input.TXid)], input.Index)
}
}
}
}
if len(block.PrevHash) == 0 {
fmt.Printf("區塊遍歷完成退出!")
break
}
}
return UTXO
//
}
八、在Trancations.go中新增判斷是否爲挖礦交易的方法,如果是則跳過input記錄前output
func (tx *Transaction)IsCoinBaseTX(txs *Transaction)bool{
//1.TXid爲空
//2.index爲-1
//3.只有一個Input
if len(txs.TXinputs) ==1{
if len(txs.TXinputs[0].TXid)==0 && txs.TXinputs[0].Index==-1{
return true
}
}
return false
}
/*個人認爲沒有這個方法,也不會影響收益查看,因爲餘額雖然會在多個交易中查詢UTXOS,但是挖礦交易始終是收益的源頭,也就是第一條數據,而當遍歷到第一條交易數據時,遍歷output的循環不會在下一次執行了,所以大家可以發現就算沒有添加此函數,餘額也不會受影響,
但添加此函數有也添加的好處,比如會提交代碼運行效率等
*/
九、創建普通交易
思路
[創建普通交易邏輯上要實現以下:(var utxos []output)
1.找到匹配的utxos,遍歷餘額,返回map[tanscation.TXid][outputIndex],賬戶餘額(resvalue)
2.判斷餘額與轉賬金額的大小,若餘額小於轉賬金額,則返回nil,若餘額大於轉賬金額則:
2.1新建TXinputs,記錄output的ID和索引
2.2.新建txoutput,進行找零,output[amount,to] output[resValuea - mount,from],且返回*Transcation
實現:
1.在Transcation中實現普通交易的NewTranscations方法
func NewTranscations(from, to string,amount float64,bc *BlockChain )*Transaction{
utxos,resValue:=bc.FindNeedUtxos(from,amount)
if resValue<amount{
fmt.Println("餘額不足,請檢查錢包額餘")
return nil
}else {
//額餘充足,進行轉賬和找零
var inputs []TXinput
var outputs []TXoutput
for id,indexList :=range utxos{
for _,index :=range indexList{
inputs=append(inputs, TXinput{[]byte(id),int64(index),from})
}
}
outputs=append(outputs,TXoutput{amount,to})
//找零
outputs=append(outputs,TXoutput{resValue-amount,from})
tx:=Transaction{
TXID:[]byte{},
TXinputs:inputs,
TXoutputs:outputs,
}
tx.SetHash()
return &tx
}
}
2.在blockChain中實現查找所需餘額的FindNeedUTXOS方法
func (bc *BlockChain)FindNeedUtxos(from string,amount float64)(map[string][]uint64,float64){
it:=bc.NewIterator()
utxos:=make(map[string][]uint64)
resValue:=float64(0.0)
spentoutput :=make(map[string][]uint64)
for {
block:=it.Next()
for _,transcation :=range block.Transactions{
outputList:=[]uint64{}
//output獲取
OUTPUT:
for index,output :=range transcation.TXoutputs{
if resValue>=amount{
return utxos,resValue
}
if spentoutput[string(transcation.TXID)]!=nil{
for _,value :=range spentoutput[string(transcation.TXID)]{
if value==uint64(index){
continue OUTPUT
}
}
}
if output.PubKeyHash==from{
outputList=append(outputList, uint64(index))
utxos[string(transcation.TXID)]=outputList
resValue+=output.Value
fmt.Printf("找到滿足的金額:%f\n",output.Value)
}
}
//input篩選
if !transcation.IsCoinBaseTX(transcation){
inputList:=[]uint64{}
for _,input :=range transcation.TXinputs{
if input.Sig==from{
inputList=append(inputList, uint64(input.Index))
spentoutput[string(input.TXid)]=inputList
}
}
}
}
if len(block.PrevHash)==0{
break
}
}
fmt.Printf("轉賬結束\n")
return utxos,resValue
}
至此,我們已經實現了UTXO的轉賬,特別提醒一點,一個交易中是不會存在兩個同地址output(其中一個已經用過,另一個沒有用過。)兩個同地output,要麼同時沒有被用過,要麼都被用過。因爲區塊鏈轉賬時實時,是全部轉完的,即使自己有剩餘,也會先拿出來,最後轉給自己。所以,我們可以通過這一點,將兩個utxo函數,高聚合化,但是爲了節約時候,這裏我就不再詳細說明。
十、補充makerkleRoot生成函數,以及優化時間戳
func (this *Block)MakeMakerkleRoot()[]byte{
//我們進行哈希拼接
final:=[]byte{}
for _,j :=range this.Transactions{
final=append(final, j.TXID...)
}
hash:=sha256.Sum256(final)
return hash[:]
}
//時間戳
fmt.Printf("時間戳:%s\n",time.Unix(int64(block.TimeStamp),0).Format("2006-1-2 15:04:05"))
七、實現錢包地址和交易加密(V5版本)
在V4版本中,我們已經模擬實現了比特幣UTXO,但是我們的錢包還是字符串,亟需修改。我們知道比特幣中使用的數字簽名算法是橢圓曲線數字籤。所以我們有必要開發一個新的版本。
一、非對稱加密簽名算法--(橢圓曲線加密/ECDSA)的實現
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"math/big"
)
func main(){
//創建曲線
curve:=elliptic.P256()
//生成私匙
privateKey,err:=ecdsa.GenerateKey(curve,rand.Reader)
if err!=nil{
fmt.Println(err)
}
//生成公鑰
publicKey:=privateKey.PublicKey
//對數據進行哈希運算
data:="666666666"
hash:=sha256.Sum256([]byte(data))
//數據簽名
//func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error) {
r,s,er:=ecdsa.Sign(rand.Reader,privateKey,hash[:])
if er!=nil{
fmt.Println(err)
}
//把r、s進行序列化傳輸
//1.傳輸
signature:=append(r.Bytes(),s.Bytes()...)
//2.獲取、定義兩個輔助的BIG.INT
r1:=big.Int{}
s1:=big.Int{}
//3.拆分並賦值
r1.SetBytes(signature[:len(signature)/2])
s1.SetBytes(signature[len(signature)/2:])
//數據校驗
//func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
status:=ecdsa.Verify(&publicKey,hash[:],&r1,&s1)
fmt.Println(status)
}
二、創建公鑰、私匙,以及實現NewWallet命令
1.新建Wallet.go
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
)
type Wallet struct {
PrivateKey *ecdsa.PrivateKey
PublicKey []byte
}
func NewWallet()*Wallet{
curve:=elliptic.P256()
privateKey,err:=ecdsa.GenerateKey(curve,rand.Reader)
if err!=nil{fmt.Println(err)}
publicKeyOrign:=privateKey.PublicKey
publicKey:=append(publicKeyOrign.X.Bytes(),publicKeyOrign.Y.Bytes()...)
return &Wallet{privateKey,publicKey}
}
2.修改cl.go,並添加命令(略)
3.地址生成
3.1下載 ripemd160源碼包
由於偉大的牆,我們需要手動下載源碼包
1.在{GOPATH}中的golang.org\x 進行gitlone
git clone https://github.com/golang/crypto.git
生成地址分爲幾個過程
1.將publicKey進行哈希運算,生成hash,並將生成的hash進行ripemd160運算,生成哈希
2.將生成的哈希與version拼接,並進行哈希運算,生成hash1
3.拷貝hash1,並再對hash1進行哈希運算生成hash2,截取hash2的前四個字節,並命名爲checkCode
4.將hash1和checkCode拼接,並進行base58編碼,得到錢包地址
流程如圖:
實現:
func (this *Wallet)NewAdress()string{
//1.獲取pubKey
pubKey:=this.PublicKey
//2.獲取ripemd160哈希
ripeHash:=Newripe160Hash(pubKey)
//3.將ripeHash與version進行拼接
version:=byte(00)
payload:=append(ripeHash,[]byte{version}...)
//4.拷貝一份payload做hash後截取前4個字節
hash1:=sha256.Sum256(payload)
CheckCode:=CheckCode(hash1[:])
//5.再次拼接
payload=append(payload,CheckCode...)
//6.做base58
address:=base58.Encode(payload)
return address
}
func CheckCode(hash1 []byte)[]byte{
//再做一次hash
hash2:=sha256.Sum256(hash1)
return hash2[:4]
}
func Newripe160Hash(pubKey []byte)[]byte{
hash:=sha256.Sum256(pubKey)
//創建編碼器
ripe:=ripemd160.New()
//寫入
_,err:=ripe.Write(hash[:])
if err!=nil {
fmt.Println(err)
}
//生成哈希
ripehash:=ripe.Sum(nil)
return ripehash
}
三、創建wallets,實現本地存儲地址
老問題,上面創建的地址,在關閉程序後就會消失,這時候我們需要把地址存在某個地方。因爲地址是加密處理過的,所以我們選擇最簡單的方式--存在本地。
一、創建Wallets結構體,保存[address]*wallet
二、創建NewWallets方法,從本地獲取反序列化的結構體,對對Wallets賦值,(在賦值之前,要先判斷本地文件是否存在)
三、創建CreateWallets方法,生成的address的生成和對Wallets結構體通過map,序列化到本地文件中。
代碼如下:
package main
import (
"btcutil/base58"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"golang.org/x/crypto/ripemd160"
)
type Wallet struct {
PrivateKey *ecdsa.PrivateKey
PublicKey []byte
}
func NewWallet()*Wallet{
curve:=elliptic.P256()
privateKey,err:=ecdsa.GenerateKey(curve,rand.Reader)
if err!=nil{fmt.Println(err)}
publicKeyOrign:=privateKey.PublicKey
publicKey:=append(publicKeyOrign.X.Bytes(),publicKeyOrign.Y.Bytes()...)
return &Wallet{privateKey,publicKey}
}
func (this *Wallet)NewAdress()string{
//1.獲取pubKey
pubKey:=this.PublicKey
//2.獲取ripemd160哈希
ripeHash:=Newripe160Hash(pubKey)
//3.將ripeHash與version進行拼接
version:=byte(00)
payload:=append(ripeHash,[]byte{version}...)
//4.拷貝一份payload做hash後截取前4個字節
hash1:=sha256.Sum256(payload)
CheckCode:=CheckCode(hash1[:])
//5.再次拼接
payload=append(payload,CheckCode...)
//6.做base58
address:=base58.Encode(payload)
return address
}
func CheckCode(hash1 []byte)[]byte{
//再做一次hash
hash2:=sha256.Sum256(hash1)
return hash2[:4]
}
func Newripe160Hash(pubKey []byte)[]byte{
hash:=sha256.Sum256(pubKey)
//創建編碼器
ripe:=ripemd160.New()
//寫入
_,err:=ripe.Write(hash[:])
if err!=nil {
fmt.Println(err)
}
//生成哈希
ripehash:=ripe.Sum(nil)
return ripehash
}
四、添加Cli命令,編寫PrintAddressList方法,對錢包地址進行遍歷
func (this *Cli)PrintAddressList(){
wallets:=NewWallets()
addressList:=wallets.PrintAdress()
for _,address :=range addressList{
fmt.Printf("地址爲:%s\n",address)
}
}
四、修改Transcation.go等文件,將錢包地址與轉賬融合
1.修改TXinput和TXoutput結構體
type TXinput struct {
TXid []byte//引用的交易ID
Index int64//引用的output索引值
//Sig string//解鎖腳本
//簽名,由r、s拼接成的hash
Sigrnature []byte
//公鑰,由X Y拼接的公鑰
PublicKey []byte
}
type TXoutput struct {
Value float64//轉賬金額
//PubKeyHash string//鎖定腳本
//公鑰的哈希
PublickHash []byte
}
2.創建TXout結構體的Lock函數,實現反解到PublicHash,
func (TX *TXoutput)Lock(address string){
//1.base58解碼
payLoad:=base58.Decode(address)
PublicHash:=payLoad[1:len(payLoad)-4]
TX.PublickHash=PublicHash
}
3.創建NewTXoutput,實現對TX的賦值
func NewTXoutput(value float64,address string)*TXoutput{
TX:=TXoutput{
Value:value,
}
TX.Lock(address)
return &TX
}
五、實現交易驗證
交易加密:
1.拷貝一份新生成的Transcation並命名爲txCopy,並txCopy進行trimmed修剪,txCopy的所有TXnput的sinature和publik賦值爲[]byte
2.循環整個區塊鏈,找到與TXinput的Tid相同Transcation,並生成爲map[string(TXinput.Tid)]Transcation,
3.循環txCopy.TXinput,通過TXid和索引找到map中的Transcation的TXoutput,並將TXoutput的publicHash賦值給TXinput.public
4.對txCopy進行setHash,並賦值給Signature
5.將signature做ecdsa.sign簽名,得到R/S/ERR ,並將r、s進行append拼接,得到最終的signature並賦值給交易生成的Transcation.TXinput.signature
6.將txCopy的signature和public賦值爲[]byte,方便下一次循環
實現:
func (bc *BlockChain) SignTransaction(tx *Transaction, privateKey *ecdsa.PrivateKey){
prevTXs := make(map[string]Transaction)
for _,input :=range tx.TXinputs{
Tid:=input.TXid
tx,err:=bc.FindTransactionByTXid(Tid)
if err!=nil{
continue
}
prevTXs[string(input.TXid)] = tx
}
tx.Sign(privateKey, prevTXs)
}
func (bc *BlockChain) FindTransactionByTXid(TXid []byte)(Transaction,error){
it:=bc.NewIterator()
for {
block:=it.Next()
for _,tx :=range block.Transactions{
if bytes.Equal(tx.TXID,TXid){
return *tx,nil
}
}
if len(block.PrevHash)==0{
break
}
}
return Transaction{},errors.New("not find ")
}
func (tx *Transaction)Sign(privateKey *ecdsa.PrivateKey, prevTXs map[string]Transaction){
//1.循環tx交易,獲取TXinput,並賦值output的publickHash
txCopy:=tx.TrimmedCopy()
for index,input :=range txCopy.TXinputs{
prevTranscation:=prevTXs[string(input.TXid)]
if len(prevTranscation.TXID)==0{
log.Panic("交易錯誤,..........")
}
txCopy.TXinputs[index].PublicKey=prevTranscation.TXinputs[input.Index].PublicKey
txCopy.SetHash()
txCopy.TXinputs[index].PublicKey=[]byte{}
r,s,err:=ecdsa.Sign(rand.Reader,privateKey,txCopy.TXID)
if err!=nil{log.Panic(err)}
signature:=append(r.Bytes(),s.Bytes()...)
tx.TXinputs[index].Sigrnature=signature
}
}
func (tx *Transaction)TrimmedCopy()Transaction{
var inputs[]TXinput
var outputs[]TXoutput
for _,input :=range tx.TXinputs{
inputs=append(inputs, TXinput{input.TXid,input.Index,nil,nil})
}
for _,output :=range tx.TXoutputs{
outputs=append(outputs, output)
}
return Transaction{tx.TXID,inputs,outputs}
}
校驗:
1.檢驗函數在AddBlock時執行,屬於TX的方法
2.得到交易的簽名前的數據signature
3.得到rs
4.通過public得到X Y從而 得到originPublicKey、
5.進行ecdsa.verify
實現:
func (tx *Transaction)Verify(prevTXs map[string]Transaction)bool{
if !tx.IsCoinBaseTX(tx){
return true
}
txCopy:=tx.TrimmedCopy()
for index,input :=range tx.TXinputs{
prevTranscation:=prevTXs[string(input.TXid)]
if len(prevTranscation.TXID)==0{
log.Panic("error......")
}
txCopy.TXinputs[index].PublicKey=prevTranscation.TXoutputs[input.Index].PublickHash
txCopy.SetHash()
txCopy.TXinputs[index].PublicKey=nil
originSignature:=txCopy.TXID
signature:=input.Sigrnature
publicKey:=input.PublicKey
r :=big.Int{}
s :=big.Int{}
r.SetBytes(signature[:len(signature)/2])
s.SetBytes(signature[len(signature)/2:])
x :=big.Int{}
y :=big.Int{}
x.SetBytes(publicKey[:len(publicKey)/2])
y.SetBytes(publicKey[len(publicKey)/2:])
originPublicKey:=ecdsa.PublicKey{elliptic.P256(),&x,&y}
//func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool {
if !ecdsa.Verify(&originPublicKey,originSignature,&r,&s){
return false
}
}
return true
}
//以下在blockChain.go中實現
func (bc *BlockChain) VerifyTranscation(tx *Transaction)bool{
if tx.IsCoinBaseTX(tx){
return true
}
prevTXs := make(map[string]Transaction)
for _,input :=range tx.TXinputs{
Tid:=input.TXid
tx,err:=bc.FindTransactionByTXid(Tid)
if err!=nil{
continue
}
prevTXs[string(input.TXid)] = tx
}
return tx.Verify(prevTXs)
}
對交易添加Sting方法進行遍歷:
func (tx Transaction) String() string {
var lines []string
lines = append(lines, fmt.Sprintf("--- Transaction %x:", tx.TXID))
for i, input := range tx.TXinputs {
lines = append(lines, fmt.Sprintf(" Input %d:", i))
lines = append(lines, fmt.Sprintf(" TXID: %x", input.TXid))
lines = append(lines, fmt.Sprintf(" Out: %d", input.Index))
lines = append(lines, fmt.Sprintf(" Signature: %x", input.Sigrnature))
lines = append(lines, fmt.Sprintf(" PubKey: %x", input.PublicKey))
}
for i, output := range tx.TXoutputs{
lines = append(lines, fmt.Sprintf(" Output %d:", i))
lines = append(lines, fmt.Sprintf(" Value: %f", output.Value))
lines = append(lines, fmt.Sprintf(" Script: %x", output.PublickHash))
}
return strings.Join(lines, "\n")
}
至此,go重構BTC源碼結束。反過來回顧的時候,我發現區塊鏈也不是用很難的知識點實現的,沒有複雜的算法,也沒用複雜的結構。僅是簡單的密碼學基礎和結構體的反覆調用。區塊鏈讓21世紀的我們眼前一亮。究其原因、是中本聰先生將密碼學和去中心化的思想融會貫通了。對於我們普通的開發人員來說,並不是一定要用多牛逼的技術實現某些程序,而是要將最簡單的理論識融會貫通。非精不是明其理,非博不能制其約。我想大概就是區塊鏈誕生的名言警句吧~~
小夥伴們,下一節,我們將繼續學習區塊鏈知識。剖析--以太坊(ETH)源碼
您身邊喜歡綠色的朋友:
wechat:laughing_jk