本文译自 Building Blockchain in Go. Part 4: Transactions 1
简介
交易是比特币的核心,区块链的唯一目的是以安全可靠的方式存储交易,因此在创建交易后没有人可以修改它们。 今天我们开始实施交易。 但因为这是一个相当大的主题,所以我将其分为两部分:在这一部分中,我们将实现交易的一般机制,在第二部分中我们将讨论细节。
另外,由于代码更改很大,因此在这里描述所有更改是没有意义的。 您可以在这里看到所有更改。
相较于传统支付
如果您曾经开发过 Web 应用程序,为了实现付款,您可能会在数据库中创建这些表:帐户和交易。 帐户将存储有关用户的信息,包括他们的个人信息和余额,而交易将存储有关从一个帐户转移到另一个帐户的资金的信息。 在比特币中,支付是以完全不同的方式实现的。 在这里:
- 没有账户。
- 没有余额。
- 没有地址。
- 没有币。
- 没有发送者和接收者。
由于区块链是一个公共且开放的数据库,我们不想存储有关钱包所有者的敏感信息。 币不收集在帐户中。 交易不会将资金从一个地址转移到另一个地址。 没有保存帐户余额的字段或属性。 只有交易。 那么交易里面有什么?
比特币交易
交易是输入和输出的组合:
1
2
3
4
5
|
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}
|
新交易的输入引用先前交易的输出(不过有一个例外,我们将在稍后讨论)。 输出是实际存储币的地方。 下图展示了交易的互连:
请注意:
- 有些输出未链接到输入。
- 在一笔交易中,输入可以引用多个交易的输出。
- 输入必须引用输出。
在整篇文章中,我们将使用“钱”、“币”、“花费”、“发送”、“账户”等词语。但比特币中没有这样的概念。 交易只是用脚本锁定值,该脚本只能由锁定它们的人解锁。
交易输出
让我们首先从输出开始:
1
2
3
4
|
type TXOutput struct {
Value int
ScriptPubKey string
}
|
实际上,它是存储“币”的输出(注意上面的值字段)。 存储意味着用一个数学迷题锁定它们,该谜题存储在 ScriptPubKey 中。 在内部,比特币使用一种称为 Script 的脚本语言,用于定义输出锁定和解锁逻辑。 该语言非常原始(这是有意为之,以避免可能的黑客攻击和误用),但我们不会详细讨论它。 您可以在这里找到它的详细解释。
在比特币中,value字段存储的是satoshis的数量,而不是BTC的数量。 聪是比特币的一亿分之一(0.00000001 BTC),因此这是比特币中最小的货币单位(如美分)。
由于我们没有实现地址,因此我们现在将避免整个脚本相关逻辑。 ScriptPubKey 将存储任意字符串(用户定义的钱包地址)。
顺便说一句,拥有这样的脚本语言意味着比特币也可以用作智能合约平台。
关于输出的一个重要的事情是它们是不可分割的,这意味着您不能引用其值的一部分。 当新交易中引用输出时,它会作为一个整体被使用。 如果其值大于所需值,则会生成更改并将其发送回发送者。 这类似于现实世界的情况,例如,您用 5 美元的钞票购买 1 美元的商品,然后找回 4 美元。
交易输入
这是输入:
1
2
3
4
5
|
type TXInput struct {
Txid []byte
Vout int
ScriptSig string
}
|
如前所述,输入引用先前的输出:Txid 存储该交易的 ID,Vout 存储交易中输出的索引。 ScriptSig 是一个脚本,提供在输出的 ScriptPubKey 中使用的数据。 如果数据正确,则可以解锁输出,并且其值可以用于生成新的输出; 如果不正确,则无法在输入中引用输出。 这是保证用户不能花费属于其他人的币的机制。
同样,由于我们还没有实现地址,ScriptSig 将仅存储任意用户定义的钱包地址。 我们将在下一篇文章中实现公钥和签名检查。
让我们总结一下。 输出是存储“币”的地方。 每个输出都带有一个解锁脚本,它决定了解锁输出的逻辑。 每笔新交易必须至少有一个输入和输出。 输入引用先前交易的输出,并提供在输出的解锁脚本中使用的数据(ScriptSig 字段)来解锁它并使用其值创建新的输出。
但先有什么:输入还是输出?
先有鸡还是先有蛋
在比特币中,先有蛋,后有鸡。 输入-引用-输出逻辑是经典的“先有鸡还是先有蛋”的情况:输入产生输出,输出使输入成为可能。 在比特币中,输出先于输入。
当矿工开始挖掘一个区块时,它会向其中添加一个 coinbase 交易。 coinbase 交易是一种特殊类型的交易,不需要先前存在的输出。 它凭空创造输出(即“币”)。 有蛋无鸡。 这是矿工开采新区块所获得的奖励。
如您所知,区块链的开头有创世块。 正是这个区块生成了区块链中的第一个输出。 并且不需要先前的输出,因为没有先前的交易,也没有这样的输出。
让我们创建一个 coinbase 交易:
1
2
3
4
5
6
7
8
9
10
11
12
|
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 中存储脚本。相反,任意数据都存储在那里。
在比特币中,第一笔 Coinbase 交易包含以下消息:“泰晤士报 2009 年 1 月 3 日财政大臣即将对银行进行第二次救助”。你可以自己看看。
补贴是奖励的金额。 在比特币中,这个数字不存储在任何地方,仅根据区块总数计算:区块数量除以210000。挖掘创世区块产生50 BTC,每210000个区块奖励减半。 在我们的实现中,我们将把奖励存储为常量(至少现在是这样😉)。
在区块链中存储交易
从现在开始,每个区块必须存储至少一笔交易,并且不再可能在没有交易的情况下挖掘区块。这意味着我们应该删除块的数据字段并存储交易:
1
2
3
4
5
6
7
|
type Block struct {
Timestamp int64
Transactions []*Transaction
PrevBlockHash []byte
Hash []byte
Nonce int
}
|
NewBlock 和 NewGenesisBlock 也必须相应更改:
1
2
3
4
5
6
7
8
|
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{})
}
|
接下来要改变的是创建一个新的区块链:
1
2
3
4
5
6
7
8
9
10
11
12
|
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())
...
})
...
}
|
现在,该函数采用一个地址,该地址将获得挖掘创世块的奖励。
工作量证明
工作量证明算法必须考虑存储在区块中的交易,以保证区块链作为交易存储的一致性和可靠性。所以现在我们必须修改 ProofOfWork.prepareData 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
}
|
我们现在使用 pow.block.HashTransactions() 代替 pow.block.Data,它是:
1
2
3
4
5
6
7
8
9
10
11
|
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 树,并在工作量证明系统中使用树的根哈希。 这种方法允许快速检查一个块是否包含特定交易,仅具有根哈希而无需下载所有交易。
让我们检查一下到目前为止一切是否正确:
1
2
3
4
|
$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a
Done!
|
好的!我们收到了第一笔挖矿奖励。但我们如何查看余额呢?
未花费的交易输出
我们需要找到所有未花费的交易输出(UTXO)。 未花费意味着这些输出没有在任何输入中引用。 在上图中,这些是:
- tx0,输出1;
- tx1,输出0;
- tx3,输出0;
- tx4,输出0。
当然,当我们检查余额时,我们不需要全部,而只需要那些可以用我们拥有的密钥解锁的(目前我们没有实现密钥,将使用用户定义的地址代替)。 首先,我们定义输入和输出的锁定/解锁方法:
1
2
3
4
5
6
7
|
func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
return in.ScriptSig == unlockingData
}
func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
return out.ScriptPubKey == unlockingData
}
|
这里我们只是将脚本字段与unlockingData进行比较。在我们实现基于私钥的地址之后,这些部分将在以后的文章中得到改进。
下一步 - 查找包含未使用输出的交易 - 非常困难:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
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
}
|
由于交易存储在区块中,因此我们必须检查区块链中的每个区块。我们从输出开始:
1
2
3
|
if out.CanBeUnlockedWith(address) {
unspentTXs = append(unspentTXs, tx)
}
|
如果输出被我们正在搜索未使用交易输出的同一地址锁定,那么这就是我们想要的输出。但在获取之前,我们需要检查输入中是否已经引用了输出:
1
2
3
4
5
6
7
|
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {
if spentOut == outIdx {
continue Outputs
}
}
}
|
我们跳过输入中引用的那些(它们的值已移至其他输出,因此我们无法对它们进行计数)。 检查输出后,我们收集所有可以解锁使用所提供的地址锁定的输出的输入(这不适用于 coinbase 交易,因为它们不解锁输出):
1
2
3
4
5
6
7
8
|
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)
}
}
}
|
该函数返回包含未花费输出的交易列表。为了计算余额,我们还需要一个函数来接受交易并仅返回输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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 命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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)
}
|
账户余额是该账户地址锁定的所有未花费交易输出值的总和。
让我们在挖掘创世块后检查一下我们的余额:
1
2
|
$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10
|
这是我们的第一笔钱!
发送币
现在,我们想发送一些币给其他人。 为此,我们需要创建一个新交易,将其放入一个区块中,然后挖掘该区块。 到目前为止,我们只实现了coinbase交易(这是一种特殊类型的交易),现在我们需要一个通用交易:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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方法:
1
2
3
4
5
|
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
...
newBlock := NewBlock(transactions, lastHash)
...
}
|
最后,我们来实现发送命令:
1
2
3
4
5
6
7
8
|
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中取出所有交易并创建一个候选块。 仅当包含交易的区块被开采并添加到区块链时,交易才会被确认。
让我们检查一下发送币是否有效:
1
2
3
4
5
6
7
8
9
10
|
$ 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
|
好的!现在,让我们创建更多交易并确保从多个输出发送工作正常:
1
2
3
4
5
6
7
8
9
|
$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf
Success!
$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa
Success!
|
现在,海伦的币被锁定在两个输出中:一个来自佩德罗,一个来自伊万。让我们将它们发送给其他人:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ 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
|
看起来不错!现在我们来测试一下失败情况:
1
2
3
4
5
6
7
8
|
$ 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
|
结论
唷! 这并不容易,但我们现在有交易了! 尽管如此,类似比特币的加密货币缺少一些关键特征:
- 地址。 我们还没有真正的、基于私钥的地址。
- 奖励。 开采区块绝对不赚钱!
- UTXO 设置。 获得平衡需要扫描整个区块链,当区块数量很多时,这可能需要很长时间。 此外,如果我们想验证以后的交易,可能会花费很多时间。 UTXO集合旨在解决这些问题并使交易操作变得快速。
- 内存池。 这是交易在打包到区块之前存储的地方。 在我们当前的实现中,一个区块只包含一个交易,这是相当低效的。