使用Golang构建区块链Part4:交易-1

在对照实现本部分内容时,我进行的稍微困难,因为本篇中的实现代码与前面的代码相差的太多。所以,当遇到困难时,就直接去查源码。在后面会有对应代码改动的链接。

介绍

交易是比特币的核心,将交易安全可靠的存储是区块链唯一目的,所以没有人可以在交易创建之后进行修改。今天我们将开始实现交易。但由于这是一个较为庞大的部分,我将它分成了两部分:在本部分,我们会实现交易的通用机制,在后续的那部分中我们将会实现细节部分。

对了,由于代码改动了很多,并且没有必要全部细说。你们可以在这里找到所有的改动。

There is no spoon(这没勺子)

如果你曾经开发过Web应用,为了实现支付你会在数据库中创建这样的tables:accountstransactions 。一个账户会保存一个用户的信息,包含他们的私人信息以及账户余额,同时一个交易会存储money从一个账户到另一个账户的转移信息。在比特币中,支付是实现于一个完全不同的方式。在这:

  1. 没有账户。
  2. 没有余额。
  3. 没有地址。
  4. 没有币。
  5. 没有发送者和接受者。

因为区块链是一个公共开放的数据库,我们不想存储钱包所有者的敏感信息在里面。币不会在账户中被收集。

交易不会把money从一个地址转移到另一个。这里也没有字段或者说是特征来持有账户的余额。这里只有交易。但是,交易里有什么呢?

比特币交易

Github译者补充:

点击 这里 在 blockchain.info 查看下图中的交易信息。

交易包含一些输入(inputs)和一些输出(outputs):

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

一个新的交易的入账/输入取决于上一个交易的出账/输出(不过这儿有一个例外,我们在稍后会讨论到)。出账/输出是币实际上存储在哪里。下面的图表对交易的内在联系进行了示范:

请注意:

  1. 有的出账没有和入账相连。
  2. 在一个交易中,入账涉及多个交易的出账。
  3. 一个入账必须依赖一个出账。

整篇文章中,我们将会使用像“钱”、“币”、“花费”、“发出”、“账户”等等这样的字眼。但是对比特币并不存在这样的概念,交易仅仅是通过一个脚本来锁定一些值,而这些实际的值只可以被锁定他们的人解锁。

( Transactions just lock values with a script, which can be unlocked only by the one who locked them.)

交易出账

让我们从交易出账开始:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

实际上,这是存储“币”的出账(注意一下上面的“value”字段)。这个存储被一个保存在__ScriptPubKey__里的“谜题”锁定。往深了说,比特币使用了一个叫做__Script__的脚本语言,它用于定义交易输出(出账)的锁定和解锁逻辑。这个语言有一些原始(它有意这样设计以便避免可能的入侵和滥用),但是我们不会讨论它的细节。你可以在这里找到所有细节的解释。

在比特币中,_value_字段存储着_satoshis_的数量,而不是比特币的数量。1 _satoshis_是1亿比特币分之1(0.00000001 BTC)。所以这是比特币货币中最小的单位(像是分)。

因为我们没有实现地址,我们现在将避免涉及到全部逻辑相关的脚本。__ScriptPubKey__将会存储为一个专用的字段(用户定义钱包地址)。

顺便说一下,拥有脚本语言意味着比特币也可以用做智能合约平台。

关于交易输出有一个重要的点是它们是不可分割的,这意味着你不能使用这个值的一部分。当一个交易输出被一个新的交易引用时,它会全部花费。如果这个值比所需要的多,找零会自动生成并返回给发送者。这有些相似于现实世界中你支付的场景,就是说,一个5元的钞票去买一个1元的东西,会得到4元找零。

交易入账

这儿是交易入账:

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

如前面所述,一个交易的入账涉及到前一个交易的出账:Txid__存储这个交易的__ID,__Vout__存储这个交易中一个出账的索引。__ScriptSig__是一个为出账时提供使用的__ScriptPubKey__所需要数据的脚本(ScriptSig是个脚本,这个脚本提供数据,提供的数据是ScriptPubKey所需要的,ScriptPubKey是出账时所需要的脚本)。如果数据正确,交易出账可以被解锁,它的值可以用于生成新的出账;如果不正确,这个出账则不能被入账所引用。这就是保证用户不能花费属于别人的币的一个机制。

再说一次,因为我们没有实现地址,__ScriptSig__将仅仅存储一个专用的用户定义钱包地址。我们在下一篇文章将会实现公钥和签名检查。

让我们总结一下。交易出账是“币”所存在的地方。每一个交易出账都带有一个解锁的脚本,决定着解锁这个交易出账的逻辑。每一个新的交易必须至少有一个入账和出账。一个入账会引用上个交易的出账和数据(ScriptSig 字段),这些被用来在出账中解锁脚本去解锁并使用其中的值创建新的出账。(好吧我长难句不过关…原文: An input references an output from a previous transaction and provides data (the ScriptSig field) that is used in the output’s unlocking script to unlock it and use its value to create new outputs.)

但是,先有谁:入账还是出账?

The egg(蛋)

在比特币中,是先有蛋,后有鸡的。这个交易入账和交易出账的相互关系逻辑是经典的“鸡和蛋”的关系:入账产生出账,而出账使入账成为可能。在比特币中,交易出账产生于交易入账之前,也就是,先有交易出账。

当矿工开始挖矿时,他会添加一笔币基交易coinbase(我们可以理解为凭空造币的交易)。一个币基交易是一种特殊的交易,不需要任何先前的交易出账的存在。它就会“莫名其妙的”产生交易出账,可以理解为凭空造钱。这个蛋就不需要鸡。这是对矿工挖出新块的一个奖励。

