从零开始学习比特币开发:生成地址

栏目: 数据库 · 发布时间: 5年前

内容简介:如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。比特币核心提供了很多 RPC 来供客户端调用,其中一个就是我们这里要讲的

生成地址

如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。

比特币核心提供了很多 RPC 来供客户端调用,其中一个就是我们这里要讲的 getnewaddress 生成一个新的地址,通过这个 RPC ,我们就可以生成一个新的地址,有了这个地址,别人就可以给我们转账了。

getnewaddress RPC 可以接收两个参数,第一个地址的标签,第二个是地址的类型。如果没有提供标签,那么默认的标签就是空,地址的类型当前支持:legacy、p2sh-segwit、bech32,默认类型由  -addresstype 参数指定,当前为 p2sh-segwit。

如果我们想看下这个 RPC 的帮助文档,可以执行如下的命令:

./src/bitcoin-cli -regtest help getnewaddress

就会显示帮助信息

这个 RPC 对应的方法实现位于 src/wallet/rpcwallet.cpp 文件,方法名称就是 RPC 名称,下面我们来看这个方法。

生成地址流程

  1. 根据请求参数获得对应的钱包。

    std::shared_ptr
    <cwallet>
      const wallet = GetWalletForJSONRPCRequest(request);
    CWallet* const pwallet = wallet.get();
    </cwallet>

    GetWalletForJSONRPCRequest 方法内部实现如下:

    • 调用 GetWalletNameFromJSONRPCRequest 方法,从请求对象中取得钱包的名字,如果用户指定了钱包名字,那么把钱包名字保存在参数  wallet_name 上,并返回真,否则返回假。

    • 如果可以获得用户指定的钱包名称,则调用 GetWallet 方法,从钱包集合  vpwallets 中取得指定的钱包,然后返回钱包。

    • 如果用户没有指定钱包或指定的钱包不存在,那么调用 GetWallets 方法,返回钱包集合  vpwallets 。如果钱包集合中只有一个钱包,或者在用户指定了帮助的情况下,至少有一个以上的钱包,那么返回第一个钱包,即默认的钱包。默认钱包在系统启动时候创建的。

  2. 接下来,要确保钱包可用。如果钱包不可用,则直接 NullUniValue 对象。

        if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) {
            return NullUniValue;
        }
  3. 如果指定了 help 参数或请求参数数量多于2个,则显示钱包的帮助信息。

  4. 检查钱包是否设置了禁止私钥,即钱包是只读的 watch-only/pubkeys 。如果是,则抛出异常。

        if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
            throw JSONRPCError(RPC_WALLET_ERROR, "Error: Private keys are disabled for this wallet");
        }
  5. 如果指定了标签,则调用 LabelFromValue 方法,检查标签,确保其不是  * 。如果是,则抛出异常。

        std::string label;
        if (!request.params[0].isNull())
            label = LabelFromValue(request.params[0]);
  6. 如果指定了地址类型,则调用 ParseOutputType 方法,检查地址类型,确保其是  legacyp2sh-segwitbech32 之一,如果不指定则默认是  p2sh-segwit ,并把地址类型保存在  output_type 变量中。

        OutputType output_type = pwallet->m_default_address_type;
        if (!request.params[1].isNull()) {
            if (!ParseOutputType(request.params[1].get_str(), output_type)) {
                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str()));
            }
        }
  7. 如果钱包没有被锁定,则调用 TopUpKeyPool 方法填充密钥池。

        if (!pwallet->IsLocked()) {
            pwallet->TopUpKeyPool();
        }

    TopUpKeyPool 填充密钥这个方法,我们前面已经讲过,这里只简单解释下,不做详细分析。因为在衍生子钥的过程中, setExternalKeyPoolsetInternalKeyPool 已经完全填充完了,所以导致  missingExternalmissingInternal 两个变量都为 0,从而不会重新再次衍生子密钥,所以实际上本方法在这里基本没有执行,而直接返回真。

  8. 调用钱包的 GetKeyFromPool 方法,从密钥池中生成一个公钥。如果不能生成,则抛出异常。

    CPubKey newKey;
    if (!pwallet->GetKeyFromPool(newKey)) {
        throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
    }

    GetKeyFromPool 方法,我们在下面详细讲解,此处略过。

  9. 调用钱包对象的 LearnRelatedScripts 方法,对公钥的脚本进行处理。方法内部执行如下:如果公钥是压缩的,并且地址类型是  p2sh-segwit ,或者  bech32 ,那么:

    • 调用 WitnessV0KeyHash 方法,生成  WitnessV0KeyHash 对象。

      CTxDestination witdest = WitnessV0KeyHash(key.GetID());
    • 调用 GetScriptForDestination 方法,获取对应的脚本。

      CScript witprog = GetScriptForDestination(witdest);

      GetScriptForDestination 方法内部调用  boost::apply_visitor(CScriptVisitor(&script), dest) ,以访问者模式来根据不同的 id,获取其对应的脚本对象。

      CScriptVisitor 对象继承自  boost::static_visitor 对象,实现了访问者模式,并通过重载  () 操作符来定义不同类型的 id。

      • 如果目标参数类型是 CNoDestination ,则调用脚本对象的  script 方法,清除脚本内容。

      • 如果目标参数类型是 CKeyID ,则:首先调用脚本对象的  script 方法,清除脚本内容;然后,初始化脚本  *script << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG

      • 如果目标参数类型是 CScriptID ,则:首先调用脚本对象的  script 方法,清除脚本内容;然后,初始化脚本  *script << OP_HASH160 << ToByteVector(scriptID) << OP_EQUAL

      • 如果目标参数类型是 WitnessV0KeyHash ,则:首先调用脚本对象的  script 方法,清除脚本内容;然后,初始化脚本  *script << OP_0 << ToByteVector(id)

      • 如果目标参数类型是 WitnessV0ScriptHash ,则:首先调用脚本对象的  script 方法,清除脚本内容;然后,初始化脚本  *script << OP_0 << ToByteVector(id)

      • 如果目标参数类型是 WitnessUnknown ,则:首先调用脚本对象的  script 方法,清除脚本内容;然后,初始化脚本  *script << CScript::EncodeOP_N(id.version) << std::vector(id.program, id.program + id.length)

    • 调用 AddCScript 方法,保存脚本对象。 AddCScript 方法,首先调用  CCryptoKeyStore::AddCScript 方法,把脚本保存到 key store 的  mapScripts 集合中;然后,调用数据库访问对象的  WriteCScript 方法,以  cscript 为键把脚本保存到数据库中。

      bool CWallet::AddCScript(const CScript& redeemScript)
      {
          if (!CCryptoKeyStore::AddCScript(redeemScript))
              return false;
          return WalletBatch(*database).WriteCScript(Hash160(redeemScript), redeemScript);
      }
  10. 调用 GetDestinationForKey 方法,获取目的地  CTxDestination 对象。 CTxDestination 是一个具有特定目标的交易输出脚本模板。定义如下: typedef boost::variant CTxDestination ,可能是以下几种类型之一:

    • CNoDestination没有目的地设置

    • CKeyIDP2PKH 目的

    • CScriptIDP2SH 目的

    • WitnessV0ScriptHashP2WSH 目的

    • WitnessV0KeyHashP2WPKH 目的

    • WitnessUnknown未知目的 P2W???

    GetDestinationForKey 方法,使用  case 表达式来根据不同的地址类型,生成不同的目的  CTxDestination

    • 如果地址类型是 legacy ,则直接返回公钥的  KeyID 。内部把公钥的数据通过 SHA256 和 RIPEMD160 双重哈希之后,构造一个  CKeyID 对象。

    • 如果地址类型是 p2sh-segwit ,或  bech32 ,则处理如下:

      • 如果公钥不是压缩的,处理方法 legacy

        if (!key.IsCompressed()) return key.GetID();
      • 否则,生成 WitnessV0KeyHash 对象,然后调用  GetScriptForDestination 方法,获取对应的脚本,最后根据不同的地址类型生成的目的。

        CTxDestination witdest = WitnessV0KeyHash(key.GetID());
        CScript witprog = GetScriptForDestination(witdest);
        if (type == OutputType::P2SH_SEGWIT) {
            return CScriptID(witprog);
        } else {
            return witdest;
        }

        对于默认的、不传地址类型的情况,就会返回 CScriptID 类型的  CTxDestination ,这个返回值在下面两步中都会用到。

  11. 调用钱包对象的 SetAddressBook 方法,来保存公钥地址。

    pwallet->SetAddressBook(dest, label, "receive");

    SetAddressBook 方法执行如下:

    • mapAddressBook 集合中,取得对应的目的数据。

      std::map
      <ctxdestination nbsp="" caddressbookdata="">
       ::iterator mi = mapAddressBook.find(address);
      </ctxdestination>
    • 根据集合中是否有对应的数据设置变量是否为更新。

      fUpdated = mi != mapAddressBook.end();
    • 把标签保存为地址对应的 CAddressBookData 的  name 属性。

      mapAddressBook[address].name = strName;
    • 如果参数 strPurpose 不空,则更新地址对应的  CAddressBookData 的  purpose 属性。

      if (!strPurpose.empty()) mapAddressBook[address].purpose = strPurpose;
    • 调用数据库访问对象的 WritePurpose 方法,保存参数  strPurpose 到数据库中。

      if (!strPurpose.empty() && !WalletBatch(*database).WritePurpose(EncodeDestination(address), strPurpose))
          return false;
    • 调用数据库访问对象的 WritePurpose 方法,保存地址到数据库中。

      WalletBatch(*database).WriteName(EncodeDestination(address), strName);

      strName 为用户提供的标签。 EncodeDestination 方法,我们在下一步讲解。

  12. 调用 EncodeDestination 方法,解码目的地址,并返回其结果。 EncodeDestination 方法同样采用了访问者模式  return boost::apply_visitor(DestinationEncoder(Params()), dest)DestinationEncoder 类继承了  boost::static_visitor ,实现了访问者模式,通过重载  () 操作符来定义不同类型的 id。与前面相对应,这个方法会处理  CKeyIDCScriptIDWitnessV0KeyHashWitnessV0ScriptHashWitnessUnknown 这几种不同情况。对于我们的默认情况来说,目的地址类型为  CScriptID ,下面我们就看下这种情况的处理,其他情况可自行阅读。

    • 调用当前网络参数的 Base58Prefix 方法,返回脚本前缀。

      std::vector
      <unsigned nbsp="" char="">
        data = m_params.Base58Prefix(CChainParams::SCRIPT_ADDRESS);
      </unsigned>

      对于主网络前缀是 5,测试网络是 196,回归测试网络是 196。

    • 把当前 20 个字节的数据加在前缀后面形成 21 个字节的字符串。

      data.insert(data.end(), id.begin(), id.end());
    • 调用 EncodeBase58Check 方法,编码成 Base58Check 格式,并返回其值。

      return EncodeBase58Check(data);

      下面,我们来看下 EncodeBase58Check 这个方法的处理。它的内部执行流程如下:用 21 个字节的字符串生成一个向量,同时调用  Hash 方法,生成一个 32 字节长的哈希字符串;然后把其最前面的 4个字节作为校验各加在 21 个字节的向量尾部,从而生成一个长度为 25个字节的字符串;最后,调用  EncodeBase58 方法,进行 Base58 编码。

      std::vector
      <unsigned nbsp="" char="">
        vch(vchIn);
      uint256 hash = Hash(vch.begin(), vch.end());
      vch.insert(vch.end(), (unsigned char*)&hash, (unsigned char*)&hash + 4);
      return EncodeBase58(vch);
      </unsigned>

      Hash 这个方法中,使用了双重 SHA256 哈希算法。 EncodeBase58 这个方法,读者可以自行阅读,这里不再展开。

