Introduction
交易是比特币的核心,区块链的唯一目的是以安全可靠的方式存储交易,因此没有人可以在创建交易后对其进行修改。今天我们开始实施交易。但是因为这是一个相当大的话题,我将它分为两部分:在这部分中,我们将实现交易的一般机制,在第二部分,我们将通过细节进行处理。
此外,由于代码更改很大,因此在这里描述所有代码都没有意义。您可以看到所有更改here.
There is no spoon
如果您曾经开发过Web应用程序,为了实现付款,您可能会在数据库中创建这些表:accounts
和 transactions
。帐户将存储关于用户的信息,包括他们的个人信息和余额,并且交易将存储关于从一个帐户转移到另一个帐户的钱的信息。在比特币中,支付以完全不同的方式实现。有:
- No accounts.
- No balances.
- No addresses.
- No coins.
- 没有发件人和收件人。
由于区块链是公共和开放数据库,我们不希望存储有关钱包所有者的敏感信息。帐户中不会收集硬币。交易不会将钱从一个地址转移到另一个地址。没有保存帐户余额的字段或属性。只有交易。但是交易中有什么?
Bitcoin Transaction
交易是输入和输出的组合:
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}
输入前一个事务的新事务引用输出(虽然有一个例外,我们将在后面讨论)。输出是硬币实际存储的地方。下图演示了事务的互连:
Notice that:
- 有些输出与输入无关。
- 在一个事务中,输入可以引用多个事务的输出。
- 输入必须引用输出。
在整篇文章中,我们将使用“钱”,“硬币”,“花”,“发送”,“帐户”等词语。但比特币中没有这样的概念。事务只是用脚本锁定值,只能由锁定它们的人解锁。
Transaction Outputs
让我们先从输出开始:
type TXOutput struct {
Value int
ScriptPubKey string
}
实际上,它的输出存储“硬币”(请注意Value
上面的字段)。并且存储意味着用谜题锁定它们,谜题存储在谜题中ScriptPubKey
。在内部,比特币使用一种叫做的脚本语言
,用于定义输出锁定和解锁逻辑。这种语言非常原始(这是故意制作的,以避免可能的黑客攻击和滥用),但我们不会详细讨论。你可以找到它的详细解释here.
In Bitcoin, value字段存储的数量satoshis,而不是BTC的数量。一个satoshi是比特币的百万分之一(0.00000001 BTC),因此这是比特币中最小的货币单位(如一分钱)。
由于我们没有实现地址,因此我们现在将避免使用与脚本相关的整个逻辑。ScriptPubKey
将存储任意字符串(用户定义的钱包地址)。
顺便说一句,拥有这样的脚本语言意味着比特币也可以用作智能合约平台。
关于产出的一个重要问题是它们是不可分割的,这意味着你不能引用它的一部分价值。在新事务中引用输出时,它将作为一个整体使用。如果其值大于要求,则会生成更改并将其发送回发件人。这类似于现实世界的情况,当你支付5美元的钞票,价格为1美元并且变化4美元。
Transaction Inputs
这是输入:
type TXInput struct {
Txid []byte
Vout int
ScriptSig string
}
如前所述,输入引用了以前的输出:Txid
存储此类交易的ID,以及Vout
存储事务中输出的索引。ScriptSig
是一个脚本,提供要在输出中使用的数据ScriptPubKey
。如果数据正确,则可以解锁输出,并且可以使用其值来生成新输出;如果不正确,则无法在输入中引用输出。这是保证用户不能花钱属于其他人的机制。
再说一遍,既然我们还没有实现地址,ScriptSig
将只存储任意用户定义的钱包地址。我们将在下一篇文章中实现公钥和签名检查。
让我们总结一下。输出是存储“硬币”的地方。每个输出都带有一个解锁脚本,它确定解锁输出的逻辑。每个新事务必须至少有一个输入和输出。输入引用先前事务的输出并提供数据(ScriptSig
字段)在输出的解锁脚本中用于解锁它并使用其值来创建新输出。
但首先是:输入还是输出?
The egg
在比特币中,它是鸡肉之前的鸡蛋。输入 - 参考 - 输出逻辑是经典的“鸡肉或鸡蛋”情况:输入产生输出和输出使输入成为可能。在比特币中,输出在输入之前。
当一个矿工开始挖掘一个区块时,它会为它添加一个coinbase transaction。 coinbase transaction是一种特殊类型的事务,不需要以前存在的输出。它无处不在地创造输出(即“硬币”)。鸡蛋没有鸡肉。这是矿工开采新区块的奖励。
如你所知,区块链开头就有创世块。正是这个块在区块链中生成了第一个输出。并且不需要先前的输出,因为没有先前的交易且没有这样的输出。
让我们创建一个coinbase transaction:
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 transaction只有一个输入。在我们的实施中Txid
为空并且Vout
等于-1。此外,coicoinbase transaction不会存储脚本ScriptSig
。相反,任意数据存储在那里。
在比特币中,第一个coinbase transaction包含以下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。You can see it yourself.
subsidy
是奖励金额。在比特币中,此数字不存储在任何地方,仅基于块总数计算:块数除以210000
。挖掘成因块产生了50个BTC,并且每个210000
阻止奖励减半。在我们的实施中,我们将奖励存储为常数(至少现在为止)。
在区块链中存储交易
从现在开始,每个块必须至少存储一个事务,并且不可能在没有事务的情况下挖掘块。这意味着我们应该删除 Block的
Data字段而是存储交易:
type Block struct {
Timestamp int64
Transactions []*Transaction
PrevBlockHash []byte
Hash []byte
Nonce int
}
NewBlock
and NewGenesisBlock
也必须相应改变:
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
...
}
func NewGenesisBlock(coinbase *Transaction) *Block {
return NewBlock([]*Transaction{coinbase}, []byte{})
}
接下来要改变的是创建一个新的区块链:
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))
err = b.Put(genesis.Hash, genesis.Serialize())
...
})
...
}
现在,该函数获取一个地址,该地址将获得挖掘生成块的奖励。
Proof-of-Work
工作证明算法必须考虑存储在块中的事务,以保证区块链作为事务存储的一致性和可靠性。所以现在我们必须修改ProofOfWork.prepareData
方法:
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
pow.block.PrevBlockHash,
pow.block.HashTransactions(), // This line was changed
IntToHex(pow.block.Timestamp),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
Instead of pow.block.Data
we now use pow.block.HashTransactions()
which is:
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 tree并在Proof-of-Work系统中使用树的根哈希。此方法允许快速检查块是否包含特定事务,仅具有根哈希并且不下载所有事务。
我们到目前为止检查一切是否正确:
$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a
Done!
好!我们收到了第一次采矿奖励。但是我们如何检查结余?
未花费的交易输出
我们需要找到所有未使用的事务输出(UTXO)。
表示这些输出未在任何输入中引用。在上图中,这些是:
- tx0, output 1;
- tx1, output 0;
- tx3, output 0;
- tx4, output 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)
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)
}
}
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
}
由于事务存储在块中,我们必须检查区块链中的每个块。我们从输出开始:
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 []TXOutput
unspentTransactions := 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 := 0
UTXOs := bc.FindUTXO(address)
for _, out := range UTXOs {
balance += out.Value
}
fmt.Printf("Balance of '%s': %d\n", address, balance)
}
帐户余额是帐户地址锁定的所有未使用的交易输出的值的总和。
我们在挖掘起源块后检查我们的结余:
$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10
这是我们的第一笔钱!
Sending Coins
现在,我们想向其他人发送一些硬币。为此,我们需要创建一个新事务,将其放在一个块中,然后挖掘块。到目前为止,我们只实现了coinbase transaction(这是一种特殊类型的事务),现在我们需要一个通用事务:
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)
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) {
...
newBlock := NewBlock(transactions, lastHash)
...
}
最后,让我们来实现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获取所有事务并创建候选块。仅当包含它们的块被挖掘并添加到区块链时,才会确认事务。
让我们检查发送硬币是否有效:
$ blockchain_go send -from Ivan -to Pedro -amount 6
00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37
Success!
$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4
$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6
太好了!现在,让我们创建更多事务并确保从多个输出发送工作正常:
$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf
Success!
$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa
Success!
现在,海伦的硬币锁定在两个输出中:一个来自佩德罗,一个来自伊万。让我们把它们发给别人:
$ blockchain_go send -from Helen -to Rachel -amount 3
000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0
Success!
$ 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
Conclusion
唷!这不容易,但我们现在有交易!虽然缺少类比特币加密货币的一些关键特征:
- 地址。我们还没有真正的基于私钥的地址。
- 奖励。采矿区绝对没有利润!
- UTXO设置。获得平衡需要扫描整个区块链,这可能需要很长时间才能有很多块。此外,如果我们想要验证以后的事务,可能需要很长时间。 UTXO集旨在解决这些问题并快速进行事务处理。
- 内存池。这是在块打包之前存储事务的地方。在我们当前的实现中,块只包含一个事务,这是非常低效的。
英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/
更多文章欢迎访问 http://www.apexyun.com/
联系邮箱:public@space-explore.com
(未经同意,请勿转载)