正如你所知道的,在区块链的最开始有一个创世区块。这个区块是区块链中最开始的一个出账。由于没有先前的交易和输出,所以它不需要前面的交易输出。

让我们创建一个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中存储脚本。取而代之的,专有数据存储在这里。

在比特币中,最开始的一个交易中带有这样的信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”. 在这里可以自个看

subsidy是奖励的总数。在区块链中,这个值不会存储在任何地方而是仅仅根据区块的总数量进行计算:区块被分为210000个。挖这些创世区块产生50BTC,并且每210000个区块这个奖励减半。在我们的实现中,我们将这个奖励存储为一个常量(至少现在是这样😉)。

在区块链中存储交易

从现在开始,每个区块必须存储至少1个交易同时没有可能挖一个不包含交易的区块。这意味我们应该移出Block中的Data字段然后代替的,存储交易字段:

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	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 {
    return NewBlock([]*Transaction{coinbase}, []byte{})
}

在这部分以及后面的代码中,由于我理解能力的原因(或是作者描述的略微粗略),这部分以及以后的代码都是在Part3和Part4代码比较里一一对应继续修改的。链接在文章开始的地方。

接下来调整创建区块链的部分:

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())
		...
	})
	...
}

我将完整部分也给大家贴上:

// creates a new blockchain db
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain is already exists.")
		os.Exit(1)
	}

	var tip []byte
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

	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())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), genesis.Hash)

		if err != nil {
			log.Panic(err)
		}

		tip = genesis.Hash

		return nil

	})

	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip, db}

	return &bc
}

现在,这个函数接收了一个地址,这个地址就是接受挖出创世区块的奖励的。

工作量证明

工作量证明算法必须考虑到存储在区块中的交易,去保证区块链作为一个存储交易仓库的一致性和可靠性。所以,我们现在必须修改Proof-Of-Work.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
}

现在我们用pow.block.HashTransactions()来代替pow.block.Data,就是:

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代表一个区块中包含的所有交易并且在工作量证明系统中使用树的根哈希值。这个方法运行快速的检查一个区块是否包含某个确定的交易,只需要树根的哈希而不需要下载整个交易。

让我们检查一下到目前为止一切是否正确:

$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!

实话说做到这里,这个我没做出来,应该还是缺一些的。在整篇文章涉及到的代码全部完成后,我才实现这样的内容。

好!我们现在收到我们的第一笔挖矿奖励。但是,我们怎样检查我们的余额呢?

未花费交易输出

我们需要找到全部的未花费交易输出(unspent transaction outputs - UTXO)。未花费表示这些输出没有在任何地方被任何交易入账所引用(也就是哪一个交易的入账都没有使用它)。在上面的图解中,这些就是的:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0。

当然,在我们检查余额时,我们并不需要全部的这些,只需要那些可以被我们所拥有的key解锁的(目前我们还没有key的实现,我们会用用户定义地址去代替)。首先,让我们在入账和出账上定义加锁和解锁方法:

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交易上,因为它们没有解锁出账):

PS:这里我翻译不好,把原文贴上了

After checking outputs we gather all inputs that could unlock outputs locked with the provided address (this doesn’t apply to coinbase transactions, since they don’t unlock outputs)

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

这是我们的第一桶金!

发送币

现在,我们想发送一些币给其他人。为此,我们需要创建一个新的交易,把它放进区块里,然后挖矿。到目前为止,我们只实现了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)

		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方法所做的。在那之后,入账所需要引用的出账就被找出来了。接下来,我们创建两个输出:

  1. 一个由接收者的地址锁定。这就是真实的币到一个地址的转移。
  2. 一个由发送者的地址锁定。这是一个改变。它仅仅在未花费交易输出的总额大于新交易所需要的总额时被创建。记住,输出是__不可分割__的。

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)
	...
}

完整代码:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lasthash []byte

	// BoltDB的只读事务,读取最后一个区块的hash,用它挖下一个区块的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)
	}

	// 用提供的交易以及上一个区块的hash构建新的区块
	newBlock := NewBlock(transactions, lasthash)

	// 挖到新区块后进行DB存储并更新“l”键
	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)
		}

		// 更新“l”键
		err = b.Put([]byte("l"), newBlock.Hash)
		if err != nil {
			log.Panic(err)
		}

		bc.tip = newBlock.Hash

		return 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!")
}

发送币意味着创建一个交易,并且通过挖出一个区块将其添加到区块链上。但是比特币没有像我们实现这些。相反的,它把所有交易存储到内存池(一般叫做矿池)中,当矿工准备好挖出一个矿时,它打包内存池中的所有交易并且产生一个候选区块。交易只有在包含它的区块被挖出来并且添加到区块链之后才被确认。

然我们检查发送币命令的运行:

$ 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!

现在,Helen’s的币被两个出账锁定:一个来自Pedro一个来自Ivan。让我们分别发送它们:

$ 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

总结

噢!它并不容易,但好歹我们现在有交易了!虽然,一些类似于区块链这种加密货币的特性遗失了:

  1. 地址。我们没有真实的,基于私钥的地址。
  2. 奖励。挖矿是毫无利益可图的。
  3. UTXO集合。需要扫描整个区块获得余额,当区块非常非常多的时候这很花费时间。而且在后面我们要验证交易的话也非常花费时间。UTXO集就是为了解决这些问题并且让交易操作更快捷。
  4. 内存池(矿池)。这是交易在打包到区块之前所要存储的地方。在我们目前的实现中,一个区块只包含一个交易,这是非常低效的。

Links:

  1. Full source codes
  2. Transaction
  3. Merkle tree
  4. Coinbase
自认为是幻象波普星的来客
Built with Hugo
主题 StackJimmy 设计