当前位置:   article > 正文

使用go语言构建区块链 Part4.事务1_go 逻辑事务

go 逻辑事务

英文源地址

简介

事务是比特币的核心, 区块链的唯一目的是以安全可靠的方式存储交易, 因此在交易创建后没有人可以修改. 今天我们开始实现事务, 但由于这是一个相当大的主题, 我将它分成两部分: 在这一部分中, 我们将实现事务的通用机制, 在第二部分中, 我们将研究细节.
此外, 由于代码的变化是巨大的, 在这里描述它们是没有意义的. 你可以在这里查看到所有的变化.

There is no spoon(黑客帝国台词)

如果你曾经开发过一个web应用程序, 为了实现支付, 你可能会在数据库中创建这些表: 账户和交易.一个账户将存储有关用户的信息, 包括他们的个人信息和余额, 一次交易将存储一个账户到另一个账户的资金转移的信息. 在比特币中, 交易以完全不同的方式实现. 它们是:

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

由于区块链是一个公开和开放的数据库, 我们不想存储关于钱包所有者的任何敏感信息.硬币不在账户中收纳. 交易不会把钱从一个地址转移到另一个地址.没有保存账户余额的字段或属性. 只有交易, 但是交易里面有什么呢?

比特币交易
交易是输入和输出的组合

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}
  • 1
  • 2
  • 3
  • 4
  • 5

对于每一笔新的交易, 它的输入会引用前一笔交易的输出(这里有个例外, coinbase交易), 引用就是花费的意思.所谓引用之前的一个输入, 也就是将之前的一个输出包含在另一笔交易的输入当中, 就是花费之前的交易输出. 交易的输出, 就是币实际存储的地方. 下面的图示阐释了交易之间的互相关联.
在这里插入图片描述
需要注意的是:

  1. 有一些输出并没有被关联到某个输入上
  2. 一笔交易的输入可以引用之前多笔交易的输出
  3. 一个输入必须引用一个输出

贯穿全文, 我们将会使用像’money’, ‘coin’, ‘spend’, ‘send’, 'account’等等这样的词. 但是在比特币中, 其实并不存在这些概念. 交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value), 而这些值只可以被锁定它们的人解锁(unlock).
(每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。)

交易输出

先从输出开始

type TXOutput struct {
	Value        int
	ScriptPubKey string
}
  • 1
  • 2
  • 3
  • 4

输出主要包含两部分:

  1. 一定量的比特币(Value)
  2. 一个锁定脚本(ScriptPubKey), 要花这笔钱, 必须解锁该脚本

实际上, 正式输出里面存储了’币’(注意, 也就是上面的Value字段). 而这里的存储, 指的是用一个数学难题对输出进行锁定, 这个难题被存储在ScriptPubKey里面. 在内部, 比特币使用了一个叫做Script的脚本语言, 用它来定义锁定和解锁输出的逻辑. 虽然这个语言相当的原始(这是为了避免潜在的黑客攻击和滥用而有意为之), 并不复杂, 但是我们也并不会讨论它的细节. 你可以在这里找到详细解释.

在比特币中, value字段存储的是satoshi的数量, 而不是BTC的数量. 一个satoshi等于一亿分之一的BTC(0.00000001BTC), 这也是比特币里面最小的货币单位(就像是一分的硬币)

由于没有实现地址(address), 所以目前我们会避免涉及逻辑相关的完整脚本. ScriptPubKey将会存储一个任意的字符串(用户定义的钱包地址).

顺表说一下, 有了一个这样的脚本语言, 也意味着比特币其实可以作为一个智能合约平台.

关于输出, 非常重要的一点是: 它们是不可再分的(indivisible). 也就是说, 你无法仅引用其中的一部分. 要么不用, 如果要用, 必须一次性用完. 当一个新的交易中引用了某个输出, 那么这个输出必须被全部花费. 如果它的值比需要的值大, 那么就会产生一个找零, 找零会返还给发送方.这跟现实世界的场景十分类似, 当你像要支付时, 如果一个东西值1美元, 而你给了一个5美元的纸币, 那么你会得到一个4美元的找零.

