英文源地址
简介
事务是比特币的核心, 区块链的唯一目的是以安全可靠的方式存储交易, 因此在交易创建后没有人可以修改. 今天我们开始实现事务, 但由于这是一个相当大的主题, 我将它分成两部分: 在这一部分中, 我们将实现事务的通用机制, 在第二部分中, 我们将研究细节.
此外, 由于代码的变化是巨大的, 在这里描述它们是没有意义的. 你可以在这里查看到所有的变化.
There is no spoon(黑客帝国台词)
如果你曾经开发过一个web应用程序, 为了实现支付, 你可能会在数据库中创建这些表: 账户和交易.一个账户将存储有关用户的信息, 包括他们的个人信息和余额, 一次交易将存储一个账户到另一个账户的资金转移的信息. 在比特币中, 交易以完全不同的方式实现. 它们是:
- 没有账户
- 没有余额
- 没有地址
- 没有硬币
- 没有发送者和接收者
由于区块链是一个公开和开放的数据库, 我们不想存储关于钱包所有者的任何敏感信息.硬币不在账户中收纳. 交易不会把钱从一个地址转移到另一个地址.没有保存账户余额的字段或属性. 只有交易, 但是交易里面有什么呢?
比特币交易
交易是输入和输出的组合
type Transaction struct {ID []byteVin []TXInputVout []TXOutput
}
对于每一笔新的交易, 它的输入会引用前一笔交易的输出(这里有个例外, coinbase交易), 引用就是花费的意思.所谓引用之前的一个输入, 也就是将之前的一个输出包含在另一笔交易的输入当中, 就是花费之前的交易输出. 交易的输出, 就是币实际存储的地方. 下面的图示阐释了交易之间的互相关联.
需要注意的是:
- 有一些输出并没有被关联到某个输入上
- 一笔交易的输入可以引用之前多笔交易的输出
- 一个输入必须引用一个输出
贯穿全文, 我们将会使用像’money’, ‘coin’, ‘spend’, ‘send’, 'account’等等这样的词. 但是在比特币中, 其实并不存在这些概念. 交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value), 而这些值只可以被锁定它们的人解锁(unlock).
(每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。)
交易输出
先从输出开始
type TXOutput struct {Value intScriptPubKey string
}
输出主要包含两部分:
- 一定量的比特币(Value)
- 一个锁定脚本(ScriptPubKey), 要花这笔钱, 必须解锁该脚本
实际上, 正式输出里面存储了’币’(注意, 也就是上面的Value字段). 而这里的存储, 指的是用一个数学难题对输出进行锁定, 这个难题被存储在ScriptPubKey里面. 在内部, 比特币使用了一个叫做Script的脚本语言, 用它来定义锁定和解锁输出的逻辑. 虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之), 并不复杂, 但是我们也并不会讨论它的细节. 你可以在这里找到详细解释.
在比特币中, value字段存储的是satoshi的数量, 而不是BTC的数量. 一个satoshi等于一亿分之一的BTC(0.00000001BTC), 这也是比特币里面最小的货币单位(就像是一分的硬币)
由于没有实现地址(address), 所以目前我们会避免涉及逻辑相关的完整脚本. ScriptPubKey将会存储一个任意的字符串(用户定义的钱包地址).
顺表说一下, 有了一个这样的脚本语言, 也意味着比特币其实可以作为一个智能合约平台.
关于输出, 非常重要的一点是: 它们是不可再分的(indivisible). 也就是说, 你无法仅引用其中的一部分. 要么不用, 如果要用, 必须一次性用完. 当一个新的交易中引用了某个输出, 那么这个输出必须被全部花费. 如果它的值比需要的值大, 那么就会产生一个找零, 找零会返还给发送方.这跟现实世界的场景十分类似, 当你像要支付时, 如果一个东西值1美元, 而你给了一个5美元的纸币, 那么你会得到一个4美元的找零.
交易输入
这里是输入
type TXInput struct {Txid []byteVout intScriptSig string
}
正如之前所提到的, 一个输入引用了一个输出: Txid存储的时之前交易的ID, Vout存储的时该输出在那笔交易中所有输出的索引(因为一笔交易可以有多个输出, 需要有信息指明时具体的哪一个).ScriptSig是一个脚本, 提供了可解锁输出结构里面ScriptPubKey字段的数据. 如果ScriptSig提供的数据是正确的, 那么输出就会解锁, 然后被解锁的值可以被用于产生新的输出; 如果数据不正确, 输出就无法被引用在输入中, 或者说, 无法使用这个输出. 这种机制, 保证了用户无法花费属于其他人的币.
再次强调, 由于我们还没有实现地址, 所以目前ScriptSig将仅仅存储一个用户自定义的任意钱包地址. 我们会在下一篇文章中实现公钥(public key)和签名(signature).
来简要总结一下.输出, 就是’币’存储的地方. 每个输出都会带有一个解锁脚本, 这个脚本定义了解锁该输出的逻辑. 每笔新的交易, 必须至少有一个输入与输出. 一个输入引用了之前一笔的输出, 并提供了解锁数据(也就是ScriptSig字段), 该数据会被用于在输出的解锁脚本中解锁输出, 解锁完成后即可使用它的值去产生新的输出.
每一笔输入都是之前一笔交易的输出, 那么假设从某一笔交易开始不断往前追溯, 它所涉及的输入和输出到底是谁先存在呢?换个说法, 这是个鸡和蛋谁先谁后的问题, 是先有蛋还是先有鸡呢?
先有蛋
在比特币中, 是先有蛋, 然后才有鸡的. 输入引用输出的逻辑, 是经典的’蛋还是鸡’的问题: 输入先产生输出, 然后输出使得输入成为可能. 在比特币中, 最先有输出, 然后才有输入. 换而言之, 每一笔交易只有输出, 没有输入.
当miner挖出一个新的区块时, 它会向新的区块添加一个coinbase交易. coinbase交易是一种特殊的交易, 它不需要引用之前一笔交易的输出. 它’凭空’产生了币(也就是产生了新币), 这是miner获得挖出mining的奖励, 也可以理解为’发行新币’.
在区块链的最初, 也就是第一个块, 叫做创世区块. 正是这个创世区块, 产生了区块链最开始的输出.对于创世区块, 不需要引用之前交易的输出.因为在创世区块之前根本不存在交易, 也就是不存在交易输出.
来创建一个coinbase交易:
func NewCoinbaseTX(to, data string) *Transaction {if data == "" {data = fmt.Sprintf("Reward to %s", to)}txin := TXInput{[]byte{}, -1, data}txout := TXOutput{subsidy, to}tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}tx.SetID()return &tx
}
coinbase交易只有一个输出, 没有输入. 在我们的实现中, 它表现为Txid为空, Vout等于-1.并且, 在当前实现中, coinbase交易也没有再ScriptSig中存储脚本, 而只是存储了一个任意的字符串data.
在比特币中, 第一笔coinbase交易包含了如下信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看
subsidy是挖出新区块的奖励金. 在比特币中, 实际并没有存储这个数字, 而是基于区块总数进行计算而得: 区块总数除以210000就是subsidy. 挖出创世区块的奖励是50BTC, 每挖出210000个区块后, 奖励减半. 在我们的实现中, 这个奖励值将会是一个常量(至少目前是).
将交易保存到区块链
从现在开始, 每个区块必须存储至少一笔交易. 如果没有交易, 也就不可能出新的块. 这意味着我们应该溢出Block的Data字段, 取而代之的是存储交易:
type Block struct {Timestamp int64Transactions []*TransactionPrevBlockHash []byteHash []byteNonce int
}
NewBlock和NewGenesisBlock也必须做出相应的改变:
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 = noncereturn block
}func NewGenesisBlock(coinbase *Transaction) *Block {return NewBlock([]*Transaction{coinbase}, []byte{})
}
接下来修改创建区块链的函数:
func CreateBlockchain(address string) *Blockchain {var tip []bytedb, _ := bolt.Open(dbFile, 0600, nil)_ = db.Update(func(tx *bolt.Tx) error {cbtx := NewCoinbaseTX(address, genesisCoinbaseData)genesis := NewGenesisBlock(cbtx)b, _ := tx.CreateBucket([]byte(blocksBucket))if b == nil {b, _ := tx.CreateBucket([]byte(blocksBucket))_ = b.Put(genesis.Hash, genesis.Serialize())_ = b.Put([]byte("l"), genesis.Hash)tip = genesis.Hash} else {tip = b.Get([]byte("l"))}return nil})bc := Blockchain{tip, db}return &bc
}
现在, 这个函数会接受一个地址作为参数, 这个地址将会被用来接收挖出创世区块的奖励.
工作量证明
工作量证明算法必须要将存储在区块里面的交易考虑进去, 从而保证区块链交易存储的一致性和可靠性. 所以, 我们必须修改ProofOfWork.prepareData方法:
func (pow *ProofOfWork) prepareData(nonce int) []byte {data := bytes.Join([][]byte{pow.block.PrevBlockHash,pow.block.HashTransactions(), // This line was changedIntToHex(pow.block.Timestamp),IntToHex(int64(targetBits)),IntToHex(int64(nonce)),},[]byte{},)return data
}
不像之前使用pow.block.Data, 现在我们使用pow.block.HashTransactions():
func (b *Block) HashTransactions() []byte {var txHashes [][]bytevar txHash [32]bytefor _, tx := range b.Transactions {txHashes = append(txHashes, tx.ID)}txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))return txHash[:]
}
通过哈希值提供数据的唯一表示, 这种做法我们已经不是第一次遇到了. 我们想要通过仅仅一个哈希值, 就可以识别一个块里面的所有交易. 为此, 先获得每笔交易的哈希值, 然后将它们关联起来, 最后获得一个连接后的组合哈希值.
比特币使用了一个更加复杂的技术: 它将一个区块里面包含的所有交易表示为一个Merkle tree, 然后在工作量证明系统中使用树的根哈希(root hash),这个方法能够让我们快速检索一个块里面是否包含了某笔交易, 即只需root hash而无需下载所有交易即可完成判断.
来检查目前为止是否正确:
╰─ ./blockchain_impl_in_go createblockchain -address Ivan ─╯
00000060b65eb7a78b68206835d12e06c8b00940da37b5c773c1d465a8a3a35fDone!
很好, 我们已经获得了第一笔mining奖励, 但是, 我们要如何查看余额呢?
未花费的交易输出
我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO), 未花费(unspent)指的是这个输出还没有被包含在任何交易的输入中, 或者说没有被任何输入引用. 在上面图示中, 未花费的输出是
- tx0, output 1;
- tx1, output 0;
- tx3, output 0;
- tx4, output 0.
当然了, 检查余额时, 我们并不需要知道整个区块链上所哟䣌UTXO, 只需要关注那些我们能解锁的那些UTXO(目前我们还没有实现密钥, 所以我们将会使用用户定义的地址来代替). 首先, 让我们定义在输入和输出上的锁定和解锁方法:
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {return in.ScriptSig == unlockingData
}func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {return out.ScriptPubKey == unlockingData
}
在这里, 我们只是将script字段与unlockingData进行了比较. 在后续文章我们基于私钥实现了地址以后, 会对这部分进行改进.
下一步, 找到包含未花费输出的交易, 这一步其实相当困难:
func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {var upsentTXs []TransactionspentTXOs := make(map[string][]int)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] {if spentOut == outIdx {continue Outputs}}}if out.CanBeUnlockedWith(address) {upsentTXs = append(upsentTXs, *tx)}}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 upsentTXs
}
由于交易被存储在区块里, 所以我们不得不检查区块链里的每一笔交易.从输出开始:
if out.CanBeUnlockedWith(address) {unspentTXs = append(unspentTXs, tx)
}
如果一个输出被一个地址锁定, 并且这个地址恰好是我们要找的地址, 那么这个输出就是我们想要的. 不过在获得它之前, 我们需要检查该输出是否已经被包含在一个交易的输出中, 也就是检查它是否已经被花费了:
if spentTXOs[txID] != nil {for _, spentOut := range spentTXOs[txID] {if spentOut == outIdx {continue Outputs}}
}
我们跳过那些已经被包含在其他输入中的输出(这说明这个输出已经被花费, 无法再使用了). 检查完输出以后, 我们将给定地址所有能够解锁输出的输入聚合起来(这并不适用于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)}}
}
这个函数返回了一个交易列表, 里面包含了未花费输出.为了计算余额, 我们还需要一个函数将这些交易作为输入, 然后返回一个输出:
func (bc *Blockchain) FindUTXO(address string) []TXOutput {var UTXOs []TXOutputunspentTransactions := bc.FindUnspentTransactions(address)for _, tx := range unspentTransactions {for _, out := range tx.Vout {if out.CanBeUnlockedWith(address) {UTXOs = append(UTXOs, out)}}}return UTXOs
}
就是这么多了!现在我们来实现getbalance命令
func (cli *CLI) getBalance(address string) {bc := NewBlockchain(address)defer bc.db.Close()balance := 0UTXOs := bc.FindUTXO(address)for _, out := range UTXOs {balance += out.Value}fmt.Printf("Balance of '%s': %d\n", address, balance)
}
账户余额就是由账户地址锁定的所有未花费交易输出的综合.
在挖出创世区块后, 来检查一下我们的余额:
╰─ ./blockchain_impl_in_go getbalance -address Ivan ─╯
Balance of Ivan: 10
这就是我们的第一笔钱!
发送币
现在, 我们想要给其他人发送一些币. 为此, 我们需要创建一笔新的交易, 将它放到一个区块里, 然后挖出这个区块. 之前我们只实现了coinbase交易(这是一种特殊的交易), 现在我们需要一种通用的普通交易.
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {var inputs []TXInputvar outputs []TXOutputacc, validOutputs := bc.FindSpendableOutputs(from, amount)if acc < amount {log.Panic("ERROR: Not enough funds")}for txid, outs := range validOutputs {txID, _ := 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}tx.SetID()return &tx
}
在创建新的输出前, 我们首先必须找到所有的未花费输出, 并且确保它们有足够的价值(value),这就是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.ValueunspentOutputs[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_ = bc.db.View(func(tx *bolt.Tx) error {b := tx.Bucket([]byte(blocksBucket))lastHash = b.Get([]byte("l"))return nil})newBlock := NewBlock(transactions, lastHash)_ = bc.db.Update(func(tx *bolt.Tx) error {b := tx.Bucket([]byte(blocksBucket))_ = b.Put(newBlock.Hash, newBlock.Serialize())_ = b.Put([]byte("l"), newBlock.Hash)bc.tip = newBlock.Hashreturn nil})
}
最后, 让我们实现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),然后当miner准备挖出一个新区块时, 它从内存池中取出所有交易, 创建一个候选块. 只有当包含这些交易的区块被挖出来, 并添加到区块链以后, 里面的交易才开始确认.
让我们检查一下发送币是否能工作:
$ blockchain_go send -from Ivan -to Pedro -amount 6
000000655594c9b0c6c1034ec0236d91d2115bbd74ed008901ea81c29f231d7fSuccess!╰─ ./blockchain_impl_in_go getbalance -address Ivan ─╯
Balance of Ivan: 4╰─ ./blockchain_impl_in_go getbalance -address Pedro ─╯
Balance of Pedro: 6
很好!现在, 让我们创建更多的交易, 确保从多个输出中发送币也正常工作:
╰─ ./blockchain_impl_in_go send -from Pedro -to Helen -amount 2 ─╯
0000003d3c12819d42b9c9a2968a803b651a775af1af262d51384d6c2577f8e1Success!╰─ ./blockchain_impl_in_go send -from Ivan -to Helen -amount 2 ─╯
00000050e41ef1982c2aab6ad43d748b7f65c720a34a6fb9f144960d911e4711Success!
现在, Helen的币被锁定在了两个输出中: 一个来自Pedro, 一个来自Lvan.让我们把它们发送给其他人:
╰─ ./blockchain_impl_in_go send -from Helen -to Rachel -amount 3 ─╯
000000417eabcad236c9d42c5c33d72c5e63a293c2b5491390f362d1d3fcdb89Success!$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3
看起来没问题!现在, 来测试一些失败的情况:
$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2
总结
虽然不容易, 但是现在终于实现交易了!不过, 我们依然缺少了一些像比特币那样的一些关键特性:
- 地址(address). 我们现在还没有基于私钥(private key)的真实地址.
- 奖励(reward). 现在mining时肯定无法盈利的!
- UTXO集. 获取余额需要扫描整个区块链, 而当区块非常多时, 这么做就会花费很长时间. 并且, 如果我们想要验证后续交易, 也需要花费很长时间. 而UTXO集就是为了解决这些问题, 加快交易相关的操作.
- 内存池(mempool). 在交易被打包到区块之前, 这些交易被存储在内存池里面. 在我们目前的实现中,一个块仅仅包含一笔交易, 这是相当低效的.
link
Full source codes
Transaction
Merkle tree
Coinbase