1. 程式人生 > >從零開始學習比特幣開發(十一)-建立錢包

從零開始學習比特幣開發(十一)-建立錢包

比特幣使用者最關心除了交易之外就是地址、錢包、私鑰了,交易、地址、錢包、私鑰這些不同概念之間具有內在的聯絡,要了解交易必須先要了解地址、錢包、私鑰這幾個概念,從本章開始,我們開始學習這一部分內容。

建立錢包整體流程

前面我們提到 RPC 的概念,RPC 是 remote process call 這個過程的縮寫,也就是遠端過程呼叫。比特幣核心提供了很多 RPC 來供客戶端呼叫,其中一個就是我們這裡要講的 createwallet 建立一個錢包,通過這個 RPC ,我們就可以生成一個新的錢包。

createwallet RPC 可以接收兩個引數,第一個錢包名稱,第二個是否禁止私鑰。第一個引數是必填引數,第二個是可選引數,預設為假。

下面我們通過這個 RPC 來看下怎麼生成一個錢包。

  1. 從引數中取得錢包的名稱。

      std::string wallet_name = request.params[0].get_str();
      std::string error;
      std::string warning;
    
  2. 如果提供了第2個引數則取得是否禁止私鑰。

      bool disable_privatekeys = false;
      if (!request.params[1].isNull()) {
          disable_privatekeys = request.params[1].get_bool();
      }
    
  3. 通過錢包名稱加上其儲存的路徑來判斷錢包名稱是否已經存在。

      fs::path wallet_path = fs::absolute(wallet_name, GetWalletDir());
      if (fs::symlink_status(wallet_path).type() != fs::file_not_found) {
        throw JSONRPCError(RPC_WALLET_ERROR, "Wallet " + wallet_name + " already exists.");
      }
    
  4. 檢查錢包檔案的路徑可以建立、不與別的錢包重複、路徑是一個目錄,同時為了向後相容,錢包路徑要在 -walletdir

    指定的路徑下面。

      if (!CWallet::Verify(wallet_name, false, error, warning)) {
          throw JSONRPCError(RPC_WALLET_ERROR, "Wallet file verification failed: " + error);
      }
    
  5. 建立錢包,如果建立失敗,則丟擲異常。

    std::shared_ptr<CWallet> const wallet = CWallet::CreateWalletFromFile(wallet_name, fs::absolute(wallet_name, GetWalletDir()), (disable_privatekeys ? (uint64_t)WALLET_FLAG_DISABLE_PRIVATE_KEYS : 0));
      if (!wallet) {
          throw JSONRPCError(RPC_WALLET_ERROR, "Wallet creation failed.");
      }
    

    CreateWalletFromFile 是我們這部分內部的主體,所以放在下面進行詳細說明,此處略過不講。

  6. 呼叫AddWallet 方法,把錢包物件加入 vpwallets 向量中。

  7. 呼叫錢包物件的 postInitProcess 方法,進行一些後置的處理。

  8. 返回錢包名字和警告資訊組成的物件。

建立錢包方法 CreateWalletFromFile

