Go语言实现区块链与加密货币-Part2(交易与地址,余额翻倍漏洞)

准备工作:
安装依赖包:$ go get golang.org/x/crypto/ripemd160
安装失败请查看:https://blog.csdn.net/ak47000gb/article/details/79561358

交易

交易(transaction)是比特币的核心所在,而区块链唯一的目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。今天,我们将会开始实现交易。不过,由于交易是很大的话题,我会把它分为两部分来讲:在今天这个部分,我们会实现交易的基本框架。在第二部分,我们会继续讨论它的一些细节。
由于比特币采用的是 UTXO(未使用交易输出) 模型,并非账户模型,并不直接存在“余额”这个概念,余额需要通过遍历整个交易历史得来。

比特币的交易

点击这里blockchain.info 查看下图中的交易信息:
在这里插入图片描述

一笔交易由一些输入(input)和输出(output)组合而来:

// 一笔交易由一些输入和输出构成
type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,coinbase 交易),引用就是花费的意思。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中,就是花费之前的交易输出。交易的输出,就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:
在这里插入图片描述
注意:
1.有一些输出并没有被关联到某个输入上
2.一笔交易的输入可以引用之前多笔交易的输出
3.一个输入必须引用一个输出
贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,其实并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value),而这些值只可以被锁定它们的人解锁(unlock)。
每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。

交易的输出

先从输出(output)开始:

// 输出包含两部分
//Value:有多少币,存储在这里
//ScriptPubkey:对输出进行锁定,在这里,将仅用一个字符串来代替
type TXOutput struct {
	Value      int
	PubKeyHash []byte  //相当于ScriptPubKey
}

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

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

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

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

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

交易的输入

这里是输入(input):

// Txid:一个交易输入引用了之前一笔交易的一个输出,ID表明的是之前的哪笔交易
// Vout:一笔交易可能有多个输出,Vout为输出的索引
//ScriptSig:提供解锁输出,ID+Out的数据
type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string  
	
}

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

先有输出

在比特币中,是先有蛋,然后才有鸡。输入引用输出的逻辑,是经典的“蛋还是鸡”问题:输入先产生输出,然后输出使得输入成为可能。在比特币中,最先有输出,然后才有输入。换而言之,第一笔交易只有输出,没有输入。
当矿工挖出一个新的块时,它会向新的块中添加一个 coinbase 交易。coinbase 交易是一种特殊的交易,它不需要引用之前一笔交易的输出。它“凭空”产生了币(也就是产生了新币),这是矿工获得挖出新块的奖励,也可以理解为“发行新币”。
在区块链的最初,也就是第一个块,叫做创世块。正是这个创世块,产生了区块链最开始的输出。对于创世块,不需要引用之前的交易输出。因为在创世块之前根本不存在交易,也就没有不存在交易输出。
来创建一个 coinbase 交易:

// 这个函数创建一笔Coinbase交易,这笔交易没有输入,只有一个输出。这个函数返回一笔交易的地址。
func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to '%s'", to)   //to就是目标地址
	}

	txin := TXInput{[]byte{}, -1, data}   //没有输入,表现为Txid为空,Vout=-1,ScripSig中只是存储了任意的data
	txout := NewTXOutput(subsidy, to)                  //只有一个输出,subsidy是奖励金
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}}
	tx.SetID()

	return &tx
}

在比特币中,第一笔 coinbase 交易包含了如下信息:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。
subsidy 是挖出新块的奖励金。在比特币中,实际并没有存储这个数字,而是基于区块总数进行计算而得:区块总数除以 210000 就是 subsidy。挖出创世块的奖励是 50 BTC,每挖出 210000 个块后,奖励减半。在我们的实现中,这个奖励值将会是一个常量(至少目前是)。

将交易保存到区块链

从现在开始,每个块必须存储至少一笔交易。如果没有交易,也就不可能出新的块。这意味着我们应该移除 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}
	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{})
}

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

// 创建一个新的区块链数据库
// address用来接收挖出创世块的奖励
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain already exists.")
		os.Exit(1)
	}

	var tip []byte

	cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
	genesis := NewGenesisBlock(cbtx)

	db, err := bolt.Open(dbFile, 0600, nil)
	//这是打开一个BoltDB文件的标准做法。注意,即便不存在这样的文件,它也不会返回错误 
	//在BoltDB中,数据库操作通过一个事务(transaction)进行操作
	//这里打开的是一个读写事务(db.Update(...)),因为我们可能会向数据库中添加创世块
	if err != nil {
		log.Panic(err)
	}

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte(blocksBucket))//创建一个名为“blocks”的Bucket
		if err != nil {
			log.Panic(err)
		}

		err = b.Put(genesis.Hash, genesis.Serialize())//将创世区块序列化后,与该块的哈希(作为键值)一起存入Bucket
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), genesis.Hash)//用“1”作为创世哈希的键,因为此时创世块作为最后一个块存在
		if err != nil {
			log.Panic(err)
		}
		tip = genesis.Hash//指向创世区块

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

	bc := Blockchain{tip, db}

	return &bc
}

工作量证明

工作量证明算法必须要将存储在区块里面的交易考虑进去,从而保证区块链交易存储的一致性和可靠性。所以,我们必须修改 ProofOfWork.prepareData 方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(),  //这里做出了改变
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

不像之前使用 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.Hash())   //先获取每笔交易的哈希再连接起来
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))  //组合后的哈希

	return txHash[:]
}

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

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

来检查一下到目前为止是否正确:
在这里插入图片描述
nice!我们已经获得了第一笔挖矿奖励,但是,我们要如何查看余额呢?

未花费交易输出

我们需要找到所有的未花费交易输出(unspent transactions outputs, UTXO)。未花费(unspent) 指的是这个输出还没有被包含在任何交易的输入中,或者说没有被任何输入引用。在“比特币的交易”一节的图示中,未花费的输出是:
1.tx0, output 1;
2.tx1, output 0;
3.tx3, output 0;
4.tx4, output 0.
当然了,检查余额时,我们并不需要知道整个区块链上所有的 UTXO,只需要关注那些我们能够解锁的那些 UTXO(目前我们还没有实现**,所以我们将会使用用户定义的地址来代替)。首先,让我们定义在输入和输出上的锁定解锁方法:
在这里插入图片描述
在这里,我们只是将 script 字段与 unlockingData 进行了比较。在后续文章我们基于私钥实现了地址以后,会对这部分进行改进。
下一步,找到包含未花费输出的交易,这一步其实相当困难:

// 返回一个交易列表,找到未花费输出的交易
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []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 {
				// 跳过那些已经被包含在其他输入中的输出,即已经被花费的输出
				if spentTXOs[txID] != nil {
					for _, spentOutIdx := range spentTXOs[txID] {
						if spentOutIdx == outIdx {
							continue Outputs
						}
					}
				}
				//如果该交易输出可以被解锁,即可以被花费
				if out.IsLockedWithKey(pubKeyHash) {
					unspentTXs = append(unspentTXs, *tx)      //此处有问题!!!
				}
			}
			//将给定地址所有能够解锁输出的输入聚集起来
			//这并不适用于coinbase交易,因为它们不解锁输出
			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.UsesKey(pubKeyHash) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}

		if len(block.PrevBlockHash) == 0 {
			break
		}
	}

	return unspentTXs
}

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

// 找到并返回所有的未花费交易输出,仅返回一个
func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	unspentTransactions := bc.FindUnspentTransactions(pubKeyHash)

	for _, tx := range unspentTransactions {
		for _, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) {  //已经被锁定的
				UTXOs = append(UTXOs, out)
			}
		}
	}

	return UTXOs
}

就是这么多了!现在我们来实现 getbalance 命令:
在这里插入图片描述
账户余额就是由账户地址锁定的所有未花费交易输出的总和。

在挖出创世块以后,来检查一下我们的余额:
在这里插入图片描述
这就是我们的第一桶金!

发送币

现在,我们想要给其他人发送一些币。为此,我们需要创建一笔新的交易,将它放到一个块里,然后挖出这个块。之前我们只实现了 coinbase 交易(这是一种特殊的交易),现在我们需要一种通用的普通交易:

// NewUTXOTransaction 创建一笔新的普通交易
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput
	//找到足够的未花费输出,确保余额是足够的(VALUE)
	wallets, err := NewWallets()
	if err != nil {
		log.Panic(err)
	}
	wallet := wallets.GetWallet(from)
	pubKeyHash := HashPubKey(wallet.PublicKey)
	acc, validOutputs := bc.FindSpendableOutputs(pubKeyHash, amount)

	if acc < amount {
		log.Panic("ERROR: Not enough funds")
	}

	// 对于每个找到的输入,会创建一个引用该输出的输入
	for txid, outs := range validOutputs {
		txID, err := hex.DecodeString(txid)
		if err != nil {
			log.Panic(err)
		}

		for _, out := range outs {
			input := TXInput{txID, out, nil, wallet.PublicKey}
			inputs = append(inputs, input)
		}
	}

	// 创建输出,由接受者地址锁定,这是给其他地址实际转移的币
	outputs = append(outputs, *NewTXOutput(amount, to))

	//如果UTXO总数超过所需,则产生找零,由发送者地址锁定
	if acc > amount {
		outputs = append(outputs, *NewTXOutput(acc-amount, from)) // a change
	}

	tx := Transaction{nil, inputs, outputs}
	tx.SetID ()

	return &tx
}

