8.4 源码解析

8.4.1 目录结构

我们来看一下微链程序的目录结构,如下所示:

image.png

1)cmd:这是程序的主程序目录,其中包含了入口main函数以及一个命令行接口环境,微链就在这里启动。

2)blockchain:区块链程序,其主要的数据结构就是区块,其中就定义了微链的区块结构。

3)encrypt:微链中使用RSA算法生成私钥公钥以及钱包地址,并使用SHA256算法对事务以及区块计算哈希值,这些算法都定义在此目录中。

4)transaction:比特币中将一次转账交易或者挖矿获得新币的动作都称为事务,微链中也一样,在这个目录中,定义了事务的数据结构。

5)script:区块链应用中一个非常典型的特点就是可编程合约,比特币中使用了一组锁定和解锁脚本来表明一笔比特币的所有权,以太坊扩展了这个脚本的能力,将其变成了图灵完备的合约编程,微链中模拟了比特币的一组指令,这些功能就定义在这个目录中。

6)utxo:这是比特币中发起的一个概念,叫未花费输出,微链中同样模拟了这个结构。

7)miner:这是挖矿的定义。

8)tinynet:定义了微链的网络接口,支持RPC网络访问以及数据监听,可以认为是微链的网络模块。通过这个模块,可以使外部程序访问微链的核心,也可以使不同的微链节点之间进行通信。

9)rundata:作为一个演示程序,没必要将产生的数据都写入到硬盘中,也可以记录在内存中,比如区块链账本数据、UTXO数据等,以太坊的模拟程序TestRPC便是将数据都模拟在内存中产生的。

10)utils:定义了一些工具方法,比如堆栈操作等。

以上便是一个常见的区块链应用所具备的代码目录结构,通过目录结构的名称我们也能看到所具备的功能设定,当然这里 每一个目录中的功能都是可以有不同的实现的,比如上述的UTXO,在有些应用中并不使用这样的结构,而是使用了账户结构(以太坊与比特币的区别之一)。另 外,通过目录可以看到,微链中集成了核心节点功能、钱包以及挖矿功能。大家在自己进行实验编程的时候,可以根据自己的理解设定工程目录。

总之,一个区块链应用程序无非就是那么几个模块,用一句话来说就是:这是一种软件,使用区块链的结构存储数据,可 以通过钱包进行转账交易等合约性质的事务操作,发生的事务会广播到其他运行的节点,这些事务数据最终通过矿工以执行挖矿算法的方式获得打包权后存储到区块 链结构的数据中。可以看到,除了特有的区块链数据结构以及挖矿机制,其他的都没什么,就是一个普通的软件而已。(当然,就这两项的发明也已经够天才了。)

8.4.2 代码之旅

本节通过对代码做功能分析,让大家了解一下一个最简单的区块链程序到底是怎么组成的,有兴趣的读者也可以使用自己 熟悉的语言环境尝试编写。这是一个很有趣的过程,区块链应用本来就是充满着实验性的软件,用它可以实验各种各样的想法,本身通过一套软件系统来发行货币就 已经是够不可思议的了,这完全打破了人们对于软件作用的认识。

1.主程序

有过软件开发经验的读者肯定知道,再小的程序也会有一个入口的启动程序,也就是主程序,在主程序中会进行一些系统参数的初始化、命令解释器的设置和一些服务的启动,如下所示:

 

package main

import (
   "fmt"
   "os"
   "tinychain/tinynet"
   "tinychain/utils"
   "gopkg.in/urfave/cli.v1"
)
var tinyapp = cli.NewApp()

//应用初始化
func init() {
   tinyapp.Name = "gtinychain"
   tinyapp.Description = "a tiny example of blockchain procedure"
   tinyapp.Version = clientRevision
   tinyapp.Author = "白话区块链"

   //设置子命令
   tinyapp.Commands = []cli.Command{
       initCommand,
       versionCommand,
   }
   
   //设置命令参数
   tinyapp.Flags = []cli.Flag{
       utils.DataDirFlag,
       utils.NetworkIdFlag,
       utils.RPCEnabledFlag,
       utils.RPCPortFlag,
       utils.ListenPortFlag,
   }

   tinyapp.Action = gtinychain
}

//启动命令行主程序
func gtinychain(ctx *cli.Context) error {

   //发现并连接其他节点
   tinynet.DiscoverNodes()

   //进行区块主链的数据同步
   go tinynet.SyncBlockchain()

   //启动rpc与数据监听服务
   go tinynet.StartRpcServer(utils.Rpcport)
   go tinynet.StartListenServer(utils.Listenport)

   //启动命令解释器
   DoCommandInterface()

   return nil
}

func main() {
   if err := tinyapp.Run(os.Args); err != nil {
       fmt.Fprintln(os.Stderr, err)
       
       os.Exit(1)
   }
}


上述便是主程序的示例代码,由于微链被设计为一个命令行程序,因此使用了一个命令行程序开发框架以便于实现子命令 以及命令参数等功能,代码中引用的gopkg.in/urfave/cli.v1就是一个Go语言实现的命令行程序开发框架,这是一个托管在GitHub 上的开源框架,可以通过网址https://github.com/urfave/cli查看源码实现以及详细使用说明。