上面一節的第5部提到CreateWalletFromFile這個方法。下面我們詳細講解它,它接收 3 個引數,第一個引數是錢包的名稱,第二個引數是錢包的絕對路徑,第三個引數是錢包的標誌。具體邏輯如下:

  1. 生成兩個變數,一個是錢包檔案,一個是錢包的交易元資料,用來在清除交易之後恢復錢包交易。

      const std::string& walletFile = name;
      std::vector<CWalletTx> vWtx;
    
  2. 如果啟動引數指定了 -zapwallettxes,那麼生成一個臨時錢包,並呼叫其 ZapWalletTx 方法來清除交易。如果出現錯誤,則返回空指標。

      if (gArgs.GetBoolArg("-zapwallettxes", false)) {
          std::unique_ptr<CWallet> tempWallet = MakeUnique<CWallet>(name, WalletDatabase::Create(path));
          DBErrors nZapWalletRet = tempWallet->ZapWalletTx(vWtx);
          if (nZapWalletRet != DBErrors::LOAD_OK) {
              InitError(strprintf(_("Error loading %s: Wallet corrupted"), walletFile));
              return nullptr;
          }
      }
    

    生成錢包時,需要指定錢包檔案的路徑,從而可以讀取錢包中儲存的交易資訊。

    下面,我們來看下 ZapWalletTx 這個方法。方法的主體是生成一個可以訪問錢包的資料庫的物件,並呼叫後者的 ZapWalletTx 方法來完成清理交易的。在後者的方法定義如下:

      DBErrors WalletBatch::ZapWalletTx(std::vector<CWalletTx>& vWtx)
      {
          std::vector<uint256> vTxHash;
          DBErrors err = FindWalletTx(vTxHash, vWtx);
          if (err != DBErrors::LOAD_OK)
              return err;
          for (const uint256& hash : vTxHash) {
              if (!EraseTx(hash))
                  return DBErrors::CORRUPT;
          }
          return DBErrors::LOAD_OK;
      }
    

    在上面的方法中,呼叫 FindWalletTx 方法,從錢包資料庫中讀取所有的交易雜湊和交易本身,並生成對應的 CWalletTx 物件。然後,遍歷所有的交易雜湊,呼叫 EraseTx 方法來刪除對應的交易。

    為什麼要清理錢包資料庫中的交易呢?因為有交易費用過低,導致無法打包到區塊中,對於這些交易我們需要從錢包檔案中清理出去。

  3. 生成兩個變數,一個是當前時間,一個是表示第一次執行錢包的標誌。

      int64_t nStart = GetTimeMillis();
      bool fFirstRun = true;
    
  4. 根據錢包名稱和絕對路徑生成錢包物件。

      std::shared_ptr<CWallet> walletInstance(new CWallet(name, WalletDatabase::Create(path)), ReleaseWallet);
    
  5. 呼叫錢包物件的 LoadWallet 方法載入錢包。如果出現錯誤,則進行相應的處理。

      DBErrors nLoadWalletRet = walletInstance->LoadWallet(fFirstRun);
      if (nLoadWalletRet != DBErrors::LOAD_OK)
      {
          if (nLoadWalletRet == DBErrors::CORRUPT) {
              InitError(strprintf(_("Error loading %s: Wallet corrupted"), walletFile));
              return nullptr;
          }
          else if (nLoadWalletRet == DBErrors::NONCRITICAL_ERROR)
          {
              InitWarning(strprintf(_("Error reading %s! All keys read correctly, but transaction data or address book entries might be missing or incorrect."),
                  walletFile));
          }
          else if (nLoadWalletRet == DBErrors::TOO_NEW) {
              InitError(strprintf(_("Error loading %s: Wallet requires newer version of %s"), walletFile, _(PACKAGE_NAME)));
              return nullptr;
          }
          else if (nLoadWalletRet == DBErrors::NEED_REWRITE)
          {
              InitError(strprintf(_("Wallet needed to be rewritten: restart %s to complete"), _(PACKAGE_NAME)));
              return nullptr;
          }
          else {
              InitError(strprintf(_("Error loading %s"), walletFile));
              return nullptr;
          }
      }
    

    錢包物件 LoadWallet 方法的主體是生成一個可以訪問錢包的資料庫的物件,從而呼叫錢包資料庫物件的的 LoadWallet 方法來載入錢包。從錢包資料庫中載入錢包的方法在下面詳細說明,此處略過。

      DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
      {
          LOCK2(cs_main, cs_wallet);
          fFirstRunRet = false;
          DBErrors nLoadWalletRet = WalletBatch(*database,"cr+").LoadWallet(this);
          if (nLoadWalletRet == DBErrors::NEED_REWRITE)
          {
              if (database->Rewrite("\x04pool"))
              {
                  setInternalKeyPool.clear();
                  setExternalKeyPool.clear();
                  m_pool_key_to_index.clear();
              }
          }
          {
              LOCK(cs_KeyStore);
              fFirstRunRet = mapKeys.empty() && mapCryptedKeys.empty() && mapWatchKeys.empty() && setWatchOnly.empty() && mapScripts.empty() && !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
          }
          if (nLoadWalletRet != DBErrors::LOAD_OK)
              return nLoadWalletRet;
    
          return DBErrors::LOAD_OK;
      }
    
  6. 如果啟動引數 -upgradewallet 為真,或者沒有指定啟動引數 -upgradewallet 為假,但是第一次執行建立這個錢包,那麼進行如下處理:

    • 獲取啟動引數 -upgradewallet 的值,預設為0,儲存到變數 nMaxVersion 中。

      int nMaxVersion = gArgs.GetArg("-upgradewallet", 0);
      
    • 如果變數 nMaxVersion 為0,即沒有指定啟動引數,那麼設定 nMaxVersion 等於 FEATURE_LATEST(目前為 169900),即支援 HD 分割,同時呼叫錢包物件的 SetMinVersion 方法,設定錢包最小版本為這個版本。

        if (nMaxVersion == 0) // the -upgradewallet without argument case
        {
            walletInstance->WalletLogPrintf("Performing wallet upgrade to %i\n", FEATURE_LATEST);
            nMaxVersion = FEATURE_LATEST;
            walletInstance->SetMinVersion(FEATURE_LATEST); // permanently upgrade the wallet immediately
        }
        else
            walletInstance->WalletLogPrintf("Allowing wallet upgrade up to %i\n", nMaxVersion);
      
    • 如果變數 nMaxVersion 小於錢包當前的版本,則直接返回空指標。

        int nMaxVersion = gArgs.GetArg("-upgradewallet", 0);
        if (nMaxVersion < walletInstance->GetVersion())
        {
            InitError(_("Cannot downgrade wallet"));
            return nullptr;
        }
      
    • 呼叫錢包物件的 SetMinVersion 方法,設定錢包最大版本。

        walletInstance->SetMaxVersion(nMaxVersion);
      
  7. 如果沒有指定啟動引數 -upgradewallet ,或者指定了但為假,那麼升級到 HD 錢包。具體處理如下:

    • 如果錢包不支援 FEATURE_HD_SPLIT,並且錢包的版本大於等於 139900 (FEATURE_HD_SPLIT)且小於等於 169900 (FEATURE_PRE_SPLIT_KEYPOOL),則返回空指標。

        int max_version = walletInstance->nWalletVersion;
        if (!walletInstance->CanSupportFeature(FEATURE_HD_SPLIT) && max_version >=FEATURE_HD_SPLIT && max_version < FEATURE_PRE_SPLIT_KEYPOOL) {
            return nullptr;
        }
      
    • 如果錢包支援 FEATURE_HD,且當前沒有啟用 HD,那麼呼叫 SetMinVersion 方法,設定最小版本為 FEATURE_HD,同時呼叫 GenerateNewSeed 生成新的隨機種子,然後呼叫 SetHDSeed 方法來設定隨機種子。

        bool hd_upgrade = false;
        bool split_upgrade = false;
        if (walletInstance->CanSupportFeature(FEATURE_HD) && !walletInstance->IsHDEnabled()) {
            walletInstance->SetMinVersion(FEATURE_HD);
            CPubKey masterPubKey = walletInstance->GenerateNewSeed();
            walletInstance->SetHDSeed(masterPubKey);
            hd_upgrade = true;
        }
      
    • 如果錢包支援 FEATURE_HD_SPLIT,那麼呼叫 SetMinVersion 方法,設定最小版本為 FEATURE_HD_SPLIT。如果 FEATURE_HD_SPLIT 大於先前的版本,則設定變數 split_upgrade 為真。

        if (walletInstance->CanSupportFeature(FEATURE_HD_SPLIT)) {
            walletInstance->WalletLogPrintf("Upgrading wallet to use HD chain split\n");
            walletInstance->SetMinVersion(FEATURE_PRE_SPLIT_KEYPOOL);
            split_upgrade = FEATURE_HD_SPLIT > prev_version;
        }
      
    • 如果變數 split_upgrade 為真,則呼叫 MarkPreSplitKeys 方法,將當前位於金鑰池中的所有金鑰標記為預拆分。

        if (split_upgrade) {
            walletInstance->MarkPreSplitKeys();
        }
      
    • 如果是 HD 升級,那麼重新生成金鑰池。

        if (hd_upgrade) {
            if (!walletInstance->TopUpKeyPool()) {
                InitError(_("Unable to generate keys"));
                return nullptr;
            }
        }
      
  8. 如果是第一次執行,即第一次建立這個錢包,那麼:

    • 設定錢包最小版本為 FEATURE_LATEST,當前為 FEATURE_PRE_SPLIT_KEYPOOL

        walletInstance->SetMinVersion(FEATURE_LATEST);
      
    • 如果建立引數指定不能包含私鑰,那麼設定錢包這個標記;否則,呼叫 GenerateNewSeed 方法,生成新的隨機種子,然後呼叫 SetHDSeed 方法,儲存隨機種子。

        if ((wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
            walletInstance->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
        } else {
            CPubKey seed = walletInstance->GenerateNewSeed();
            walletInstance->SetHDSeed(seed);
        }
      
    • 如果建立引數沒有指定不能包含私鑰,那麼填充金鑰池。如果失敗,即不能生成初始金鑰,則返回空指標。

        if (!walletInstance->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !walletInstance->TopUpKeyPool()) {
            return nullptr;
        }
      
    • 重新整理到資料庫。

        walletInstance->ChainStateFlushed(chainActive.GetLocator());
      
  9. 否則,如果建立引數指定不能包含私鑰,那麼返回 NULL。

  10. 如果指定了啟動引數 -addresstype,但是解析失敗,則返回空指標。

  11. 如果指定了啟動引數 -changetype,但是解析失敗,則返回空指標。

  12. 如果設定了最小交易費用,則解析並設定錢包的最小交易費用。

     if (gArgs.IsArgSet("-mintxfee")) {
         CAmount n = 0;
         if (!ParseMoney(gArgs.GetArg("-mintxfee", ""), n) || 0 == n) {
             return nullptr;
         }
         walletInstance->m_min_fee = CFeeRate(n);
     }
    
  13. 根據不同網路取得是否啟用回退費用。如果啟動引數設定了 -fallbackfee 用以在估算費用不足時將使用的費率,那麼就解析設定的費率,如果解析錯誤,則列印錯誤日誌,並返回空指標,如果解析OK,但是大於規定的最大值,則列印警告日誌。如果兩者都沒有問題,則設定錢包的回退費率。

     walletInstance->m_allow_fallback_fee = Params().IsFallbackFeeEnabled();
     if (gArgs.IsArgSet("-fallbackfee")) {
         CAmount nFeePerK = 0;
         if (!ParseMoney(gArgs.GetArg("-fallbackfee", ""), nFeePerK)) {
             InitError(strprintf(_("Invalid amount for -fallbackfee=<amount>: '%s'"), gArgs.GetArg("-fallbackfee", "")));
             return nullptr;
         }
         if (nFeePerK > HIGH_TX_FEE_PER_KB) {
             InitWarning(AmountHighWarn("-fallbackfee") + " " +
                         _("This is the transaction fee you may pay when fee estimates are not available."));
         }
         walletInstance->m_fallback_fee = CFeeRate(nFeePerK);
         walletInstance->m_allow_fallback_fee = nFeePerK != 0; //disable fallback fee in case value was set to 0, enable if non-null value
     }
    
  14. 如果啟動引數設定了 -discardfee 用以規定在費用小於多少時捨棄,那麼就解析設定的費率,如果解析錯誤,則列印錯誤日誌,並返回空指標,如果解析OK,但是大於規定的最大值,則列印警告日誌。如果兩者都沒有問題,則設定錢包的丟棄費率。

     if (gArgs.IsArgSet("-discardfee")) {
         CAmount nFeePerK = 0;
         if (!ParseMoney(gArgs.GetArg("-discardfee", ""), nFeePerK)) {
             InitError(strprintf(_("Invalid amount for -discardfee=<amount>: '%s'"), gArgs.GetArg("-discardfee", "")));
             return nullptr;
         }
         if (nFeePerK > HIGH_TX_FEE_PER_KB) {
             InitWarning(AmountHighWarn("-discardfee") + " " +
                         _("This is the transaction fee you may discard if change is smaller than dust at this level"));
         }
         walletInstance->m_discard_rate = CFeeRate(nFeePerK);
     }
    
  15. 如果啟用引數設定了 -paytxfee 指定交易費用,那麼就解析設定的費率,如果解析錯誤,則列印錯誤日誌,並返回空指標,如果解析OK,但是大於規定的最大值,則列印警告日誌。如果兩者都沒有問題,則設定錢包的交易費用。如果交易費用小於規定的最小值,則認為交易費用為為,則列印警告日誌,並返回空指標。

     if (gArgs.IsArgSet("-paytxfee")) {
       CAmount nFeePerK = 0;
       if (!ParseMoney(gArgs.GetArg("-paytxfee", ""), nFeePerK)) {
           InitError(AmountErrMsg("paytxfee", gArgs.GetArg("-paytxfee", "")));
           return nullptr;
       }
       if (nFeePerK > HIGH_TX_FEE_PER_KB) {
           InitWarning(AmountHighWarn("-paytxfee") + " " +
                       _("This is the transaction fee you will pay if you send a transaction."));
       }
       walletInstance->m_pay_tx_fee = CFeeRate(nFeePerK, 1000);
       if (walletInstance->m_pay_tx_fee < ::minRelayTxFee) {
           InitError(strprintf(_("Invalid amount for -paytxfee=<amount>: '%s' (must be at least %s)"),
               gArgs.GetArg("-paytxfee", ""), ::minRelayTxFee.ToString()));
           return nullptr;
       }
     }
    
  16. 設定交易確認的平均區塊數(預設為6)、是否傳送未確認的變更、是否啟用 full-RBF opt-in。

     walletInstance->m_confirm_target = gArgs.GetArg("-txconfirmtarget", DEFAULT_TX_CONFIRM_TARGET);
     walletInstance->m_spend_zero_conf_change = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
     walletInstance->m_signal_rbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
    
  17. 接下來填充金鑰池,如果錢包被鎖定,則不進行任何操作。

     walletInstance->TopUpKeyPool();
    
    
  18. 獲取區塊鏈的創世區塊索引。如果沒有指定重新掃描區塊鏈,或指定不掃描,那麼進行下面的操作。

    生成一個可以訪問錢包資料庫的物件,然後從資料庫讀取最佳區塊,即關鍵字為 bestblock 的區塊。如果可以找到這個區塊,那麼呼叫 FindForkInGlobalIndex 方法,返回分叉的區塊。

    最佳區塊是一個區塊定位器,它描述了塊鏈中到另一個節點的位置,這樣如果另一個節點沒有相同的分支,它就可以找到最近的公共中繼。

    CBlockIndex *pindexRescan = chainActive.Genesis();
    if (!gArgs.GetBoolArg("-rescan", false))
    {
        WalletBatch batch(*walletInstance->database);
        CBlockLocator locator;
        if (batch.ReadBestBlock(locator))
            pindexRescan = FindForkInGlobalIndex(chainActive, locator);
    }
    
    

    pindexRescan 為重新掃描區塊鏈的區塊,預設從創世區塊開始,下面會進行重新設定。

    FindForkInGlobalIndex 方法根據區塊定位器中包含的區塊雜湊在區塊索引集合 mapBlockIndex 中查詢對應的區塊。如果這個區塊索引存在,進一步如果當前區塊鏈中包含這個區塊索引,則返回這個區塊索引,否則如果這個區塊索引的祖先是當前區塊鏈的頂部區塊,則返回當前區塊鏈的頂部區塊。最後,如果找不到這樣的區塊,則返回當前區塊鏈的創世區塊。

    CBlockIndex* FindForkInGlobalIndex(const CChain& chain, const CBlockLocator& locator)
    {
        AssertLockHeld(cs_main);
        for (const uint256& hash : locator.vHave) {
            CBlockIndex* pindex = LookupBlockIndex(hash);
            if (pindex) {
                if (chain.Contains(pindex))
                    return pindex;
                if (pindex->GetAncestor(chain.Height()) == chain.Tip()) {
                    return chain.Tip();
                }
            }
        }
        return chain.Genesis();
    }
    
    
  19. 設定錢包最後一個處理的區塊為當前區塊鏈頂部的區塊。

     walletInstance->m_last_block_processed = chainActive.Tip();
    
    
  20. 如果當前區塊鏈頂部的區塊存在,且不等於前一步中我們找到的 pindexRescan 對應的區塊,那麼進行下面的處理:

    • 如果當前是修剪模式,從區塊鏈頂部的區塊開始向下遍歷,一直找到某個區塊的前一區塊的資料不在區塊資料庫檔案或,或者前一個區塊的交易數為0,或者這個區塊是 pindexRescan。如果最終找到的區塊不是 pindexRescan,那麼列印錯誤訊息,並返回空指標。

        if (fPruneMode)
        {
            CBlockIndex *block = chainActive.Tip();
            while (block && block->pprev && (block->pprev->nStatus & BLOCK_HAVE_DATA) && block->pprev->nTx > 0 && pindexRescan != block)
                block = block->pprev;
      
            if (pindexRescan != block) {
                InitError(_("Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of pruned node)"));
                return nullptr;
            }
        }
      
      
    • pindexRescan 區塊開始向區塊鏈頂部遍歷,找到第一個區塊建立時間小於錢包建立時間與 TIMESTAMP_WINDOW 之差的區塊。TIMESTAMP_WINDOW 當前規定為 2個小時。

        while (pindexRescan && walletInstance->nTimeFirstKey && (pindexRescan->GetBlockTime() < (walletInstance->nTimeFirstKey - TIMESTAMP_WINDOW))) {
            pindexRescan = chainActive.Next(pindexRescan);
        }
      
      
    • 生成一個 WalletRescanReserver 物件,並呼叫其 reserve 方法。如果該方法返回假,則列印錯誤日誌,並返回空指標。否則,呼叫錢包物件的 ScanForWalletTransactions 方法,掃描錢包的所有交易。

      reserve 方法內部檢查錢包的 fScanningWallet 屬性,如果這個屬性已經為真,那麼直接返回假;否則,設定其為真,並設定變數 m_could_reserve 也為真,然後返回真。具體程式碼如下:

      bool reserve()
      {
          assert(!m_could_reserve);
          std::lock_guard<std::mutex> lock(m_wallet->mutexScanning);
          if (m_wallet->fScanningWallet) {
              return false;
          }
          m_wallet->fScanningWallet = true;
          m_could_reserve = true;
          return true;
      }
      
      

      ScanForWalletTransactions 方法,具體處理邏輯如下:

      • 首先,進行些變數初始化及校驗,不詳述。

          int64_t nNow = GetTime();
          const CChainParams& chainParams = Params();
          assert(reserver.isReserved());
          if (pindexStop) {
              assert(pindexStop->nHeight >= pindexStart->nHeight);
          }
          CBlockIndex* pindex = pindexStart;
          CBlockIndex* ret = nullptr;
        
        
      • 呼叫 GuessVerificationProgress 方法,預估驗證的進度。

          fAbortRescan = false;
          CBlockIndex* tip = nullptr;
          double progress_begin;
          double progress_end;
          {
              LOCK(cs_main);
              progress_begin = GuessVerificationProgress(chainParams.TxData(), pindex);
              if (pindexStop == nullptr) {
                  tip = chainActive.Tip();
                  progress_end = GuessVerificationProgress(chainParams.TxData(), tip);
              } else {
                  progress_end = GuessVerificationProgress(chainParams.TxData(), pindexStop);
              }
          }
        
        
      • 只要 pindex 不為空,且沒有終止當前的掃描,且沒有收到關閉的請求,就沿著區塊鏈向棧頂遍歷,從硬碟上讀取區塊,如果可以讀取到區塊,並且當前活躍區塊鏈包含當前的區塊,那麼從這個區塊中同步所有的交易,如果當前活躍區塊鏈不包含當前的區塊,那麼設定當前區塊索引為退出索引,並且退出迴圈;如果不能從硬碟中讀取,同樣設定當前區塊索引為退出索引。如果當前區塊等於退出區塊的索引,那麼退出迴圈。

          double progress_current = progress_begin;
          while (pindex && !fAbortRescan && !ShutdownRequested())
          {
              if (GetTime() >= nNow + 60) {
                  nNow = GetTime();
              }
              CBlock block;
              if (ReadBlockFromDisk(block, pindex, Params().GetConsensus())) {
                  LOCK2(cs_main, cs_wallet);
                  if (pindex && !chainActive.Contains(pindex)) {
                      ret = pindex;
                      break;
                  }
                  for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
                      SyncTransaction(block.vtx[posInBlock], pindex, posInBlock, fUpdate);
                  }
              } else {
                  ret = pindex;
              }
              if (pindex == pindexStop) {
                  break;
              }
              {
                  LOCK(cs_main);
                  pindex = chainActive.Next(pindex);
                  progress_current = GuessVerificationProgress(chainParams.TxData(), pindex);
                  if (pindexStop == nullptr && tip != chainActive.Tip()) {
                      tip = chainActive.Tip();
                      progress_end = GuessVerificationProgress(chainParams.TxData(), tip);
                  }
              }
          }
        
        
      • 返回退出區塊索引。

    • 呼叫錢包物件的 ChainStateFlushed 方法,把區塊定位器作為最佳區塊儲存到錢包資料庫中。

        walletInstance->ChainStateFlushed(chainActive.GetLocator());
      
      
    • 呼叫錢包資料庫物件的 IncrementUpdateCounter 方法,增加資料庫物件的更新次數。

    • 如果啟動引數指定了 -zapwallettxes,且等於1,那麼需要重新把錢包的元資料儲存到錢包資料庫中。錢包交易前面取出來儲存在 vWtx 變數中。

        if (gArgs.GetBoolArg("-zapwallettxes", false) && gArgs.GetArg("-zapwallettxes", "1") != "2")
        {
            WalletBatch batch(*walletInstance->database);
      
            for (const CWalletTx& wtxOld : vWtx)
            {
                uint256 hash = wtxOld.GetHash();
                std::map<uint256, CWalletTx>::iterator mi = walletInstance->mapWallet.find(hash);
                if (mi != walletInstance->mapWallet.end())
                {
                    const CWalletTx* copyFrom = &wtxOld;
                    CWalletTx* copyTo = &mi->second;
                    copyTo->mapValue = copyFrom->mapValue;
                    copyTo->vOrderForm = copyFrom->vOrderForm;
                    copyTo->nTimeReceived = copyFrom->nTimeReceived;
                    copyTo->nTimeSmart = copyFrom->nTimeSmart;
                    copyTo->fFromMe = copyFrom->fFromMe;
                    copyTo->nOrderPos = copyFrom->nOrderPos;
                    batch.WriteTx(*copyTo);
                }
            }
        }
      
      
  21. 呼叫全域性方法 RegisterValidationInterface 註冊錢包繫結方法作為訊號處理器。

  22. 呼叫錢包物件的 SetBroadcastTransactions 方法,根據啟動引數設定是否廣播交易。

  23. 返回錢包物件。