再看看FindSpendableOutputs 方法。这是基于之前定义的 FindUnspentTransactions 方法:

// FindSpendableOutputs 从address中找到至少 amount 的UTXO
func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(pubKeyHash)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {  //对所有未花费交易进行迭代,并累加
		txID := hex.EncodeToString(tx.ID)//将十六进制转换为字符串形式

		for outIdx, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) && accumulated < amount {//如果输出是能被解锁的,并且此时还未凑够钱
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) //那么就把对应的ID添加到索引中

				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOutputs  //返回累加值,同时还有通过交易ID进行分组的输出索引
}

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

// MineBlock 通过给出的交易创建一个新的块
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	for _, tx := range transactions {
		if bc.VerifyTransaction(tx) != true {   //在一笔交易被放入一个块之前进行验证
			log.Panic("ERROR: Invalid transaction")
		}
	}

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

	newBlock := NewBlock(transactions, lastHash)

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

		err = b.Put([]byte("l"), newBlock.Hash)
		if err != nil {
			log.Panic(err)
		}

		bc.tip = newBlock.Hash

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

最后,让我们来实现 send 方法:

func (cli *CLI) send(from, to string, amount int) { //传入发送方和接收方的地址,以及发送的数量
	if !ValidateAddress(from) {//检查地址是否合法
		log.Panic("ERROR: Sender address is not valid")
	}
	if !ValidateAddress(to) {
		log.Panic("ERROR: Recipient address is not valid")
	}

	bc := NewBlockchain(from) //创建一条新的区块链
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)//创建一笔普通交易
	bc.MineBlock([]*Transaction{tx})//通过这笔交易创建一个新块
	fmt.Println("Success!")
}

发送币意味着创建新的交易,并通过挖出新块的方式将交易打包到区块链中。不过,比特币并不是一连串立刻完成这些事情(虽然我们目前的实现是这么做的)。相反,它会将所有新的交易放到一个内存池中(mempool),然后当矿工准备挖出一个新块时,它就从内存池中取出所有交易,创建一个候选块。只有当包含这些交易的块被挖出来,并添加到区块链以后,里面的交易才开始确认。
让我们来检查一下发送币是否能工作:
在这里插入图片描述
很好!现在,让我们创建更多的交易,确保从多个输出中发送币也正常工作:
在这里插入图片描述
现在,xiaohong 的币被锁定在了两个输出中:一个来自 xiaoming,一个来自 daming。让我们把它们发送给其他人:
在这里插入图片描述
看起来没问题!现在,来测试一些失败的情况:
在这里插入图片描述

总结

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

地址

在上一篇文章中,我们已经初步实现了交易。相信你应该了解了交易中的一些天然属性,这些属性没有丝毫“个人”色彩的存在:在比特币中,没有用户账户,不需要也不会在任何地方存储个人数据(比如姓名,护照号码或者 SSN)。但是,我们总要有某种途径识别出你是交易输出的所有者(也就是说,你拥有在这些输出上锁定的币)。这就是比特币地址(address)需要完成的使命。在上一篇中,我们把一个由用户定义的任意字符串当成是地址,现在我们将要实现一个跟比特币一样的真实地址。

比特币地址

这就是一个真实的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是史上第一个比特币地址,据说属于中本聪。比特币地址是完全公开的,如果你想要给某个人发送币,只需要知道他的地址就可以了。但是,地址(尽管地址也是独一无二的)并不是用来证明你是一个“钱包”所有者的信物。实际上,所谓的地址,只不过是将公钥表示成人类可读的形式而已,因为原生的公钥人类很难阅读。在比特币中,你的身份(identity)就是一对(或者多对)保存在你的电脑(或者你能够获取到的地方)上的公钥(public key)和私钥(private key)。比特币基于一些加密算法的组合来创建这些**,并且保证了在这个世界上没有其他人能够取走你的币,除非拿到你的**。下面,让我们来讨论一下这些算法到底是什么。

公钥加密

