美文网首页
Go语言的btcwallet源码分析

Go语言的btcwallet源码分析

作者: 蛊惑佬 | 来源:发表于2018-04-24 14:08 被阅读916次

Go语言的btcwallet源码分析

项目地址

https://github.com/btcsuite/btcwallet

前言

目前开源社区上有很多不同语言领域的区块链项目和程序,尽管代码开源,但每个项目的架构和设计都各有特色,对初入门的开发者去学习区块链会有较大门槛和难度。为此原因,我开始对一些主流的开源项目进行代码分析,帮助入门开发者了解区块链是如何实现。而且在这个过程中,有可能分析源代码的漏洞哦!!!对源代码的开发者也是一种帮助。

因为主流的项目代码会不断更新,而且我也有可能理解错误的地方,大家可以在评论中指出。我会完善文档。

程序入口分析

按照官方安装教程,我们最终编译出一个btcwallet的命令行工具。我们通过btcwallet命令完成钱包的创建,所以btcwallet是钱包的入口程序,先从它的源文件进行分析。

btcwallet.go


func main() {
    // Use all processor cores.
    runtime.GOMAXPROCS(runtime.NumCPU())

    // 可以看到walletMain是处理,钱包启动的主函数
    if err := walletMain(); err != nil {
        os.Exit(1)
    }
}

func walletMain() error {
    //loadConfig解析命令,里面完成钱包创建
    tcfg, _, err := loadConfig()
    if err != nil {
        return err
    }
    cfg = tcfg
    defer func() {
        if logRotator != nil {
            logRotator.Close()
        }
    }()

        //下面的代码启动服务监听
        ...
}

config.go

这个文件钱包的用户配置管理,提供初始默认配置,解析用户命令更在配置功能。


// loadConfig initializes and parses the config using a config file and command
// line options.
//
// The configuration proceeds as follows:
//      1) Start with a default config with sane settings
//      2) Pre-parse the command line to check for an alternative config file
//      3) Load configuration file overwriting defaults with any specified options
//      4) Parse CLI options and overwrite/add any specified options
//
// The above results in btcwallet functioning properly without any config
// settings while still allowing the user to override settings with config files
// and command line options.  Command line options always take precedence.
func loadConfig() (*config, []string, error) {
        //初始化默认配置,解析命令
        ...
        
        //如果命令为create,创建钱包。
    if cfg.Create {
        // 钱包已经创建过了,就不能再创建
        if dbFileExists {
            err := fmt.Errorf("The wallet database file `%v` "+
                "already exists.", dbPath)
            fmt.Fprintln(os.Stderr, err)
            return nil, nil, err
        }

        // 确保钱包对应的区块链网络数据文件夹存在
        if err := checkCreateDir(netDir); err != nil {
            fmt.Fprintln(os.Stderr, err)
            return nil, nil, err
        }

        // 执行初始化钱包.
        if err := createWallet(&cfg); err != nil {
            fmt.Fprintln(os.Stderr, "Unable to create wallet:", err)
            return nil, nil, err
        }

        // Created successfully, so exit now with success.
        os.Exit(0)
    } 
}

walletsetup.go

这个文件负责根据用户配置创建钱包目录及钱包模型。


// 创建钱包过程同时提示用户输入密码
func createWallet(cfg *config) error {
    dbDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
    loader := wallet.NewLoader(activeNet.Params, dbDir)

    // 如果本地已经有一个keystore文件,尝试从keystore中恢复密钥
    netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
    keystorePath := filepath.Join(netDir, keystore.Filename)
    var legacyKeyStore *keystore.Store
    _, err := os.Stat(keystorePath)
    ...

    // 命令行提示用户设置私钥密码
    reader := bufio.NewReader(os.Stdin)
    privPass, err := prompt.PrivatePass(reader, legacyKeyStore)

    // 如果keystore存在,且密码通过,解密keystore
    if legacyKeyStore != nil {
        err = legacyKeyStore.Unlock(privPass)
        //处理keystore的地址迁移
        ...
    }

    // 命令行提示用户是否需要设置一个密码来加密公钥,如果不设置
    // 默认使用"public"作为密码
    pubPass, err := prompt.PublicPass(reader, privPass,
        []byte(wallet.InsecurePubPassphrase), []byte(cfg.WalletPass))

    // 命令行提示用户是否需要设置已有的种子,用户不提供,则随机生成新的
    seed, err := prompt.Seed(reader)

    //得到公钥密码,私钥密码,种子后,创建钱包模型
    fmt.Println("Creating the wallet...")
    w, err := loader.CreateNewWallet(pubPass, privPass, seed)

    ...
}

wallet包分析

这个包负责分层确定性模型下的钱包抽象实现。实现诸如,创建账户,查询账户余额,查询账户交易记录等等。

loader.go

Loader是一个加载工具,主要用途为:创建新钱包,打开新钱包,并回调给其他子线程加载钱包。


// 创建新钱包
func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte) (*Wallet, error) {
    
    ...

    // 拼接一个钱包数据路径,btcwallet用的数据是boltdb,是一个go原生写的数据库程序。
    dbPath := filepath.Join(l.dbDirPath, walletDbName)
    
    ...
    // 在该路径上创建文件数据库
    db, err := walletdb.Create("bdb", dbPath)

    // 创建钱包
    err = Create(db, pubPassphrase, privPassphrase, seed, l.chainParams)

    // 打开钱包
    w, err := Open(db, pubPassphrase, nil, l.chainParams)
    w.Start()

    l.onLoaded(w, db)
    return w, nil
}

wallet.go

这个文件比较负责,创建钱包流程分开了几个函数


// 创建钱包
func Create(db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Params) error {
    // 如果用户没有提供种子,程序会随机创建种子。
    // 这个种子是钱包的一切,只要被被黑客拿到,无论你的私钥密码或公钥密码没有泄露.
    // 黑客都可以按分层确定性模型,用户暴力算法找到有utxo的私钥。所以钱包程序尽量地少与seed交互。
    if seed == nil {
        hdSeed, err := hdkeychain.GenerateSeed(
            hdkeychain.RecommendedSeedLen)
        if err != nil {
            return err
        }
        seed = hdSeed
    }
    ...
    // boltdb是一款KV数据库,所以每个钱包都有自己的索引键。
    addrMgrNamespace, err := db.Namespace(waddrmgrNamespaceKey)
    
    //这里才是正真创建钱包的主逻辑,我们跳转到waddrmgr进行分析
    err = waddrmgr.Create(addrMgrNamespace, seed, pubPass, privPass,
        params, nil)

    // 创建交易单表
    txMgrNamespace, err := db.Namespace(wtxmgrNamespaceKey)
    
    return wtxmgr.Create(txMgrNamespace)
}

waddrmgr包分析

这个包负责按照BIP32,BIP44,即分层确定性模型管理钱包。

manager.go


