本章源代码地址:[https://github.com/daleboy/blockchain4]
# 关于区块链的交易
交易是一件复杂的事情,我们在本章中实现交易的通用部分,后面将就细节进行实现。
交易(transaction)是比特币的核心所在,而区块链的唯一目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。
与现实不同,在比特币中,支付是另外一种完全不同的方式:
1. 没有账户(account)
2. 没有余额(balance)
3. 没有住址(address)
4. 没有货币(coin)
5. 没有发送人和接收人
# 比特币交易
一笔交易由一些输入(input)和输出(output)组合而来:
![](https://img.kancloud.cn/09/06/0906055857723da1b343822fd7b4120b_700x361.png)
~~~
type Transaction struct {//交易的结构
ID []byte
Vin []TXInput
Vout []TXOutput
}
~~~
对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,也就是我们待会儿要谈到的 coinbase 交易)。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中。交易的输出,也就是币实际存储的地方。
注意:
1. 有一些输出并没有被关联到某个输入上
2. 一笔交易的输入可以引用之前多笔交易的输出
3. 一个输入必须引用一个输出
贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,实际并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些价值(value),而这些价值只可以被锁定它们的人解锁(unlock)。
# 交易输出
让我们先从输出(output)开始:
~~~
type TXOutput struct {//交易输出的结构
Value int//存储的币
ScriptPubKey string//解锁脚本,这个脚本定义了解锁该输出的逻辑。
}
~~~
实际上,正是输出里面存储了“币”(注意,也就是上面的`Value`字段)。而这里的存储,指的是用一个数学难题对输出进行锁定,这个难题被存储在`ScriptPubKey`里面。在内部,比特币使用了一个叫做*Script*的脚本语言,用它来定义锁定和解锁输出的逻辑。虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之),并不复杂,但是我们并不会在这里讨论它的细节。你可以在[这里](https://link.jianshu.com/?t=https://en.bitcoin.it/wiki/Script)找到详细解释。
~~~
在比特币中,`value`字段存储的是*satoshi*的数量,而不是>有 BTC 的数量。一个*satoshi*等于一百万分之一的 >BTC(0.00000001 BTC),这也是比特币里面最小的货币单位>(就像是 1 分的硬币)。
~~~
由于还没有实现地址(address),所以目前我们会避免涉及逻辑相关的完整脚本。`ScriptPubKey`将会存储一个任意的字符串(用户定义的钱包地址)。
~~~
顺便说一下,有了一个这样的脚本语言,也意味着比特币其实也可以作为一个智能合约平台。
~~~
关于输出,非常重要的一点是:它们是**不可再分的(invisible)**,这也就是说,你无法仅引用它的其中某一部分。要么不用,如果要用,必须一次性用完。当一个新的交易中引用了某个输出,那么这个输出必须被全部花费。如果它的值比需要的值大,那么就会产生一个找零,找零会返还给发送方。
# 交易输入
这里是输入:
~~~
type TXInput struct {//交易输入的结构
Txid []byte//这笔交易的 ID
Vout int//该输出在这笔交易中所有输出的索引(因为一笔交易可能有多个输出,需要有信息指明是具体的哪一个)。
ScriptSig string
}
~~~
`ScriptSig`是一个脚本,提供了可作用于一个输出的`ScriptPubKey`的数据。如果`ScriptSig`提供的数据是正确的,那么输出就会被解锁,然后被解锁的值就可以被用于产生新的输出;如果数据不正确,输出就无法被引用在输入中,或者说,也就是无法使用这个输出。这种机制,保证了用户无法花费属于其他人的币。
再次强调,由于我们还没有实现地址,所以`ScriptSig`将仅仅存储一个任意用户定义的钱包地址。我们会在下一篇文章中实现公钥(public key)和签名(signature)。
在比特币中,每一笔输入都是之前一笔交易的输出,那么从一笔交易开始不断往前追溯,它涉及的输入和输出到底是谁先存在呢?答案是:最先有输出,然后才有输入。换而言之,第一笔交易只有输出,没有输入。
当矿工挖出一个新的块时,它会向新的块中添加一个**coinbase**交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这也是矿工获得挖出新块的奖励,可以理解为“发行新币”。
在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前交易的输出。因为在创世块之前根本不存在交易,也就没有不存在有交易输出。
来创建一个 coinbase 交易:
~~~
func NewCoinbaseTX(to, data string) *Transaction {
if data == "" {
data = fmt.Sprintf("Reward to '%s'", to)
}
txin := TXInput{[]byte{}, -1, data}//输入结构:Txid为空,Vout为-1
txout := TXOutput{subsidy, to}//subsidy为奖励的数额
tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}//只有一个输出,没有输入。
tx.SetID()
return &tx
}
~~~
`subsidy`是奖励的数额。在比特币中,实际并没有存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是`subsidy`。挖出创世块的奖励是 50 BTC,每挖出`210000`个块后,奖励减半。在我们的实现中,这个奖励值将会是一个常量(至少目前是)。
# 将交易保存到区块链
从现在开始,每个块必须存储至少一笔交易。如果没有交易,也就不可能挖出新的块。这意味着我们应该移除`Block`的`Data`字段,取而代之的是存储交易:
~~~
type Block struct {
Timestamp int64
Transactions []*Transaction//交易替代data string
PrevBlockHash []byte
Hash []byte
Nonce int
}
~~~
`NewBlock`和`NewGenesisBlock`也必须做出相应改变:
~~~
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
...
}
func NewGenesisBlock(coinbase *Transaction) *Block {//注意,参数是coinbase交易
return NewBlock([]*Transaction{coinbase}, []byte{})
}
~~~
接下来修改创建新链的函数:
~~~
func CreateBlockchain(address string) *Blockchain {
...
err = db.Update(func(tx *bolt.Tx) error {
cbtx := NewCoinbaseTX(address, genesisCoinbaseData)//创建一个coinbase交易
genesis := NewGenesisBlock(cbtx)//包含coinbase交易挖出创始区块
d := genesis.Serialize()
err = b.Put(genesis.Hash, d) //将创始区块序列化后插入到数据库表中
if err != nil {
log.Panic(err)
}
b, err := tx.CreateBucket([]byte(blocksBucket))
err = b.Put(genesis.Hash, genesis.Serialize())
...
})
...
}
~~~
现在,这个函数会接受一个地址作为参数,这个地址会用来接收挖出创世块的奖励。
工作量证明算法必须要将存储在区块里面的交易考虑进去,以此保证区块链交易存储的一致性和可靠性。所以,我们必须修改`ProofOfWork.prepareData`方法:
~~~
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.HashTransactions(), // 计算交易的哈希,而不再是data string的哈希
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
~~~
计算交易的哈希方法如下:
~~~
func (b *Block) HashTransactions() []byte {
var txHashes [][]byte
var txHash [32]byte
for _, tx := range b.Transactions {//一个block可能有多个交易
txHashes = append(txHashes, tx.ID)//只使用交易ID,不使用交易的输入和输出
}
/注意,Join第二个参数是一个空的byte数组,也就是说,连接时候,两个相邻的[]byte之间不留间隔符
txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))//将所有交易ID连接起来计算处理的哈希,作为区块的交易哈希
return txHash[:]
}
~~~
~~~
比特币使用了一个更加复杂的技术:它将一个块里面包含的所有交易表示为一个[Merkle tree](https://link.jianshu.com/?t=https://en.wikipedia.org/wiki/Merkle_tree),然后在工作量证明系统中使用树的根哈希(root hash)。这个方法能够让我们快速检索一个块里面是否包含了某笔交易,即只需 root hash 而无需下载所有交易即可完成判断。
~~~
# 未花费的交易输出
我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO)。**未花费(unspent)**指的是这个输出还没有被包含在任何交易的输入中,或者说没有被任何输入引用。
当然了,当我们检查余额时,一般是指那些我们能够解锁的那些 UTXO(目前我们还没有实现密钥,所以我们将会使用用户定义的地址来代替,所以这里查询的是某个地址的余额)。首先,让我们定义在输入和输出上的锁定和解锁方法:
~~~
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
~~~
首先,让我们定义在输入和输出上的解锁方法:
~~~
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
~~~
在这里,我们只是将 script 字段与`unlockingData`进行了比较(能解锁,说明交易与其有关)。在后续文章我们基于私钥实现了地址以后,会对这部分进行改进。
下一步,找到包含未花费输出的交易:
~~~
//查询address地址的未花费输出的交易
//如果一个输出被一个地址锁定,并且这个地址恰好是我们要找的未花费交易输出的地址,那么这个输出就是我们想要的。
//不过在获取它之前,我们需要检查该输出是否已经被包含在一个输入中,也就是检查它是否已经被花费了:
//由于交易被存储在区块里,所以我们不得不检查区块链里的每一笔交易。从输出开始:
func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
var unspentTXs []Transaction//未花费交易(输出还没包含到其它的交易的输入之中)
spentTXOs := make(map[string][]int)//已花费交易输出(查询所有交易输入可获得)
//查询获得所有已花费输出:spentTXOs
bci := bc.Iterator()
for {//查询区块链中所有区块
block := bci.Next()
for _, tx := range block.Transactions {//查询区块里面的所有交易
txID := hex.EncodeToString(tx.ID)
if tx.IsCoinbase() == false {//如果不是创始区块交易(创始区块没有输入)
for _, in := range tx.Vin {//查询交易中的每一个输入
if in.CanUnlockOutputWith(address) {//address地址是否可以解锁输入中的输入
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)//可以解锁,将当前交易加入到已花费交易输出数组中
}
}
}
}
if len(block.PrevBlockHash) == 0 {//已经循环结束:创始区块的PrevBlockHash=[]byte{)
break
}
}
//查询获得所有为花费交易:unspentTXs
bci = bc.Iterator()
for {//查询区块链中所有区块
block := bci.Next()
for _, tx := range block.Transactions {//查询区块里面的所有交易
txID := hex.EncodeToString(tx.ID)
Outputs:
for outIdx, out := range tx.Vout {//查询交易中的每一个输出
// 输出已经花费了吗?
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {//如果交易ID在已花费支出map中已经存在,说明已经花费了,检查下一个输出
if spentOut == outIdx {
continue Outputs//返回到前面的Outputs位置
}
}
}
//该输出没有出现在已花费支出中,加入到未花费交易输出数组中
if out.CanBeUnlockedWith(address) {//address地址是否可以解锁输出中的输出
unspentTXs = append(unspentTXs, *tx)//可以解锁,将当前交易加入到未花费交易输出数组中
}
}
if len(block.PrevBlockHash) == 0 {//已经循环结束:创始区块的PrevBlockHash=[]byte{)
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
}
~~~
# 通过未花费输出计算账户余额
~~~
func (cli *CLI) getBalance(address string) {
bc := NewBlockchain(address)
defer bc.db.Close()
balance := 0
UTXOs := bc.FindUTXO(address)
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of '%s': %d\n", address, balance)
}
~~~
#转账
将金钱从一个账户转移到另外一个账户,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的交易:
~~~
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
var inputs []TXInput
var outputs []TXOutput
//validOutputs为sender为此交易提供的输出,不一定是sender的全部输出
//acc为sender发出的全部币数
acc, validOutputs := bc.FindSpendableOutputs(from, amount)
if acc < amount {
log.Panic("ERROR: 没有交易所需的足够的钱")
}
// 建立输入参数
for txid, outs := range validOutputs {
txID, err := hex.DecodeString(txid)
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}) //找零,退给转账人
}
tx := Transaction{nil, inputs, outputs}//初始交易ID设为nil
tx.SetID()//紧接着设置交易的ID
return &tx
}
~~~
转账方法:
~~~
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("转账成功!")
}
~~~
# 程序执行验证
## 创建新的区块链
![](https://img.kancloud.cn/13/7b/137b67c95ef0b30045df44a679d7cac6_483x132.png)
## 获得余额
![](https://img.kancloud.cn/5f/53/5f53b239053eb74327ab6c9b4e4fb554_731x80.png)
## 转账
![](https://img.kancloud.cn/f5/28/f52822da2f6c194f704d5f682ac3ea28_785x202.png)
## 打印区块链
![](https://img.kancloud.cn/c2/d9/c2d958ce1dbd76eb595eaf7da61a6f2d_1017x504.png)
- 重要更新说明
- linechain发布
- linechain新版设计
- 引言一
- 引言二
- 引言三
- vs-code设置及开发环境设置
- BoltDB数据库应用
- 关于Go语言、VS-code的一些Tips
- 区块链的架构
- 网络通信与区块链
- 单元测试
- 比特币脚本语言
- 关于区块链的一些概念
- 区块链组件
- 区块链第一版:基本原型
- 区块链第二版:增加工作量证明
- 区块链第三版:持久化
- 区块链第四版:交易
- 区块链第五版:实现钱包
- 区块链第六版:实现UTXO集
- 区块链第七版:网络
- 阶段小结
- 区块链第八版:P2P
- P2P网络架构
- 区块链网络层
- P2P区块链最简体验
- libp2p建立P2P网络的关键概念
- 区块链结构层设计与实现
- 用户交互层设计与实现
- 网络层设计与实现
- 建立节点发现机制
- 向区块链网络请求区块信息
- 向区块链网络发布消息
- 运行区块链
- LineChain
- 系统运行流程
- Multihash
- 区块链网络的节点发现机制深入探讨
- DHT
- Bootstrap
- 连接到所有引导节点
- Advertise
- 搜索其它peers
- 连接到搜到的其它peers
- 区块链网络的消息订发布-订阅机制深入探讨
- LineChain:适用于智能合约编程的脚本语言支持
- LineChain:解决分叉问题
- LineChain:多重签名
- libp2p升级到v0.22版本
- 以太坊基础
- 重温以太坊的树结构
- 世界状态树
- (智能合约)账户存储树
- 交易树
- 交易收据树
- 小结
- 以太坊的存储结构
- 以太坊状态数据库
- MPT
- 以太坊POW共识算法
- 智能合约存储
- Polygon Edge
- block结构
- transaction数据结构
- 数据结构小结
- 关于本区块链的一些说明
- UML工具-PlantUML
- libp2p介绍
- JSON-RPC
- docker制作:启动多个应用系统
- Dockerfile
- docker-entrypoint.sh
- supervisord.conf
- docker run
- nginx.conf
- docker基础操作整理
- jupyter计算交互环境
- git技巧一
- git技巧二
- 使用github项目的最佳实践
- windows下package管理工具