50

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

 5 years ago
source link: http://www.apexyun.com/zai-gozhong-gou-jian-qu-kuai-lian-di-4bu-fen-jiao-yi-1/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Introduction

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

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

There is no spoon

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

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

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

Bitcoin Transaction

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

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

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

6FzMvqF.png!web

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

唷!这不容易,但我们现在有交易!虽然缺少类比特币加密货币的一些关键特征:地址。我们还没有真正的基于私钥的地址。奖励。采矿区绝对没有利润!UTXO设置。获得平衡需要扫描整个区块链,这可能需要很长时间才能有很多块。此外,如果我们想要验证以后的事务,可能需要很长时间。 UTXO集旨在解决这些问题并快速进行事务处理。内存池。这是在块打包之前存储事务的地方。在我们当前的实现中,块只包含一个事务,这是非常低效的。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK