UTXO区块链
PART1
实验目的
- 了解区块链上的简单数据结构
- 实现Merkle树的构建
- 初步理解UTXO的使用和验证
- 理解比特币上的交易创建
实验内容
区块链的基本结构
type BlockChain struct{
tip []byte//最新区块的哈希值
db *bolt.DB//数据库连接
}
区块链通过链式结构连接各个区块,形成一个不断增长的分布式账本系统。
区块的基本结构
作为区块链的主要组成部分,区块由区块头和区块体构成,区块头存储了版本号,上一个区块的哈希值,当前区块交易的哈希值,时间戳等信息,唯一标识一个区块。
type BlkHeader struct {
Version int64
PrevBlockHash []byte
MerkleRoot []byte
Timestamp int64
Bits int64
Nonce int64
}
实验步骤
Merkle树
每一个区块的交易节点从底向上构建Merkle树,采用哈希加密(单次sha256)。
新建Merkle节点,分为叶子节点和非叶子节点
func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode { NewNode:=&MerkleNode{} NewNode.Left=left NewNode.Right=right if left==nil && right==nil{ data_sha:=sha256.Sum256(data) NewNode.Data=data_sha[:] } else{ data_c:=append(left.Data,right.Data...) data_sha:=sha256.Sum256(data_c) NewNode.Data=data_sha[:] } return NewNode }
新建Merkle树,调用新建节点的函数自底向上构建,直到结点数为1,并且保证节点数量为偶数
func NewMerkleTree(data [][]byte) *MerkleTree { NewTree:=&MerkleTree{} var nodes []MerkleNode NewTree.Leaf=data for _,data_i := range data{ node_i:=NewMerkleNode(nil,nil,data_i) nodes=append(nodes,*node_i) } for len(nodes)>1{ var newnodes []MerkleNode for i:=0;i<len(nodes)/2;i++{ node:=NewMerkleNode(&nodes[2*i],&nodes[2*i+1],nil) newnodes=append(newnodes,*node) } if len(nodes)%2==1{ node:=NewMerkleNode(&nodes[len(nodes)-1],&nodes[len(nodes)-1],nil) newnodes=append(newnodes,*node) } nodes=newnodes } NewTree.RootNode=&nodes[0] return NewTree }
SPV证明路径,自底向上获取节点的路径的兄弟结点的哈希值
先根据结点的index判断节点在树的具体位置,自顶向下访问路径,再将路径逆序即可获取SPV证明路径
func (t *MerkleTree) SPVproof(index int) ([][]byte, error) { t_len:=len(t.Leaf) if index>t_len{ return nil,fmt.Errorf("Outof Index") } depth:=0 leaf_number:=1 t_node:=t.RootNode.Left for t_node!=nil{ t_node=t_node.Left depth+=1 leaf_number*=2 } var n_path [][]byte t_node=t.RootNode for t_node!=nil && t_node.Left!=nil{ if index>=leaf_number/2{ index-=leaf_number/2 n_path=append(n_path,t_node.Left.Data) t_node=t_node.Right } else{ n_path=append(n_path,t_node.Right.Data) t_node=t_node.Left } leaf_number/=2 } var path [][]byte for i:=0;i<len(n_path);i++{ path=append(path,n_path[len(n_path)-1-i]) } return path, nil }
验证SPV路径,将该节点的哈希值和SPV证明路径依次计算,最终和根节点的哈希值比较即可完成SPV路径的验证。
func (t *MerkleTree) VerifyProof(index int, path [][]byte) (bool, error) { if index>len(t.Leaf){ return false,fmt.Errorf("Outof Index") } data_sha:=sha256.Sum256(t.Leaf[index]) n_data:=data_sha[:] for _,t_data :=range path{ if index%2==1{ temp:=append(t_data,n_data...) temp2:=sha256.Sum256(temp) n_data=temp2[:] } else{ temp:=append(n_data,t_data...) temp2:=sha256.Sum256(temp) n_data=temp2[:] } index/=2 } for i:=0;i<len(n_data);i++{ if t.RootNode.Data[i]!=n_data[i]{ return false,nil } } return true, nil }
判断Coinbase交易
每个区块新建立时,可以加入一笔coinbase交易,这笔交易只有输出,没有输入,判断的基本依据为:
- 只出现在第一笔交易,其他交易均不是coinbase交易
- 输入的Txid为空,Vout为-1
func (t *Transaction) IsCoinBase() bool {
if len(t.Vin[0].Txid)==0 && (t.Vin[0].Vout==-1){
for i:=1;i<len(t.Vin);i++{
if len(t.Vin[i].Txid)==0 && (t.Vin[i].Vout==-1){
return false}}
return true}
return false}
计算公钥对应的地址
比特币上的地址的计算如下
- 计算公钥的哈希值(
RIPEMD16(SHA256(PubKey))
) - 地址计算前加入版本号
- 把步骤2的内容通过计算公钥哈希的双重SHA256哈希加密,取前4个字节作为校验和
版本号,公钥哈希,校验和
的组合通过Base58加密生成比特币的地址
func (w *Wallet) GetAddress() []byte {
key_hash:=HashPublicKey(w.PublicKey)
var temp []byte
temp=append(temp,version)
temp=append(temp,key_hash...)
checksum:=sha256.Sum256(temp)
hash_checksum:=checksum[:]
checksum=sha256.Sum256(hash_checksum)
hash_checksum=checksum[:]
var res []byte
res=append(res,version)
res=append(res,key_hash...)
res=append(res,hash_checksum[0:checkSumlen]...)
res=[]byte(base58.Encode(res))
return res
}
P2PKH锁定部分
P2PKH指的是向公钥的哈希支付,根据公钥的地址的计算方法,类似的可以通公钥地址获取公钥的哈希:
公钥地址经过base58解码后,第一位是version版本号,后四位为校验和,中间即为公钥哈希
func (out *TXOutput) Lock(address []byte) {
data,_:=base58.Decode(string(address))
out.PubKeyHash=data[1:len(data)-4]
}
实验结果
借助go test
对实验结果进行检验
PS E:\2023spring\blockchain\lab2\lab2> go test
PASS
ok lab2 0.657s
PART2
实验目的
- 进一步理解区块链上的数据结构
- 实现基于的POW共识算法出块
- 实现比特币上的账户创建和查询
- 理解UTXO的基本使用方法
- 实现区块链与数据库的交互
实验内容
工作量证明
工作量的证明机制(POW),简单来说就是通过提交一个容易检测,但是难以计算的结果,来证明节点做过一定量的工作。
首先完成Run
函数,该函数代表矿工通过如下流程完成挖矿工作:
- 首先构建当前区块头,区块头包含版本号,上⼀个区块哈希值(32位),当前区块数据对应哈希(32位,即区块数据的merkle根),时间戳,区块难度,计数器(nonce)。
- 添加计数器,作为随机数。计算器从0开始基础,每个回合+1
- 对于上述的数据来进行一个哈希的操作。
- 判断结果是否满足计算的条件:
- 如果符合,则得到了满足结果。
- 如果没有符合,从2开始重新直接2、3、4步骤。
func (pow *ProofOfWork) Run() (int64, []byte) {
nonce := int64(0)
var hash [32]byte
var hashInt big.Int
blkh := pow.block.Header
for nonce < maxNonce {
var data []byte
data=append(data,IntToHex(blkh.Version)...)
data=append(data,blkh.PrevBlockHash[:]...)
data=append(data,blkh.MerkleRoot[:]...)
data=append(data,IntToHex(blkh.Timestamp)...)
data=append(data,IntToHex(blkh.Bits)...)
data=append(data,IntToHex(nonce)...)
hash = sha256.Sum256(data)
hashInt.SetBytes(hash[:])
if hashInt.Cmp(pow.target) == -1 {
break
} else {
nonce++
}
}
return nonce, hash[:]
}
借助Validate
函数完成对挖矿结果的验证,即验证矿工给出的Nonce
是否是该难题的答案。
func (pow *ProofOfWork) Validate() bool {
var hashInt big.Int
var hash [32]byte
blkh := pow.block.Header
var data []byte
data=append(data,IntToHex(blkh.Version)...)
data=append(data,blkh.PrevBlockHash[:]...)
data=append(data,blkh.MerkleRoot[:]...)
data=append(data,IntToHex(blkh.Timestamp)...)
data=append(data,IntToHex(blkh.Bits)...)
data=append(data,IntToHex(blkh.Nonce)...)
hash = sha256.Sum256(data)
hashInt.SetBytes(hash[:])
return hashInt.Cmp(pow.target) == -1
}
UTXO
UTXO(Unspent Transaction Outputs),即没有花掉的交易输出,实际可以理解为在一次转账时剩余没有转出的资金。UTXO的交易模型上,用户通过使用未使用的交易输出(UTXO)来执行一笔交易。
构建新的UTXO交易的方法如下所示:
- 找到输入地址对应的交易PubKeyHash,借助
findunspentOutputs
找到输入账户的UTXO - 根据UTXO构建交易并进行签名
- 完成找零操作:剩余的金额作为UTXO返回到原输入中
func NewUTXOTransaction(from, to []byte, amount int, UTXOSet *UTXOSet) *Transaction {
var inputs []TXInput
var outputs []TXOutput
wal , _ := NewWallets()
bc := UTXOSet.Blockchain
pubKeyHash := HashPublicKey(wal.GetWallet(from).PublicKey)
acc, validOutputs := UTXOSet.FindUnspentOutputs(pubKeyHash, amount)
if acc < amount {
log.Panic("ERROR: Not enough funds")
}
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
if err != nil {
log.Panic(err)
}
for _, out := range outs {
input := TXInput{
Txid: txID,
Vout: out,
Signature: nil,
PubKey: wal.GetWallet(from).PublicKey,
}
inputs = append(inputs, input)
}
out_1 := TXOutput{amount,nil}
out_1.Lock(to)
outputs = append(outputs, out_1)
if acc > amount {
out_2 := TXOutput{acc-amount,nil}
out_2.Lock(from)
outputs = append(outputs, out_2)
}
tx := Transaction{nil, inputs, outputs}
bc.SignTransaction(&tx,wal.GetWallet(from).PrivateKey)
tx.SetID()
return &tx
}
UTXO池
比特币所有的交易都是放在UTXO池中,这样的好处是为了快速的得到某个UTXO是否当前可用。需要通过公私钥的唯一标识来算出我们当前的余额。在构建UTXO的交易时,我们需要通过UTXO池来需要当前属于自己的UTXO,然后获得相对应的账户金额,然后再生成对应的输出。FindUnspentOutputs
函数来查询用户当前未未花费的UTXO金额及其映射关系(存储txid和对应的index,便于后续查找),具体实现如下所示:
func (u UTXOSet) FindUnspentOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) {
unspentOutputs := make(map[string][]int)
totalAmount := 0
db := u.Blockchain.db
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(utxoBucket))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
outs := DeserializeOutputs(v)
txid := hex.EncodeToString(k)
for outIndex, out := range outs.Outputs {
if out.IsLockedWithKey(pubkeyHash) {
unspentOutputs[txid] = append(unspentOutputs[txid], outIndex)
totalAmount += out.Value
if totalAmount >= amount {
return nil
}
}
}
}
return nil
})
if err != nil {
log.Panic(err)
}
return totalAmount, unspentOutputs
}
BlockChain
除此之外,还需要完成mineblock
来生成新的区块:针对一组交易在区块链上写入一个新块,维护区块链的相关信息
func (bc *Blockchain) MineBlock(transactions []*Transaction) *Block {
var last_hash []byte
err := bc.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
last_hash = b.Get([]byte("l"))
return nil
})
if err != nil {
log.Panic(err)
}
var prev_hash [32]byte
copy(prev_hash[:],(last_hash)[:32])
newBlock := NewBlock(transactions,prev_hash)
err = bc.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(blocksBucket))
b.Put(newBlock.CalCulHash(), newBlock.Serialize())
b.Put([]byte("l"), newBlock.CalCulHash())
bc.tip = newBlock.CalCulHash()
return nil
})
if err != nil {
log.Panic(err)
}
return newBlock
}
另外,为了维护UTXO,需要找到所有UTXO并构建映射,通过函数findUTXO
实现:
- UTXO的特点:“未使用的交易”
- 对于每笔非CoinBase交易,每个输入都指向之前的输出,根据所有交易的输入判断之前的交易是否已被输出
- 维护UTXO和spentTXOs通过循环求得最终的UTXO
func (bc *Blockchain) FindUTXO() map[string]TXOutputs {
UTXO := make(map[string]TXOutputs)
spentTXOs := make(map[string][]int)
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.GetTransactions() {
txID := hex.EncodeToString(tx.ID)
Outputs:
for outIdx, out := range tx.Vout {
if spentTXOs[txID] != nil {
for _, spentOutIdx := range spentTXOs[txID] {
if spentOutIdx == outIdx {
continue Outputs
}
}
}
outs := UTXO[txID]
outs.Outputs = append(outs.Outputs, out)
UTXO[txID] = outs
}
if tx.IsCoinBase() == false {
for _, in := range tx.Vin {
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
}
}
}
if block.GetPrevhash() == [32]byte{} {
break
}
}
return UTXO
}
实验结果
本次实验基本完成了基于比特币的区块链搭建,具有以下功能:
go run . createblockchain -address ADDRESS //创建区块链
go run . createwallet //创建钱包
go run . getbalance -address ADDRESS //查询账户余额
go run . listaddresses //查询区块链上的地址
go run . printchain //打印区块链
go run . reindexutxo //重构UTXO池
go run . send -from FROM -to TO -amount AMOUNT //发起转账交易
对实现的区块链进行go test
...
PASS
ok lab3 0.188s