我们来看看在主程序中主要做了如下哪些事情。

1)加载支持命令及命令参数。

加载后,可以在命令行中执行微链支持的各种主程序指令。假设微链的主程序名是gtinychain,则可以在命令 行中通过gtinychain version输出微链的版本号;通过gtinychain init进行数据目录和创世区块的初始化;通过gtinychain--datadir指定微链的数据目录等,以下列出了部分支持的命令。


 

//输出版本号
gtinychain version

//默认以当前所在目录进行微链初始化
gtinychain init

//以当前目录为数据目录启动,这是默认参数
gtinychain --datadir "./"

//指定rpc服务和数据监听端口启动
gtinychain  --rpcport 52000 --port 62000

//根据ipc文件启动rpc命令控制台
gtinychain attach --ipcfile=’ipc文件路径’


这里说明一下微链初始化的命令,由gtinychain init可以通过读取配置文件来初始化一个创世区块(区块链的第一个区块),并且可以自动创建出一个钱包地址作为测试使用,钱包地址中也可以初始化一个可 用金额,一般在进行初始化的时候还可以指定一个目录,那就会用到--datadir参数,则命令变成了如下形式:


 

gtinychain init --datadir "./"


这个命令将当前目录作为数据目录进行初始化,我们看看初始化命令大致是怎么做的:


 

initCommand = cli.Command{
       Action: func(c *cli.Context) {

           //根据参数指定路径,创建Data与Keystore
           //utils.Datadir就是通过参数--datadir传入的路径
           os.MkdirAll(utils.Datadir+"/Keystore", 0777)
           os.MkdirAll(utils.Datadir+"/Data", 0777)

           //在Keystore目录下创建公钥私钥文件
           encrypt.GenerateRSAKey()
           //加载公钥私钥
           rundata.SetPrvPubKey()
           //加载账户地址,实际就是对公钥的格式化处理
           rundata.Account = encrypt.GetWalletAddr()
           
           //读取datadir指定目录下的genesis文件建立创世区块
           genesisFile, _ := ioutil.ReadFile(utils.Datadir + "genesis.json")
           var gsf GenesisFile
           json.Unmarshal([]byte(string(genesisFile)), &gsf)
           blockchain.CreateGenesisBlock()
       },


这就是init命令的大致过程了,主要任务就是创建钥匙和数据文件夹以及创建创世区块。钥匙文件夹 (Keystore目录)是专门用来存储创建的私钥(钱包地址信息)的,这个文件夹极其重要,一旦丢失,等于这个钱包地址中的资产就丢了,跟现实生活中丢 了钱包是一个道理,所以必须备份好。

注意这里的“加载公钥私钥”和“加载账户地址”,通常的区块链应用中并不必需有这么一个加载的动作,这是为了测试 方便取数,所以将这些信息加载到内存中了(以太坊就有一个模拟测试程序叫TestRPC,是在内存中模拟加载整个环境的,目的还是为了便于测试)。实际上 这里的公钥私钥创建是属于钱包的功能,这部分的功能在初始化过程中是可有可无的,如果在初始化的时候不创建,则可以在节点启动后通过命令另行创建。

2)区块链数据同步。通常一个区块链应用在初始运行的时候,或者说启动的时候,都会干一件事情,那就是区块链数据 的同步(当然,这里指的是核心节点,如果只是使用独立的钱包功能或者挖矿程序,则其本身并没有同步完整区块链数据的需求),所有的操作都应该要等数据同步 完成后才能进行,这是通过两个步骤来完成的:一个是发现其他节点,一个就是从其他节点获取数据。发现其他节点的方法有很多种,比如通过设计一个“发现协 议”以广播的形式寻找同伴,类似于大家约定一个暗号,简单点的做法可以将其他节点的地址信息直接加载进来(类似于比特币的种子节点或者以太坊的星火节 点),联系上其他节点后,就可以要求其他节点发送数据给自己了,其实就是一个下载的过程,只不过可以从多个联系上的节点那里同时下载,下载完成后,自己的 节点就拥有了与网络中的主链一致的区块数据。

3)启动服务、RPC服务和数据监听服务。在Go中可以分别使用net/rpc以及net包来实现,就是一个网络 监听服务而已。为什么这里要搞成两种网络监听服务呢?主要还是对比特币的一个模拟,在比特币中,如果使用外部命令或程序访问核心客户端,只能通过RPC的 方式,并且与核心客户端要在同一机器上,也就是说禁止以远程的方式直接访问比特币的核心客户端,这是一个安全性的考虑。如果是核心客户端之间或者说是节点 之间进行区块数据同步、数据交换等,则使用专门的数据监听服务。这两者的网络端口也是不一样的。微链在这里只是一个模拟,读者自己在尝试的时候,可以根据 需要来决定。

RPC的小知识

RPC也就是Remote Procedure Call,远程过程调用的意思,它是一种基于网络的远程功能调用协议,比如我们打开一款天气预报的手机App,软件向服务器发送一个获取天气情况的功能请 求,远端的服务器收到请求后获取数据,再将结果响应给手机App,这就是完成了一次RPC。注意,RPC只是一种协议规范,不是一个具体的程序实现,这是 一个比较泛的概念,因此有多种实现方式,比如数据的传输方式可以承载在HTTP或者TCP等协议上,而数据的编码可以采用json、xml、 protobuf等格式。各种组合也各有优劣,这里不再赘述。