公钥加密(public-key cryptography)算法使用的是成对的**:公钥和私钥。公钥并不是敏感信息,可以告诉其他人。但是,私钥绝对不能告诉其他人:只有所有者(owner)才能知道私钥,能够识别,鉴定和证明所有者身份的就是私钥。在加密货币的世界中,你的私钥代表的就是你,私钥就是一切。
本质上,比特币钱包也只不过是这样的**对而已。当你安装一个钱包应用,或是使用一个比特币客户端来生成一个新地址时,它就会为你生成一对**。在比特币中,谁拥有了私钥,谁就可以控制所有发送到这个公钥的币。
私钥和公钥只不过是随机的字节序列,因此它们无法在屏幕上打印,人类也无法通过肉眼去读取。这就是为什么比特币使用了一个转换算法,将公钥转化为一个人类可读的字符串(也就是我们看到的地址)。

如果你用过比特币钱包应用,很可能它会为你生成一个助记符。这样的助记符可以用来替代私钥,并且可以被用于生成私钥。BIP-039 已经实现了这个机制。

好了,现在我们已经知道了在比特币中证明用户身份的是私钥。那么,比特币如何检查交易输出(和存储在里面的币)的所有权呢?

数字签名

在数学和密码学中,有一个数字签名(digital signature)的概念,算法可以保证:
1.当数据从发送方传送到接收方时,数据不会被修改;
2.数据由某一确定的发送方创建;
3.发送方无法否认发送过数据这一事实。

通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章,比方说我做了一幅画,完了用印章一盖,就说明了这幅画是我的作品。给数据生成签名,就是给数据盖了章。
为了对数据进行签名,我们需要下面两样东西:
1.要签名的数据
2.私钥

应用签名算法可以生成一个签名,并且这个签名会被存储在交易输入中。为了对一个签名进行验证,我们需要以下三样东西:
1.被签名的数据
2.签名
3.公钥
简单来说,验证过程可以被描述为:检查对象签名是由被签名数据加上私钥得来,并且公钥恰好是由该私钥生成

数据签名并不是加密,你无法从一个签名重新构造出数据。这有点像哈希:你在数据上运行一个哈希算法,然后得到一个该数据的唯一表示。签名与哈希的区别在于**对:有了**对,才有签名验证。但是**对也可以被用于加密数据:私钥用于加密,公钥用于解密数据。不过比特币并不使用加密算法。

在比特币中,每一笔交易输入都会由创建交易的人签名。在被放入到一个块之前,必须要对每一笔交易进行验证。除了一些其他步骤,验证意味着:
1.检查交易输入有权使用来自之前交易的输出
2.检查交易签名是正确的

如图,对数据进行签名和对签名进行验证的过程大致如下:
在这里插入图片描述
现在来回顾一个交易完整的生命周期:

  • 起初,创世块里面包含了一个 coinbase 交易。在 coinbase 交易中,没有输入,所以也就不需要签名。coinbase
    交易的输出包含了一个哈希过的公钥(使用的是 RIPEMD16(SHA256(PubKey)) 算法)
  • 当一个人发送币时,就会创建一笔交易。这笔交易的输入会引用之前交易的输出。每个输入会存储一个公钥(没有被哈希)和整个交易的一个签名。
  • 比特币网络中接收到交易的其他节点会对该交易进行验证。除了一些其他事情,他们还会检查:在一个输入中,公钥哈希与所引用的输出哈希相匹配(这保证了发送方只能花费属于自己的币);签名是正确的(这保证了交易是由币的实际拥有者所创建)。
  • 当一个矿工准备挖一个新块时,他会将交易放到块中,然后开始挖矿。
  • 当新块被挖出来以后,网络中的所有其他节点会接收到一条消息,告诉其他人这个块已经被挖出并被加入到区块链。
  • 当一个块被加入到区块链以后,交易就算完成,它的输出就可以在新的交易中被引用。

椭圆曲线加密

正如之前提到的,公钥和私钥是随机的字节序列。私钥能够用于证明持币人的身份,需要有一个条件:随机算法必须生成真正随机的字节。因为没有人会想要生成一个私钥,而这个私钥意外地也被别人所有。
比特币使用椭圆曲线来产生私钥。椭圆曲线是一个复杂的数学概念,我们并不打算在这里作太多解释(如果你真的十分好奇,可以查看这篇文章,注意:有很多数学公式!)我们只要知道这些曲线可以生成非常大的随机数就够了。在比特币中使用的曲线可以随机选取在 0 与 2 ^ 2 ^ 56(大概是 10^77, 而整个可见的宇宙中,原子数在 10^78 到 10^82 之间) 的一个数。有如此高的一个上限,意味着几乎不可能发生有两次生成同一个私钥的事情。
比特币使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)椭圆曲线数字签名算法来对交易进行签名,我们也会使用该算法。

