1 区块链中的网络
区块链是一个分布式数据库,所谓的分布式就是分散在多台电脑上面,所以需要具有网络通信功能。区块链是一个分布式系统,意味着其中没有客户端和服务器,所有的网路节点都是平等的。一个节点兼具服务器和客户端俩种角色,这与传统的网络应用非常不同。
区块链网络使用P2P网络,即网络中的节点都是俩俩直接互联的。下图是p2p网络拓扑图。它的拓扑是扁平化的,因为其中不存在层级。这种网络中的节点实现起来都很有难度,因为它们要执行大量操作。每个节点都需要与大量其它节点进行通信交互,它需要接收其它节点的状态,并和自己的状态进行比较,当自身状态过时以后需要进行状态更新。
2 节点角色
尽管区块链中节点都是平等的,但它们还是可以网络中扮演不同的角色。主要有一下几种:
1. 矿工
这些节点运行在性能强劲或专用硬件上面(比如ASIC),它们的目标就是尽可能快的挖掘出新区快。挖矿只存在于使用PoW共识机制的区块链中,因为挖矿实际上就是在解答PoW谜语。比如在PoS区块链中,就不需要挖矿。
2. 全节点
这些节点同步并验证由旷工挖掘出的新区快,同时也验证其中的交易。为了做到这一点,它们必须有整个区块链的全拷贝。同时全节点还扮演着网络路由的角色,帮助其它节点互相发现。区块链网络中存在大量全节点是一件很重要的事情,因为正是这些节点在扮演决策角色:它们决定一个区块或一笔交易是否有效。
3. SPV
SPV用于做简单支付验证(Simplified Payment Verification)。这些节点只保存区块链部分数据,但是它们还可以验证交易(SPV只能验证全部交易的一个子集,只能验证部分发送到特定地址的交易)。SPV节点需要依靠全节点来获取数据,可以有很多SPV节点连接到一个全节点。SPV使得钱包应用成为可能:一个人不需要下载全部数据,但是仍然可以验证它们的交易。
3 网络简化
为了实现区块链网络,本文中做了一些简化。我们没有很多电脑来模拟一个多节点网络。我们可以使用虚拟机或Docker来解决这个问题,但是这将带来大量关于虚拟机和Docker配置相关的工作。由于我的目标只集中于区块链实现,所以我想在一台电脑上面运行运行多个区块链节点。为了达到这种效果我们使用端口号而不是ip地址来区分不同节点,例如我们将使用地址127.0.0.1:3000 , 127.0.0.1:3001 , 127.0.0.1:3002等。我们将通过设置环境变量NODE_ID来设置端口。我们将打开多个终端,为每个终端设置一个NODE_ID环境变量,然后再运行多个区块链节点。
这种方案要求有多个不同的区块链和钱包文件,我们用NODE_ID对应的端口号来区分它们,比如像 blockchain_3000.db , blockchain_30001.db 和 wallet_3000.db , wallet_30001.db等。
4 实现
当我们第一次运行一个比特币节点的时候,节点将连接到其它节点去下载比特币区块链的最新状态,但是电脑是怎么找到其它的节点地址的呢?硬编码节点地址到比特币代码中将会是一个错误,因为这些节点可能会被攻击会关机,这样会导致其它的节点连接不到区块链网络。比特币代码中硬编码有DNS种子。这些种子不是区块链节点而是DNS服务器,它们知道一些节点的地址。当你启动比特币客户端的时候,它将连接到其中一个节点去获取一套完整的区块链数据。
在我们的方案中,我们将会采用一种中心化的方式,我们有三种节点类型:
1. 中心节点。其它节点都会连接到中心节点,中心节点会把数据转发给所有节点。
2. 矿工节点。矿工节点会在内存中存储交易,当交易到达足够数量的时候,它将挖掘出新区快。
3. 钱包节点。钱包节点会在不同钱包见转账。不同于SPV,它们保存区块链全部数据。
5 方案
本文将实现下面的方案:
1. 中心节点创建区块链。
2. 钱包节点连接到中心节点并下载最新数据。
3. 矿工节点连接到中心节点并下载最新数据。
4. 钱包节点创建交易。
5. 矿工节点接受交易并把他们缓存在内存中。
6. 当内存中缓存有足够的交易后,矿工节点将会挖掘出新区快。
7. 当新区快被挖掘出来后,将会被转发给中心节点。
8. 钱包节点和中心节点进行同步。
9. 钱包节点检查他们的交易是否成功。
这些类似于比特币的机制。尽管我们不准备构建一个真正的P2P网络,我们还是会实现一个真正的具有比特币区块链最主要特性的一个区块链。
version
节点之间通过消息进行通信。当一个新节点运行时,它从DNS服务器取得几个节点,向它们发送version消息,在我们实现中它是这样的:
type version struct {
Version int
BestHeight int
AddrFrom string
}
我们只有一个区块链版本,所以version字段不保存重要信息,BestHeight字段当前节点的区块链高度,AddFrom保存消息发送者的地址。
当一个节点接收到version消息后,它将回应一个version消息。这是一次握手,这不只是礼貌问题,还是为了找出更长的额区块链。当一个节点收到一个version消息时,若它检查到version消息中的数据库高度比当前节点的BestHeight还大时,它就从发送该消息的节点处去下载区块。
为了接收消息,我们定义一个server:
var nodeAddress string
var knownNodes = []string{"localhost:3000"}
func StartServer(nodeID, minerAddress string) {
nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
miningAddress = minerAddress
ln, err := net.Listen(protocol, nodeAddress)
defer ln.Close()
bc := NewBlockchain(nodeID)
if nodeAddress != knownNodes[0] {
sendVersion(knownNodes[0], bc)
}
for {
conn, err := ln.Accept()
go handleConnection(conn, bc)
}
}
首先我们硬编码中心节点的地址,就是knowNodes[0]。miningAddress定义了挖矿节点接收挖矿奖励的地址。下面这句:
if nodeAddress != knownNodes[0] {
sendVersion(knownNodes[0], bc)
}
若当前节点不是中心节点,则它必须发送version消息给中心节点去检查它的区块链是否已经过时。
func sendVersion(addr string, bc *Blockchain) {
bestHeight := bc.GetBestHeight()
payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
request := append(commandToBytes("version"), payload...)
sendData(addr, request)
}
我们的消息是一个字节数组,它的前12字节制定了命令名称,在这个例子中是"version",接下来的字节是用godEcode序列化以后的消息体结构。commandToBytes长成这样:
func commandToBytes(command string) []byte {
var bytes [commandLength]byte
for i, c := range command {
bytes[i] = byte(c)
}
return bytes[:]
}
它创建了一个12字节的数组,然后将命令名称复制进来。它有一个对应的反操作函数:
func bytesToCommand(bytes []byte) string {
var command []byte
for _, b := range bytes {
if b != 0x0 {
command = append(command, b)
}
}
return fmt.Sprintf("%s", command)
}
当一个节点接收到消息,它使用bytesToCommand去提取命令名称,根据命令名称去执行对应的方法。
func handleConnection(conn net.Conn, bc *Blockchain) {
request, err := ioutil.ReadAll(conn)
command := bytesToCommand(request[:commandLength])
fmt.Printf("Received %s command\n", command)
switch command {
...
case "version":
handleVersion(request, bc)
default:
fmt.Println("Unknown command!")
}
conn.Close()
}
下面就是version命令的处理函数:
func handleVersion(request []byte, bc *Blockchain) {
var buff bytes.Buffer
var payload verzion
buff.Write(request[commandLength:])
dec := gob.NewDecoder(&buff)
err := dec.Decode(&payload)
myBestHeight := bc.GetBestHeight()
foreignerBestHeight := payload.BestHeight
if myBestHeight < foreignerBestHeight {
sendGetBlocks(payload.AddrFrom)
} else if myBestHeight > foreignerBestHeight {
sendVersion(payload.AddrFrom, bc)
}
if !nodeIsKnown(payload.AddrFrom) {
knownNodes = append(knownNodes, payload.AddrFrom)
}
}
首先我们解码消息数据并提取负载数据。所有的处理函数这个部分都一样,所以在以后的代码解说中我们都会忽略这一步。
一个节点比较它自身的区块高度BestHeight和version消息中的高度,如果它自身的更长,它就回应一个version消息给对方,否则,它就向对方发送getblocks消息去请求区块数据。
getblocks
type getblocks struct {
AddrFrom string
}
getblocks消息的意思就是“告诉我你有哪些区块”。注意,它并没有说“给我你所有的区块”,它请求一个区块哈希列表。这样做是为了减小网络负载,因为区块可以从各个不同的节点去下载,我们不想从一个节点去下载好多G的数据。
这个消息处理函数非常简单:
func handleGetBlocks(request []byte, bc *Blockchain) {
...
blocks := bc.GetBlockHashes()
sendInv(payload.AddrFrom, "block", blocks)
}
在我们简单实现中,我们让它返回所有区块hash。
inv
type inv struct {
AddrFrom string
Type string
Items [][]byte
}
比特币使用inv消息去告诉其它节点它拥有的区块或交易。它也不包含完整区块或交易数据,而只是它们的哈希。Type字段指明是交易还是区块类型。
inv的处理函数复杂一点:
func handleInv(request []byte, bc *Blockchain) {
...
fmt.Printf("Recevied inventory with %d %s\n", len(payload.Items), payload.Type)
if payload.Type == "block" {
blocksInTransit = payload.Items
blockHash := payload.Items[0]
sendGetData(payload.AddrFrom, "block", blockHash)
newInTransit := [][]byte{}
for _, b := range blocksInTransit {
if bytes.Compare(b, blockHash) != 0 {
newInTransit = append(newInTransit, b)
}
}
blocksInTransit = newInTransit
}
if payload.Type == "tx" {
txID := payload.Items[0]
if mempool[hex.EncodeToString(txID)].ID == nil {
sendGetData(payload.AddrFrom, "tx", txID)
}
}
}
当接收完哈希数据,我们将它保存在blocksInTransit数组中以便跟踪下载区块,这允许我们可以从不同节点下载区块。一旦我们将区块哈希保存后,我们马上向inv消息发送者发送一个getdata消息去请求一个区块数据,然后我们再更新blocksInTransit数组。
在我们的实现中,我们用inv发送交易消息时只发送一条,所以这也为什么在"payload.Type == "tx"中我们只提取payload.Item[0]。我们检查我们的内存池中是否已经有这条交易哈希,如果没有,则会发送getdata消息去获取。
getdata
type getdata struct {
AddrFrom string
Type string
ID []byte
}
getdata用来请求区块或交易,它只能带一个区块或交易哈希做参数。
func handleGetData(request []byte, bc *Blockchain) {
...
if payload.Type == "block" {
block, err := bc.GetBlock([]byte(payload.ID))
sendBlock(payload.AddrFrom, &block)
}
if payload.Type == "tx" {
txID := hex.EncodeToString(payload.ID)
tx := mempool[txID]
sendTx(payload.AddrFrom, &tx)
}
}
getdata消息的处理函数很简单,如果请求区块,就返回区块;如果请求交易,就返回交易。注意,我们在这里并没有检查区块或交易是否存在,这是一个漏洞。
block和tx
type block struct {
AddrFrom string
Block []byte
}
type tx struct {
AddFrom string
Transaction []byte
}
就是这些消息用来进行实际的数据传输。
处理block消息非常简单:
func handleBlock(request []byte, bc *Blockchain) {
...
blockData := payload.Block
block := DeserializeBlock(blockData)
fmt.Println("Recevied a new block!")
bc.AddBlock(block)
fmt.Printf("Added block %x\n", block.Hash)
if len(blocksInTransit) > 0 {
blockHash := blocksInTransit[0]
sendGetData(payload.AddrFrom, "block", blockHash)
blocksInTransit = blocksInTransit[1:]
} else {
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
}
}
当节点接收到一个新区快,就将它加入到区块链中,如果还存在更多需要下载的区块,我们将再次向之前的区块发送方去请求它们。当我们终于下载了所有区块,UTXOSet将会重新索引。
todo:我们在将每一个新到来的区块加入到区块链中前需要验证它,而不是无条件的信任它。
todo:我们应该使用UTXOSet.update(block),而不是运行UTXOSet.Reindex()。因为区块链很大,重新索引整个UTXOSet是一件很耗时的事情。
处理tx消息的函数是最难的部分:
func handleTx(request []byte, bc *Blockchain) {
...
txData := payload.Transaction
tx := DeserializeTransaction(txData)
mempool[hex.EncodeToString(tx.ID)] = tx
if nodeAddress == knownNodes[0] {
for _, node := range knownNodes {
if node != nodeAddress && node != payload.AddFrom {
sendInv(node, "tx", [][]byte{tx.ID})
}
}
} else {
if len(mempool) >= 2 && len(miningAddress) > 0 {
MineTransactions:
var txs []*Transaction
for id := range mempool {
tx := mempool[id]
if bc.VerifyTransaction(&tx) {
txs = append(txs, &tx)
}
}
if len(txs) == 0 {
fmt.Println("All transactions are invalid! Waiting for new ones...")
return
}
cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)
newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
fmt.Println("New block is mined!")
for _, tx := range txs {
txID := hex.EncodeToString(tx.ID)
delete(mempool, txID)
}
for _, node := range knownNodes {
if node != nodeAddress {
sendInv(node, "block", [][]byte{newBlock.Hash})
}
}
if len(mempool) > 0 {
goto MineTransactions
}
}
}
}
首先将接收到的交易放入mempool中(注意,这里也还需要对交易进行验证才能加入)。下一段:
if nodeAddress == knownNodes[0] {
for _, node := range knownNodes {
if node != nodeAddress && node != payload.AddFrom {
sendInv(node, "tx", [][]byte{tx.ID})
}
}
}
检查是否当前节点是否是中心节点,若是则将当前交易向网络所有已知节点进行广播。
下一大段都是关于挖矿节点的,我们分解来看:
if len(mempool) >= 2 && len(miningAddress) > 0 {
只有矿工节点才会设置miningAddress,当内存池中有不少于2个交易时,矿工节点就开始挖矿。
for id := range mempool {
tx := mempool[id]
if bc.VerifyTransaction(&tx) {
txs = append(txs, &tx)
}
}
if len(txs) == 0 {
fmt.Println("All transactions are invalid! Waiting for new ones...")
return
}
这里验证mempool中的交易,只有验证通过的交易才会加入到挖矿交易中。若所有交易都验证失败,则挖矿会中断。
cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)
newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()
fmt.Println("New block is mined!")
这里加入了一个coinbase交易进来,为了给矿工发送奖励。然后挖矿,当挖矿成功后产生新区快,UTXOSet会被重新索引。
todo:UTXOSet.Reindex应该被UTXOSet.Update来替换掉。
for _, tx := range txs {
txID := hex.EncodeToString(tx.ID)
delete(mempool, txID)
}
for _, node := range knownNodes {
if node != nodeAddress {
sendInv(node, "block", [][]byte{newBlock.Hash})
}
}
if len(mempool) > 0 {
goto MineTransactions
}
当一个交易被打包到新区快后,它就会从mempool中移除。然后矿工节点向所有已知节点广播这个新区快。其它节点可以在收到消息后向矿工节点请求新区块。
5 实验
代码见https://github.com/Jeiwan/blockchain_go
准备工作
我们先生成可执行文件。然后再在本地新建3个文件夹,命名为Node1,Node2,Node3。将可执行文件拷贝到3个目录中。然后在这三个文件夹中打开3个3个终端。对于第一个终端,设置一个环境变量NODE_ID为3000,这可以通过命令:
C:\Users\lzj\Desktop\Node1>set NODE_ID=3000
对于第二个终端,设置NODE_ID为3001,第三个终端设置NODE_ID为3002。我们将在第1个文件夹中创建中心节点,再第2个文件夹中创建钱包节点,在第3个文件夹中创建矿工节点。
先在第1个文件夹中新建4个钱包:
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createwallet
Your new address: 1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe listaddresses
19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
第1个文件夹中会出现wallet_3000.dat钱包文件。然后将该钱包文件拷贝到第2个文件夹中,重命名为wallet_3001.dat,拷贝到第3个文件夹中,重命名为wallet_3002.dat。现在,三个节点都能访问这些钱包了。
我们现在第1个文件夹中新建区块链节点:
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe createblockchain -a
ddress 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
41371b1df93a5d88c7bfe7deced007ef658160ab1c8961bf342a44a30b0c859e
Done!
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address
19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 10
这个时候第1个文件夹中会出现一个名为blockchain_3000.db的区块链数据库文件。它包括了创世区块。我们将这个数据库拷贝到第2个文件夹中,重命名为blockchain_3001.db,拷贝到第3个文件夹中,重命名为blockchain_3002.db。现在,这3个节点都拥有共同的创世区块了。
启动中心节点
在节点1中执行转账:
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p -amount 4 -mine
e55627437bf39553e7305117cc8aba3e5ba695679d189fe494558fe6d1256047
Success!
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
Balance of '1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p': 4
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s -amount 4 -mine
9eda2cb402dc79039a2b263bf2d98e45bb0eef46f25487e171bcd8bd942d2956
Success!
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
Balance of '1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s': 4
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe getbalance -address 19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 22
这里使用 -mine 标记去让节点立即去挖掘新区快。由于一开始还没有矿工节点,所以我们加入这个标记。现在可以看到19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z的余额为22,1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p的余额为4,。由于中心节点原来有10个,转出了8个,但是由于挖出了2个区块奖励了20,所以现在余额是22。
现在我们启动节点1:
C:\Users\lzj\Desktop\Node1>go_build_blockchain_go_master.exe startnode
节点1作为中心节点,服务启动后就让它一直保持运行知道实验停止。
启动钱包节点
在文件夹2中执行:
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe startnode
Starting node 3001
Received version command
Received inv command
Recevied inventory with 3 block
Received block command
Recevied a new block!
Added block 0000b2cdb0fd731118694e297b7eb51125a8ce7297ed1fae1d4e13cdf46ac9d9
Received block command
Recevied a new block!
Added block 0000c24f671f0582e4d3777d20929fa5ebc240e3a7bcb75cd12b902c51b09880
Received block command
Recevied a new block!
Added block 0000920773da4ee647ee4bb37ae3a71f8e97a49d334a74963da91bc42511cfc9
可以看到钱包节点会从中心节点同步区块链数据过来,由于中心节点发起了俩笔转账挖掘了2个区块,所以这里一共有3个区块。
终端钱包节点来查看一下余额是不是对的:
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z
Balance of '19bDbkDETDRrkjKSP5kSm5ynjgKgeUNo3Z': 22
正确!说明区块确实同步成功了。
启动挖矿节点
我们使用下面命令启动钱包节点,它使用 -miner 标签来设置基准账户1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR:
C:\Users\lzj\Desktop\Node3>go_build_blockchain_go_master.exe startnode -miner 1H
cEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Starting node 3002
Mining is on. Address to receive rewards: 1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Received version command
Received inv command
Recevied inventory with 3 block
Received block command
Recevied a new block!
Added block 0000b2cdb0fd731118694e297b7eb51125a8ce7297ed1fae1d4e13cdf46ac9d9
Received block command
Recevied a new block!
Added block 0000c24f671f0582e4d3777d20929fa5ebc240e3a7bcb75cd12b902c51b09880
Received block command
Recevied a new block!
Added block 0000920773da4ee647ee4bb37ae3a71f8e97a49d334a74963da91bc42511cfc9
一开始也是同步数据。然后在等待交易到来。现在是有挖矿节点来接手挖矿任务了。
使用钱包节点发起交易
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p -amount 2
Success!
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe send -from 19bDbkDE
TDRrkjKSP5kSm5ynjgKgeUNo3Z -to 1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s -amount 5
Success!
挖矿节点显示:
Received inv command
Recevied inventory with 1 tx
Received tx command
Received inv command
Recevied inventory with 1 tx
Received tx command
283f49596c43c11bda8ef260aeb95dc435ab2d62baa797fae21472cb933f8547
New block is mined!
Received getdata command
挖矿节点接收到2笔交易,然后开始挖矿并挖掘除了一个新区快,然后中心服务器向它请求区块数据。
中心节点显示:
Received getdata command
Received tx command
Received getdata command
Received inv command
Recevied inventory with 1 block
Received block command
Recevied a new block!
Added block 00005247783988340d2c60904c81fd61095febc9dea1ca425d07cf65e844d816
中心节点叶也收到了那俩笔交易,但是它只是顺手转发。等矿工节点挖掘并广播新区快后,它就请求新区快并将其加入区块链中。
重新开启钱包节点并同步完数据,再关闭,然后查询余额:
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p
Balance of '1ALJkuHPXvWUniARtJgajEEUL5FP9pda4p': 6
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s
Balance of '1EYiRBrcoaG31EmciWPa9Bq5FFX4NJAN6s': 9
C:\Users\lzj\Desktop\Node2>go_build_blockchain_go_master.exe getbalance -address
1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR
Balance of '1HcEXXGmYhrV1rkjvtEhomaRvFFtTLycMR': 10
可以看到钱包转账成功了。
6 结论
这是这系列文章最后一篇了,我想实现一个P2P网络,但是我没有时间。我希望这些文章能够回答你们关于比特币技术的一些问题。在比特币中还存在许多有趣的其它技术,你可以自己去找到答案!好运!
|
请发表评论