Go实现区块链(四)---交易事物(一)
1.前言
上一篇我知道了区块链如何持久化存储,接下来我们将开始实现区块链中交易是如何产生的如何防止被串改,如何在网络中分布式记账。我们将交易分成两部分:交易实现一般机制,后面将实现网络、奖励机制等。
2.知识准备
知识点 | 学习网页 | 特性 |
---|---|---|
bitcoin交易 | 交易 | 不可串改 |
Coinbase | 创世块交易信息 | 创世块交易 |
3.基本交易过程
在区块链中每次发生交易,用户需要将新的交易记录写到比特币区块链网络中,等待网络确认为交易完成。每个交易包括了一些输入和一些输出,未经使用的交易的输出(Transaction Outputs,UTXO)可以被新的交易引用作为合法输入,被使用过的交易的输出(Spent Transaction Outputs,STO)则无法被引用作为合法输入。
注意:这里的比特币交易与我们传统的金钱付款交易是不同的,并没有账号、没有余额等。详情参考
4.代码实现
交易:
//交易事物
type Transaction struct {
ID []byte //交易hash
Vin []TXInput //事物输入
Vout []TXOutput //事物输出
}
流程图:
交易引用图
注意:
- 有些输出和输入无关。
- 在一个交易中,投入可以参考多个交易的输出。
- 输入必须引用输出。
比特币中没有这样的概念。事务只是用脚本锁定值,只能由锁定它的人解锁。
交易输出:
//一个事物输出
type TXOutput struct {
Value int //值
ScriptPubKey string //解锁脚本key
}
实际上,它是存储“硬币”的输出(注意Value上面的字段)。而存储意味着用一个拼图锁定它们,这是存储在ScriptPubKey。在内部,比特币使用称为脚本的脚本语言,用于定义输出锁定和解锁逻辑。这个语言很原始(这是故意的,以避免可能的黑客和滥用),但我们不会详细讨论它。你可以在本章知识点bitcoin交易详细解释。
在比特币中,价值领域存储satoshis的数量,而不是BTC的数量。甲聪是100000000分之1一个比特币(0.00000001 BTC)的,因此,这是货币的比特币的最小单位(如百分比)。
由于我们没有实现地址,现在我们将避免整个脚本相关的逻辑。ScriptPubKey将存储任意字符串(用户定义的钱包地址)。
交易输入:
//一个事物输入
type TXInput struct {
Txid []byte //交易ID的hash
Vout int //交易输出
ScriptSig string //解锁脚本
}
如前所述,输入引用前一个输出:Txid存储此类事务的ID,并Vout在事务中存储输出的索引。ScriptSig是一个提供数据以在输出中使用的脚本ScriptPubKey。如果数据是正确的,输出可以被解锁,并且它的值可以被用来产生新的输出; 如果不正确,则输出中不能引用输出。这是保证用户不能花钱属于其他人的硬币的机制。
同样,由于我们还没有实现地址,ScriptSig因此将只存储任意用户定义的钱包地址。我们将在下一篇文章中实现公钥和签名检查。
我们总结一下。产出是储存“硬币”的地方。每个输出都带有一个解锁脚本,它决定了解锁输出的逻辑。每个新事务都必须至少有一个输入和输出。输入引用前一个事务的输出,并提供ScriptSig输出的解锁脚本中使用的数据(字段),以解除锁定并使用其值创建新的输出。
coinbase交易:
上面我们知道输入参考输出逻辑,而输出又参考了输入。这样就产生了我们常见的一个问题:先有鸡还是先有蛋呢?
当矿工开始挖矿时,它会添加一个coinbase交易。coinbase交易是一种特殊类型的交易,不需要以前存在的输出。它无处不在地创造产出(即“硬币”)。没有鸡的鸡蛋。这是矿工获得开采新矿区的奖励。
如您所知,区块链开始处有起始块。这个区块在区块链中产生了第一个输出。由于没有以前的交易并且没有这样的输出,因此不需要先前的输出。
我们来创建一个coinbase事务:
//创建Coinbase事物
func NewCoinbaseTX(to,data string) *Transaction {
if data==""{
data=fmt.Sprintf("Reward to '%s'",to)
}
//这里Vout-1 data:const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
txin :=TXInput{[]byte{},-1,data}
//subsidy是奖励的金额
txout := TXOutput{subsidy, to}
tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
//设置32位交易hash
tx.SetID()
return &tx
}
//设置交易ID hash
func (tx *Transaction) SetID(){
var encoded bytes.Buffer
var hash [32]byte //32位的hash字节
enc := gob.NewEncoder(&encoded)
err := enc.Encode(tx)
if err != nil {
log.Panic(err)
}
//将交易信息sha256
hash = sha256.Sum256(encoded.Bytes())
//生成hash
tx.ID = hash[:]
}
一个coinbase交易只有一个输入。在我们的实现中它Txid是空的,Vout等于-1。另外,coinbase事务不会存储脚本ScriptSig。相反,任意数据存储在那里。
在比特币中,第一个coinbase交易包含以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。查看知识点Coinbase可以知道。
subsidy是奖励的金额。在比特币中,这个数字没有存储在任何地方,只根据块的总数进行计算:块的数量除以210000。挖掘创世纪块产生50 BTC,每210000块奖励减半。在我们的实现中,我们会将奖励作为常量存储(至少现在是😉)。
在区块链中存储交易:
我们将开始区块里面的data改成transactions
//区块结构
type Block struct {
Hash []byte //hase值
Transactions []*Transaction//交易数据
PrevBlockHash []byte //存储前一个区块的Hase值
Timestamp int64 //生成区块的时间
Nonce int //工作量证明算法的计数器
}
对应的NewBlock,NewGensisBlock也应该修改:
//生成一个新的区块方法
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block{
//GO语言给Block赋值{}里面属性顺序可以打乱,但必须制定元素 如{Timestamp:time.Now().Unix()...}
block := &Block{Timestamp:time.Now().Unix(), Transactions:transactions, PrevBlockHash:prevBlockHash, Hash:[]byte{},Nonce:0}
//工作证明
pow :=NewProofOfWork(block)
//工作量证明返回计数器和hash
nonce, hash := pow.Run()
block.Hash = hash[:]
block.Nonce = nonce
return block
}
//创建并返回创世纪Block
func NewGenesisBlock(coinbase *Transaction) *Block {
return NewBlock([]*Transaction{coinbase}, []byte{})
}
blockchain:
/ 创新一个新的区块数据
func CreateBlockchain(address string) *Blockchain {
...
err = db.Update(func(tx *bolt.Tx) error {
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())
...
}
这里,函数将获得一个地址,该地址将获得挖掘创世块的奖励。(我们这里奖励为10)
工作量证明:
Proof-of-Work算法必须考虑存储在块中的事务,以保证区块链作为事务存储的一致性和可靠性。所以现在我们必须修改ProofOfWork.prepareData方法:
//将区块体里面的数据转换成一个字节码数组,为下一个区块准备数据
func (pow *ProofOfWork) prepareData(nonce int) []byte {
//注意一定要将原始数据转换成[]byte,不能直接从字符串转
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.HashTransactions(),
utils.IntToHex(pow.block.Timestamp),
utils.IntToHex(int64(targetBits)),
utils.IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
将data改成hashTransactions:
//返回块状事务的hash
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[:]
}
我们使用散列作为提供数据的唯一表示的机制。我们希望块中的所有事务都由一个散列唯一标识。为了达到这个目的,我们得到每个事务的哈希值,连接它们,并获得连接组合的哈希值。
比特币使用更复杂的技术:它将所有包含在块中的事务表示为Merkle树,并在Proof-of-Work系统中使用树的根散列。这种方法允许快速检查块是否包含某个事务,只有根散列并且不下载所有事务。(后续会详细讲解Merkle算法)
好了我们现在尝试一下CreateBlockchain:
编译:
C:\go-worke\src\github.com\study-bitcoin-go>go build github.com/study-bitcoin-go
执行createblockchain命令:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address even
输出:
Dig into mine 00000860adb64e2ca9c83d0f665f7ec3148ec9d32b64cb97f2481712c4d94d79
Done!
目前为止我们实现开采创世块奖励。但我们要如何实现查询余额呢?
未使用交易输出
我们需要找到所有未使用的交易输出(UTXO)。未使用意味着这些输出在任何输入中都未被引用。在上图中,这些是:
- tx0,输出1;
- tx1,输出0;
- tx3,输出0;
- tx4,输出0。
当然,当我们检查余额时,我们不需要所有这些,但只有那些可以用我们拥有的密钥解锁的(当前我们没有实现密钥并且将使用用户定义的地址)。首先,我们来定义输入和输出上的锁定 - 解锁方法:
//通过检查地址是否启动了事务
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
//检查输出是否可以使用所提供的数据进行解锁
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
这里我们只是比较脚本字段unlockingData。在我们实现基于私钥的地址后,这些作品将在未来的文章中得到改进。
下一步 - 查找包含未使用产出的交易 - 相当困难:
//查询未处理的事务
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 {
txID := hex.EncodeToString(tx.ID) //交易ID转换成string
Outputs:
for outIdx, out := range tx.Vout {
// Was the output spent?
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) {
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
}
}
}
}
if len(block.PrevBlockHash) == 0 {
break
}
}
return unspentTXs
}
该函数返回一个包含未使用输出的事务列表。为了计算余额,我们需要一个函数来处理事务并仅返回输出:
//发现并返回所有未使用的事务输出
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
}
客户端cli getbalance命令:
//查询余额
func (cli *CLI) getBalance(address string) {
bc := block.NewBlockchain(address)
defer block.Close(bc)
balance := 0
//查询所有未经使用的交易地址
UTXOs := bc.FindUTXO(address)
//算出未使用的交易地址的value和
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of '%s': %d\n", address, balance)
}
账户余额是由账户地址锁定的所有未使用的交易输出值的总和。
现在我们检查一下地址为even的钱:
重新编译后
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
Balance of 'even': 10
这就是第一笔钱,接下来我们需要实现给一个地址转币
现在,我们要发送一些硬币给别人。为此,我们需要创建一个新的事务,将它放在一个块中,然后挖掘块。到目前为止,我们只实现了coinbase交易(这是一种特殊类型的交易),现在我们需要一个一般交易:
//创建一个新的未经使用的交易输出
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")
}
// Build a list of inputs
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
if err != nil {
log.Panic(err)
}
for _, out := range outs {
input := TXInput{txID, out, from}
inputs = append(inputs, input)
}
}
// Build a list of outputs
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
}
在创建新的输出之前,我们首先必须找到所有未使用的输出并确保它们存储足够的值。这是什么FindSpendableOutputs方法。之后,为每个找到的输出创建一个引用它的输入。接下来,我们创建两个输出:
- 一个与接收器地址锁定的。这是硬币实际转移到其他地址。
- 一个与发件人地址锁定在一起。这是一个变化。只有在未使用的输出持有比新事务所需的更多价值时才会创建。记住:输出是不可分割的。
FindSpendableOutputs方法基于FindUnspentTransactions我们之前定义的方法:
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 {
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
}
该方法迭代所有未使用的事务并累积其值。当累计值大于或等于我们要转移的金额时,它停止并返回按事务ID分组的累计值和输出索引。我们不想花更多的钱。
现在我们可以修改该Blockchain.MineBlock方法:
//开采区块
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
var lastHash []byte//最新一个hash
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)
//修改"l"的hash
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)
}
bc.tip = newBlock.Hash
return nil
})
}
cli添加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!")
}
发送硬币意味着创建一个交易并通过挖掘一个块将其添加到区块链。但比特币不会立即做到这一点(就像我们一样)。相反,它将所有新事务放入内存池(或mempool)中,并且当矿工准备开采块时,它将从mempool获取所有事务并创建候选块。交易只有在包含它们的区块被挖掘并添加到区块链时才会被确认。
现在我们来试试发币:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send -from even -to jim -amount 3
Mining the block containing "S4t�.U��̧�ϤH��vWE���[�P�╔���"
Dig into mine 00000cde90398b754eebe6d7820dab6e6260ae724712b72706846ec6d331fe2c
Success!
分别查询even、tom钱包:
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address even
Balance of 'even': 7
C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address jim
Balance of 'jim': 3
好了目前我们实现了交易功能。缺少:
- 地址。我们还没有真实的,基于私钥的地址。
- 奖励。采矿块绝对没有利润!
- UTXO设置。达到平衡需要扫描整个区块链,当区块数量很多时可能需要很长时间。此外,如果我们想验证以后的交易,可能需要很长时间。UTXO集旨在解决这些问题并快速处理交易。
- 内存池。这是交易在打包成块之前存储的地方。在我们当前的实现中,一个块只包含一个事务,而且效率很低。
后续降会实现地址、钱包、挖矿奖励、网络等。
5.比特币交易示例总结
交易 | 目的 | 输入 | 输出 | 签名 | 差额 |
---|---|---|---|---|---|
T0 | A转给B | 他人向A交易的输出 | B账号可以使用该交易 | A签确认 | 输入减去输出,为交易服务费 |
T1 | B转给C | T0的输出 | C账户可以使用该交易 | B签名确认 | 输入减去输出,为交易服务费 |
··· | X转给Y | 他人向X交易的输出 | Y账户可以使用该交易 | X签名确认 | 输入减去输出,为交易服务费 |
这就是简单交易流程,我们以上代码并没有实现挖矿奖励。后续将实现钱包,优化查询交易,挖矿奖励、网络。
资料
- 原文来源:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/
- 本文源码:https://github.com/Even521/study-bitcion-go/tree/part4
- java学习:https://www.jianshu.com/p/66c065018c7a
- 区块链基础视频学习:https://www.bilibili.com/video/av19620321/
- 区块链测试demo:https://anders.com/blockchain/blockchain.html
- 区块链QQ交流群:489512556