Base58

回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa 。现在,我们已经知道了这是公钥用人类可读的形式表示而已。如果我们对它进行解码,就会看到公钥的本来面目(16 进制表示的字节):
0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特币使用 Base58 算法将公钥转换成人类可读的形式。这个算法跟著名的 Base64 很类似,区别在于它使用了更短的字母表:为了避免一些利用字母相似性的攻击,从字母表中移除了一些字母。也就是,没有这些符号:0(零),O(大写的 o),I(大写的i),l(小写的 L),因为这几个字母看着很像。另外,也没有 + 和 / 符号。
下图是从一个公钥获得一个地址的过程:
在这里插入图片描述
因此,上面提到的公钥解码后包含三个部分:
Version || Public key hash || Checksum
00 ||62E907B15CBF27D5425399EBF6F0FB50EBB88F18|| C29B7D93
由于哈希函数是单向的(也就说无法逆转回去),所以不可能从一个哈希中提取公钥。不过通过执行哈希函数并进行哈希比较,我们可以检查一个公钥是否被用于哈希的生成。
好了,所有细节都已就绪,来写代码吧。很多概念只有当写代码的时候,才能理解地更透彻。

实现地址

这部分需要安装依赖包:$ go get golang.org/x/crypto/ripemd160
安装失败请查看:https://blog.csdn.net/ak47000gb/article/details/79561358
我们先从钱包 Wallet 结构开始:

// 钱包结构体包括公钥和私钥(**对)
type Wallet struct {
	PrivateKey ecdsa.PrivateKey
	PublicKey  []byte
}
// Wallets stores a collection of wallets
type Wallets struct {
	Wallets map[string]*Wallet
}

// 新建钱包函数,会生成一个新的**对
func NewWallet() *Wallet {
	private, public := newKeyPair()
	wallet := Wallet{private, public}

	return &wallet
}

// NewWallets creates Wallets and fills it from a file if it exists
func NewWallets() (*Wallets, error) {
	wallets := Wallets{}
	wallets.Wallets = make(map[string]*Wallet)

	err := wallets.LoadFromFile()

	return &wallets, err
}

func newKeyPair() (ecdsa.PrivateKey, []byte) {   //基于椭圆曲线,生成一个新的秘钥对
	curve := elliptic.P256()     //需要一个椭圆曲线
	private, err := ecdsa.GenerateKey(curve, rand.Reader) //使用椭圆曲线生成一个私钥
	if err != nil {
		log.Panic(err)
	}
	pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)  //从私钥生成公钥。公钥是曲线上的点(x,y坐标的组合)

	return *private, pubKey
}

一个钱包只有一个**对而已。我们需要 Wallets 类型来保存多个钱包的组合,将它们保存到文件中,或者从文件中进行加载。Wallet 的构造函数会生成一个新的**对。newKeyPair 函数非常直观:ECDSA 基于椭圆曲线,所以我们需要一个椭圆曲线。接下来,使用椭圆生成一个私钥,然后再从私钥生成一个公钥。有一点需要注意:在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是 X,Y 坐标的组合。在比特币中,这些坐标会被连接起来,然后形成一个公钥。
现在,来生成一个地址:

// 生成一个地址(Base58)
func (w Wallet) GetAddress() []byte {
	pubKeyHash := HashPubKey(w.PublicKey)

	versionedPayload := append([]byte{version}, pubKeyHash...)  //给哈希加上(地址生成算法的)版本前缀
	checksum := checksum(versionedPayload) //使用SHA256(SHA256(payload))对加了前缀的哈希进行再哈希,计算校验和,这是最终结果的后4个字节

	fullPayload := append(versionedPayload, checksum...)  //将校验和附加到 version+PubKeyHash 的组合中
	address := Base58Encode(fullPayload)  //使用 Base58 对 version+PubKeyHash+checksum 组合进行编码

	return address
}

// HashPubKey hashes public key
func HashPubKey(pubKey []byte) []byte {
	publicSHA256 := sha256.Sum256(pubKey)

	RIPEMD160Hasher := ripemd160.New()
	_, err := RIPEMD160Hasher.Write(publicSHA256[:])   //对公钥哈希两次,即 RIPEMD160(SHA256(PubKey))
	if err != nil {
		log.Panic(err)
	}
	publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)

	return publicRIPEMD160
}