從資料庫中讀取錢包的 LoadWallet 方法

在這裡,我們仔細看下錢包資料庫的 LoadWallet 方法。這個方法的執行邏輯如下:

  1. 首先,初始化幾個變數。

      CWalletScanState wss;
      bool fNoncriticalErrors = false;
      DBErrors result = DBErrors::LOAD_OK;
    
    
  2. 從資料庫中讀取錢包的最小版本。如果最小版本大於當前的最新版本,則返回資料庫異常;否則,呼叫錢包物件的 LoadMinVersion 方法,設定錢包的版本相關屬性,nWalletVersion 屬性為資料庫讀取取的 nMinVersionnWalletMaxVersion 屬性為 nWalletMaxVersion 與這個值的較大者。

      int nMinVersion = 0;
      if (m_batch.Read((std::string)"minversion", nMinVersion))
      {
          if (nMinVersion > FEATURE_LATEST)
              return DBErrors::TOO_NEW;
          pwallet->LoadMinVersion(nMinVersion);
      }
    
    
  3. 獲取資料庫遊標,如果出錯,則返回資料庫遊標錯誤。

      Dbc* pcursor = m_batch.GetCursor();
      if (!pcursor)
      {
          pwallet->WalletLogPrintf("Error getting wallet database cursor\n");
          return DBErrors::CORRUPT;
      }
    
    
  4. 進放 while (true){ ... } 迴圈讀取資料庫中的所有資料。具體處理如下:

    • 呼叫 ReadAtCursor 方法從遊標中讀取對應的資料。如果沒有讀取到資料,則退出迴圈。如果出現錯誤,則呼叫錢包物件的 WalletLogPrintf 方法列印,然後返回資料庫錯誤。

        CDataStream ssKey(SER_DISK, CLIENT_VERSION);
        CDataStream ssValue(SER_DISK, CLIENT_VERSION);
        int ret = m_batch.ReadAtCursor(pcursor, ssKey, ssValue);
        if (ret == DB_NOTFOUND)
            break;
        else if (ret != 0)
        {
            pwallet->WalletLogPrintf("Error reading next record from wallet database\n");
            return DBErrors::CORRUPT;
        }
      
      
    • 呼叫 ReadKeyValue 方法,讀取遊標中當前的內容。如果讀取出錯,則根據錯誤進行相應的處理,具體不細說。如果讀取資料成功,其內部根據讀取到的資料進行具體處理如下:

      • 如果當前的的 Key 是 name,那麼從流中讀取對應的地址到變數 strAddress中,呼叫 DecodeDestination 方法解碼這個地址,然後設定錢包物件 mapAddressBook 集合對應的 CAddressBookData 物件的 name 屬性為流中讀取到的名字值。

          if (strType == "name")
          {
              std::string strAddress;
              ssKey >> strAddress;
              ssValue >> pwallet->mapAddressBook[DecodeDestination(strAddress)].name;
          }
        
        
      • 否則,如果前的 Key 是 purpose ,那麼從流中讀取對應的地址到變數 strAddress中,呼叫 DecodeDestination 方法解碼這個地址,然後設定錢包物件 mapAddressBook 集合對應的 CAddressBookData 物件的 purpose 屬性為流中讀取到的名字值。

          else if (strType == "purpose")
          {
              std::string strAddress;
              ssKey >> strAddress;
              ssValue >> pwallet->mapAddressBook[DecodeDestination(strAddress)].purpose;
          }
        
        
      • 否則,如果當前的 Key 是 tx ,即交易,那麼處理如下。

        從流中讀取交易雜湊和錢包交易物件,然後呼叫 CheckTransaction 方法,檢查讀取的錢包交易物件,如果出錯,則返回假。

        uint256 hash;
        ssKey >> hash;
        CWalletTx wtx(nullptr /* pwallet */, MakeTransactionRef());
        ssValue >> wtx;
        CValidationState state;
        if (!(CheckTransaction(*wtx.tx, state) && (wtx.GetHash() == hash) && state.IsValid()))
            return false;
        
        

        如果錢包交易物件的 fTimeReceivedIsTxTime 屬性大於等於 31404,且小於等於 31703,因為撤銷序列化在 31600 中進行了變更,所以進行下面的序列化處理。

        if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703)
        {
            if (!ssValue.empty())
            {
                char fTmp;
                char fUnused;
                std::string unused_string;
                ssValue >> fTmp >> fUnused >> unused_string;
                strErr = strprintf("LoadWallet() upgrading tx ver=%d %d %s", wtx.fTimeReceivedIsTxTime, fTmp, hash.ToString());
                wtx.fTimeReceivedIsTxTime = fTmp;
            }
            else
            {
                strErr = strprintf("LoadWallet() repairing tx ver=%d %s", wtx.fTimeReceivedIsTxTime, hash.ToString());
                wtx.fTimeReceivedIsTxTime = 0;
            }
            wss.vWalletUpgrade.push_back(hash);
        }
        
        

        如果錢包交易物件的 nOrderPos 為-1,那麼設定錢包掃描狀態物件的 fAnyUnordered 為真。

        if (wtx.nOrderPos == -1)
            wss.fAnyUnordered = true;
        
        

        呼叫錢包物件的 LoadToWallet,載入資料庫中讀到的錢包交易物件到錢包中。

      • 否則,如果當前的 Key 是 watchs ,那麼從流中讀取對應的序列化指令碼,並讀取其對應的值。如果其值等於1,那麼呼叫錢包物件的 LoadWatchOnly 方法,載入一個只讀的指令碼。

          wss.nWatchKeys++;
          CScript script;
          ssKey >> script;
          char fYes;
          ssValue >> fYes;
          if (fYes == '1')
              pwallet->LoadWatchOnly(script);
        
        
      • 否則,如果當前的 Key 是 key 或者 wkey,那麼處理如下。

        從流中讀取對應的公鑰,如果 Key 是 key,那麼從流中讀取出對應的私鑰,否則從流中讀取對應的錢包私鑰,從中取得對應的私鑰。

        CPubKey vchPubKey;
        ssKey >> vchPubKey;
        if (!vchPubKey.IsValid())
        {
            strErr = "Error reading wallet database: CPubKey corrupt";
            return false;
        }
        CKey key;
        CPrivKey pkey;
        uint256 hash;
        if (strType == "key")
        {
            wss.nKeys++;
            ssValue >> pkey;
        } else {
            CWalletKey wkey;
            ssValue >> wkey;
            pkey = wkey.vchPrivKey;
        }
        
        

        從流中讀取對應的雜湊值。

        ssValue >> hash;
        
        

        如果雜湊不空,則處理如下:

        if (!hash.IsNull())
        {
            std::vector<unsigned char> vchKey;
            vchKey.reserve(vchPubKey.size() + pkey.size());
            vchKey.insert(vchKey.end(), vchPubKey.begin(), vchPubKey.end());
            vchKey.insert(vchKey.end(), pkey.begin(), pkey.end());
        
            if (Hash(vchKey.begin(), vchKey.end()) != hash)
            {
                strErr = "Error reading wallet database: CPubKey/CPrivKey corrupt";
                return false;
            }
        
            fSkipCheck = true;
        }
        
        

        呼叫 CKey 物件的 Load 方法載入金鑰。

        if (!key.Load(pkey, vchPubKey, fSkipCheck))
        {
            strErr = "Error reading wallet database: CPrivKey corrupt";
            return false;
        }
        
        

        呼叫錢包物件的 LoadKey 方法,載入金鑰。

        if (!pwallet->LoadKey(key, vchPubKey))
        {
            strErr = "Error reading wallet database: LoadKey failed";
            return false;
        }
        
        
      • 否則,如果當前的 Key 是 mkey,則從流中讀取對應的主金鑰,並儲存在錢包 mapMasterKeys 對應的位置。

          else if (strType == "mkey")
          {
              unsigned int nID;
              ssKey >> nID;
              CMasterKey kMasterKey;
              ssValue >> kMasterKey;
              if(pwallet->mapMasterKeys.count(nID) != 0)
              {
                  strErr = strprintf("Error reading wallet database: duplicate CMasterKey id %u", nID);
                  return false;
              }
              pwallet->mapMasterKeys[nID] = kMasterKey;
              if (pwallet->nMasterKeyMaxID < nID)
                  pwallet->nMasterKeyMaxID = nID;
          }
        
        
      • 否則,如果當前的 Key 是 ckey,則從流中讀取對應的公鑰、私鑰,並呼叫錢包物件的 LoadCryptedKey 方法,載入加密的金鑰。

          else if (strType == "ckey")
          {
              CPubKey vchPubKey;
              ssKey >> vchPubKey;
              if (!vchPubKey.IsValid())
              {
                  strErr = "Error reading wallet database: CPubKey corrupt";
                  return false;
              }
              std::vector<unsigned char> vchPrivKey;
              ssValue >> vchPrivKey;
              wss.nCKeys++;
        
              if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey))
              {
                  strErr = "Error reading wallet database: LoadCryptedKey failed";
                  return false;
              }
              wss.fIsEncrypted = true;
          }
        
        
      • 否則,如果當前的 Key 是 keymeta,那麼從流中讀取對應的公鑰及其元資料,呼叫錢包物件的 LoadKeyMetadata 方法,載入金鑰元資料。

          else if (strType == "keymeta")
          {
              CPubKey vchPubKey;
              ssKey >> vchPubKey;
              CKeyMetadata keyMeta;
              ssValue >> keyMeta;
              wss.nKeyMeta++;
              pwallet->LoadKeyMetadata(vchPubKey.GetID(), keyMeta);
          }
        
        
    • 否則,如果當前的 Key 是 watchmeta,那麼從流中讀取指令碼和對應的元資料,並呼叫錢包物件的 LoadScriptMetadata 方法,載入指令碼的元資料。

      else if (strType == "watchmeta")
      {
          CScript script;
          ssKey >> script;
          CKeyMetadata keyMeta;
          ssValue >> keyMeta;
          wss.nKeyMeta++;
          pwallet->LoadScriptMetadata(CScriptID(script), keyMeta);
      }
      
      
    • 否則,如果當前的 Key 是 defaultkey,那麼從流中讀取對應的公鑰。

      else if (strType == "defaultkey")
        {
            CPubKey vchPubKey;
            ssValue >> vchPubKey;
            if (!vchPubKey.IsValid()) {
                strErr = "Error reading wallet database: Default Key corrupt";
                return false;
            }
        }
      
    • 否則,如果當前的 Key 是 pool,那麼從流中讀取對應的金鑰池,並呼叫錢包物件的 LoadKeyPool 方法,載入金鑰池。

      else if (strType == "pool")
        {
            int64_t nIndex;
            ssKey >> nIndex;
            CKeyPool keypool;
            ssValue >> keypool;
            pwallet->LoadKeyPool(nIndex, keypool);
        }
      
    • 否則,如果當前的 Key 是 version,那麼從流中讀取對應的版本到錢包掃描狀態物件的 nFileVersion 屬性中。如果這個值等於 10300,那麼設定錢包物件的這個屬性為 300。

      else if (strType == "version")
        {
            ssValue >> wss.nFileVersion;
            if (wss.nFileVersion == 10300)
                wss.nFileVersion = 300;
        }
      
    • 否則,如果當前的 Key 是 cscript,那麼從流中讀取對應的指令碼,並呼叫錢包物件的 LoadCScript 方法載入指令碼。

      else if (strType == "cscript")
        {
            uint160 hash;
            ssKey >> hash;
            CScript script;
            ssValue >> script;
            if (!pwallet->LoadCScript(script))
            {
                strErr = "Error reading wallet database: LoadCScript failed";
                return false;
            }
        }
      
    • 否則,如果當前的 Key 是 orderposnext,那麼從流中讀取對應的值到錢包物件的 nOrderPosNext 屬性中。

       else if (strType == "orderposnext")
        {
            ssValue >> pwallet->nOrderPosNext;
        }
      
    • 否則,如果當前的 Key 是 destdata,那麼從流中讀取對應的資料,並呼叫錢包物件的 LoadDestData 方法,處理這些資料。

       else if (strType == "destdata")
        {
            std::string strAddress, strKey, strValue;
            ssKey >> strAddress;
            ssKey >> strKey;
            ssValue >> strValue;
            pwallet->LoadDestData(DecodeDestination(strAddress), strKey, strValue);
        }
      
    • 否則,如果當前的 Key 是 hdchain,那麼從流中讀取 HD 鏈,並呼叫錢包物件的 SetHDChain方法,設定錢包的 HD 鏈中。

    • else if (strType == "hdchain")
        {
            CHDChain chain;
            ssValue >> chain;
            pwallet->SetHDChain(chain, true);
        }
      
    • 否則,如果當前的 Key 是 ,那麼呼叫錢包的 方法,設定其標誌。

       else if (strType == "flags") {
            uint64_t flags;
            ssValue >> flags;
            if (!pwallet->SetWalletFlags(flags, true)) {
                strErr = "Error reading wallet database: Unknown non-tolerable wallet flags found";
                return false;
            }
        } 
      
    • 否則,如果當前的 Key 不是 bestblockbestblock_nomerkleminversionacentry ,則錢包掃描狀態物件的 m_unknown_records 未知道記錄屬性加1。

  5. 返回真。