数据监听服务的实现在下面章节中有专门介绍,我们先来看一下RPC服务的示例代码:


 

package main

import (
   "net"
   "net/rpc"
   "net/rpc/jsonrpc"
   "tinychain/blockchain"
   "tinychain/transaction"
)

type Account int
type Block int
type Tnc int
type Miner int

///<summary>
///根据地址账号获得余额
///</summary>
///<param name="Account">地址账号</param>
///<param name="RemainAmount">返回余额</param>
func (ac *Account) GetBalance(Account string, RemainAmount *int) error {
   return nil
}

///<summary>
///根据区块号获得区块信息
///</summary>
///<param name="BlockNumber">区块号</param>
///<param name="BlockInfo">返回区块信息</param>
func (ac *Block) GetBlockInfo(BlockNumber int, BlockInfo *blockchain.BlockInfo) error {
   return nil
}

///<summary>
///发送交易事务
///</summary>
///<param name="TransactionInfo">构造交易事务</param>
///<param name="Result">返回执行结果</param>
func (ac *Tnc) SendTransaction(TransactionInfo transaction.TransactionInfo, Result *int) error {
   return nil
}

///<summary>
///关闭节点服务
///</summary>
///<param name="Signal">关闭信号</param>
///<param name="Result">返回执行结果</param>
func (ac *Tnc) Close(Signal int, Result *int) error {
   return nil
}

///<summary>
///开启挖矿
///</summary>
///<param name="Signal">开启信号</param>
///<param name="Result">返回执行结果</param>
func (ac *Miner) Start(Signal int, Result *int) error {
   return nil
}

///<summary>
///关闭挖矿
///</summary>
///<param name="Signal">关闭信号</param>
///<param name="Result">返回执行结果</param>
func (ac *Miner) Stop(Signal int, Result *int) error {
   return nil
}

func StartRpcServer(port int) {

   lsn, _ := net.Listen("tcp", ":"+strconv.Itoa(port))
   defer lsn.Close()
   srv := rpc.NewServer()
   srv.RegisterName("Account", new(Account))
   srv.RegisterName("Block", new(Block))
   srv.RegisterName("Tnc", new(Tnc))
   srv.RegisterName("Miner", new(Miner))

   for {
       conn, _ := lsn.Accept()

       go srv.ServeCodec(jsonrpc.NewServerCodec(conn))
   }

}


可以看到,在RPC服务中内置了一组支持的命令方法,比如获取区块信息、启动挖矿等,通过内置的命令解释器(下面 会介绍)可以直接进行调用访问,也可以单独再提供一个客户端程序,通过RPC的方式连接访问,上述示例代码演示的是Go语言中的RPC编写方法,限于篇幅 没有再给出每个方法的详细实现,读者了解是什么意思即可。在Go中,调用RPC服务也很简单,下面给出一个示例:


 

import (
   "fmt"
   "net/rpc/jsonrpc"
   "strconv"
)

func ClientForBalance(port int) {
   client, _ := jsonrpc.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))

   var targetAccount = "MIGfMA0GCSqGSIb3DQEB"
   var replyAmount int
   client.Call("Account.GetBalance", targetAccount, &replyAmount)

   fmt.Printf(strconv.Itoa(replyAmount))

}


通过json-rpc的调用即可实现与RPC服务的交互,上述代码演示了对获取账户地址余额的调用。通过上述的展 示,我们可以看到,虽然区块链应用是一个个独立的客户端程序,运行过程中不需要专门连接一个服务器,但是其本身却集成了服务端功能,可以供外部访问调用。 在现实世界中,比特币、以太坊等区块链应用都集成了类似的服务端,那些运行着的节点,其实就是服务器。

4)启动一个命令解释器,可以输入微链支持的命令与核心进行交互,可以看一下命令解释器的实现代码:


 

func DoCommandInterface() {

   client, _ := jsonrpc.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))
   defer client.Close()
   var cmd string

   for {
       fmt.Print(">>")
       fmt.Scanln(&cmd)

       if cmd == "exit" {

           //退出命令控制台
           os.Exit(1)
           fmt.Println("\n")

       } else if cmd == "tnc.getbalance()" {

           //获得当前账号的余额
           var targetAccount string
           var replyAmount int
           fmt.Print("请输入账号地址:")
           fmt.Scanln(&targetAccount)

           client.Call("Account.GetBalance", targetAccount, &replyAmount)

           fmt.Print(strconv.Itoa(replyAmount))

       }

   }
}


实际上连接RPC服务后,进入一个死循环,然后接受各种支持的字符串指令,这里演示了两个功能:第一个是退出命令解释器,第二个是调取某个账号地址的当前余额。显然根据RPC服务支持的功能,支持的命令还远不止这些,但是原理都是一样的,常用的命令如下:

·admin.close():关闭连接的节点服务,注意不是关闭RPC服务而是关闭整个节点实例的运行。

·tnc.getblocknumber():获得当前最新的区块号。

·tnc.getbalance():获得当前账号的余额。

·tnc.sendtransaction():发送一笔转账交易。

·miner.start():开启挖矿。

