1. 程式人生 > >Go語言實現區塊鏈與加密貨幣-Part3(交易優化,單機模擬多節點通訊)

Go語言實現區塊鏈與加密貨幣-Part3(交易優化,單機模擬多節點通訊)

交易(二)

在這個系列文章的一開始,我們就提到了,區塊鏈是一個分散式資料庫。不過在之前的文章中,我們選擇性地跳過了“分散式”這個部分,而是將注意力都放到了“資料庫”部分。到目前為止,我們幾乎已經實現了一個區塊鏈資料庫的所有元素。今天,我們將會分析之前跳過的一些機制。而在本篇文章中,我們將會開始討論區塊鏈的分散式特性。

獎勵

在上一篇文章中,我們略過的一個小細節是挖礦獎勵。現在,我們已經可以來完善這個細節了。
挖礦獎勵,實際上就是一筆 coinbase 交易。當一個挖礦節點開始挖出一個新塊時,它會將交易從佇列中取出,並在前面附加一筆 coinbase 交易。coinbase 交易只有一個輸出,裡面包含了礦工的公鑰雜湊。

實現獎勵,非常簡單,更新 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()
	UTXOSet := UTXOSet{bc}
	defer bc.
db.Close() tx := NewUTXOTransaction(from, to, amount, &UTXOSet) cbTx := NewCoinbaseTX(from, "") //建立一筆coinbase交易 txs := []*Transaction{cbTx, tx} //把coinbase交易新增到交易前面 newBlock := bc.MineBlock(txs) fmt.Println("Success!") }

在我們的實現中,建立交易的人同時挖出了新塊,所以會得到一筆獎勵。

UTXO集

在 Part 1: 持久化和命令列介面 中,我們研究了 Bitcoin Core 是如何在一個數據庫中儲存塊的,並且瞭解到區塊被儲存在 blocks 資料庫,交易輸出被儲存在 chainstate 資料庫。回顧一下 chainstate 的結構:

  • c + 32 位元組的交易雜湊 -> 該筆交易的未花費交易輸出記錄
  • B + 32 位元組的塊雜湊 -> 未花費交易輸出的塊雜湊
    在之前那篇文章中,雖然我們已經實現了交易,但是並沒有使用 chainstate 來儲存交易的輸出。所以,接下來我們繼續完成這部分。

chainstate 不儲存交易。它所儲存的是 UTXO 集,也就是未花費交易輸出的集合。除此以外,它還儲存了“資料庫表示的未花費交易輸出的塊雜湊”,不過我們會暫時略過塊雜湊這一點,因為我們還沒有用到塊高度(但是我們會在接下來的文章中繼續改進)。

那麼,我們為什麼需要 UTXO 集呢?
來思考一下我們早先實現的 Blockchain.FindUnspentTransactions 方法:
在這裡插入圖片描述
這個函式找到有未花費輸出的交易。由於交易被儲存在區塊中,所以它會對區塊鏈裡面的每一個區塊進行迭代,檢查裡面的每一筆交易。截止 2017 年 9 月 18 日,在比特幣中已經有 485,860 個塊,整個資料庫所需磁碟空間超過 140 Gb。這意味著一個人如果想要驗證交易,必須要執行一個全節點。此外,驗證交易將會需要在許多塊上進行迭代。

整個問題的解決方案是有一個僅有未花費輸出的索引,這就是 UTXO 集要做的事情:這是一個從所有區塊鏈交易中構建(對區塊進行迭代,但是隻須做一次)而來的快取,然後用它來計算餘額和驗證新的交易。截止 2017 年 9 月,UTXO 集大概有 2.7 Gb。

好了,讓我們來想一下實現 UTXO 集的話需要作出哪些改變。目前,找到交易用到了以下一些方法:

  • Blockchain.FindUnspentTransactions -
    找到有未花費輸出交易的主要函式。也是在這個函式裡面會對所有區塊進行迭代。

  • Blockchain.FindSpendableOutputs -
    這個函式用於當一個新的交易建立的時候。如果找到有所需數量的輸出。使用
    Blockchain.FindUnspentTransactions.

  • Blockchain.FindUTXO - 找到一個公鑰雜湊的未花費輸出,然後用來獲取餘額。使用
    Blockchain.FindUnspentTransactions.

  • Blockchain.FindTransation - 根據 ID 在區塊鏈中找到一筆交易。它會在所有塊上進行迭代直到找到它。