// Checksum generates a checksum for a public key
func checksum(payload []byte) []byte {
	firstSHA := sha256.Sum256(payload)
	secondSHA := sha256.Sum256(firstSHA[:])  //使用SHA256(SHA256(payload))对加了前缀的哈希进行再哈希

	return secondSHA[:addressChecksumLen]
}

将一个公钥转换成一个 Base58 地址需要以下步骤:

  • 使用 RIPEMD160(SHA256(PubKey)) 哈希算法,取公钥并对其哈希两次
  • 给哈希加上地址生成算法版本的前缀
  • 对于第二步生成的结果,使用 SHA256(SHA256(payload)) 再哈希,计算校验和。校验和是结果哈希的后四个字节。
  • 将校验和附加到 version+PubKeyHash 的组合中。
  • 使用 Base58 对 version+PubKeyHash+checksum 组合进行编码。

至此,就可以得到一个真实的比特币地址,你甚至可以在 blockchain.info 查看它的余额。不过我可以负责任地说,无论生成一个新的地址多少次,检查它的余额都是 0。这就是为什么选择一个合适的公钥加密算法是如此重要:考虑到私钥是随机数,生成同一个数字的概率必须是尽可能地低。理想情况下,必须是低到“永远”不会重复。
另外,注意:你并不需要连接到一个比特币节点来获得一个地址。地址生成算法使用的多种开源算法可以通过很多编程语言和库实现。
现在我们需要修改输入和输出来使用地址:

// Txid:一个交易输入引用了之前一笔交易的一个输出,ID表明的是之前的哪笔交易
// Vout:一笔交易可能有多个输出,Vout为输出的索引
//ScriptSig:提供解锁输出,ID+Out的数据
type TXInput struct {
	Txid      []byte
	Vout      int
	Signature []byte  //ScriptSig 会被分为 Signature 和 PubKey 字段
	PubKey    []byte
}


// 输出包含两部分
//Value:有多少币,存储在这里
//ScriptPubkey:对输出进行锁定,在这里,将仅用一个字符串来代替
type TXOutput struct {
	Value      int
	PubKeyHash []byte  //相当于ScriptPubKey
}



// UsesKey 检查地址是否可用
func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
	lockingHash := HashPubKey(in.PubKey)  //这个函数要求的是哈希后的公钥

	return bytes.Compare(lockingHash, pubKeyHash) == 0
}


// 对输出进行签名(锁定)
func (out *TXOutput) Lock(address []byte) {
	pubKeyHash := Base58Decode(address)       //地址是唯一参数,将其解码
	pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] //提取出公钥哈希
	out.PubKeyHash = pubKeyHash
}

// IsLockedWithKey 检查是否提供的公钥哈希被用于锁定输出,也就是检查这个公钥是不是该地址的持有者
//这是一个 UsesKey 的辅助函数,并且它们都被用于 FindUnspentTransactions 来形成交易之间的联系
func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {
	return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
}

注意,现在我们已经不再需要 ScriptPubKey 和 ScriptSig 字段,因为我们不会实现一个脚本语言。相反,ScriptSig 会被分为 SignaturePubKey 字段,ScriptPubKey 被重命名为 PubKeyHash。我们会实现跟比特币里一样的输出锁定/解锁和输入签名逻辑,不同的是我们会通过方法(method)来实现。
UsesKey 方法检查输入使用了指定**来解锁一个输出。注意到输入存储的是原生的公钥(也就是没有被哈希的公钥),但是这个函数要求的是哈希后的公钥。IsLockedWithKey 检查是否提供的公钥哈希被用于锁定输出。这是一个 UsesKey 的辅助函数,并且它们都被用于 FindUnspentTransactions 来形成交易之间的联系。
Lock 只是简单地锁定了一个输出。当我们给某个人发送币时,我们只知道他的地址,因为这个函数使用一个地址作为唯一的参数。然后,地址会被解码,从中提取出公钥哈希并保存在 PubKeyHash 字段。
现在,来检查一下是否都能如期工作:
在这里插入图片描述

nice!现在我们来实现交易签名。

实现签名

交易必须被签名,因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。如果一个签名是无效的,那么这笔交易就会被认为是无效的,因此,这笔交易也就无法被加到区块链中。
我们现在离实现交易签名还差一件事情:用于签名的数据。一笔交易的哪些部分需要签名?又或者说,要对完整的交易进行签名?选择签名的数据相当重要。因为用于签名的这个数据,必须要包含能够唯一识别数据的信息。比如,如果仅仅对输出值进行签名并没有什么意义,因为签名不会考虑发送方和接收方。
考虑到交易解锁的是之前的输出,然后重新分配里面的价值,并锁定新的输出,那么必须要签名以下数据:
1.存储在已解锁输出的公钥哈希。它识别了一笔交易的“发送方”。
2.存储在新的锁定输出里面的公钥哈希。它识别了一笔交易的“接收方”。
3.新的输出值。

在比特币中,锁定/解锁逻辑被存储在脚本中,它们被分别存储在输入和输出的 ScriptSig 和 ScriptPubKey
字段。由于比特币允许这样不同类型的脚本,它对 ScriptPubKey 的整个内容进行了签名。

可以看到,我们不需要对存储在输入里面的公钥签名。因此,在比特币里, 所签名的并不是一个交易,而是一个去除部分内容的输入副本,输入里面存储了被引用输出的 ScriptPubKey

获取修剪后的交易副本的详细过程在这里. 虽然它可能已经过时了,但是我并没有找到另一个更可靠的来源。

看着有点复杂,来开始写代码吧。先从 Sign 方法开始:

// Sign对交易的每一笔输入进行签名
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
	//coinbase交易因为没有实际输入,所以没有被签名
	if tx.IsCoinbase() {
		return
	}

	for _, vin := range tx.Vin {
		if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
			log.Panic("ERROR: Previous transaction is not correct")
		}
	}
	//将会被签署的是一个修剪后的交易版本,而不是一个完整交易
	//这个副本包含了所有的输入和输出,但是TXInput,Signature和 TXInput.PubKey被设置为nil
	txCopy := tx.TrimmedCopy()  //创建一个副本
	//接下来,我们会迭代副本中每一个输入:
	for inID, vin := range txCopy.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
		txCopy.Vin[inID].Signature = nil  //
		txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
		// Hash方法对交易进行序列化。并使用SHA-256算法进行哈希,最后结果就是我们要签名的数据
		txCopy.ID = txCopy.Hash()
		//获取完哈希,我们应该重置 Pubkey字段,以便于它不会影响后面的迭代。
		txCopy.Vin[inID].PubKey = nil
		
		//关键!!!通过 privKey对 txcopy.ID进行签名
		r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
		if err != nil {
			log.Panic(err)
		}
		//一个ECDSA签名就是一对数字,我们把这对数字连接起来,并存储在输入的Signature字段
		signature := append(r.Bytes(), s.Bytes()...)

		tx.Vin[inID].Signature = signature
	}
}

在每个输入中,Signature 被设置为 nil (仅仅是一个双重检验),PubKey 被设置为所引用输出的 PubKeyHash。现在,除了当前交易,其他所有交易都是“空的”,也就是说他们的 Signature 和 PubKey 字段被设置为 nil。因此,输入是被分开签名的,尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入。
现在,实现验证函数:

// Verify 这个函数验证交易中输入的签名
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	if tx.IsCoinbase() {
		return true
	}

	for _, vin := range tx.Vin {
		if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
			log.Panic("ERROR: Previous transaction is not correct")
		}
	}
	//首先我们需要同一笔交易的副本
	txCopy := tx.TrimmedCopy()
	//然后,我们需要相同的区块用于生成秘钥对
	curve := elliptic.P256()
	//接下来,检查每个输入中的签名

	for inID, vin := range tx.Vin {
		prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
		txCopy.Vin[inID].Signature = nil
		txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
		txCopy.ID = txCopy.Hash()
		txCopy.Vin[inID].PubKey = nil
		//解包存储在 TXInput.Signature 和 TXInput.Pubkey 中的值
		//一个签名就是一对数字,一个公钥就是一对坐标


		r := big.Int{}
		s := big.Int{}
		sigLen := len(vin.Signature)
		r.SetBytes(vin.Signature[:(sigLen / 2)])
		s.SetBytes(vin.Signature[(sigLen / 2):])

		x := big.Int{}
		y := big.Int{}
		keyLen := len(vin.PubKey)
		x.SetBytes(vin.PubKey[:(keyLen / 2)])
		y.SetBytes(vin.PubKey[(keyLen / 2):])
		//使用从输入提取的公钥创建了一个ecdsa.Publickey,通过传入输出中提取的签名执行了 ecdsa.Verify
		//如果所有的输入都被验证,返回true;如果有任何一个交易失败,返回 false


		rawPubKey := ecdsa.PublicKey{curve, &x, &y}
		if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
			return false
		}
	}

	return true
}

现在,我们需要一个函数来获得之前的交易。由于这需要与区块链进行交互,我们将它放在了 Blockchain 的方法里面:

// 通过ID来找到一笔交易,这需要在链上迭代所有区块
func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _, tx := range block.Transactions {
			if bytes.Compare(tx.ID, ID) == 0 {
				return *tx, nil
			}
		}

		if len(block.PrevBlockHash) == 0 {
			break
		}
	}

	return Transaction{}, errors.New("Transaction is not found")
}

// 传入一笔交易,找到它引用的交易,并对它签名
func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		if err != nil {
			log.Panic(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	tx.Sign(privKey, prevTXs)
}

// 对一笔交易的输入签名进行验证
func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
	prevTXs := make(map[string]Transaction)

	for _, vin := range tx.Vin {
		prevTX, err := bc.FindTransaction(vin.Txid)
		if err != nil {
			log.Panic(err)
		}
		prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
	}

	return tx.Verify(prevTXs)
}

这几个比较简单:FindTransaction 通过 ID 找到一笔交易(这需要在区块链上迭代所有区块);SignTransaction 传入一笔交易,找到它引用的交易,然后对它进行签名;VerifyTransaction 做的是相同的事情,不过是对交易进行验证。
现在,我们需要实际签名和验证交易。签名在 NewUTXOTransaction 中进行:
在这里插入图片描述
在一笔交易被放入一个块之前进行验证:
在这里插入图片描述
就是这些了!让我们再来检查一下所有内容:
在这里插入图片描述
如果注释掉 NewUTXOTransaction 里面的bc.SignTransaction(&tx, wallet.PrivateKey) 的调用,那么未签名的交易将无法被打包。
再次提醒如果提示缺少包"golang.org/x/crypto/ripemd160"请安装依赖:
$ go get golang.org/x/crypto/ripemd160

余额翻倍漏洞

震惊!竟然还能这么干

这个系统真的万无一失吗?
要是突发奇想,我来尝试一下自己给自己发送币:
在这里插入图片描述
先从A地址发送给B地址1个币,此时查看A的余额为9,正常。
然后从A地址发送给A地址1个币,再查看A的余额,竟然翻倍,变成18了!不可思议!
也就是说,这个区块链系统中,当出现一个地址给自身发送币时,会使余额翻倍,这样的漏洞真是太可怕了,那么一个人可以不停的给自己发币使币的数量无限增加!这样的系统毫无安全可言!
那么BUG究竟是出在什么地方呢?

漏洞产生的原因

现在我们再次仔细看看FindUnspentTransactions这个方法:
在这里插入图片描述
我们可以看到,这个方法所找到的,所有未花费交易是以一个列表形式返回的,而这个列表存储的是指向交易的指针。
在循环体中,若经过判断,如果:

  • 某一笔交易的某个输出没有被包含在其他交易的输入
  • 这个输出可以用指定的pubKeyHash(通过形参传入)解锁

那么就说明这个输出是我们要找的 且没有被用过的。
如此一来,指向这笔交易的指针就会被添加(append)到列表中。
这个列表接下来会用于余额的查询。
一般情况下,上面这个逻辑是没有问题的,但是它忽略了这个情况:发送方和接受方的地址是相同的
我用上面提到的例子(A给B转1,然后A给A转1)画一个图来具体说明:
在这里插入图片描述
从图中可以看到,在tx2这笔交易中存在两个Output,虽然它们锁定的地址都是A,但是显然它们都满足以上的2个判断条件(都未花费,都能被A的pubKey解锁),那么tx2这笔交易就会被添加(append)两次!(Output0使得tx2被添加一次,Output1使得tx2又被添加一次)
所以,在接下来的余额查询时,tx2中A的余额(1+8=9)就会被计算两次,因为列表中存在两个tx2!最后使得A的余额变为9*2=18了,也就是出现了余额翻倍的漏洞!

漏洞的解决

这个漏洞可以用多种方式来解决,最简单的就是禁止自己给自己发送币了,也就是禁止发送方和接收方地址相同的情况出现!
那么我们修改一下send函数:
在这里插入图片描述
再次试验一下,自己给自己转1个币:
在这里插入图片描述
看,成功修复了漏洞!

总结

现在的区块链技术发展还停留在起步阶段,各种所谓的区块链2.0、3.0就已经被炒作得沸沸扬扬了,然而它们依旧还存在的许多漏洞,否则也不会隔三差五的爆出某某币、某某链又被黑客攻击的新闻了。要想从源头避免被攻击,还得从保证代码的可靠性入手,否则又得闹"无限产生币"的闹剧了!