·miner.stop():停止挖矿。

实际支持的命令可以根据需要去拓展,这里只是一个代码样式的演示,这些命令都是通过微链的节点核心来执行的。其他 的区块链应用也基本都会提供这样的访问接口,在比特币中,可以通过图形界面的客户端程序进行命令交互的调用,也可以通过一个独立的命令行程序来访问,在以 太坊中,则可以在节点程序启动的时候同时启动一个交互式的控制台来访问节点。

再介绍一个小功能,有时候我们可能希望同时开启多个命令控制台,比如在1号控制台运行挖矿指令,在2号控制台运行 区块查询指令,就像我们日常工作时,经常会为电脑连接多个显示器一样。要实现这个功能很简单,只要在启动第一个命令控制台的时候,在某个目录下生成一个文 本文件,可以命名为tiynchain.ipc或者任何其他的名字,文件中存储RPC服务的连接地址即可,然后通过gtinychain attach--ipcfile='文件路径'这样的命令来启动一个新的命令控制台。

2.区块的定义

作为一个区块链应用程序,其核心的数据结构就是区块了。一般来说,区块中包含的信息主要分为区块头和区块体,区块 头中包含区块的摘要信息,区块体中包含区块事务。至于区块事务是指什么,取决于不同的应用程序,比如比特币中主要就是交易信息,从一个地址到另外一个地址 的交易记录。微链也同样模拟了这一点,我们来看一下区块的定义:


 

type BlockInfo struct {

   //区块编号
   blockNumber int

   //前一个区块哈希
   hashPrevBlock string

   //交易事务的merkle根
   hashMerkleRoot string

   //区块打包的时间戳
   nTime uint32

   //难度位数
   nBits uint32

   //随机目标值
   nNonce uint32

   //交易事务
   trans []transaction.TransactionInfo
}


可以看到,在这个区块中同时包含了摘要信息和区块交易事务。在摘要信息中,当前区块通过“前一个区块哈希”与之前 的区块连接,这也是区块链名词的来源。其中的区块编号就是区块的高度,一个一个区块通过区块哈希连接起来后,每增加一个新的区块高度就增加1,当我们需要 查询某个区块的信息时,可以提供一个方法传入区块编号输出区块的说明信息。交易事务的merkle根是对区块中所有的交易事务进行哈希计算,构造一棵 merkle树而得来,可以用来验证区块的完整性。难度位数和随机目标值是与挖矿有关的参数。

在大多数的区块链应用中,区块的结构定义基本上都采用了上述方案。这里的交易事务其实也不仅仅只能用来表示交易。 微链中只支持数字货币的转账交易,但是在很多功能比较强大的区块链应用(如以太坊)中,事务的概念是更加宽泛的,除了表示转账交易外,也可以表示某个状态 的变更,比如多重签名、合约有效期变更等。

上述代码中提及的哈希算法是SHA256算法,对于实验代码来说,使用何种哈希算法是没有要求的,如果是正式版的程序,要使用比较强壮的抗碰撞能力强的算法。看下微链中的示例代码:


 

//sha256
func GetSHA256(msg string) string {
   hData := sha256.New()
   hData.Write([]byte(msg))
   return fmt.Sprintf("%x", hData.Sum(nil))
}


这是一个很简单的使用。

3.事务的定义

微链中参照了比特币的事务结构,使用了输入和输出的方式来表示一个交易事务:


 

type OutPoint struct {
   //事务哈希
   tranHash string
   //事务的输出部分的索引号
   n int
}

type TxIn struct {
   //指向前一次的输出
   prevOut    OutPoint
   //前一次的输出索引
   sequence   int
   //解锁脚本
   scriptSign script.ScriptAction
}

type TxOut struct {
   //金额
   amount       int
   //锁定脚本
   scriptPubKey script.ScriptAction
}

type TransactionInfo struct {
   //交易事务哈希
   tranHash string
   //输入集合
   txIn []TxIn
   //输出集合
   txOut []TxOut
   //时间戳
   lockTime int64
}


上述代码示例中,TransactionInfo就是事务的结构定义,一条事务在这里可以理解为一笔交易,每一笔 交易都有自己的哈希值,就像身份证号一样,唯一地表示了某一笔发生的交易。我们日常在进行银行转账的时候,通常会先往自己的账户里存钱,然后再转出到目标 账户。换句话说,就是有一个存入和存出,在这里也是一样的,事务的结构中,txin表示存入或者说来源,txout表示输出。如果账户里本来就有钱,不要 先存入再转出,可以直接就转出,那这里的输入还需要吗?回答是:需要,而且必须要。因为在微链中并不会把存入的金额记下来,而只会记录每一笔进账和出账的 流水账,因此每一笔的输出都要指定它的输入来源,这样才能保证账是平的。

对于输出,很好理解,我们可以看到TxOut的结构定义,就是一个金额然后一个锁定脚本(一段指令程序),关于脚本我们下一节再解释,这里可以理解为一个标记,标记着这笔交易的接收方,接收方可以使用解锁脚本(也是一段指令程序)来使用这笔发给自己的金额。

对于输入,实际上就是指向之前其他事务对自己的输出,比如别人之前对我有一笔100的输出,现在我把这个输出作为输入,输出或者说转给另外一个人。因此我们看到在输入TxIn的定义中,主要定义了指向前一次的输出,然后就是一个解锁脚本。