可以看到,所有方法都對資料庫中的所有塊進行迭代。但是目前我們還沒有改進所有方法,因為 UTXO 集沒法儲存所有交易,只會儲存那些有未花費輸出的交易。因此,它無法用於 Blockchain.FindTransaction
下面是我們接下來的目標,也就是需要改進成的樣子——

  • Blockchain.FindUTXO - 通過對區塊進行迭代找到所有未花費輸出。
  • UTXOSet.Reindex - 使用 UTXO 找到未花費輸出,然後在資料庫中進行儲存。這裡就是快取的地方。
  • UTXOSet.FindSpendableOutputs - 類似
    Blockchain.FindSpendableOutputs,但是使用 UTXO 集。
  • UTXOSet.FindUTXO - 類似 Blockchain.FindUTXO,但是使用 UTXO 集。
  • Blockchain.FindTransaction 跟之前一樣。

因此,從現在開始,兩個最常用的函式將會使用 cache(快取記憶體)!來開始寫程式碼吧。


// UTXOSet結構表示UTXO集
type UTXOSet struct {
	Blockchain *Blockchain
}

我們將會使用一個單一資料庫,但是我們會將 UTXO 集從儲存在不同的 bucket 中。因此,UTXOSet 是與Blockchain 一起的。

// Reindex 初始化UTXO集
func (u UTXOSet) Reindex() {
	db := u.Blockchain.db
	bucketName := []byte(utxoBucket)

	err := db.Update(func(tx *bolt.Tx) error {
		err := tx.DeleteBucket(bucketName)//如果bucket存在就先移除
		if err != nil && err != bolt.ErrBucketNotFound {
			log.Panic(err)
		}

		_, err = tx.CreateBucket(bucketName)
		if err != nil {
			log.Panic(err)
		}

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

	UTXO := u.Blockchain.FindUTXO()//從區塊鏈中獲取所有的未花費輸出

	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket(bucketName)

		for txID, outs := range UTXO {
			key, err := hex.DecodeString(txID)
			if err != nil {
				log.Panic(err)
			}

			err = b.Put(key, outs.Serialize())//最終將輸出儲存在bucket中
			if err != nil {
				log.Panic(err)
			}
		}

		return nil
	})
}

這個方法初始化了 UTXO 集。首先,如果 bucket 存在就先移除,然後從區塊鏈中獲取所有的未花費輸出,最終將輸出儲存到 bucket 中。

Blockchain.FindUTXO 幾乎跟 Blockchain.FindUnspentTransactions 一模一樣,但是現在它返回了一個 TransactionID -> TransactionOutputs 的 map。

func (bc *Blockchain) FindUTXO() map[string]TXOutputs {
	UTXO := make(map[string]TXOutputs)
	spentTXOs := make(map[string][]int)
	bci := bc.Iterator()

	//...

	return UTXO
}

現在,UTXO 集可以用於傳送幣:

// FindSpendableOutputs 找到並返回未花費的輸出
func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	accumulated := 0//未花費累計值
	db := u.Blockchain.db

	err := db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))
		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			txID := hex.EncodeToString(k)
			outs := DeserializeOutputs(v)

			for outIdx, out := range outs.Outputs {
				if out.IsLockedWithKey(pubkeyHash) && accumulated < amount {
					accumulated += out.Value
					unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)
				}
			}
		}

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

	return accumulated, unspentOutputs
}

或者檢查餘額:

// FindUTXO 檢查屬於這個公鑰雜湊的餘額
func (u UTXOSet) FindUTXO(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	db := u.Blockchain.db

	err := db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))
		c := b.Cursor()

		for k, v := c.First(); k != nil; k, v = c.Next() {
			outs := DeserializeOutputs(v)

			for _, out := range outs.Outputs {
				if out.IsLockedWithKey(pubKeyHash) {
					UTXOs = append(UTXOs, out)
				}
			}
		}

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

	return UTXOs
}

這是 Blockchain 方法的簡單修改後的版本。這個 Blockchain 方法已經不再需要了。

