在本篇博客中,我们将深入探讨一个用Go语言编写的简易区块链实现。通过这个示例,我们可以理解区块链背后的核心概念,包括如何创建和管理区块、如何处理交易以及如何通过工作量证明算法保证区块链的安全性。我们还会探讨如何使用BoltDB这个轻量级的键值数据库来持久化存储区块链数据。在这个过程中,我们将一步一步构建起区块链的基本结构,并演示如何通过命令行界面(CLI)与区块链进行交互。
建议观看顺序:
1.block.go
区块的序列化与反序列化
为了能够在网络中传输或在磁盘上存储,我们需要将区块序列化和反序列化。序列化是将区块转换为字节序列的过程,而反序列化是将字节序列还原为原始区块的过程。
序列化 (
Serialize
方法): 使用Go的encoding/gob
包来进行序列化操作。这个方法将Block
结构体编码成字节流,以便于存储或网络传输。反序列化 (
DeserializeBlock
函数): 与序列化相反,这个函数将字节流解码回Block
结构体。
区块的创建
新建区块 (
NewBlock
函数): 当需要添加新的区块时,我们会调用这个函数。该函数接受交易列表和前一个区块的哈希值作为参数,创建一个新的区块。在这个过程中,我们还会进行工作量证明的计算。创世区块 (
NewGenesisBlock
函数): 创世区块是区块链中的第一个区块。这个函数通过调用NewBlock
函数并传递一个特殊的coinbase交易来创建创世区块。
// -*- coding: utf-8 -*- // Time : 2024/4/15 22:15 // Author : blue // File : block.go // Software: Goland package main import ( "bytes" "crypto/sha256" "encoding/gob" "log" "time" ) // Block keeps block headers type Block struct { Timestamp int64 // 时间戳 Transactions []*Transaction // 交易 PrevBlockHash []byte // 前一个区块的哈希 Hash []byte // 当前区块的哈希 Nonce int // 随机数 } // Serializes 是将区块序列化为字节数组 func (b *Block) Serialize() []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) err := encoder.Encode(b) if err != nil { log.Panic(err) } return result.Bytes() } // HashTransactions将区块中的所有交易序列化并返回一个哈希值 func (b *Block) HashTransactions() []byte { var txHashes [][]byte var txHash [32]byte // 遍历区块中的所有交易 for _, tx := range b.Transactions { txHashes = append(txHashes, tx.ID) } txHash = sha256.Sum256(bytes.Join(txHashes, []byte{})) return txHash[:] } // NewBlock 将创建一个新的区块 func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block { // 创建区块 block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0} // 创建工作量证明 pow := NewProofOfWork(block) // 运行工作量证明 nonce, hash := pow.Run() block.Hash = hash[:] block.Nonce = nonce return block } // NewGenesisBlock 将创建并返回创世区块 func NewGenesisBlock(coinbase *Transaction) *Block { return NewBlock([]*Transaction{coinbase}, []byte{}) } // DeserializeBlock 将区块的字节数组反序列化为区块 func DeserializeBlock(d []byte) *Block { var block Block decoder := gob.NewDecoder(bytes.NewReader(d)) err := decoder.Decode(&block) if err != nil { log.Panic(err) } return &block }
2.blockchains.go
这段代码实现了区块链的核心数据结构和功能,包括区块链、区块迭代器以及创建、查询和添加区块等操作。让我们深入分析:
数据结构
- Blockchain: 区块链结构体,包含最新区块的哈希值和数据库指针。
- BlockchainIterator: 区块链迭代器,用于遍历区块链,包含当前区块的哈希值和数据库指针。
函数解析
- MineBlock: 挖矿函数,根据交易列表创建新区块,并将其添加到区块链中。
- 首先获取最新区块的哈希值。
- 然后创建新的区块,并进行工作量证明计算。
- 最后将新区块序列化并存储到数据库中,更新区块链的最新区块哈希值。
- FindUnspentTransactions: 查找指定地址的未花费交易。
- 遍历区块链,检查每个交易的输出是否属于指定地址,以及是否已被花费。
- 将未花费的交易添加到结果列表中。
- FindUTXO: 查找指定地址的未花费交易输出 (UTXO)。
- 调用
FindUnspentTransactions
获取未花费交易。 - 遍历未花费交易的输出,将属于指定地址的输出添加到结果列表中。
- 调用
- FindSpendableOutputs: 查找满足指定金额的未花费交易输出。
- 遍历未花费交易的输出,累加金额,直到满足指定金额。
- 返回累加金额和满足条件的未花费交易输出。
- Iterator: 创建区块链迭代器,从最新区块开始遍历区块链。
- Next: 获取区块链迭代器指向的当前区块,并将迭代器指向下一个区块。
- dbExists: 检查区块链数据库文件是否存在。
- NewBlockchain: 打开区块链数据库,并创建区块链对象。
- CreateBlockchain: 创建新的区块链,包含创世区块。
- 首先创建 coinbase 交易,奖励给指定地址。
- 然后创建创世区块,包含 coinbase 交易。
- 最后将创世区块存储到数据库中,并创建区块链对象。
- AddBlock: 添加新区块到区块链。
- 首先创建 coinbase 交易,奖励给指定地址。
- 然后调用MineBlock挖矿并添加新区块到区块链中。
// -*- coding: utf-8 -*- // Time : 2024/4/15 22:15 // Author : blue // File : blockchains.go // Software: Goland package main import ( "encoding/hex" "fmt" "github.com/boltdb/bolt" "log" "os" ) const dbFile = "blockchain.db" //数据库文件 const blocksBucket = "blocks" //存储区块的桶 const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks" //创世块的交易数据 // Blockchain 包含一个区块链 type Blockchain struct { tip []byte //最新区块的哈希 db *bolt.DB //数据库指针 } // BlockchainIterator 将用于迭代区块链 type BlockchainIterator struct { currentHash []byte //当前区块的哈希 db *bolt.DB //数据库指针 } // MineBlock 将用于挖掘新块 func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte //记录最新区块的哈希 //获取最新区块的哈希 err := bc.db.View(func(tx *bolt.Tx) error { //获取区块桶 b := tx.Bucket([]byte(blocksBucket)) //获取最新区块的哈希 lastHash = b.Get([]byte("l")) return nil }) if err != nil { log.Panic(err) } //创建新区块(包含验证) newBlock := NewBlock(transactions, lastHash) //将新区块存储到数据库中 err = bc.db.Update(func(tx *bolt.Tx) error { //获取区块桶 b := tx.Bucket([]byte(blocksBucket)) //将新区块存储到数据库中 err := b.Put(newBlock.Hash, newBlock.Serialize()) if err != nil { log.Panic(err) } //将最新区块的哈希存储到数据库中 err = b.Put([]byte("l"), newBlock.Hash) if err != nil { log.Panic(err) } //更新区块链的tip bc.tip = newBlock.Hash return nil }) } // FindUnspentTransactions 返回未花费的交易 func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction { //未花费的交易 var unspentTXs []Transaction //已花费的输出 spentTXOs := make(map[string][]int) //迭代区块链 bci := bc.Iterator() for { //获取下一个区块 block := bci.Next() //遍历区块中的交易 for _, tx := range block.Transactions { //将交易ID转换为字符串 txID := hex.EncodeToString(tx.ID) Outputs: //遍历交易中的输出 for outIdx, out := range tx.Vout { //检查输出是否已经被花费 if spentTXOs[txID] != nil { //遍历已花费的输出 for _, spentOut := range spentTXOs[txID] { //如果输出已经被花费,则跳过 if spentOut == outIdx { continue Outputs } } } //如果输出可以被解锁,则将交易添加到未花费的交易中 if out.CanBeUnlockedWith(address) { unspentTXs = append(unspentTXs, *tx) } } //如果交易不是coinbase交易,则遍历交易的输入 if tx.IsCoinbase() == false { //遍历交易的输入 for _, in := range tx.Vin { //如果输入可以解锁,则将输出添加到已花费的输出中 if in.CanUnlockOutputWith(address) { //将交易ID转换为字符串 inTxID := hex.EncodeToString(in.Txid) //将输出添加到已花费的输出中 spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) } } } } //如果区块的前一个区块哈希为空,则退出循环 if len(block.PrevBlockHash) == 0 { break } } //返回未花费的交易 return unspentTXs } // FindUTXO 返回未花费的输出 func (bc *Blockchain) FindUTXO(address string) []TXOutput { var UTXOs []TXOutput //未花费的交易 unspentTransactions := bc.FindUnspentTransactions(address) //遍历未花费的交易 for _, tx := range unspentTransactions { //遍历交易的输出 for _, out := range tx.Vout { //如果输出可以被解锁,则将输出添加到未花费的输出中 if out.CanBeUnlockedWith(address) { UTXOs = append(UTXOs, out) } } } return UTXOs } // FindSpendableOutputs 返回足够的未花费输出以满足要求的金额 func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) { //未花费的输出 unspentOutputs := make(map[string][]int) //未花费的交易 unspentTXs := bc.FindUnspentTransactions(address) //累计金额 accumulated := 0 Work: //遍历未花费的交易 for _, tx := range unspentTXs { //将交易ID转换为字符串 txID := hex.EncodeToString(tx.ID) //遍历交易的输出 for outIdx, out := range tx.Vout { //如果输出可以被解锁且累计金额小于要求的金额,则将输出添加到未花费的输出中 if out.CanBeUnlockedWith(address) && accumulated < amount { //将输出添加到未花费的输出中 accumulated += out.Value //将输出的索引添加到未花费的输出中 unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) //如果累计金额大于等于要求的金额,则退出循环 if accumulated >= amount { break Work } } } } //返回累计金额和未花费的输出 return accumulated, unspentOutputs } // Iterator 返回一个迭代器 func (bc *Blockchain) Iterator() *BlockchainIterator { //迭代器对象是一个指向区块链的指针和一个指向数据库的指针 bci := &BlockchainIterator{bc.tip, bc.db} return bci } // Next 返回区块链中的下一个区块 func (i *BlockchainIterator) Next() *Block { var block *Block //获取当前区块 err := i.db.View(func(tx *bolt.Tx) error { //获取区块桶 b := tx.Bucket([]byte(blocksBucket)) //获取当前区块 encodedBlock := b.Get(i.currentHash) //反序列化区块 block = DeserializeBlock(encodedBlock) return nil }) if err != nil { log.Panic(err) } //更新当前区块的哈希 i.currentHash = block.PrevBlockHash return block } // dbExists 检查数据库是否存在 func dbExists() bool { if _, err := os.Stat(dbFile); os.IsNotExist(err) { return false } return true } // NewBlockchain 创建一个新的区块 func NewBlockchain(address string) *Blockchain { //检查数据库是否存在 if dbExists() == false { fmt.Println("No existing blockchain found. Create one first.") os.Exit(1) } //存储最新区块的哈希 var tip []byte db, err := bolt.Open(dbFile, 0600, nil) if err != nil { log.Panic(err) } //更新数据库 err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) tip = b.Get([]byte("l")) return nil }) if err != nil { log.Panic(err) } //创建一个区块链对象 bc := Blockchain{tip, db} return &bc } // CreateBlockchain 创建一个新的区块链 func CreateBlockchain(address string) *Blockchain { //检查数据库是否存在 if dbExists() { fmt.Println("Blockchain already exists.") os.Exit(1) } var tip []byte //打开数据库 db, err := bolt.Open(dbFile, 0600, nil) if err != nil { log.Panic(err) } //更新数据库 err = db.Update(func(tx *bolt.Tx) error { //创建一个新的coinbase交易 cbtx := NewCoinbaseTX(address, genesisCoinbaseData) //创建一个新的区块 genesis := NewGenesisBlock(cbtx) //创建一个新的桶 b, err := tx.CreateBucket([]byte(blocksBucket)) if err != nil { log.Panic(err) } //将区块存储到数据库中 err = b.Put(genesis.Hash, genesis.Serialize()) if err != nil { log.Panic(err) } err = b.Put([]byte("l"), genesis.Hash) if err != nil { log.Panic(err) } tip = genesis.Hash return nil }) if err != nil { log.Panic(err) } bc := Blockchain{tip, db} return &bc } // AddBlock 将用于添加区块到区块链 func AddBlock(address string) { //创建一个新的区块 bc := NewBlockchain(address) defer bc.db.Close() //创建一个新的coinbase交易 //cbtx := NewCoinbaseTX(address, "") //挖掘新块并存放到数据库中 bc.MineBlock([]*Transaction{NewCoinbaseTX(address, "")}) //往address地址发送奖励 fmt.Println("Coinbase交易完成,以奖励的方式将硬币发送到地址:", address) fmt.Println("Success!") }
3.proofwork.go
这段代码实现了区块链中的工作量证明 (Proof of Work, PoW) 机制,用于确保区块链的安全性,防止恶意攻击。让我们深入理解其原理和实现:
工作量证明概述
PoW 是一种共识机制,要求节点进行一定的计算工作才能将新区块添加到区块链中。这种机制通过消耗计算资源来增加攻击成本,从而保证区块链的安全性。
代码解析
ProofOfWork 结构体:
block
: 指向区块的指针,包含区块头信息。target
: 指向一个大整数的指针,代表挖矿难度目标。哈希值必须小于该目标才算有效。
NewProofOfWork:
- 创建一个新的 ProofOfWork 对象,根据难度目标设置
target
值。 - 难度目标通过
targetBits
常量控制,值越小,难度越大。
- 创建一个新的 ProofOfWork 对象,根据难度目标设置
prepareData:
- 准备用于哈希运算的数据,包括区块头信息、随机数
nonce
等。 - 将数据拼接成字节数组,方便进行哈希运算。
- 准备用于哈希运算的数据,包括区块头信息、随机数
Run:
- 执行挖矿过程,不断尝试不同的
nonce
值,直到找到满足难度目标的哈希值。 - 循环计算哈希值,并与
target
进行比较。 - 如果哈希值小于
target
,则挖矿成功,返回nonce
和哈希值。 - 否则,继续尝试下一个
nonce
值。
- 执行挖矿过程,不断尝试不同的
Validate:
- 验证区块的工作量证明是否有效。
- 根据区块头信息和
nonce
计算哈希值,并与target
比较。 - 如果哈希值小于
target
,则验证通过,返回true
。 - 否则,验证失败,返回
false
。
// -*- coding: utf-8 -*- // Time : 2024/4/14 23:48 // Author : blue // File : proofwork.go // Software: Goland package main import ( "bytes" "crypto/sha256" "fmt" "math" "math/big" ) var ( maxNonce = math.MaxInt64 ) const targetBits = 16 // ProofOfWork 包含一个指向区块的指针和一个指向big.Int的指针 type ProofOfWork struct { block *Block target *big.Int } // NewProofOfWork 创建并返回一个ProofOfWork结构 func NewProofOfWork(b *Block) *ProofOfWork { target := big.NewInt(1) target.Lsh(target, uint(256-targetBits)) pow := &ProofOfWork{b, target} return pow } // prepareData 准备数据 func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join( [][]byte{ pow.block.PrevBlockHash, pow.block.HashTransactions(), IntToHex(pow.block.Timestamp), IntToHex(int64(targetBits)), IntToHex(int64(nonce)), }, []byte{}, ) return data } // Run 查找有效的哈希 func (pow *ProofOfWork) Run() (int, []byte) { var hashInt big.Int var hash [32]byte nonce := 0 fmt.Printf("Mining a new block") for nonce < maxNonce { data := pow.prepareData(nonce) hash = sha256.Sum256(data) fmt.Printf("\r%x", hash) hashInt.SetBytes(hash[:]) if hashInt.Cmp(pow.target) == -1 { break } else { nonce++ } } fmt.Print("\n\n") return nonce, hash[:] } // Validate 验证工作量证明 func (pow *ProofOfWork) Validate() bool { var hashInt big.Int data := pow.prepareData(pow.block.Nonce) hash := sha256.Sum256(data) hashInt.SetBytes(hash[:]) isValid := hashInt.Cmp(pow.target) == -1 return isValid }
4.Transaction.go
这段代码定义了区块链中的交易数据结构,包括交易输入、交易输出和交易本身,以及创建 coinbase 交易和普通交易的函数。让我们深入分析:
数据结构
- Transaction: 交易结构体,包含交易 ID、输入列表和输出列表。
- TXInput: 交易输入结构体,包含引用的交易 ID、输出索引和解锁脚本。
- TXOutput: 交易输出结构体,包含金额和锁定脚本。
拓展:
在区块链技术,特别是比特币及其衍生加密货币中,交易的验证过程涉及到两个重要的概念:锁定脚本(ScriptPubKey)和解锁脚本(ScriptSig)。这两个脚本共同工作,确保只有资产的合法拥有者才能花费这些资产。在本文Transaction.go
代码示例中,为了便于理解和实现,锁定脚本和解锁脚本简化为货币持有者的地址。
锁定脚本(ScriptPubKey)
// TXInput 表示交易输入 type TXInput struct { Txid []byte //引用的交易ID,表示交易的哈希 Vout int //引用的输出索引 ScriptSig string //解锁脚本 }
在TXOutput
结构体中,ScriptPubKey
字段代表锁定脚本。它的作用是指定谁有权利花费这个输出中的资金。在真实的比特币系统中,ScriptPubKey
可以包含更复杂的脚本,但在这个简化模型中,它被设置为接收方的地址。
解锁脚本(ScriptSig)
// TXOutput 表示交易输出 type TXOutput struct { Value int // 输出金额 ScriptPubKey string //锁定脚本 }
与此同时,在TXInput
结构体中,ScriptSig
字段代表解锁脚本。当你尝试花费某个输出时,你需要提供一个ScriptSig
,这个脚本能够"解锁"那个输出中的ScriptPubKey
所施加的限制。
在真实的比特币系统中,解锁脚本通常包含与锁定脚本相匹配的公钥和一个签名,这个签名是使用与公钥相对应的私钥生成的。这样,只有拥有正确私钥的人才能生成有效的签名,从而成功解锁输出。
简化说明
在本文代码示例中,锁定脚本和解锁脚本被简化为直接使用地址字符串。这意味着,如果一个输出的ScriptPubKey
与某个地址匹配,那么任何包含相同地址作为ScriptSig
的输入都能解锁并花费这个输出。
// CanUnlockOutputWith 将检查输出是否可以使用提供的数据解锁 func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { return in.ScriptSig == unlockingData } // CanBeUnlockedWith 将检查输出是否可以使用提供的数据解锁 func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { return out.ScriptPubKey == unlockingData }
这种简化的实现方式有助于理解区块链交易的基本原理,即资产的转移需要通过正确的解锁条件。然而,在实际的区块链应用中,这种机制要复杂得多,涉及到密钥对和加密签名,以确保交易的安全性和用户的隐私。
函数解析
- IsCoinbase: 判断交易是否为 coinbase 交易。Coinbase 交易是区块中的第一笔交易,用于奖励矿工,其输入为空。
- SetID: 计算交易的哈希值,并将其设置为交易 ID。
- CanUnlockOutputWith: 检查交易输入的解锁脚本是否可以解锁指定的锁定脚本。
- CanBeUnlockedWith: 检查交易输出的锁定脚本是否可以被指定的解锁脚本解锁。
- NewCoinbaseTX: 创建 coinbase 交易。
- 输入为空,输出为指定地址和奖励金额。
- 设置交易 ID。
- NewUTXOTransaction: 创建普通交易。
- 根据发送方地址和转账金额,查找满足条件的未花费交易输出 (UTXO)。
- 如果 UTXO 余额不足,则报错。
- 否则,构建交易输入和输出列表。
- 如果 UTXO 余额大于转账金额,则创建找零输出。
- 设置交易 ID。
- IntToHex 将整数转换为字节数组。
// -*- coding: utf-8 -*- // Time : 2024/4/15 22:16 // Author : blue // File : Transaction.go // Software: Goland package main import ( "bytes" "crypto/sha256" "encoding/gob" "encoding/hex" "fmt" "log" ) // 表示每个区块链的奖励 const subsidy = 10 // Transaction 表示一笔交易 type Transaction struct { ID []byte //交易ID,表示交易的哈希 Vin []TXInput //交易输入 Vout []TXOutput //交易输出 } // TXInput 表示交易输入 type TXInput struct { Txid []byte //引用的交易ID,表示交易的哈希 Vout int //引用的输出索引 ScriptSig string //解锁脚本 } // TXOutput 表示交易输出 type TXOutput struct { Value int // 输出金额 ScriptPubKey string //锁定脚本 } // IsCoinbase 将检查交易是否为coinbase交易 func (tx Transaction) IsCoinbase() bool { return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1 } // SetID 将设置交易ID,交易ID是交易的哈希 func (tx *Transaction) SetID() { //创建一个缓冲区 var encoded bytes.Buffer //创建一个哈希 var hash [32]byte //创建一个编码器 enc := gob.NewEncoder(&encoded) //编码 err := enc.Encode(tx) if err != nil { log.Panic(err) } //计算哈希 hash = sha256.Sum256(encoded.Bytes()) //设置ID tx.ID = hash[:] } // CanUnlockOutputWith 将检查输出是否可以使用提供的数据解锁 func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { return in.ScriptSig == unlockingData } // CanBeUnlockedWith 将检查输出是否可以使用提供的数据解锁 func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { return out.ScriptPubKey == unlockingData } // NewCoinbaseTX 将创建一个新的coinbase交易 func NewCoinbaseTX(to, data string) *Transaction { // 如果没有数据,则使用默认数据 if data == "" { data = fmt.Sprintf("Reward to '%s'", to) } //coinbase交易没有输入,所以Txid为空,Vout为-1 txin := TXInput{[]byte{}, -1, data} //创建一个输出 txout := TXOutput{subsidy, to} //创建一个交易 tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}} tx.SetID() return &tx } // NewUTXOTransaction 将创建一个新的UTXO交易 func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { var inputs []TXInput var outputs []TXOutput //获取未花费的输出 acc, validOutputs := bc.FindSpendableOutputs(from, amount) //检查余额是否足够 if acc < amount { log.Panic("ERROR: Not enough funds") } // 构建一个输入列表 for txid, outs := range validOutputs { //将交易ID转换为字节数组 txID, err := hex.DecodeString(txid) if err != nil { log.Panic(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}) // a change } //创建一个交易 tx := Transaction{nil, inputs, outputs} tx.SetID() return &tx } // IntToHex 将整数转换为字节数组 func IntToHex(num int64) []byte { buff := new(bytes.Buffer) err := binary.Write(buff, binary.BigEndian, num) if err != nil { log.Panic(err) } return buff.Bytes() }
5.cli.go
这段代码实现了一个简单的命令行界面 (CLI),用于与区块链进行交互,包括创建区块链、查询余额、打印区块链和发送交易等功能。让我们逐一解析:
CLI 结构体
CLI
结构体用于处理命令行参数和执行相应操作。
函数解析
- createBlockchain: 创建新的区块链,并将创世区块奖励发送到指定地址。
- getBalance: 查询指定地址的余额。
- printUsage: 打印命令行用法帮助信息。
- validateArgs: 验证命令行参数是否合法。
- printChain: 打印整个区块链的信息,包括每个区块的哈希、时间戳、交易等。
- send: 从一个地址向另一个地址发送指定数量的币。
- run: 运行命令行界面,循环提示用户输入命令,并根据命令执行相应操作。
- addBlock: 添加新区块到区块链。
// -*- coding: utf-8 -*- // Time : 2024/4/15 22:18 // Author : blue // File : cli.go // Software: Goland package main import ( "fmt" "os" "strconv" "time" ) // CLI 处理命令行参数 type CLI struct { } // createBlockchain 创建区块链 func (cli *CLI) createBlockchain(address string) { // 创建区块链 bc := CreateBlockchain(address) bc.db.Close() fmt.Println("Done!") } // getBalance 获取地址的余额 func (cli *CLI) getBalance(address string) { // 创建区块链 bc := NewBlockchain(address) defer bc.db.Close() balance := 0 // 获取地址的UTXO UTXOs := bc.FindUTXO(address) // 计算余额 for _, out := range UTXOs { balance += out.Value } fmt.Printf("Balance of '%s': %d\n", address, balance) } // printUsage 打印用法 func (cli *CLI) printUsage() { fmt.Println("Usage:") fmt.Println(" English: getbalance -address ADDRESS - Get balance of ADDRESS、、、中文:getbalance - address ADDRESS -获取地址的余额") fmt.Println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS") fmt.Println(" printchain - Print all the blocks of the blockchain") fmt.Println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO") } // validateArgs 验证参数 func (cli *CLI) validateArgs() { // 验证参数 if len(os.Args) < 2 { //什么意思:如果命令行参数少于2个 // 打印用法 cli.printUsage() os.Exit(1) //什么意思:退出 } } // printChain 打印区块链 func (cli *CLI) printChain() { // 创建区块链 bc := NewBlockchain("") defer bc.db.Close() // 创建迭代器 bci := bc.Iterator() // 循环打印区块链中的区块 for { block := bci.Next() fmt.Println("=========================================") //把时间戳转换为时间 timeFormat := time.Unix(block.Timestamp, 0) fmt.Println("timestamp:", timeFormat) fmt.Printf("PrevBlockHash: %x\n", block.PrevBlockHash) fmt.Printf("BlockHash: %x\n", block.Hash) //显示完整的交易信息 fmt.Println("Transactions:") for _, tx := range block.Transactions { //显示交易ID //fmt.Printf("Transaction ID: %x\n", tx.ID) var str string for _, value := range tx.ID { str += strconv.Itoa(int(value)) } fmt.Println("Transaction ID: " + str) fmt.Println("TXInput:") for _, tx := range tx.Vin { fmt.Println(tx.Txid) fmt.Println(tx.Vout) fmt.Println(tx.ScriptSig) } fmt.Println("TXOutputs:") for _, tx := range tx.Vout { fmt.Println(tx.Value) fmt.Println(tx.ScriptPubKey) } } //fmt.Println("Transactions:", block.Transactions) fmt.Printf("Nonce: %d\n", block.Nonce) pow := NewProofOfWork(block) fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate())) fmt.Println() if len(block.PrevBlockHash) == 0 { break } } } // send 发送 func (cli *CLI) send(from, to string, amount int) { bc := NewBlockchain(from) defer bc.db.Close() // 创建交易 tx := NewUTXOTransaction(from, to, amount, bc) // 挖矿 bc.MineBlock([]*Transaction{tx}) fmt.Println("Success!") } // 新建一个run函数,实现在程序框显示功能,并给出提示,然后根据相应的命令执行相应的操作 func (cli *CLI) run() { for { fmt.Println("1. getbalance -address ADDRESS - 获取地址的余额 ") fmt.Println("2. createblockchain -address ADDRESS - 创建区块链并将创世区块奖励发送到ADDRESS") fmt.Println("3. printchain - 打印区块链") fmt.Println("4. send -from FROM -to TO -amount AMOUNT - 从FROM地址向TO发送AMOUNT硬币") fmt.Println("5. mine ADDRESS挖矿创建区块链并将创世区块奖励发送到ADDRESS") fmt.Println("6. exit - Exit") fmt.Println("Please enter the command:") var cmd string fmt.Scanln(&cmd) switch cmd { case "1": fmt.Println("Please enter the address:") var address string fmt.Scanln(&address) cli.getBalance(address) fmt.Println() case "2": fmt.Println("Please enter the address:") var address string fmt.Scanln(&address) cli.createBlockchain(address) fmt.Println() case "3": cli.printChain() case "4": fmt.Println("Please enter the from address:") var from string fmt.Scanln(&from) fmt.Println("Please enter the to address:") var to string fmt.Scanln(&to) fmt.Println("Please enter the amount:") var amount int fmt.Scanln(&amount) cli.send(from, to, amount) fmt.Println() case "5": fmt.Println("Please enter the address:") var address string fmt.Scanln(&address) //我们要创建新区块 //挖掘新区块 //AddBlock(address) cli.addBlock(address) fmt.Println() case "6": os.Exit(0) default: fmt.Println("Invalid command") } } } // addBlock 添加区块 func (cli *CLI) addBlock(address string) { // 创建区块链 AddBlock(address) }
6.main.go
这段代码是整个区块链项目的入口,它创建了一个 CLI
对象,并调用其 run
方法启动命令行界面。
- main 函数: 程序的入口函数。
- cli := CLI{}: 创建一个
CLI
对象,用于处理命令行参数和执行相应操作。 - cli.run(): 调用
CLI
对象的run
方法,启动命令行界面,并开始与用户交互。
// -*- coding: utf-8 -*- // Time : 2024/4/15 22:15 // Author : blue // File : main.go // Software: Goland package main func main() { cli := CLI{} //cli.Run() cli.run() }
总结
通过本篇博客,我们详细探讨了如何使用Go语言实现一个简易的区块链系统。我们从区块的数据结构出发,探讨了区块链的创建、交易的处理、工作量证明算法的实现,以及通过CLI与区块链进行交互的方法。此外,我们还了解了如何使用BoltDB对区块链数据进行持久化存储,这对于保证区块链数据的持久性和可靠性至关重要。
实现一个区块链系统虽然复杂,但通过逐步分析和实现,我们可以更好地理解其背后的原理和技术。希望这篇博客能为那些对区块链技术感兴趣的读者提供一定的帮助,并激发大家进一步探索和实践区块链技术的热情。