我们发现,在微链的定义中,除了区块是一个个串接起来的,交易事务也是串接起来的。要构造一个交易事务,其实就是 构造事务的输入和输出,下面简单演示一个挖矿的交易(没错,矿工挖矿的收入所得也属于一种交易,为了区分普通的转账交易,这种交易通常称为 coinbase交易)。


 

//coinbase事务的输入部分
var txIn []TxIn
txIn = make([]TxIn, 1)
txIn[0].prevOut = OutPoint{tranHash: "", n: 0}
txIn[0].scriptSign = script.ScriptAction{InSignData: "", InPubKey: "", OutPubkey: ""}
txIn[0].sequence = 0

//coinbase事务的输出部分
var txOut []TxOut
txOut = make([]TxOut, 1)
txOut[0].amount = targetAmount
txOut[0].scriptPubKey = script.ScriptAction{InSignData: "", InPubKey: "", OutPubkey: targetPubKey}

//事务时间戳
curTime := time.Now()
timestamp := curTime.UnixNano() / 1000000

//事务哈希
var tranHashCoinbase string
//通过一个方法计算出整条事务的哈希值
tranHashCoinbase = GetTransactionHash(txIn, txOut, timestamp)

tranInfo.lockTime = timestamp
tranInfo.tranHash = tranHashCoinbase
tranInfo.txIn = txIn
tranInfo.txOut = txOut

return tranInfo, nil

在coinbase交易中,输入方比较特殊,并不是来自之前的输出,而是由系统通过奖励的方式直接发行出来的。注意, 若是一个矿工挖到了矿(获得了区块打包权),在打包一个区块中的交易时,通常会把属于自己的coinbase交易放到区块中所有交易的第一位。打包完成后 会将区块信息广播出去,等待其他节点来进行数据校验以及同步,当大多数节点校验通过后,这笔交易就算是被网络认可了。

每进行一次事务交易,就会产生一个新的输出,这些新的输出都是属于某个地址的可花费输出,当某个地址的所有者需要向其他人转账交易的时候,可以创建一笔新的输入和输出,这个新的输入就是来自自己的可花费输出。输入输出的关系如下图所示:

image.png

可以看到,事务的输入输出中,是彼此衔接的关系,生活中有很多这样的例子,比如仓库中的入库和出库,银行账户中的存款和取款,通过这样的流转实现了价值的转移,因此可以说带有金融属性的区块链网络是一个可以实现价值传输的网络,而且还是去中心化的。

为了方便查阅某个账户地址下的可花费输出,也就是UTXO(Unspent Transaction Output),通常会单独设置一个独立的UTXO数据存储,比如:


 

//未花费输出
type UTXO struct {
   TranHash     string
   Sequence     int
   Amount       int
   ScriptPubKey script.ScriptAction
}

//账户的UTXO
var TinyUTXO []UTXO


未花费输出的结构与事务中的输出其实是一致的,只不过这里多了一个“未花费”的条件约束,相当于净值。每当需要对别人进行转账的时候,可以直接到属于自己的“未花费输出”中去搜索指定,如果想知道自己的地址下一共有多少余额,也可以通过“未花费输出”来获取:

 

func GetRemainAmount() int {

   var rAmount int
   for _, v := range TinyUTXO {

       rAmount = rAmount + v.Amount
   }
   return rAmount
}


总而言之,UTXO的存在,是为了方便计算某个地址下的可用输出(余额)。

4.脚本的定义

脚本可以说是区块链应用中一个极其重要的特性,我们经常说基于区块链的各种数字货币都是可编程货币,基于区块链的各种合约也是可编程合约,这种特性开启了可编程社会的一个新的起点。我们现在就来看看,这个可编程到底是什么意思,它大致是怎么实现的。

在介绍事务的时候,我们看到在事务的输入中有解锁脚本,在事务的输出中有锁定脚本,那么,在一个事务中的这两个脚本有什么关系呢?

image.png

这是一个简单的示意图,大家注意其中的“本次输入”,在一次交易事务中,是由一组“本次输入”和“本次输出”组成,而“本次输入”又是来自“前一次输出”,因此对应“本次输出”的真正来源其实是“前一次输出”,这么说也许还有些抽象,让我们给这些动作赋予一些角色吧。image.png

这里我们假设Bob本来是一无所有的,他之所以能够转账输出给Lily,是因为之前Alice转了一笔钱给他。Alice转账给Bob的时候,通过锁定脚 本标识了这笔钱的所有权,而这个锁定的标识只有Bob通过自己的钥匙才能解开,从而能够使用Alice给他的这笔钱,Bob用来解开这个标识所使用的工具 就是解锁脚本。Bob解锁了Alice给他的输出后,就可以自由地使用Alice转给他的钱了,那么让我们站在解锁与锁定的角度再来看一下这幅图:

image.png

这里的锁定与解锁脚本其实就是一段验证程序,原理跟古时候的军队虎符差不多,在微链中,是通过使用公钥算法来实现这种虎符机制的,公钥算法的原理在这里不 再赘述了,在之前的章节中已经有了叙述。Alice使用Bob的公钥锁定了自己对Bob的转账输出,这段锁定程序就是Alice对Bob的输出锁定脚本。 Bob在对Lily转账的时候,首先解锁了Alice对自己的那笔输出,通过自己的私钥解锁了Alice对自己的那段锁定程序,相当于虎符匹配上了,然后 再使用Lily的公钥锁定自己对Lily的转账输出,从而等到Lily想要使用这笔钱的时候,得使用Lily自己的私钥去解锁Bob对自己的这段输出锁 定。看看,多么环环相扣的设计啊,这也是比特币的转账事务原理。

上述只是一个举例说明,并不是说Bob对Lily的转账一定要使用Alice对Bob的那笔输出,如果Bob有很多人对他转账,除了Alice还有Eric、Gerge等,那么Bob是可以任意选择要花哪一笔的(反正都是自己的钱)。

让我们看下微链中的脚本定义吧!


 

type ScriptAction struct {

   //解锁脚本中的私钥签名
   InSignData string

   //解锁脚本中的公钥
   InPubKey string

   //锁定脚本中的公钥
   OutPubKey string
}


还是以上述的Bob为例,在Bob要转账给Lily的时候,Bob提供了Alice对自己那笔输出的解锁脚本,解 锁脚本中包含了自己的私钥签名和公钥,也就是在此时,脚本中的InsignData和InPubKey都是Bob提供的,然后在构建对Lily的输出的时 候,使用了Lily的公钥来锁定(因为Lily的公钥只有Lily使用自己的私钥才能解密,从而也就保证了这笔账款的输出只有Lily的私钥拥有者才能解 锁),也就是此时的OutPutKey是指Lily的公钥数据。

在微链中,生成一个用户的钱包地址时,过程如下:

image.png

实际上无论是比特币还是以太坊等应用,基本也都是这样的一个生成关系,先生成一个私钥,然后通过私钥计算出公钥,接着 对公钥进行一个加工产生一个所谓的钱包地址,只是各自的算法方式和编码方式有所差别而已,在微链中,使用了RSA算法作为私钥公钥的生成算法,而钱包地址 则直接取得了公钥的前面20位(大家自己试验的时候可以根据自己的设计来,总之让私钥、公钥和地址符合上面的生成关系即可),通过这些我们也能看到,实际 上并不存在一个真正的所谓的钱包地址,只有私钥和公钥,地址只是公钥经过某种转化的数据而已。

我们看一下发起一个交易的时候,锁定和解锁脚本是怎么工作的?我们设定一个场景,假设之前Alice转账100给了Bob,现在Bob要将这100转账给Lily。

1)第1步自然是要构造一个事务,即如之前所述,Bob要转账给Lily,首先得把Alice给自己的转账输出解锁,看一下如下过程:

image.png

2)Bob的解锁脚本将自己的私钥签名和公钥压入了一个堆栈,这个是用来解锁Alice给自己的转账输出的,看一下代码示例。


 

//压栈方法
func (sa *ScriptAction) OP_PUSH(anyData interface{}) {
   myStack.Push(anyData.(string))
}

//压入Bob的私钥签名和公钥到一个堆栈中
OP_PUSH(BobSign)
OP_PUSH(BobPubKey)


堆栈的定义这里就不再赘述了,总之就是一个后进先出的存储结构,这个步骤相当于Bob亮出了自己的身份证,接下来就希望Alice给自己的那段输出锁定来验证自己的身份。

3)Alice给Bob的输出脚本中包含了Bob的地址,这个地址要与Bob的地址进行匹配,看看是否一致,怎么处理呢?首先在堆栈中要给出Bob的地址,然后将Alice输出脚本中包含的Bob的地址也压入堆栈,通过一个方法来判断是否一致。

image.png

4)还记得上面讲述的私钥、公钥与地址的关系吗?在这一步中,首先在堆栈中复制了一个Bob的公钥,然后将复制的这个公钥转换为地址,这样就实现了在堆栈中给出Bob的地址,可以看下代码示例:


 

//将栈顶的公钥数据取出后取得前面20位
//这20位作为钱包地址,并且再次压入堆栈
func (sa *ScriptAction) OP_PUB20() {
   var pubKey = myStack.Pop().(string)
   var s = []rune(pubKey)
   var bfr20 = s[0:19]
   myStack.Push(bfr20)
}


代码很简单,就是将公钥转换为地址,各个不同的区块链应用有不同的转换方式,这里就取得公钥的前20位作为一个例子(比起比特币中的方法可是简陋多了)。

接下来将Alice输出脚本中包含的Bob的地址也压入堆栈。

image.png

以下代码演示了从堆栈中取出两个数据,并比较是否相等的过程:


 

//从堆栈中取出两个数据,比较是否相等
func (sa *ScriptAction) OP_EQUALVERIFY() bool {
       //myStack.Pop()会从堆栈中取出栈顶数据后,然后在堆栈中删除掉
   fstData := myStack.Pop().(string)
   sndData := myStack.Pop().(string)
   return strings.EqualFold(fstData, sndData)
}


