以太坊源码分析 账户管理

本文分析以太坊的账户管理的源码,主要包括两个部分: 获取钱包列表、订阅钱包事件。

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( ...