有了 UTXO 集,也就意味著我們的資料(交易)現在已經被分開儲存:實際交易被儲存在區塊鏈中,未花費輸出被儲存在 UTXO 集中。這樣一來,我們就需要一個良好的同步機制,因為我們想要 UTXO 集時刻處於最新狀態,並且儲存最新交易的輸出。但是我們不想每生成一個新塊,就重新生成索引,因為這正是我們要極力避免的頻繁區塊鏈掃描。因此,我們需要一個機制來更新 UTXO 集:

// Update 從區塊鏈中更新UTXO集

func (u UTXOSet) Update(block *Block) {
	db := u.Blockchain.db

	err := db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(utxoBucket))

		for _, tx := range block.Transactions {
			if tx.IsCoinbase() == false {
				for _, vin := range tx.Vin {
					updatedOuts := TXOutputs{}
					outsBytes := b.Get(vin.Txid)
					outs := DeserializeOutputs(outsBytes)

					for outIdx, out := range outs.Outputs {
						if outIdx != vin.Vout {
							updatedOuts.Outputs = append(updatedOuts.Outputs, out)
						}
					}

					if len(updatedOuts.Outputs) == 0 {
						err := b.Delete(vin.Txid)
						if err != nil {
							log.Panic(err)
						}
					} else {
						err := b.Put(vin.Txid, updatedOuts.Serialize())
						if err != nil {
							log.Panic(err)
						}
					}

				}
			}

			newOutputs := TXOutputs{}
			for _, out := range tx.Vout {
				newOutputs.Outputs = append(newOutputs.Outputs, out)
			}

			err := b.Put(tx.ID, newOutputs.Serialize())
			if err != nil {
				log.Panic(err)
			}
		}

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

雖然這個方法看起來有點複雜,但是它所要做的事情非常直觀。當挖出一個新塊時,應該更新 UTXO 集。**更新意味著移除已花費輸出,並從新挖出來的交易中加入未花費輸出。**如果一筆交易的輸出被移除,並且不再包含任何輸出,那麼這筆交易也應該被移除。相當簡單!

現在讓我們在必要的時候使用 UTXO 集:

func (cli *CLI) createBlockchain(address string) {
	if !ValidateAddress(address) {
		log.Panic("ERROR: Address is not valid")
	}
	bc := CreateBlockchain(address)
	defer bc.db.Close()

	UTXOSet := UTXOSet{bc}
	UTXOSet.Reindex()

	fmt.Println("Done!")
}

當一個新的區塊鏈被建立以後,就會立刻進行重建索引。目前,這是 Reindex 唯一使用的地方,即使這裡看起來有點“殺雞用牛刀”,因為一條鏈開始的時候,只有一個塊,裡面只有一筆交易,Update 已經被使用了。不過我們在未來可能需要重建索引的機制。
在這裡插入圖片描述
當挖出一個新塊時,UTXO 集就會進行更新。
讓我們來檢查一下如否如期工作:
在這裡插入圖片描述
很好!1NQ8HQjUv7Xvzbo9wZFs7pNwW2dG6jC7uy 地址接收到了 3 筆獎勵:
1.一次是挖出創世塊
2.一次是挖出塊 000068b1b5fbdb932029a07efa26578115cf2249f727ab839159f81e38eaa9f1
3.一個是挖出塊 000028902eaa65d298908a42fa3fe089774afc4a2e9ac0ed90e2322a7c52a061

Merkle樹

在這篇文章中,我還想要再討論一個優化機制。
上如上面所提到的,完整的比特幣資料庫(也就是區塊鏈)需要超過 140 Gb 的磁碟空間。因為比特幣的去中心化特性,網路中的每個節點必須是獨立,自給自足的,也就是每個節點必須儲存一個區塊鏈的完整副本。隨著越來越多的人使用比特幣,這條規則變得越來越難以遵守:因為不太可能每個人都去執行一個全節點。並且,由於節點是網路中的完全參與者,它們負有相關責任:節點必須驗證交易和區塊。另外,要想與其他節點互動和下載新塊,也有一定的網路流量需求。