交易输入

这里是输入

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}
  • 1
  • 2
  • 3
  • 4
  • 5

正如之前所提到的, 一个输入引用了一个输出: Txid存储的时之前交易的ID, Vout存储的时该输出在那笔交易中所有输出的索引(因为一笔交易可以有多个输出, 需要有信息指明时具体的哪一个).ScriptSig是一个脚本, 提供了可解锁输出结构里面ScriptPubKey字段的数据. 如果ScriptSig提供的数据是正确的, 那么输出就会解锁, 然后被解锁的值可以被用于产生新的输出; 如果数据不正确, 输出就无法被引用在输入中, 或者说, 无法使用这个输出. 这种机制, 保证了用户无法花费属于其他人的币.
再次强调, 由于我们还没有实现地址, 所以目前ScriptSig将仅仅存储一个用户自定义的任意钱包地址. 我们会在下一篇文章中实现公钥(public key)和签名(signature).
来简要总结一下.输出, 就是’币’存储的地方. 每个输出都会带有一个解锁脚本, 这个脚本定义了解锁该输出的逻辑. 每笔新的交易, 必须至少有一个输入与输出. 一个输入引用了之前一笔的输出, 并提供了解锁数据(也就是ScriptSig字段), 该数据会被用于在输出的解锁脚本中解锁输出, 解锁完成后即可使用它的值去产生新的输出.
每一笔输入都是之前一笔交易的输出, 那么假设从某一笔交易开始不断往前追溯, 它所涉及的输入和输出到底是谁先存在呢?换个说法, 这是个鸡和蛋谁先谁后的问题, 是先有蛋还是先有鸡呢?

先有蛋

在比特币中, 是先有蛋, 然后才有鸡的. 输入引用输出的逻辑, 是经典的’蛋还是鸡’的问题: 输入先产生输出, 然后输出使得输入成为可能. 在比特币中, 最先有输出, 然后才有输入. 换而言之, 每一笔交易只有输出, 没有输入.
当miner挖出一个新的区块时, 它会向新的区块添加一个coinbase交易. coinbase交易是一种特殊的交易, 它不需要引用之前一笔交易的输出. 它’凭空’产生了币(也就是产生了新币), 这是miner获得挖出mining的奖励, 也可以理解为’发行新币’.
在区块链的最初, 也就是第一个块, 叫做创世区块. 正是这个创世区块, 产生了区块链最开始的输出.对于创世区块, 不需要引用之前交易的输出.因为在创世区块之前根本不存在交易, 也就是不存在交易输出.
来创建一个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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

coinbase交易只有一个输出, 没有输入. 在我们的实现中, 它表现为Txid为空, Vout等于-1.并且, 在当前实现中, coinbase交易也没有再ScriptSig中存储脚本, 而只是存储了一个任意的字符串data.

在比特币中, 第一笔coinbase交易包含了如下信息: “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。可点击这里查看

subsidy是挖出新区块的奖励金. 在比特币中, 实际并没有存储这个数字, 而是基于区块总数进行计算而得: 区块总数除以210000就是subsidy. 挖出创世区块的奖励是50BTC, 每挖出210000个区块后, 奖励减半. 在我们的实现中, 这个奖励值将会是一个常量(至少目前是).

将交易保存到区块链

从现在开始, 每个区块必须存储至少一笔交易. 如果没有交易, 也就不可能出新的块. 这意味着我们应该溢出Block的Data字段, 取而代之的是存储交易:

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

NewBlock和NewGenesisBlock也必须做出相应的改变:

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	pow := NewProofOfWork(block)
	nonce, hash := pow.Run()

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

接下来修改创建区块链的函数:

func CreateBlockchain(address string) *Blockchain {
	var tip []byte
	db, _ := bolt.Open(dbFile, 0600, nil)

	_ = db.Update(func(tx *bolt.Tx) error {
		cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
		genesis := NewGenesisBlock(cbtx)

		b, _ := tx.CreateBucket([]byte(blocksBucket))

		if b == nil {
			b, _ := tx.CreateBucket([]byte(blocksBucket))
			_ = b.Put(genesis.Hash, genesis.Serialize())
			_ = b.Put([]byte("l"), genesis.Hash)
			tip = genesis.Hash
		} else {
			tip = b.Get([]byte("l"))
		}
		return nil
	})
	bc := Blockchain{tip, db}

	return &bc
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

现在, 这个函数会接受一个地址作为参数, 这个地址将会被用来接收挖出创世区块的奖励.

工作量证明

工作量证明算法必须要将存储在区块里面的交易考虑进去, 从而保证区块链交易存储的一致性和可靠性. 所以, 我们必须修改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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

不像之前使用pow.block.Data, 现在我们使用pow.block.HashTransactions():

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[:]
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

通过哈希值提供数据的唯一表示, 这种做法我们已经不是第一次遇到了. 我们想要通过仅仅一个哈希值, 就可以识别一个块里面的所有交易. 为此, 先获得每笔交易的哈希值, 然后将它们关联起来, 最后获得一个连接后的组合哈希值.

比特币使用了一个更加复杂的技术: 它将一个区块里面包含的所有交易表示为一个Merkle tree, 然后在工作量证明系统中使用树的根哈希(root hash),这个方法能够让我们快速检索一个块里面是否包含了某笔交易, 即只需root hash而无需下载所有交易即可完成判断.

来检查目前为止是否正确:

╰─ ./blockchain_impl_in_go createblockchain -address Ivan                  ─╯ 
00000060b65eb7a78b68206835d12e06c8b00940da37b5c773c1d465a8a3a35f

Done!

  • 1
  • 2
  • 3
  • 4
  • 5

很好, 我们已经获得了第一笔mining奖励, 但是, 我们要如何查看余额呢?

未花费的交易输出

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO), 未花费(unspent)指的是这个输出还没有被包含在任何交易的输入中, 或者说没有被任何输入引用. 在上面图示中, 未花费的输出是
在这里插入图片描述

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

当然了, 检查余额时, 我们并不需要知道整个区块链上所哟䣌UTXO, 只需要关注那些我们能解锁的那些UTXO(目前我们还没有实现密钥, 所以我们将会使用用户定义的地址来代替). 首先, 让我们定义在输入和输出上的锁定和解锁方法:

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里, 我们只是将script字段与unlockingData进行了比较. 在后续文章我们基于私钥实现了地址以后, 会对这部分进行改进.
下一步, 找到包含未花费输出的交易, 这一步其实相当困难:

func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction {
	var upsentTXs []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 {
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}
				if out.CanBeUnlockedWith(address) {
					upsentTXs = append(upsentTXs, *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 upsentTXs
}
  • 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

由于交易被存储在区块里, 所以我们不得不检查区块链里的每一笔交易.从输出开始:

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, tx)
}
  • 1
  • 2
  • 3

如果一个输出被一个地址锁定, 并且这个地址恰好是我们要找的地址, 那么这个输出就是我们想要的. 不过在获得它之前, 我们需要检查该输出是否已经被包含在一个交易的输出中, 也就是检查它是否已经被花费了:

if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
		if spentOut == outIdx {
			continue Outputs
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们跳过那些已经被包含在其他输入中的输出(这说明这个输出已经被花费, 无法再使用了). 检查完输出以后, 我们将给定地址所有能够解锁输出的输入聚合起来(这并不适用于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)
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个函数返回了一个交易列表, 里面包含了未花费输出.为了计算余额, 我们还需要一个函数将这些交易作为输入, 然后返回一个输出:

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

就是这么多了!现在我们来实现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)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

账户余额就是由账户地址锁定的所有未花费交易输出的综合.
在挖出创世区块后, 来检查一下我们的余额:

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 10
  • 1
  • 2

这就是我们的第一笔钱!

发送币