GetKeyFromPool 从密钥池中获取公钥

本方法从密钥池中生成一个公钥。第一个参数为公钥的引用,第二个参数  internal ,默认为假。

内部逻辑如下:

  1. 如果钱包禁止私钥,则返回假。

    if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
        return false;
    }
  2. 调用 ReserveKeyFromKeyPool 方法,从密钥池中取出一个密钥并获取其公钥。如果不成功,则生成数据库访问对象,然后调用  GenerateNewKey 方法,生成一个公钥。

    if (!ReserveKeyFromKeyPool(nIndex, keypool, internal)) {
        if (IsLocked()) return false;
        WalletBatch batch(*database);
        result = GenerateNewKey(batch, internal);
        return true;
    }

    GenerateNewKey 这个方法,在创建钱包过程中,我们已经重点分析,这里不浪费口舌,我们重点看下  ReserveKeyFromKeyPool 方法。这个方法的执行流程如下:

    • 生成一个公钥,并设置为密钥池的 vchPubKey 属性。

      nIndex = -1;
      keypool.vchPubKey = CPubKey();
    • 如果钱包没有被锁,则填充密钥池。

      if (!IsLocked())
          TopUpKeyPool();

      TopUpKeyPool 这个方法,我们也讲过,这里直接略过。

    • 如果钱包启用了 HD,并且可以支持 HD 分割,并且参数 fRequestedInternal 为真,则设置变量  fReturningInternal 为真。在调用本方法时,这个参数没有指定,而默认为假,所以变量  fRequestedInternal 设置假。

      bool fReturningInternal = IsHDEnabled() && CanSupportFeature(FEATURE_HD_SPLIT) && fRequestedInternal;
    • 根据集合 set_pre_split_keypool 是否为空,设置变量  use_split_keypool 的值。因为这里  use_split_keypool 集合为空,所以变量  use_split_keypool 为真。

      bool use_split_keypool = set_pre_split_keypool.empty();
    • 根据变量 use_split_keypoolfReturningInternal 确定从哪个集合中获取密钥池对象。根据上面分析,我们最终会从  setExternalKeyPool 集合中取数据。

      std::set
      <int64_t>
       & setKeyPool = use_split_keypool ? (fReturningInternal ? setInternalKeyPool : setExternalKeyPool) : set_pre_split_keypool;
      </int64_t>
    • 如果要数据的集合为为空,则返回假。

      if (setKeyPool.empty()) {
          return false;
      }
    • 生成数据库访问对象。

      WalletBatch batch(*database);
    • setKeyPool 取得其第一个元素,并从集合中删除它。

      auto it = setKeyPool.begin();
      nIndex = *it;
      setKeyPool.erase(it);
    • 从数据库取得索引对应的密钥池。如果失败,则抛出异常。

      if (!batch.ReadPool(nIndex, keypool)) {
          throw std::runtime_error(std::string(__func__) + ": read failed");
      }
    • 从密钥池中取得公钥对应的 ID,并且检测其是否在 mapKeys 、或  mapCryptedKeys 集合之一,如果不在,则抛出异常。我们在创建钱包过程时候讲过,生成的私钥根据是否加密会保存在这两个集合之一。

      if (!HaveKey(keypool.vchPubKey.GetID())) {
          throw std::runtime_error(std::string(__func__) + ": unknown key in key pool");
      }
    • 如果变量 use_split_keypool 为真,并且密钥池的  fInternal 属性不等于变量  fReturningInternal ,那么抛出异常。

      if (use_split_keypool && keypool.fInternal != fReturningInternal) {
          throw std::runtime_error(std::string(__func__) + ": keypool entry misclassified");
      }
    • 如果密钥池中保存的公钥是无效的,那么抛出异常。

      if (!keypool.vchPubKey.IsValid()) {
          throw std::runtime_error(std::string(__func__) + ": keypool entry invalid");
      }
    • m_pool_key_to_index 集合中消除对应的索引。

      m_pool_key_to_index.erase(keypool.vchPubKey.GetID());
    • 返回真。

  3. 调用 KeepKey

  4. 从密钥池中取出对应的公钥。

后记

由于本人水平所限,文中错误在所难免,欢迎您踊跃指出错误,在下感激不尽。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

推荐系统

推荐系统

[奥地利] Dietmar Jannach、Markus Zanker、Alexander Felfernig、Gerhard Friedrich / 蒋 凡 / 人民邮电出版社 / 2013-6-25 / 59.00元

编辑推荐: 通过对本书的学习,读者不仅可以全面系统地了解该领域的基础原理,还能试验如何搭建一套真正的推荐系统。 —— 百度主任架构师、百度技术委员会主席 廖若雪 本书比较全面地介绍了推荐系统涉及的相关知识点,很适合对于推荐系统感兴趣的相关人员作为入门教程,目前能够系统全面介绍相关技术的中文书籍还显得匮乏,相信这本译著对于缓解这种情况大有裨益。 ——新浪微博数据挖掘技术专家 ......一起来看看 《推荐系统》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具