这篇分析一下以太坊的账户管理。
这部分比较简单,主要分”获取钱包列表“和“订阅钱包事件”两个部分,下面分别介绍。
先上一张图,理清组件间的关系:
从图中可以看出wallet、account、address这三者的区别和联系i:wallet中可能包含多个account,而每个account中包含一个address和账户所在路径(URL)。
这里有两个重要接口:Backend和Wallet。
这里主要分析KeyStore钱包。KeyStore实现了Backend接口,看一下相关字段:
下面开始分析具体代码。以太坊启动创建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()函数创建账号管理器了。
还是老规矩先上一张图:
可以看到这张图主要关注订阅相关的组件。
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
}
这段代码比较长,主要做了下面几件事:
第一件前面已经介绍过了,看第二件,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)
……
}
至此,以太坊的账号管理机制就分析完了。
更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock/article/category/7669858
或关注飞久微信公众号: