以太坊账户管理源码分析

这篇分析一下以太坊的账户管理。

这部分比较简单,主要分”获取钱包列表“和“订阅钱包事件”两个部分,下面分别介绍。

 

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)

……

}

 

至此,以太坊的账号管理机制就分析完了。

 

延伸阅读:以太坊共识引擎源码分析

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线