虚线标记的Bob地址就是来自Alice对Bob的那笔输出脚本,现在就可以来验证一下Bob的身份了,比较堆栈 中的两个Bob地址是否一致。如果不一样那就有问题了,说明这个Bob可能是一个假的Bob,如果是一致的,说明Bob提供的公钥和Alice提供的输出 中的Bob的地址是匹配的,则可以在堆栈中删除这两个Bob地址。那么,接下来是否就表明Bob的身份已经完全认证通过了呢?当然不是,Bob的公钥本来 就是公开的,谁都可以用他的公钥来做匹配,因此还需要一步,用Bob私钥签名来匹配这个Bob公钥,如果这一步也是一致的,那Bob的身份就算是确定了。

5)Bob的公钥私钥匹配。

image.png

如图,Bob提供的私钥签名与Bob的公钥进行匹配,匹配成功后,Bob就可以创建针对Lily的输出了,同样,Bob使用Lily的公钥创建了输出脚本(或锁定脚本),这样Lily想要使用这笔钱,就要上述同样的步骤来进行验证确认。

至此,就演示了微链中的锁定脚本以及解锁脚本的工作过程,注意这里的名词,一个事务中的输入脚本就是解锁脚本,而 输出脚本就是锁定脚本。这个工作过程也是模拟的比特币,可以看到,公钥密码算法在这里起到了很大的作用,这也是为什么像比特币这样的数字货币被称为加密数 字货币的原因。

5.关于脚本的一点思考

上述几节演示了脚本系统是怎么工作的。可以发现,在微链中,每个地址所拥有的货币并不是存储在一个账户的,而是通 过一组脚本来证明所有权,不断转账的过程其实就是不断进行脚本的解锁和产生新的锁定,只要这些脚本程序一直能正常运行,这种转换就能依赖脚本程序生生不息 地运转下去,不需要人为的审核,不需要查看身份证,一切都遵循着既定的规则,这其实就是“代码即法律”(code is law)的思想,是不是很酷!

那么,这些脚本除了能够用于微链这样的转账交易,还能干些什么?答案是肯定的,微链是模拟了比特币的做法,脚本指 令是固定的,也因此只能干些交易转账的事情,如果对脚本的功能进行扩展呢?比如让脚本程序可以支持更多的操作,实现更丰富的功能,有无可能?确实是可以 的,以太坊就是在比特币的基础上,大大增强了脚本的能力,不但实现了比特币的所有功能,而且还可以让用户自定义脚本,使用以太坊支持的脚本语言进行自定义 编程,实现如自定义代币(在以太坊中使用脚本程序创造自己的数字货币)、众筹合约、自治组织等各种丰富的应用程序。

以太坊通过扩展增强脚本能力,实现了除了数字货币以外的其他合约功能,也称为智能合约。事实上数字加密货币本身就可以看作一种合约,合约的有效条件就是解锁脚本与锁定脚本进行匹配。

6.网络服务

一个区块链应用如果不具备网络服务功能,那么就只能是一个单机的测试程序,没有任何实用价值。一般来说,区块链应用至少要具备如下的网络服务功能。

(1)节点发现

通常可以设计一个专门的“发现协议”,目的就是让一个个独立运行的节点之间能够互相联系,这个与我们平时交往新的 朋友是类似的。比如Alice有10个好朋友,Bob有15个好朋友,当Alice与Bob认识后,彼此之间就能互相交换朋友信息,这样Alice和 Bob就分别认识了更多的朋友,然后这些朋友之间还能彼此再认识,通过这样的方式每个人都会认识越来越多的朋友。有时候为了更加方便大家去尽快认识新朋 友,还会设置一些种子节点,这些节点会不间断地长期运行着,刚刚加入的新节点可以首先去认识它们,相当于带个路。

(2)区块主链同步

由于每一个节点都是独立运行的,大家并没有一个统一的服务器作为同步参照,因此只能靠互相之间进行数据同步,比如Alice的节点目前的区块长度是10,通过网络监听发现目前网络中最新的主链长度已经是11了,则Alice节点就会问身边的朋友要数据,大家都会彼此帮忙。

(3)新区块验证

当有矿工打包出了一个新的区块后,就会将区块数据广播出去,以尽可能地让更多的其他朋友获知,每一个节点都会敞开大门接收新的区块数据,接收到后就会进行自己的一轮验证,通过后就放到自己的仓库中(区块主链账本)。

(4)内存池维护

区块的生成是有时间间隔的,比如比特币10分钟一个区块,以太坊是15秒一个区块,但是交易并不是间隔发生的,而 是无时无刻都会发生。某个节点上发生了一笔交易后就会立即广播出去,其他的节点会负责接收,这些接收到的交易事务都是需要等待验证以及被打包到区块的。这 里面会有时间差,在没有被确认到区块主链之前,就会先保持在内存池,相当于一个临时储藏室。

在微链中,会启动一个数据监听服务与其他节点进行联络,我们看一下代码示例:


 

//启动监听服务
func StartListenServer(port int) {

   listenSocket, err := net.ListenUDP("udp4", &net.UDPAddr{
       IP:   net.IPv4(127, 0, 0, 1),
       Port: port,
   })
   if err != nil {
       fmt.Println("监听服务启动出错:" + err.Error())
   }
   fmt.Println("监听服务正在运行中...")
   defer listenSocket.Close()
   for {
       handleNodeMessage(listenSocket)
   }
}

