walletload_tests.cpp raw

   1  // Copyright (c) 2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or https://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <wallet/test/util.h>
   6  #include <wallet/wallet.h>
   7  #include <test/util/logging.h>
   8  #include <test/util/setup_common.h>
   9  
  10  #include <boost/test/unit_test.hpp>
  11  
  12  namespace wallet {
  13  
  14  BOOST_AUTO_TEST_SUITE(walletload_tests)
  15  
  16  class DummyDescriptor final : public Descriptor {
  17  private:
  18      std::string desc;
  19  public:
  20      explicit DummyDescriptor(const std::string& descriptor) : desc(descriptor) {};
  21      ~DummyDescriptor() = default;
  22  
  23      std::string ToString(bool compat_format) const override { return desc; }
  24      std::optional<OutputType> GetOutputType() const override { return OutputType::UNKNOWN; }
  25  
  26      bool IsRange() const override { return false; }
  27      bool IsSolvable() const override { return false; }
  28      bool IsSingleType() const override { return true; }
  29      bool ToPrivateString(const SigningProvider& provider, std::string& out) const override { return false; }
  30      bool ToNormalizedString(const SigningProvider& provider, std::string& out, const DescriptorCache* cache = nullptr) const override { return false; }
  31      bool Expand(int pos, const SigningProvider& provider, std::vector<CScript>& output_scripts, FlatSigningProvider& out, DescriptorCache* write_cache = nullptr) const override { return false; };
  32      bool ExpandFromCache(int pos, const DescriptorCache& read_cache, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const override { return false; }
  33      void ExpandPrivate(int pos, const SigningProvider& provider, FlatSigningProvider& out) const override {}
  34      std::optional<int64_t> ScriptSize() const override { return {}; }
  35      std::optional<int64_t> MaxSatisfactionWeight(bool) const override { return {}; }
  36      std::optional<int64_t> MaxSatisfactionElems() const override { return {}; }
  37      void GetPubKeys(std::set<CPubKey>& pubkeys, std::set<CExtPubKey>& ext_pubs) const override {}
  38  };
  39  
  40  BOOST_FIXTURE_TEST_CASE(wallet_load_descriptors, TestingSetup)
  41  {
  42      std::unique_ptr<WalletDatabase> database = CreateMockableWalletDatabase();
  43      {
  44          // Write unknown active descriptor
  45          WalletBatch batch(*database, false);
  46          std::string unknown_desc = "trx(tpubD6NzVbkrYhZ4Y4S7m6Y5s9GD8FqEMBy56AGphZXuagajudVZEnYyBahZMgHNCTJc2at82YX6s8JiL1Lohu5A3v1Ur76qguNH4QVQ7qYrBQx/86'/1'/0'/0/*)#8pn8tzdt";
  47          WalletDescriptor wallet_descriptor(std::make_shared<DummyDescriptor>(unknown_desc), 0, 0, 0, 0);
  48          BOOST_CHECK(batch.WriteDescriptor(uint256(), wallet_descriptor));
  49          BOOST_CHECK(batch.WriteActiveScriptPubKeyMan(static_cast<uint8_t>(OutputType::UNKNOWN), uint256(), false));
  50      }
  51  
  52      {
  53          // Now try to load the wallet and verify the error.
  54          const std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", std::move(database)));
  55          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::UNKNOWN_DESCRIPTOR);
  56      }
  57  
  58      // Test 2
  59      // Now write a valid descriptor with an invalid ID.
  60      // As the software produces another ID for the descriptor, the loading process must be aborted.
  61      database = CreateMockableWalletDatabase();
  62  
  63      // Verify the error
  64      bool found = false;
  65      DebugLogHelper logHelper("The descriptor ID calculated by the wallet differs from the one in DB", [&](const std::string* s) {
  66          found = true;
  67          return false;
  68      });
  69  
  70      {
  71          // Write valid descriptor with invalid ID
  72          WalletBatch batch(*database, false);
  73          std::string desc = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu";
  74          WalletDescriptor wallet_descriptor(std::make_shared<DummyDescriptor>(desc), 0, 0, 0, 0);
  75          BOOST_CHECK(batch.WriteDescriptor(uint256::ONE, wallet_descriptor));
  76      }
  77  
  78      {
  79          // Now try to load the wallet and verify the error.
  80          const std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", std::move(database)));
  81          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::CORRUPT);
  82          BOOST_CHECK(found); // The error must be logged
  83      }
  84  }
  85  
  86  bool HasAnyRecordOfType(WalletDatabase& db, const std::string& key)
  87  {
  88      std::unique_ptr<DatabaseBatch> batch = db.MakeBatch(false);
  89      BOOST_CHECK(batch);
  90      std::unique_ptr<DatabaseCursor> cursor = batch->GetNewCursor();
  91      BOOST_CHECK(cursor);
  92      while (true) {
  93          DataStream ssKey{};
  94          DataStream ssValue{};
  95          DatabaseCursor::Status status = cursor->Next(ssKey, ssValue);
  96          assert(status != DatabaseCursor::Status::FAIL);
  97          if (status == DatabaseCursor::Status::DONE) break;
  98          std::string type;
  99          ssKey >> type;
 100          if (type == key) return true;
 101      }
 102      return false;
 103  }
 104  
 105  template<typename... Args>
 106  SerializeData MakeSerializeData(const Args&... args)
 107  {
 108      DataStream s{};
 109      SerializeMany(s, args...);
 110      return {s.begin(), s.end()};
 111  }
 112  
 113  
 114  BOOST_FIXTURE_TEST_CASE(wallet_load_ckey, TestingSetup)
 115  {
 116      SerializeData ckey_record_key;
 117      SerializeData ckey_record_value;
 118      MockableData records;
 119  
 120      {
 121          // Context setup.
 122          // Create and encrypt legacy wallet
 123          std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", CreateMockableWalletDatabase()));
 124          LOCK(wallet->cs_wallet);
 125          auto legacy_spkm = wallet->GetOrCreateLegacyScriptPubKeyMan();
 126          BOOST_CHECK(legacy_spkm->SetupGeneration(true));
 127  
 128          // Retrieve a key
 129          CTxDestination dest = *Assert(legacy_spkm->GetNewDestination(OutputType::LEGACY));
 130          CKeyID key_id = GetKeyForDestination(*legacy_spkm, dest);
 131          CKey first_key;
 132          BOOST_CHECK(legacy_spkm->GetKey(key_id, first_key));
 133  
 134          // Encrypt the wallet
 135          BOOST_CHECK(wallet->EncryptWallet("encrypt"));
 136          wallet->Flush();
 137  
 138          // Store a copy of all the records
 139          records = GetMockableDatabase(*wallet).m_records;
 140  
 141          // Get the record for the retrieved key
 142          ckey_record_key = MakeSerializeData(DBKeys::CRYPTED_KEY, first_key.GetPubKey());
 143          ckey_record_value = records.at(ckey_record_key);
 144      }
 145  
 146      {
 147          // First test case:
 148          // Erase all the crypted keys from db and unlock the wallet.
 149          // The wallet will only re-write the crypted keys to db if any checksum is missing at load time.
 150          // So, if any 'ckey' record re-appears on db, then the checksums were not properly calculated, and we are re-writing
 151          // the records every time that 'CWallet::Unlock' gets called, which is not good.
 152  
 153          // Load the wallet and check that is encrypted
 154          std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", CreateMockableWalletDatabase(records)));
 155          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
 156          BOOST_CHECK(wallet->IsCrypted());
 157          BOOST_CHECK(HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 158  
 159          // Now delete all records and check that the 'Unlock' function doesn't re-write them
 160          BOOST_CHECK(wallet->GetLegacyScriptPubKeyMan()->DeleteRecords());
 161          BOOST_CHECK(!HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 162          BOOST_CHECK(wallet->Unlock("encrypt"));
 163          BOOST_CHECK(!HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 164      }
 165  
 166      {
 167          // Second test case:
 168          // Verify that loading up a 'ckey' with no checksum triggers a complete re-write of the crypted keys.
 169  
 170          // Cut off the 32 byte checksum from a ckey record
 171          records[ckey_record_key].resize(ckey_record_value.size() - 32);
 172  
 173          // Load the wallet and check that is encrypted
 174          std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", CreateMockableWalletDatabase(records)));
 175          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::LOAD_OK);
 176          BOOST_CHECK(wallet->IsCrypted());
 177          BOOST_CHECK(HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 178  
 179          // Now delete all ckey records and check that the 'Unlock' function re-writes them
 180          // (this is because the wallet, at load time, found a ckey record with no checksum)
 181          BOOST_CHECK(wallet->GetLegacyScriptPubKeyMan()->DeleteRecords());
 182          BOOST_CHECK(!HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 183          BOOST_CHECK(wallet->Unlock("encrypt"));
 184          BOOST_CHECK(HasAnyRecordOfType(wallet->GetDatabase(), DBKeys::CRYPTED_KEY));
 185      }
 186  
 187      {
 188          // Third test case:
 189          // Verify that loading up a 'ckey' with an invalid checksum throws an error.
 190  
 191          // Cut off the 32 byte checksum from a ckey record
 192          records[ckey_record_key].resize(ckey_record_value.size() - 32);
 193          // Fill in the checksum space with 0s
 194          records[ckey_record_key].resize(ckey_record_value.size());
 195  
 196          std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", CreateMockableWalletDatabase(records)));
 197          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::CORRUPT);
 198      }
 199  
 200      {
 201          // Fourth test case:
 202          // Verify that loading up a 'ckey' with an invalid pubkey throws an error
 203          CPubKey invalid_key;
 204          BOOST_CHECK(!invalid_key.IsValid());
 205          SerializeData key = MakeSerializeData(DBKeys::CRYPTED_KEY, invalid_key);
 206          records[key] = ckey_record_value;
 207  
 208          std::shared_ptr<CWallet> wallet(new CWallet(m_node.chain.get(), "", CreateMockableWalletDatabase(records)));
 209          BOOST_CHECK_EQUAL(wallet->LoadWallet(), DBErrors::CORRUPT);
 210      }
 211  }
 212  
 213  BOOST_AUTO_TEST_SUITE_END()
 214  } // namespace wallet
 215