以太坊源码分析 账户管理
本文分析以太坊的账户管理的源码,主要包括两个部分: 获取钱包列表、订阅钱包事件。
1. 获取钱包列表
账户组件之间的关系:
从图中可以看出wallet、account、address这三者的区别和联系i:wallet中可能包含多个account,而每个account中包含一个address和账户所在路径(URL)。
这里有两个重要接口:Backend和Wallet。
- Backend指的是钱包后端,目前实现了两种:KeyStore钱包和USB硬件钱包。可以看到,Backend接口有两个函数Wallets()和Subscribe(),分别对应于获取钱包列表和订阅钱包事件这两个功能。
- Wallet指的是单个钱包,可以看到包含了一些打开、关闭、签名相关的接口函数。还有一些函数如Derive()是给分层确定性(HD)钱包使用的,目前KeyStore钱包后端没有实现,USB钱包需要driver支持。
这里主要分析KeyStore钱包。KeyStore实现了Backend接口,看一下相关字段:
- storage:实现了keyStore接口,用于访问账户关联的私钥
- wallets:所有钱包的集合,每个钱包是一个keystoreWallet实例
- accountCache:缓存所有账户信息,初始化时会扫描datadir/keystore目录获取所有账号
下面开始分析具体代码。以太坊启动创建Node时调用了makeAccountManager()创建账号管理器:
func New(conf *Config) (*Node, error) { …… am, ephemeralKeystore, err := makeAccountManager(conf) …… }
看一下makeAccountManager()的实现,代码位于node/config.go:
func makeAccountManager(conf *Config) (*accounts.Manager, string, error) { scryptN, scryptP, keydir, err := conf.AccountConfig() …… if err := os.MkdirAll(keydir, 0700); err != nil { return nil, "", err } // Assemble the account manager and supported backends backends := []accounts.Backend{ keystore.NewKeyStore(keydir, scryptN, scryptP), } …… return accounts.NewManager(backends...), ephemeral, nil }
首先以700权限创建keystore目录,默认位置是datadir/keystore。
然后初始化backend列表,创建KeyStore实例。看一下NewKeyStore()函数,代码位于accounts/keystore/keystore.go:
func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { keydir, _ = filepath.Abs(keydir) ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}} ks.init(keydir) return ks }
首先初始化KeyStore实例,然后调用init()函数:
func (ks *KeyStore) init(keydir string) { …… ks.cache, ks.changes = newAccountCache(keydir) …… accs := ks.cache.accounts() ks.wallets = make([]accounts.Wallet, len(accs)) for i := 0; i < len(accs); i++ { ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks} } }
这里首先创建了一个accountCache实例,然后调用它的accounts()函数获取当前账号列表,最后填充KeyStore的钱包列表。看一下accountCache的accounts()函数,代码位于accounts/account_cache.go:
func (ac *accountCache) accounts() []accounts.Account { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() cpy := make([]accounts.Account, len(ac.all)) copy(cpy, ac.all) return cpy }
先调用maybeReload()函数加载账号列表,然后拷贝到一个数组中返回。看一下maybeReload()函数:
func (ac *accountCache) maybeReload() { …… if ac.watcher.running { ac.mu.Unlock() return // A watcher is running and will keep the cache up-to-date. } …… ac.watcher.start() ac.scanAccounts() }
这里先判断有没有watcher正在运行,其实就是检查有没有初始化过。这个watch类似于Linux中的inotify,用于监控datadir/keystore目录中有没有文件发生变化,如果有的话会及时刷新cache。
如果没有watcher正在运行,就会调用scanAccount()函数手动扫描一遍获取当前账户列表。
账户列表初始化完成以后,就调用NewManager()函数创建账号管理器了。
2. 订阅钱包事件
还是老规矩先上一张图:
可以看到这张图主要关注订阅相关的组件。
Manager中有一个updates字段,是一个channel类型,用于接收钱包相关的事件。Manager需要调用backend的Subscribe()函数把这个channel注册到后端中去。
KeyStore作为后端的实现,会把这个注册请求转发给一个Feed类型的实例。Feed会把该channel记录在案,同时返回一个feedSub类型的实例,该类型实现了Subscription接口。最后,feedSub实例会被包装进一个scopeSub类型的wrapper中,返回给Manager,Manager可以通过该接口取消事件订阅。
最中间还有一个SubscriptionScope类型,根据注释,主要是为了在大型项目中能够快速取消所有事件订阅,因此该类型包含一个map类型的字段,用于收集所有的Subscription接口实例。
下面开始分析具体代码。首先看一下NewManager()函数,代码位于accounts/manager.go
func NewManager(backends ...Backend) *Manager { // Retrieve the initial list of wallets from the backends and sort by URL var wallets []Wallet for _, backend := range backends { wallets = merge(wallets, backend.Wallets()...) } // Subscribe to wallet notifications from all backends updates := make(chan WalletEvent, 4*len(backends)) subs := make([]event.Subscription, len(backends)) for i, backend := range backends { subs[i] = backend.Subscribe(updates) } // Assemble the account manager and return am := &Manager{ backends: make(map[reflect.Type][]Backend), updaters: subs, updates: updates, wallets: wallets, quit: make(chan chan error), } for _, backend := range backends { kind := reflect.TypeOf(backend) am.backends[kind] = append(am.backends[kind], backend) } go am.update() return am }
这段代码比较长,主要做了下面几件事:
- 调用所有backend的Wallets()方法,合并成完整的钱包列表
- 创建channel,并调用所有backend的Subscribe()函数进行注册
- 初始化Manager实例,调用update()函数进入钱包事件监听循环
第一件前面已经介绍过了,看第二件,KeyStore()的Subscribe()函数:
func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { …… sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) // Subscribers require an active notification loop, start it if !ks.updating { ks.updating = true go ks.updater() } return sub }
可以看到,是先调了updateFeed的Subscribe()函数,然后再通过updateScope把结果做一层wrapper返回给调用方。先看Feed的Subscribe()函数:
func (f *Feed) Subscribe(channel interface{}) Subscription { …… chanval := reflect.ValueOf(channel) …… sub := &feedSub{feed: f, channel: chanval, err: make(chan error, 1)} …… // Add the select case to the inbox. // The next Send will add it to f.sendCases. cas := reflect.SelectCase{Dir: reflect.SelectSend, Chan: chanval} f.inbox = append(f.inbox, cas) return sub }
这里首先把channel封装进一个feedSub结构返回,同时在inbox数组中添加了一个SelectCase实例。这些SelectCase最终会在需要发送事件时被使用:首先以非阻塞的方式(TrySend())向这些channel发送事件,如果没有立即成功则阻塞在这些SelectCase上,等待发送完成。具体可以参见Feed的Send()函数。
接着看一下SubscriptionScope的Track()函数:
func (sc *SubscriptionScope) Track(s Subscription) Subscription { …… if sc.subs == nil { sc.subs = make(map[*scopeSub]struct{}) } ss := &scopeSub{sc, s} sc.subs[ss] = struct{}{} return ss }
可以发现其实就是做了一层wrapper,这个scopeSub类型也是实现了Subscription接口的。这样做的目的只是为了把所有的Subscription接口实例都收集到一个map中,从而可以实现快速取消所有订阅。
再看第三件,Manager的update()函数:
func (am *Manager) update() { …… for { select { case event := <-am.updates: // Wallet event arrived, update local cache am.lock.Lock() switch event.Kind { case WalletArrived: am.wallets = merge(am.wallets, event.Wallet) case WalletDropped: am.wallets = drop(am.wallets, event.Wallet) } am.lock.Unlock() // Notify any listeners of the event am.feed.Send(event) case errc := <-am.quit: // Manager terminating, return errc <- nil return } } }
这就是一个无限循环,监听后端发送过来的钱包事件。
细心的朋友可能发现Manager也有个feed字段,而且还有个Subscribe()函数,这个是干什么用的呢?其实是为了把钱包事件再转发给上层的订阅者,也就是Node。订阅代码参见cmd/geth/main.go中的startNode()函数:
func startNode(ctx *cli.Context, stack *node.Node) { …… events := make(chan accounts.WalletEvent, 16) stack.AccountManager().Subscribe(events) …… }
至此,以太坊的账号管理机制就分析完了。
下一章:以太坊源码分析 交易
本文分析以太坊交易的相关代码,以太坊交易的完整流程分为以下步骤:发起交易:指定目标地址和交易金额,以及需要的 gas/gaslimit交易签名:使用账户私钥对交易进行签名提交交易:把交易加入到交易缓冲池 txpool( ...