现在, 我们想要给其他人发送一些币. 为此, 我们需要创建一笔新的交易, 将它放到一个区块里, 然后挖出这个区块. 之前我们只实现了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")
	}

	for txid, outs := range validOutputs {
		txID, _ := hex.DecodeString(txid)
		
		for _, out := range outs {
			input := TXInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}
	outputs = append(outputs, TXOutput{amount, to})
	if acc >amount {
		outputs = append(outputs, TXOutput{acc - amount, from})
	}
	tx := Transaction{nil, inputs, outputs}
	tx.SetID()
	
	return &tx
}
  • 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

在创建新的输出前, 我们首先必须找到所有的未花费输出, 并且确保它们有足够的价值(value),这就是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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

这个方法对所有未花费交易进行迭代, 并对它的值进行累加. 当累加值大于或等于我们想要传送的值时, 它就会停止并返回累加值, 同时返回的还有通过交易ID进行分组的输出索引. 我们只需取出足够支付的钱就够了.
现在我们可以修改Blockchain.MineBlock方法

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

	_ = bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))

		return nil
	})

	newBlock := NewBlock(transactions, lastHash)

	_ = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		_ = b.Put(newBlock.Hash, newBlock.Serialize())
		_ = b.Put([]byte("l"), newBlock.Hash)
		bc.tip = newBlock.Hash

		return nil
	})
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

最后, 让我们实现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!")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

发送币意味着创建新的交易, 并通过挖出新的区块的方式将交易打包到区块链中. 不过比特币并不是一连串立刻完成这些事情(虽然我们目前的实现时这么做的). 相反, 它会将所有新的交易放到一个内存池中(mempool),然后当miner准备挖出一个新区块时, 它从内存池中取出所有交易, 创建一个候选块. 只有当包含这些交易的区块被挖出来, 并添加到区块链以后, 里面的交易才开始确认.
让我们检查一下发送币是否能工作:

$ blockchain_go send -from Ivan -to Pedro -amount 6
000000655594c9b0c6c1034ec0236d91d2115bbd74ed008901ea81c29f231d7f

Success!

╰─ ./blockchain_impl_in_go getbalance -address Ivan                        ─╯ 
Balance of Ivan: 4

╰─ ./blockchain_impl_in_go getbalance -address Pedro                       ─╯ 
Balance of Pedro: 6

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

很好!现在, 让我们创建更多的交易, 确保从多个输出中发送币也正常工作:

╰─ ./blockchain_impl_in_go send -from Pedro -to Helen -amount 2            ─╯ 
0000003d3c12819d42b9c9a2968a803b651a775af1af262d51384d6c2577f8e1

Success!

╰─ ./blockchain_impl_in_go send -from Ivan -to Helen -amount 2             ─╯ 
00000050e41ef1982c2aab6ad43d748b7f65c720a34a6fb9f144960d911e4711

Success!

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在, Helen的币被锁定在了两个输出中: 一个来自Pedro, 一个来自Lvan.让我们把它们发送给其他人:

╰─ ./blockchain_impl_in_go send -from Helen -to Rachel -amount 3           ─╯ 
000000417eabcad236c9d42c5c33d72c5e63a293c2b5491390f362d1d3fcdb89

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
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

看起来没问题!现在, 来测试一些失败的情况:

$ 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
  • 4
  • 5
  • 6
  • 7
  • 8

总结

虽然不容易, 但是现在终于实现交易了!不过, 我们依然缺少了一些像比特币那样的一些关键特性:

  1. 地址(address). 我们现在还没有基于私钥(private key)的真实地址.
  2. 奖励(reward). 现在mining时肯定无法盈利的!
  3. UTXO集. 获取余额需要扫描整个区块链, 而当区块非常多时, 这么做就会花费很长时间. 并且, 如果我们想要验证后续交易, 也需要花费很长时间. 而UTXO集就是为了解决这些问题, 加快交易相关的操作.
  4. 内存池(mempool). 在交易被打包到区块之前, 这些交易被存储在内存池里面. 在我们目前的实现中,一个块仅仅包含一笔交易, 这是相当低效的.

link

Full source codes
Transaction
Merkle tree
Coinbase

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/199983
推荐阅读
相关标签
  

闽ICP备14008679号