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

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

内容简介:如果有人想发送比特币给你,或者你从别人那里买几个比特币,就要把地址给对方,对方才能把币打到你指定的地址上。那么,如何才能拥有一个地址呢,下面我们就来讲讲这个问题。比特币核心提供了很多 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. 从密钥池中取出对应的公钥。

后记

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


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

查看所有标签

猜你喜欢:

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

The Effective Engineer

The Effective Engineer

Edmond Lau / The Effective Bookshelf, Palo Alto, CA. / 2015-3-19 / USD 39.00

Introducing The Effective Engineer — the only book designed specifically for today's software engineers, based on extensive interviews with engineering leaders at top tech companies, and packed with h......一起来看看 《The Effective Engineer》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具