在中本聰的 比特幣原始論文 中,對這個問題也有一個解決方案:簡易支付驗證(Simplified Payment Verification, SPV)。SPV 是一個比特幣輕節點,它不需要下載整個區塊鏈,也不需要驗證區塊和交易。相反,它會在區塊鏈查詢交易(為了驗證支付),並且需要連線到一個全節點來檢索必要的資料。這個機制允許在僅執行一個全節點的情況下有多個輕錢包。

為了實現 SPV,需要有一個方式來檢查是否一個區塊包含了某筆交易,而無須下載整個區塊。這就是 Merkle 樹所要完成的事情。

比特幣用 Merkle 樹來獲取交易雜湊,雜湊被儲存在區塊頭中,並會用於工作量證明系統。到目前為止,我們只是將一個塊裡面的每筆交易雜湊連線了起來,將在上面應用了 SHA-256 演算法。雖然這是一個用於獲取區塊交易唯一表示的一個不錯的途徑,但是它沒有利用到 Merkle 樹。

來看一下 Merkle 樹:
在這裡插入圖片描述
每個塊都會有一個 Merkle 樹,它從葉子節點(樹的底部)開始,一個葉子節點就是一個交易雜湊(比特幣使用雙 SHA256 雜湊)。葉子節點的數量必須是雙數,但是並非每個塊都包含了雙數的交易。因為,如果一個塊裡面的交易數為單數,那麼就將最後一個葉子節點(也就是 Merkle 樹的最後一個交易,不是區塊的最後一筆交易)複製一份湊成雙數。

從下往上,兩兩成對,連線兩個節點雜湊,將組合雜湊作為新的雜湊。新的雜湊就成為新的樹節點。重複該過程,直到僅有一個節點,也就是樹根。根雜湊然後就會當做是整個塊交易的唯一標示,將它儲存到區塊頭,然後用於工作量證明。

Merkle 樹的好處就是一個節點可以在不下載整個塊的情況下,驗證是否包含某筆交易。並且這些只需要一個交易雜湊,一個 Merkle 樹根雜湊和一個 Merkle 路徑。

最後,來寫程式碼:

// MerkleTree 表示一顆Merkle 樹
type MerkleTree struct {
	RootNode *MerkleNode //連線到下個結點的根節點
}

// MerkleNode 代表一個Merkle樹的節點
type MerkleNode struct {
	Left  *MerkleNode  //指向左分支
	Right *MerkleNode  //指向右分支
	Data  []byte  //資料
}

讓我們首先來建立一個新的節點:


// NewMerkleNode 建立一個新的Merkle tree 節點
func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode {
	mNode := MerkleNode{}

	if left == nil && right == nil { //當節點在葉子節點
		hash := sha256.Sum256(data)//序列化的交易傳入
		mNode.Data = hash[:]
	} else {
		prevHashes := append(left.Data, right.Data...)//當一個節點被關聯到其他節點,把其他節點的資料取過來連線
		hash := sha256.Sum256(prevHashes)//連線後再雜湊
		mNode.Data = hash[:]
	}

	mNode.Left = left
	mNode.Right = right

	return &mNode
}

當生成一棵新樹時,要確保的第一件事就是葉子節點必須是雙數。然後,資料(也就是一個序列化後交易的陣列)被轉換成樹的葉子,從這些葉子再慢慢形成一棵樹。

// NewMerkleTree 建立一棵新的merkle樹
func NewMerkleTree(data [][]byte) *MerkleTree {
	var nodes []MerkleNode //將節點用陣列形式表示

	if len(data)%2 != 0 {//確保葉子節點必須是雙數
		data = append(data, data[len(data)-1])
	}

	for _, datum := range data {
		node := NewMerkleNode(nil, nil, datum)//將資料轉化為葉子節點
		nodes = append(nodes, *node)
	}

	for i := 0; i < len(data)/2; i++ {
		var newLevel []MerkleNode

		for j := 0; j < len(nodes); j += 2 {
			node := NewMerkleNode(&nodes[j], &nodes[j+1], nil)//將葉子節點生成樹(上一次的節點)
			newLevel = append(newLevel, *node)
		}

		nodes = newLevel
	}

	mTree := MerkleTree{&nodes[0]}

	return &mTree
}

btcsuite/btcd 是用陣列實現的 merkle 樹,因為這麼做可以減少一半的記憶體使用。