var nCount = 0
//消息处理方法
func handleNodeMessage(conn *net.UDPConn) {
   defer conn.Close()
   var bufferData [1024]byte
   var recCommand string
   
   //获取接收到的数据
   for {
       n, clientAddr, err := conn.ReadFromUDP(bufferData[0:])
       if err != nil {
           fmt.Println("handle message error:" + err.Error())
       }
       
       //将监听到的数据指令放到一个字符串中
       recCommand = string(bufferData[0:n])
       //调用检测方法,确保获得的是一个合法的指令
       if CheckCommand(recCommand) == false {
           continue
       }
       nCount++
       //首次连接时发送一个欢迎词
       if nCount == 1 {
           conn.WriteToUDP([]byte("欢迎访问!"), clientAddr)
       } else {
           switch recCommand {
           case "syncblock":
               fmt.Println("区块数据同步请求")
               break
           case "transbroad":
               fmt.Println("交易事务广播")
               break
           case "nodeexchange":
               fmt.Println("节点信息交换")
               break
           default:
           fmt.Println("other")
               
           }
       }

   }
}

//指令格式校验
func CheckCommand(s string) bool {

   //一系列的命令格式校验
   return true
}


这里给出了一段监听服务的代码示例,可以看到其实就是一段普通的UDP服务,微链可以通过这个服务监听其他节点发送过来的数据同步请求以及要求交换节点网络地址和端口的信息等,这是节点与节点之间的网络通道。

7.挖矿

挖矿的目的是为了维持各个节点之间数据的共识,矿工(运行挖矿程序的计算机)通过执行运算挖矿程序抢夺到区块数据 的打包权,打包后将产生新区块的信息广播到其他节点,并同步给其他节点。而系统也通过给矿工分配挖矿的奖励来发行新币,矿工为了得到新币的奖励就会持续运 行挖矿程序,从而通过这种激励的方式维持了系统的运转。

挖矿程序通过运算一个什么样的程序来抢夺打包权呢?在区块结构的定义中,我们看到有一个难度位数和一个随机目标 值,微链中借鉴了比特币中的挖矿算法,通过对一个难度值进行随机匹配来抢夺区块数据的打包权。举个例子,每一个区块都有一个难度目标值,比如第一个区块或 者说创世区块的难度值是0x000FFF,这是一个约定的数值,在微链中认为这个难度值的难度是1。这其实就是玩一个游戏,比如我们掷骰子,要求连续掷6 次,前3次必须都是0,但是后面3次加起来的点数不能大于18,我们在玩这个游戏的时候,就得要不断地掷骰子,大家一起比赛,看谁先抛出一个符合要求的点 数出来。挖矿程序也是类似的原理,就是在不断地做这么一件事。

1)计算出当前区块的哈希值H。注意,这个区块是指矿工整理好准备要打包的新区块;

2)在H后面附加一个随机数,然后连起来再做一次哈希计算,看得到的结果是不是符合要求。如果结果不符合要求就更换随机数来继续尝试;

3)如果在挖矿过程中收到了其他节点发送过来的新区块信息,表明当前高度的区块已经被确定了,矿已经被别人挖走了,这个时候就只好放弃了。继续下一个区块数据的计算。

在这种情况下,为了保持出块速度的均衡,每隔一段时间就需要调整一下难度,比如微链的出块速度大致维持在30秒, 则可以设置每个星期调整一次难度,按照30秒来估计,一个星期大约会出20160个区块,则系统设置为每间隔20160个区块调整一次难度值,如此,则新 的难度值=老的难度值×(最近20160个区块的实际出块总秒数/604800),这里604800表示理论上出20160个区块的秒数,通过这样的公式 计算均衡了一段时间产生的算力误差。

所谓的挖矿过程基本就是这样的,我们可以看到,为了得到符合要求的结果,就必须找到那个随机数,就得不断重复尝 试,这是多累人(不,是累CPU)的活啊,也难怪大家都称之为“挖矿”。当然,目前有不少区块链应用已经发展出了其他的挖矿算法,有些是不用消耗算力的, 各种变种算法也是层出不穷。对于这部分的代码,读者可以根据自己的理解去做一个实现。

8.钱包

钱包客户端在区块链应用中主要用来存储自己的私钥,通过私钥就能获取到自己的地址上有多少数字资产(不一定是数字 货币,也可以是一个商业智能合约),也可以发起一笔转账交易或者创建一份合约等。事实上,钱包的功能并没有一个严格的规定,除了标准的私钥管理以及查询数 字资产等,还可以将钱包的功能通用化,比如可以设计一个管理多种数字货币的功能,以便于用户管理自己的各类数字资产;还可以连接主要的交易平台以方便数字 资产的交易,当然这个要与交易平台对接。总体来说,钱包的功能就是提供给用户一个区块链程序的使用工具。前面演示的发起一个交易事务,查询一个账户地址的 余额等,实际上就是属于钱包的功能。

来源:我是码农,转载请保留出处和链接!

本文链接:http://www.54manong.com/?id=69

'); (window.slotbydup = window.slotbydup || []).push({ id: "u3646208", container: s }); })();
'); (window.slotbydup = window.slotbydup || []).push({ id: "u3646147", container: s }); })();
博客
32132
07-14 366
07-12 299
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值