// 创建一个地址管理器,使用种子创建根私有和根公钥,
// 所有私钥包含“根”和“后代”都由用户设置的私钥密码加密保护,
// 所有公钥包含“根”和“后代”都由用户设置的公钥密码加密保护,
func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []byte, chainParams *chaincfg.Params, config *ScryptOptions) error {
    
    // 通过种子生成根私钥
    root, err := hdkeychain.NewMaster(seed, chainParams)

    // 根,衍生后代作为BTC币种,BIP44硬性定BTC主网在m/44'/0',更新查看BIP32和44
    coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType)
    
    // 衍生索引位0作为默认第一个账户
    acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0)

    // 导出公钥
    acctKeyPub, err := acctKeyPriv.Neuter()


    /*
    btcwallet采用这样的加密方案:
    1. 随机生成PUB密钥,加密种子生成的根公钥,生成PUBENC密文。
    2. 随机生成PRV密钥,加密种子生成的根私钥,生成PRVENC密文。
    3. 用户设置公钥密码计算出UPUB密钥,加密PUB密钥,生成UPUBENC密文。
    4. 用户设置私有密码计算出UPRV密钥,加密PRV密钥,生成UPRVENC密文。
    5. 所有密文都保存到数据库。

    这样多了一层加密的作用是可以避免黑客暴力破解了用户密码后,仍然不能直接解密出私钥。

    解密过程就是:
    1. 用户输入密码,计算出UPUB/UPRV密钥,解密UPUBENC/UPRVENC得出出PUB/PRV密钥。
    2. PUB/PRV密钥解密PUBENC/PRVENC密文,得出根公钥/根私钥。
    3. 得到根公钥/根私钥后,可以按分层确定性模型计算后代。
    */

    // 用户设置公钥密码计算出UPUB密钥
    masterKeyPub, err := newSecretKey(&pubPassphrase, config)
    
    // 用户设置私有密码计算出UPRV密钥
    masterKeyPriv, err := newSecretKey(&privPassphrase, config)

    // 随机生成“盐",好像没有用到这个。
    var privPassphraseSalt [saltSize]byte
    _, err = rand.Read(privPassphraseSalt[:])

    //随机生成PUB密钥
    cryptoKeyPub, err := newCryptoKey()
    
    //随机生成PRV密钥
    cryptoKeyPriv, err := newCryptoKey()
    
    // 随机创建赎回脚本密码
    cryptoKeyScript, err := newCryptoKey()

    // 加密PUB密钥,生成UPUBENC密文
    cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub.Bytes())
    
    // 加密PRV密钥,生成UPRVENC密文
    cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv.Bytes())

    // 加密赎回脚本,生成密文
    cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript.Bytes())
    
    // 根私钥导出根公钥
    coinTypeKeyPub, err := coinTypeKeyPriv.Neuter()

    //加密种子生成的根公钥,生成PUBENC密文
    coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String()))
    
    // 加密种子生成的根私钥,生成PRVENC密文
    coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String()))
    
    // 加密钱包首个账户
    acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String()))

    // 加密钱包首个账户
    acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String()))

    ...

    // 在一个事务中,保存所有密文
    err = namespace.Update(func(tx walletdb.Tx) error {
        // 只保存密文,记住不保存明文
        pubParams := masterKeyPub.Marshal()
        privParams := masterKeyPriv.Marshal()
        err = putMasterKeyParams(tx, pubParams, privParams)
        err = putCryptoKeys(tx, cryptoKeyPubEnc, cryptoKeyPrivEnc,
            cryptoKeyScriptEnc)
        err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc)
        err = putWatchingOnly(tx, false)
        // Save the initial synced to state.
        err = putSyncedTo(tx, &syncInfo.syncedTo)
        err = putStartBlock(tx, &syncInfo.startBlock)
        // Save the initial recent blocks state.
        err = putRecentBlocks(tx, recentHeight, recentHashes)
        // Save the information for the imported account to the database.
        err = putAccountInfo(tx, ImportedAddrAccount, nil,
            nil, 0, 0, ImportedAddrAccountName)
        err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc,
            acctPrivEnc, 0, 0, defaultAccountName)
        return err
    })
    return nil
}

到此为止,我们的钱包已经创建成功了。我们的默认第一个账户在m/44'/0'。我们在看看创建新账户的代码,看看的规则。


// 创建新账户,先检查账户表仲是否存在同名账户,因为我们之前创建钱包时已通过用户密码加密了一大堆密文。
// 所以创建新账户需要解锁根公钥。
func (m *Manager) NewAccount(name string) (uint32, error) {
    
    ...

    // 检查账户名是否唯一
    _, err := m.lookupAccount(name)
    if err == nil {
        str := fmt.Sprintf("account with the same name already exists")
        return 0, managerError(ErrDuplicateAccount, str, err)
    }

    var account uint32
    var coinTypePrivEnc []byte

    // 查找最后一个账户索引位,在事务中创建新用户
    err = m.namespace.Update(func(tx walletdb.Tx) error {
        var err error
        // 获取当前最后的账户位置
        account, err = fetchLastAccount(tx)
        if err != nil {
            return err
        }
        // 递增一位
        account++

        // 获取根私钥的密文
        _, coinTypePrivEnc, err = fetchCoinTypeKeys(tx)

        // 解密出根私钥
        serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc)
        coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv))
        zero.Bytes(serializedKeyPriv)
        
        // 衍生新账户的私钥,假如我们初始钱包后,再创建新账户,新账户在m/44'/1'。
        acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account)
        coinTypeKeyPriv.Zero()
        
        // 导出新账户公钥
        acctKeyPub, err := acctKeyPriv.Neuter()
        
        // 加密密钥对
        acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(acctKeyPub.String()))
        acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String()))
        
        // 保存新账户
        err = putAccountInfo(tx, account, acctPubEnc, acctPrivEnc, 0, 0, name)

        // 记录新账户的索引位为最后位置
        if err := putLastAccount(tx, account); err != nil {
            return err
        }
        return nil
    })
    return account, err
}

还没写完。。。To be continue

相关文章

网友评论

      本文标题:Go语言的btcwallet源码分析

      本文链接:https://www.haomeiwen.com/subject/nkfxlftx.html