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有关的信息存放到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
是一个用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(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
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"))
在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