現在,讓我們來修改 Block.HashTransactions,它用於在工作量證明系統中獲取交易雜湊:

// HashTransactions 返回區塊中交易的雜湊
func (b *Block) HashTransactions() []byte {
	var transactions [][]byte

	for _, tx := range b.Transactions {
		transactions = append(transactions, tx.Serialize())//使用序列化後的交易構建一個merkle樹
	}
	mTree := NewMerkleTree(transactions)

	return mTree.RootNode.Data //樹根作為塊交易的唯一識別符號
}

首先,交易被序列化(使用 encoding/gob),然後使用序列後的交易構建一個 Mekle 樹。樹根將會作為塊交易的唯一識別符號。

P2PKH

大家應該還記得,在比特幣中有一個 指令碼(Script)程式語言,它用於鎖定交易輸出;交易輸入提供瞭解鎖輸出的資料。這個語言非常簡單,用這個語言寫的程式碼其實就是一系列資料和操作符而已。比如如下示例:
5 2 OP_ADD 7 OP_EQUAL
5, 2, 和 7 是資料,OP_ADD 和 OP_EQUAL 是操作符。指令碼程式碼從左到右執行:將資料依次放入棧內,當遇到操作符時,就從棧內取出資料,並將操作符作用於資料,然後將結果作為棧頂元素。指令碼的棧,實際上就是一個先進後出的記憶體儲存:棧裡的第一個元素最後一個取出,後面的每一個元素都會放到前一個元素之上。
在這裡插入圖片描述
OP_ADD 從棧內取兩個元素,將這兩個元素進行相加,然後將結果重新放回棧內。OP_EQUAL 從棧內取兩個元素,然後對這兩個元素進行比較:如果它們相等,就在棧上放一個 true,否則放一個 false。指令碼執行的結果就是棧頂元素:在我們的案例中,如果是 true,那麼表明指令碼執行成功。
現在來看一下在比特幣中,是如何用指令碼執行支付的:
OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG
這個指令碼叫做 Pay to Public Key Hash(P2PKH),這是比特幣最常用的一個指令碼。它所做的事情就是向一個公鑰雜湊支付,也就是說,用某一個公鑰鎖定一些幣。這是比特幣支付的核心:沒有賬戶,沒有資金轉移;只有一個指令碼檢查提供的簽名和公鑰是否正確。

這個指令碼實際儲存為兩個部分:
1.第一個部分, ,儲存在輸入的 ScriptSig 欄位。
2.第二部分,OP_DUP OP_HASH160 OP_EQUALVERYFY OP_CHECKSIG 儲存在輸出的 ScriptPubKey 裡面。
在這裡插入圖片描述

因此,輸出定了解鎖的邏輯,輸入提供解鎖輸出的“鑰匙”。然我們來執行一下這個指令碼:
OP_DUP 對棧頂元素進行復制。OP_HASH160 取棧頂元素,然後用 RIPEMD160 對它進行雜湊,再將結果送回到棧上。OP_EQUALVERIFY 將棧頂的兩個元素進行比較,如果它們不相等,終止指令碼。OP_CHECKSIG 通過對交易進行雜湊,並使用 和 pubKey 來驗證一筆交易的簽名。最後的操作符有點複雜:它生成了一個修剪後的交易副本,對它進行雜湊(因為它是一個被簽名後的交易雜湊),然後使用提供的 和 pubKey 檢查簽名是否正確。
有了一個這樣的指令碼語言,實際上也可以讓比特幣成為一個智慧合約平臺:除了將一個單一的公鑰轉移資金,這個語言還使得一些其他的支付方案成為可能。

總結

我們已經實現了一個基於區塊鏈的加密貨幣的幾乎所有關鍵特性。我們已經有了區塊鏈,地址,挖礦和交易。但是要想給這些所有的機制賦予生命,讓比特幣成為一個全球系統,還有一個不可或缺的環節:共識(consensus)。在下一節的文章中,我們將會開始實現區塊鏈的“去中心化(decenteralized)”。

網路

