在Go中构建区块链 第4部分:交易1

651 阅读10分钟

Introduction

交易是比特币的核心,区块链的唯一目的是以安全可靠的方式存储交易,因此没有人可以在创建交易后对其进行修改。今天我们开始实施交易。但是因为这是一个相当大的话题,我将它分为两部分:在这部分中,我们将实现交易的一般机制,在第二部分,我们将通过细节进行处理。

此外,由于代码更改很大,因此在这里描述所有代码都没有意义。您可以看到所有更改here.

There is no spoon

如果您曾经开发过Web应用程序,为了实现付款,您可能会在数据库中创建这些表:accounts 和 transactions。帐户将存储关于用户的信息,包括他们的个人信息和余额,并且交易将存储关于从一个帐户转移到另一个帐户的钱的信息。在比特币中,支付以完全不同的方式实现。有:

  1. No accounts.
  2. No balances.
  3. No addresses.
  4. No coins.
  5. 没有发件人和收件人。

由于区块链是公共和开放数据库,我们不希望存储有关钱包所有者的敏感信息。帐户中不会收集硬币。交易不会将钱从一个地址转移到另一个地址。没有保存帐户余额的字段或属性。只有交易。但是交易中有什么?

Bitcoin Transaction

交易是输入和输出的组合:

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

输入前一个事务的新事务引用输出(虽然有一个例外,我们将在后面讨论)。输出是硬币实际存储的地方。下图演示了事务的互连:


Notice that:

  1. 有些输出与输入无关。
  2. 在一个事务中,输入可以引用多个事务的输出。
  3. 输入必须引用输出。

在整篇文章中,我们将使用“钱”,“硬币”,“花”,“发送”,“帐户”等词语。但比特币中没有这样的概念。事务只是用脚本锁定值,只能由锁定它们的人解锁。

Transaction Outputs

让我们先从输出开始:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

实际上,它的输出存储“硬币”(请注意Value上面的字段)。并且存储意味着用谜题锁定它们,谜题存储在谜题中ScriptPubKey。在内部,比特币使用一种叫做的脚本语言

Script

,用于定义输出锁定和解锁逻辑。这种语言非常原始(这是故意制作的,以避免可能的黑客攻击和滥用),但我们不会详细讨论。你可以找到它的详细解释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)。

Unspent

表示这些输出未在任何输入中引用。在上图中,这些是:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. 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方法所做的工作。之后,对于每个找到的输出,创建引用它的输入。接下来,我们创建两个输出:

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

最后,让我们来实现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

唷!这不容易,但我们现在有交易!虽然缺少类比特币加密货币的一些关键特征:

  1. 地址。我们还没有真正的基于私钥的地址。
  2. 奖励。采矿区绝对没有利润!
  3. UTXO设置。获得平衡需要扫描整个区块链,这可能需要很长时间才能有很多块。此外,如果我们想要验证以后的事务,可能需要很长时间。 UTXO集旨在解决这些问题并快速进行事务处理。
  4. 内存池。这是在块打包之前存储事务的地方。在我们当前的实现中,块只包含一个事务,这是非常低效的。

英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/


更多文章欢迎访问 http://www.apexyun.com/

联系邮箱:public@space-explore.com

(未经同意,请勿转载)