以太坊作为一种数字货币以太币的运行系统,显然它也会有类似于钱包的客户端程序,用来提供管理账户余额等功能。我们知道,存放(或者绑定,挂靠)以太币的账户,在代码中以Address类型变量存在,所以能够管理多个以太坊账户应该属于客户端程序基本功能之一。本文会从管理账户信息的代码包开始,自底向上的介绍以太坊客户端程序的一些主要模块。
在以太坊源代码的accounts代码包中,呈现账户地址的最小结构体叫Account{},它的主要成员就是一个common.Address类型变量;管理Account的接口类叫Wallet,类如其名,
在accounts代码包内部的各种结构体/接口中,accounts.Manager在相互调用关系上无疑是处于顶端的,它本身是公共类,向外暴露包括查询单个Account,返回单个或多个Wallet对象,订阅Wallet更新事件等方法。在其内部它维持一个Wallet列表,通过每个Wallet实现类持有一组Account账户对象,并通过一个event.Feed成员变量来管理所有向它订阅Wallet更新事件的需求。
着重介绍一下这里的订阅(subscribe)操作,Manager的Subscribe()函数定义如下:
// /accounts/manager.go
func (am *Manager) Subscribe(sink chan<- WallletEvent) event.Subscription {
return am.feed.Subscribe(sink)
}
首先注意
这个Subscribe()函数是让外部调用对象 向该Manager作订阅的操作,事实上该Manager本身也是通过相同的订阅机制去获知新添的Wallet对象,它的成员变量updates就是该Manager本身得到所订阅事件的通道。其次,Manager.Subscribe()函数只有一个chan参数,由于golang语言中channel机制的强大,
订阅操作仅仅需要一个chan对象就足够了,真是简单之极,根本不必知道背后是谁发起了订阅。尽管如此,这里依然值得思考的是,究竟是什么对象向Manager发起了订阅呢?其实,向某个Manager对象订阅Wallet更新事件的,
正是另外一个Manager对象,也就是得出以上这个结论,是很有意义的。后面可以了解到,accounts.Manager主要作为eth.Ethereum(或者les.Ethereum)的一个成员存在,而这个eth.Ethereum是以太坊客户端程序中最主要的部分,它以服务的形式提供几乎所有以太坊系统运行所需的功能,所以一个以太坊客户端可视为一个accounts.Manager的存在,那么真相就是,所有以太坊客户端之间在通过accouts.Manager相互订阅Wallet更新事件。
除Manager之外,这里其他几个重要的结构体还包括:
软件实现Wallet主要通过本地存储文件的方式来管理账户地址。同时,
keystoreWallet{}:它是accounts.
KeyStore{}:它为keystoreWallet结构体提供所有与Account相关的实质性的数据和操作。KeyStore{}内部有两个作数据缓存用的成员:
另外,KeyStore{}中有一个
Unlocked{}:公钥密钥数据类Key{}的封装类,其内部成员除了Key{}之外,还提供了一个chan类型变量abort,它会在KeyStore对于公钥密钥信息的管理机制中发挥作用。
Key{}:存放数字签名公钥密钥的数据类,其内部显式存储了一个ecdsa.PrivateKey{}类型的成员变量,前文介绍过,Golang原生代码包中的ecdsa.PrivateKey{}中含有PublicKey{}类型的成员。而Key{}中同时携带Address类型成员变量,也可以避免公钥向地址类型转化的操作重复发生。
keyStorePassphrase{}:
accountCache{}:在内存中缓存keystore中某个已知路径下所有Account对象,可提供由Address类型查找到对应Account对象的操作。
fileCache{}:keystore中可观察到的文件的缓存,它可对某个路径下存放的文件进行扫描,分别返回新增文件,缺失文件,改动文件的集合。
watcher{}:用来监测某个路径中存储的账户文件的变化,可以定时调用accountCache的方法对文件进行扫描。
accountCache缓存的帐号信息,均来自于某个已知路径下存储的本地文件集合。每个文件都是JSON格式,以显式存放Address: {Address: "@Address"},所以accountCache在读取文件后,可以直接转化成Account{}对象,在代码中使用。这里以显式文件存储Address信息没有任何问题,既不用担心Address信息泄露造成危害(无法从Address反向解析出源头的ECDSA所用公钥),又可以方便代码调用。
在使用中,watcher对象会维护一个定时器,不断的通知accountCache扫描某个给定的路径;accountCache会调用fileCache对象去扫描该路径下的文件,并根据fileCache返回的三种文件集合:新添文件、缺失文件、改动文件,在自身维护的Account集合中作相应操作。
Key{}通过ecdsa.PrivateKey对象从而同时携带ECDSA所用的公钥密钥,所以这里涉及到公钥密钥部分,都是针对Key对象做的操作。keystore机制中,在本地存储的是经过加密的Key对象的JSON格式,所用的加密方法被称为Web3 Secret Storage,其实现细节可在ethereum git wiki上找到。下图是该存储方式的简单示意图:
对一个加密存储的Key对象做操作时,总共需要三个参数,包括调用方提供一个名为passphrase的任意字符串,以及keyStorePassphrase{}中给定的两个整型数scryptN,scryptP,这两个整型参数在keyStorePassphrase对象生命周期内部是固定不变的,只能在创建时赋值。这样不管是每次新存储一个Key对象,还是取出一个已存的Key对象,调用方都必须传入正确的参数passphrase,所以在实际应用中,以太坊钱包的客户必须自行记忆该字符串。实际上,客户为每个账户创建的密码password,程序中正是这个加密参数passphrase。
Key{}对象从加密过的本地文件中取出后,会被封装成unlocked{}对象,并被KeyStore放进其map[Address]*unlocked类型成员中。由于公钥密钥的重要性,显然keystore中存有的unlocked对象也应该控制公开时长。对于不同的时限需求,KeyStore{}提供了如下两个函数:
// accounts/keystore/keystore.go
func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error {
return ks.TimedUnlock(a, passphrase, timeout:0)
}
func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error
TimedUnlock()函数会在给定的时限到达后,立即将已知Account对应的unlocked对象中的PrivateKey的私钥销毁(逐个bit清0),并将该unlocked对象从KeyStore成员中删除。而Unlock()函数会将该unlocked对象一直公开,直到程序退出。注意,这里的清理工作仅仅是针对内存中的Key对象,而以加密方式存在本地的key文件不受影响。
keystore机制以本地文件的形式提供对账户信息和数字签名公钥私钥的存储和读取,从而以软件方式实现了accounts.
以太坊除了提供软件实现的钱包之外,还有硬件实现的钱包。当然,对于硬件钱包,以太坊代码中肯定有上层代码对此进行封装。这些代码都处于/accounts/usbwallet/下,它们的UML关系如下图所示:
pkg accounts/usbwallet中 主要的结构包括wallet{}, Hub{}以及
需要注意的是,在目前以太坊的主干代码中,硬件实现钱包有关数字签名部分,目前只能提供针对交易进行原生的数字签名功能,即仅仅
在了解accounts代码包之后,我们就可以来看看以太坊源代码中最著名的类型,同时也是客户端程序中最核心的部分 - eth.Ethereum。能够以整个系统名命名的结构体类型,想必功能应该非常强大,下图是它的一个简单UML图:
上图中央就是eth.Ethereum类型,四周都是它的成员变量类型,我们来看看其中哪些是已经了解过的:
以上这些都是前文中都已经具体介绍过的代码部分,接着再来看看那些新的类型:
特别介绍下LES:Light Ethereum Subprotocol(LES) 是为轻量级客户端专门设计的子协议。相比于eth.Ethereum提供全节点服务的客户端,那些轻量级客户端不参与挖掘新区块,在与其他节点的通信中仅仅下载每个区快的头部(Block.Header),对于区块链的其他部分仅仅按需对部分同步。eth.Ehereum同时也支持LES,这样一个提供全节点服务的客户端就可以与其他轻量级客户端以相同的协议通信了。
对数字货币稍有了解的人应该都清楚p2p通信协议对于此类“去中心化”系统的重大意义。的确,把p2p通信协议称为以太坊系统的基石之一都不为过,从代码角度考虑, ProtocolManager及其代码族 也属于eth代码包的一部分,不过由于这部分代码比较复杂,会在下一篇文章中专门介绍这些通信协议的实现细节。
在了解eth.Ethereum这个核心服务之后,客户端执行程序也就呼之欲出了。首先有一个node.Node{}作为承载类似eth,Ethereum这样服务模块的容器:
Node{}对象内部有一个Service列表,所有实现了node.
接着,go-ethereum的客户端程序geth的代码就很简单了:
// /cmd/geth/main.go
func main() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func geth(ctx *cil.Context) error {
node := makeFullNode(ctx)
startNode(ctx, node)
node.Wait()
return nil
}
...
从命令行启动geth客户端的程序就是以上,创建一个node.Node对象,从配置中读出想要注册的服务名,然后一一创建相应的服务对象,Node去启动它们。
geth是go-ethereum自带的命令行客户端程序,目前市场上也存在许多种其他的以太坊客户端程序,有兴趣的读者可以去找来看看,有源代码就最好了可以比较一下。
以太坊的客户端程序,原本应该是刚接触以太坊的初学者最早遇到的部分之一。因为下载完整个源代码包之后,按照相应语言的提示进行编译,就会得到一个客户端的可执行程序。我最初首先看的客户端的代码,当追溯到eth.Ethereum{}结构体,看到那么多模块的成员变量时,就一下子明白了,整个以太坊系统运行起来的基础模块是哪些部分。