到目前為止,我們所構建的原型已經具備了區塊鏈所有的關鍵特性:匿名,安全,隨機生成的地址;區塊鏈資料儲存;工作量證明系統;可靠地儲存交易。儘管這些特性都不可或缺,但是仍有不足。能夠使得這些特性真正發光發熱,使得加密貨幣成為可能的,是網路(network)。如果實現的這樣一個區塊鏈僅僅執行在單一節點上,有什麼用呢?如果只有一個使用者,那麼這些基於密碼學的特性,又有什麼用呢?正是由於網路,才使得整個機制能夠運轉和發光發熱。

你可以將這些區塊鏈特性認為是規則(rule),類似於人類在一起生活,繁衍生息建立的規則,一種社會安排。區塊鏈網路就是一個程式社群,裡面的每個程式都遵循同樣的規則,正是由於遵循著同一個規則,才使得網路能夠長存。類似的,當人們都有著同樣的想法,就能夠將拳頭攥在一起構建一個更好的生活。如果有人遵循著不同的規則,那麼他們就將生活在一個分裂的社群(州,公社,等等)中。同樣的,如果有區塊鏈節點遵循不同的規則,那麼也會形成一個分裂的網路(例如比特幣的分叉)。

重點在於:如果沒有網路,或者大部分節點都不遵守同樣的規則,那麼規則就會形同虛設,毫無用處!

宣告:不幸的是,我並沒有足夠的時間來實現一個真實的 P2P 網路原型。本文我會展示一個最常見的場景,這個場景涉及不同型別的節點。繼續改進這個場景,將它實現為一個 P2P網路,對你來說是一個很好的挑戰和實踐!除了本文的場景,我也無法保證在其他場景將會正常工作。抱歉!

區塊鏈網路

區塊鏈網路是去中心化的,這意味著沒有伺服器,客戶端也不需要依賴伺服器來獲取或處理資料。在區塊鏈網路中,有的是節點,每個節點是網路的一個完全(full-fledged)成員。節點就是一切:它既是一個客戶端,也是一個伺服器。這一點需要牢記於心,因為這與傳統的網頁應用非常不同。
區塊鏈網路是一個 P2P(Peer-to-Peer,端到端)的網路,即節點直接連線到其他節點。它的拓撲是扁平的,因為在節點的世界中沒有層級之分。下面是它的示意圖:
在這裡插入圖片描述
要實現這樣一個網路節點更加困難,因為它們必須執行很多操作。每個節點必須與很多其他節點進行互動,它必須請求其他節點的狀態,與自己的狀態進行比較,當狀態過時的時候進行更新。

節點角色

管節點具有完備成熟的屬性,但是它們也可以在網路中扮演不同角色。比如:

  • 礦工 這樣的節點運行於強大或專用的硬體(比如
    ASIC)之上,它們唯一的目標是,儘可能快地挖出新塊。礦工是區塊鏈中唯一可能會用到工作量證明的角色,因為挖礦實際上意味著解決 PoW
    難題。在權益證明 PoS 的區塊鏈中,沒有挖礦。
  • 全節點
    這些節點驗證礦工挖出來的塊的有效性,並對交易進行確認。為此,他們必須擁有區塊鏈的完整拷貝。同時,全節點執行路由操作,幫助其他節點發現彼此。對於網路來說,非常重要的一段就是要有足夠多的全節點。因為正是這些節點執行了決策功能:他們決定了一個塊或一筆交易的有效性。
  • SPV SPV 表示 Simplified Payment
    Verification,簡單支付驗證。這些節點並不儲存整個區塊鏈副本,但是仍然能夠對交易進行驗證(不過不是驗證全部交易,而是一個交易子集,比如,傳送到某個指定地址的交易)。一個
    SPV 節點依賴一個全節點來獲取資料,可能有多個 SPV 節點連線到一個全節點。SPV
    使得錢包應用成為可能:一個人不需要下載整個區塊鏈,但是仍能夠驗證他的交易。

網路簡化

為了在目前的區塊鏈原型中實現網路,我們不得不簡化一些事情。因為我們沒有那麼多的計算機來模擬一個多節點的網路。當然,我們可以使用虛擬機器或是 Docker 來解決這個問題,但是這會使一切都變得更復雜:你將不得不先解決可能出現的虛擬機器或 Docker 問題,而我的目標是將全部精力都放在區塊鏈實現上。所以,我們想要在一臺機器上執行多個區塊鏈節點,同時希望它們有