From 63147da91154a7aaf9055617ccd89db42e2a30d0 Mon Sep 17 00:00:00 2001 From: David Burkett Date: Fri, 18 Mar 2022 06:56:27 -0400 Subject: [PATCH] Support partial rewind of outputs for locked wallets, and UpgradeCoins() function to finish rewinding once unlocked. --- .../include/mw/models/crypto/SecretKey.h | 2 + src/libmw/include/mw/models/wallet/Coin.h | 31 +++- src/libmw/include/mw/wallet/Keychain.h | 19 ++ src/libmw/src/wallet/Keychain.cpp | 32 +++- src/libmw/src/wallet/TxBuilder.cpp | 6 +- src/mweb/mweb_wallet.cpp | 81 +++++---- src/mweb/mweb_wallet.h | 13 +- src/script/descriptor.cpp | 4 +- src/test/fuzz/key.cpp | 2 +- src/wallet/rpcdump.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 33 ++-- src/wallet/scriptpubkeyman.h | 20 ++- src/wallet/wallet.cpp | 163 +++++++++++++----- src/wallet/wallet.h | 53 +++++- src/wallet/walletdb.cpp | 2 +- src/wallet/walletdb.h | 9 +- 16 files changed, 346 insertions(+), 126 deletions(-) diff --git a/src/libmw/include/mw/models/crypto/SecretKey.h b/src/libmw/include/mw/models/crypto/SecretKey.h index 479ba30d0..fb68deb2b 100644 --- a/src/libmw/include/mw/models/crypto/SecretKey.h +++ b/src/libmw/include/mw/models/crypto/SecretKey.h @@ -22,6 +22,8 @@ public: secret_key_t(std::array&& bytes) : m_value(BigInt(std::move(bytes))) { } secret_key_t(const uint8_t* bytes) : m_value(BigInt(bytes)) { } + static secret_key_t Null() { return secret_key_t(); } + static secret_key_t Random() { secret_key_t key; diff --git a/src/libmw/include/mw/models/wallet/Coin.h b/src/libmw/include/mw/models/wallet/Coin.h index b73c68f7f..7ad48bbb7 100644 --- a/src/libmw/include/mw/models/wallet/Coin.h +++ b/src/libmw/include/mw/models/wallet/Coin.h @@ -30,14 +30,14 @@ static constexpr uint32_t UNKNOWN_INDEX{std::numeric_limits::max()}; /// struct Coin : public Traits::ISerializable { // Version byte to more easily support adding new fields to the object. - uint8_t version{1}; + uint8_t version{2}; // Index of the subaddress this coin was received at. uint32_t address_index{UNKNOWN_INDEX}; // The private key needed in order to spend the coin. // May be empty for watch-only wallets. - boost::optional key; + boost::optional spend_key; // The blinding factor of the coin's output. // May be empty for watch-only wallets. @@ -58,16 +58,37 @@ struct Coin : public Traits::ISerializable { // This will only be populated when the coin has flag HAS_SENDER_INFO. boost::optional address; + // The shared secret used to generate the output key. + // By storing this, we are able to postpone calculation of the spend key. + // This allows us to scan for outputs while wallet is locked, and recalculate + // the output key once the wallet becomes unlocked. + boost::optional shared_secret; + bool IsChange() const noexcept { return address_index == CHANGE_INDEX; } bool IsPegIn() const noexcept { return address_index == PEGIN_INDEX; } bool IsMine() const noexcept { return address_index != UNKNOWN_INDEX; } bool HasAddress() const noexcept { return !!address; } + bool HasSpendKey() const noexcept { return !!spend_key; } + bool HasSharedSecret() const noexcept { return !!shared_secret; } + + void Reset() + { + version = 2; + address_index = UNKNOWN_INDEX; + spend_key = boost::none; + blind = boost::none; + amount = 0; + output_id = mw::Hash(); + sender_key = boost::none; + address = boost::none; + shared_secret = boost::none; + } IMPL_SERIALIZABLE(Coin, obj) { READWRITE(obj.version); READWRITE(VARINT(obj.address_index)); - READWRITE(obj.key); + READWRITE(obj.spend_key); READWRITE(obj.blind); READWRITE(VARINT_MODE(obj.amount, VarIntMode::NONNEGATIVE_SIGNED)); READWRITE(obj.output_id); @@ -76,6 +97,10 @@ struct Coin : public Traits::ISerializable { READWRITE(obj.sender_key); READWRITE(obj.address); } + + if (obj.version >= 2) { + READWRITE(obj.shared_secret); + } } }; diff --git a/src/libmw/include/mw/wallet/Keychain.h b/src/libmw/include/mw/wallet/Keychain.h index 24c0ee4c0..28e2b945b 100644 --- a/src/libmw/include/mw/wallet/Keychain.h +++ b/src/libmw/include/mw/wallet/Keychain.h @@ -21,13 +21,32 @@ public: m_scanSecret(std::move(scan_secret)), m_spendSecret(std::move(spend_secret)) { } + // If keychain is locked or watch-only (m_spendSecret is null), + // this will still identify outputs belonging to the wallet, but + // will not be able to calculate the coin's output key. + // It will still calculate the shared_secret though, which can be + // used to calculate the spend key when the wallet becomes unlocked. bool RewindOutput(const Output& output, mw::Coin& coin) const; + // Calculates the output spend key for the address index and shared secret. + // Requires that keychain be unlocked and not watch-only. + SecretKey CalculateOutputKey(const uint32_t index, const SecretKey& shared_secret) const; + + // Calculates the StealthAddress at the given index. + // Requires that keychain be unlocked and not watch-only. StealthAddress GetStealthAddress(const uint32_t index) const; + + // Requires that keychain be unlocked and not watch-only. SecretKey GetSpendKey(const uint32_t index) const; const SecretKey& GetScanSecret() const noexcept { return m_scanSecret; } const SecretKey& GetSpendSecret() const noexcept { return m_spendSecret; } + + // Clears the spend secret from memory, effectively making this a watch-only keychain. + void Lock() { m_spendSecret = SecretKey::Null(); } + + // Reassigns the spend secret. To be used when unlocking the wallet. + void Unlock(const SecretKey& spend_secret) { m_spendSecret = spend_secret; } private: const ScriptPubKeyMan& m_spk_man; diff --git a/src/libmw/src/wallet/Keychain.cpp b/src/libmw/src/wallet/Keychain.cpp index d8b2faf61..a959a7656 100644 --- a/src/libmw/src/wallet/Keychain.cpp +++ b/src/libmw/src/wallet/Keychain.cpp @@ -12,6 +12,7 @@ bool Keychain::RewindOutput(const Output& output, mw::Coin& coin) const return false; } + assert(!GetScanSecret().IsNull()); PublicKey shared_secret = output.Ke().Mul(GetScanSecret()); uint8_t view_tag = Hashed(EHashTag::TAG, shared_secret)[0]; if (view_tag != output.GetViewTag()) { @@ -40,33 +41,44 @@ bool Keychain::RewindOutput(const Output& output, mw::Coin& coin) const } // Calculate Carol's sending key 's' and check that s*B ?= Ke - StealthAddress wallet_addr = GetStealthAddress(index); SecretKey s = Hasher(EHashTag::SEND_KEY) - .Append(wallet_addr.A()) - .Append(wallet_addr.B()) + .Append(address.A()) + .Append(address.B()) .Append(value) .Append(n) .hash(); - if (output.Ke() != wallet_addr.B().Mul(s)) { + if (output.Ke() != address.B().Mul(s)) { return false; } - SecretKey private_key = SecretKeys::From(GetSpendKey(index)) - .Mul(Hashed(EHashTag::OUT_KEY, t)) - .Total(); + // Spend secret will be null for locked or watch-only wallets. + if (!GetSpendSecret().IsNull()) { + coin.spend_key = boost::make_optional(CalculateOutputKey(index, t)); + } coin.address_index = index; - coin.key = boost::make_optional(std::move(private_key)); coin.blind = boost::make_optional(mask.GetRawBlind()); coin.amount = value; coin.output_id = output.GetOutputID(); - coin.address = wallet_addr; + coin.address = address; + coin.shared_secret = boost::make_optional(std::move(t)); return true; } +SecretKey Keychain::CalculateOutputKey(const uint32_t index, const SecretKey& shared_secret) const +{ + assert(!m_spendSecret.IsNull()); + + return SecretKeys::From(GetSpendKey(index)) + .Mul(Hashed(EHashTag::OUT_KEY, shared_secret)) + .Total(); +} + StealthAddress Keychain::GetStealthAddress(const uint32_t index) const { + assert(!m_spendSecret.IsNull()); + PublicKey Bi = PublicKey::From(GetSpendKey(index)); PublicKey Ai = Bi.Mul(m_scanSecret); @@ -75,6 +87,8 @@ StealthAddress Keychain::GetStealthAddress(const uint32_t index) const SecretKey Keychain::GetSpendKey(const uint32_t index) const { + assert(!m_spendSecret.IsNull()); + SecretKey mi = Hasher(EHashTag::ADDRESS) .Append(index) .Append(m_scanSecret) diff --git a/src/libmw/src/wallet/TxBuilder.cpp b/src/libmw/src/wallet/TxBuilder.cpp index e759d7de9..dc64ff2d8 100644 --- a/src/libmw/src/wallet/TxBuilder.cpp +++ b/src/libmw/src/wallet/TxBuilder.cpp @@ -91,7 +91,7 @@ TxBuilder::Inputs TxBuilder::CreateInputs(const std::vector& input_coi input_coins.cbegin(), input_coins.cend(), std::back_inserter(inputs), [&blinds, &keys](const mw::Coin& input_coin) { assert(!!input_coin.blind); - assert(!!input_coin.key); + assert(!!input_coin.spend_key); BlindingFactor blind = Pedersen::BlindSwitch(input_coin.blind.value(), input_coin.amount); SecretKey ephemeral_key = SecretKey::Random(); @@ -99,12 +99,12 @@ TxBuilder::Inputs TxBuilder::CreateInputs(const std::vector& input_coi input_coin.output_id, Commitment::Blinded(blind, input_coin.amount), ephemeral_key, - input_coin.key.value() + input_coin.spend_key.value() ); blinds.Add(blind); keys.Add(ephemeral_key); - keys.Sub(input_coin.key.value()); + keys.Sub(input_coin.spend_key.value()); return input; } ); diff --git a/src/mweb/mweb_wallet.cpp b/src/mweb/mweb_wallet.cpp index f324b8d22..5978689b0 100644 --- a/src/mweb/mweb_wallet.cpp +++ b/src/mweb/mweb_wallet.cpp @@ -5,13 +5,43 @@ using namespace MWEB; +bool Wallet::UpgradeCoins() +{ + mw::Keychain::Ptr keychain = GetKeychain(); + if (!keychain || keychain->GetSpendSecret().IsNull()) { + return false; + } + + // Loop through transactions and try upgrading output coins + for (auto& entry : m_pWallet->mapWallet) { + CWalletTx* wtx = &entry.second; + RewindOutputs(*wtx->tx); + + if (wtx->mweb_wtx_info && wtx->mweb_wtx_info->received_coin) { + mw::Coin& coin = *wtx->mweb_wtx_info->received_coin; + if (!coin.HasSpendKey() && coin.HasSharedSecret()) { + coin.spend_key = keychain->CalculateOutputKey(coin.address_index, *coin.shared_secret); + + m_coins[coin.output_id] = coin; + + WalletBatch batch(m_pWallet->GetDatabase()); + batch.WriteMWEBCoin(coin); + batch.WriteTx(*wtx); + } + } + } + + return true; +} + std::vector Wallet::RewindOutputs(const CTransaction& tx) { std::vector coins; - for (const CTxOutput& txout : tx.GetOutputs()) { - if (txout.IsMWEB()) { + + if (tx.HasMWEBTx()) { + for (const Output& output : tx.mweb_tx.m_transaction->GetOutputs()) { mw::Coin mweb_coin; - if (RewindOutput(tx.mweb_tx.m_transaction, txout.ToMWEB(), mweb_coin)) { + if (RewindOutput(output, mweb_coin)) { coins.push_back(mweb_coin); } } @@ -20,43 +50,25 @@ std::vector Wallet::RewindOutputs(const CTransaction& tx) return coins; } -bool Wallet::RewindOutput(const boost::variant& parent, - const mw::Hash& output_id, mw::Coin& coin) +bool Wallet::RewindOutput(const Output& output, mw::Coin& coin) { - if (GetCoin(output_id, coin) && coin.IsMine()) { - return true; + mw::Keychain::Ptr keychain = GetKeychain(); + + if (GetCoin(output.GetOutputID(), coin) && coin.IsMine()) { + // If the coin has the spend key, it's fully rewound. + // If not, try rewinding further if we have the master spend key (i.e. wallet is unlocked). + if (coin.HasSpendKey() || !keychain || keychain->GetSpendSecret().IsNull()) { + return true; + } } - mw::Keychain::Ptr keychain = GetKeychain(); - if (!keychain) { + if (!keychain || !keychain->RewindOutput(output, coin)) { return false; } - bool rewound = false; - if (parent.type() == typeid(mw::Block::CPtr)) { - const mw::Block::CPtr& block = boost::get(parent); - for (const Output& output : block->GetOutputs()) { - if (output.GetOutputID() == output_id) { - rewound = keychain->RewindOutput(output, coin); - break; - } - } - } else { - const mw::Transaction::CPtr& tx = boost::get(parent); - for (const Output& output : tx->GetOutputs()) { - if (output.GetOutputID() == output_id) { - rewound = keychain->RewindOutput(output, coin); - break; - } - } - } - - if (rewound) { - m_coins[coin.output_id] = coin; - WalletBatch(m_pWallet->GetDatabase()).WriteMWEBCoin(coin); - } - - return rewound; + m_coins[coin.output_id] = coin; + WalletBatch(m_pWallet->GetDatabase()).WriteMWEBCoin(coin); + return true; } bool Wallet::IsChange(const StealthAddress& address) const @@ -108,6 +120,7 @@ bool Wallet::GetCoin(const mw::Hash& output_id, mw::Coin& coin) const return true; } + coin.Reset(); return false; } diff --git a/src/mweb/mweb_wallet.h b/src/mweb/mweb_wallet.h index 947ce6bc1..89eab7cd3 100644 --- a/src/mweb/mweb_wallet.h +++ b/src/mweb/mweb_wallet.h @@ -27,16 +27,17 @@ public: Wallet(CWallet* pWallet) : m_pWallet(pWallet) {} - bool IsSupported() const { return GetKeychain() != nullptr; } bool IsChange(const StealthAddress& address) const; bool GetCoin(const mw::Hash& output_id, mw::Coin& coin) const; + // Loops through the transactions in the wallet and attempts to fill + // in missing information for mw::Coin's, in particular the spend key. + // Intended to be called after unlocking an encrypted wallet. + bool UpgradeCoins(); + std::vector RewindOutputs(const CTransaction& tx); - bool RewindOutput( - const boost::variant& parent, - const mw::Hash& output_id, - mw::Coin& coin - ); + bool RewindOutput(const Output& output, mw::Coin& coin); + bool GetStealthAddress(const mw::Coin& coin, StealthAddress& address) const; bool GetStealthAddress(const uint32_t index, StealthAddress& address) const; diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index bb618551c..b032fb228 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -894,7 +894,7 @@ std::unique_ptr ParseScript(uint32_t key_exp_index, Span(std::move(pubkey), SecretKey()); // MW: TODO - Lookup scan_secret + return MakeUnique(std::move(pubkey), SecretKey::Null()); // MW: TODO - Lookup scan_secret } if (ctx == ParseScriptContext::TOP && Func("combo", expr)) { auto pubkey = ParsePubkey(key_exp_index, expr, true, out, error); @@ -1137,7 +1137,7 @@ std::unique_ptr InferDescriptor(const DestinationAddr& script, const if (script.IsMWEB()) { // MW: TODO - Lookup scan_secret CPubKey pubkey(script.GetMWEBAddress().GetSpendPubKey().vec()); - return MakeUnique(InferPubkey(pubkey, ParseScriptContext::TOP, provider), SecretKey()); + return MakeUnique(InferPubkey(pubkey, ParseScriptContext::TOP, provider), SecretKey::Null()); } return InferScript(script.GetScript(), ParseScriptContext::TOP, provider); diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp index 7a3212d9e..d84122608 100644 --- a/src/test/fuzz/key.cpp +++ b/src/test/fuzz/key.cpp @@ -182,7 +182,7 @@ void test_one_input(const std::vector& buffer) assert(v_solutions_ret_tx_multisig[2].size() == 1); OutputType output_type{}; - const CTxDestination tx_destination = GetDestinationForKey(pubkey, output_type, SecretKey()); + const CTxDestination tx_destination = GetDestinationForKey(pubkey, output_type, SecretKey::Null()); assert(output_type == OutputType::LEGACY); assert(IsValidDestination(tx_destination)); assert(CTxDestination{PKHash{pubkey}} == tx_destination); diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index 886d431fa..38cc64f3f 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -376,7 +376,7 @@ RPCHelpMan importprunedfunds() CWalletTx::Confirmation confirm(CWalletTx::Status::CONFIRMED, height, merkleBlock.header.GetHash(), txnIndex); CTransactionRef tx_ref = MakeTransactionRef(tx); - if (pwallet->IsMine(*tx_ref)) { + if (pwallet->IsMine(*tx_ref, boost::none)) { pwallet->AddToWallet(std::move(tx_ref), boost::none, confirm); return NullUniValue; } diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 474cd4cc4..4fc47a383 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -1718,20 +1718,28 @@ std::set LegacyScriptPubKeyMan::GetKeys() const void LegacyScriptPubKeyMan::SetInternal(bool internal) {} -bool LegacyScriptPubKeyMan::LoadMWEBKeychain() +void LegacyScriptPubKeyMan::LoadMWEBKeychain() { if (!m_storage.CanSupportFeature(FEATURE_HD_SPLIT)) { - return false; + return; } if (m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) || m_storage.IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)) { - return false; + return; } // try to get the seed CKey seed; if (!GetKey(m_hd_chain.seed_id, seed)) { - return false; + if (m_hd_chain.mweb_scan_key) { + m_mwebKeychain = std::make_shared( + *this, + *m_hd_chain.mweb_scan_key, + SecretKey::Null() + ); + } + + return; } CExtKey masterKey; @@ -1751,12 +1759,22 @@ bool LegacyScriptPubKeyMan::LoadMWEBKeychain() CExtKey spendKey; chainChildKey.Derive(spendKey, BIP32_HARDENED_KEY_LIMIT + 1); + m_hd_chain.nVersion = std::max(m_hd_chain.nVersion, CHDChain::VERSION_HD_MWEB_WATCH); m_mwebKeychain = std::make_shared( *this, SecretKey(scanKey.key.begin()), SecretKey(spendKey.key.begin()) ); + // Add the MWEB scan key to the CHDChain + if (!m_hd_chain.mweb_scan_key) { + m_hd_chain.mweb_scan_key = SecretKey(scanKey.key.begin()); + + if (!WalletBatch(m_storage.GetDatabase()).WriteHDChain(m_hd_chain)) { + throw std::runtime_error(std::string(__func__) + ": writing chain failed"); + } + } + // Mark change and peg-in addresses as used if (m_hd_chain.nMWEBIndexCounter == 0) { WalletBatch batch(m_storage.GetDatabase()); @@ -1767,8 +1785,6 @@ bool LegacyScriptPubKeyMan::LoadMWEBKeychain() // Generate PEGIN pubkey GenerateNewKey(batch, m_hd_chain, KeyPurpose::MWEB); } - - return true; } bool DescriptorScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error) @@ -2450,9 +2466,4 @@ const std::vector DescriptorScriptPubKeyMan::GetScriptPubKeys() script_pub_keys.push_back(script_pub_key.first); } return script_pub_keys; -} - -bool DescriptorScriptPubKeyMan::LoadMWEBKeychain() -{ - return false; } \ No newline at end of file diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 82d4cbcb6..74b06291d 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -259,8 +259,7 @@ public: virtual void SetInternal(bool internal) {} // Creates an MWEB KeyChain from the appropriate keychain paths. - virtual bool LoadMWEBKeychain() { return false; } - void UnloadMWEBKeychain() { m_mwebKeychain.reset(); } + virtual void LoadMWEBKeychain() { } const mw::Keychain::Ptr& GetMWEBKeychain() const noexcept { return m_mwebKeychain; } /** Prepends the wallet name in logging output to ease debugging in multi-wallet use cases */ @@ -520,8 +519,19 @@ public: std::set GetKeys() const override; - bool LoadMWEBKeychain() override; - SecretKey GetScanSecret() const noexcept { return m_mwebKeychain ? m_mwebKeychain->GetScanSecret() : SecretKey(); } + void LoadMWEBKeychain() override; + SecretKey GetScanSecret() const noexcept + { + if (m_mwebKeychain) { + return m_mwebKeychain->GetScanSecret(); + } + + if (IsHDEnabled() && m_hd_chain.mweb_scan_key) { + return *m_hd_chain.mweb_scan_key; + } + + return SecretKey::Null(); + }; }; /** Wraps a LegacyScriptPubKeyMan so that it can be returned in a new unique_ptr. Does not provide privkeys */ @@ -642,8 +652,6 @@ public: const WalletDescriptor GetWalletDescriptor() const EXCLUSIVE_LOCKS_REQUIRED(cs_desc_man); const std::vector GetScriptPubKeys() const; - - bool LoadMWEBKeychain() override; }; #endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e136f7658..251592918 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -343,6 +343,15 @@ std::string COutput::ToString() const return strprintf("COutput(%s, %d, %d) [%s]", tx->GetHash().ToString(), i, nDepth, FormatMoney(tx->tx->vout[i].nValue)); } +CWalletTx* CWallet::GetWalletTx(const uint256& hash) +{ + AssertLockHeld(cs_wallet); + std::map::iterator it = mapWallet.find(hash); + if (it == mapWallet.end()) + return nullptr; + return &(it->second); +} + const CWalletTx* CWallet::GetWalletTx(const uint256& hash) const { AssertLockHeld(cs_wallet); @@ -388,6 +397,7 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase, bool accept_no_key auto mweb_spk_man = GetScriptPubKeyMan(OutputType::MWEB, false); if (mweb_spk_man) { mweb_spk_man->LoadMWEBKeychain(); + mweb_wallet->UpgradeCoins(); } return true; @@ -605,6 +615,10 @@ void CWallet::AddMWEBOrigins(const CWalletTx& wtx) const mw::Hash& output_id = wtx.mweb_wtx_info->received_coin->output_id; mapOutputsMWEB.insert(std::make_pair(output_id, wtx.GetHash())); } + + for (const mw::Hash& kernel_id : wtx.tx->mweb_tx.GetKernelIDs()) { + mapKernelsMWEB.insert(std::make_pair(kernel_id, wtx.GetHash())); + } } bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase) @@ -1015,18 +1029,21 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx return true; } -bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool fUpdate) +bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const boost::optional& mweb_wtx_info, CWalletTx::Confirmation confirm, bool fUpdate) { + CWalletTx wtx(this, ptx, mweb_wtx_info); + uint256 hash = wtx.GetHash(); + const CTransaction& tx = *ptx; { AssertLockHeld(cs_wallet); if (!confirm.hashBlock.IsNull()) { - for (const CTxInput& txin : tx.GetInputs()) { + for (const CTxInput& txin : wtx.GetInputs()) { std::pair range = mapTxSpends.equal_range(txin.GetIndex()); while (range.first != range.second) { - if (range.first->second != tx.GetHash()) { - WalletLogPrintf("Transaction %s (in block %s) conflicts with wallet transaction %s\n", tx.GetHash().ToString(), confirm.hashBlock.ToString(), range.first->second.ToString()); + if (range.first->second != hash) { + WalletLogPrintf("Transaction %s (in block %s) conflicts with wallet transaction %s\n", hash.ToString(), confirm.hashBlock.ToString(), range.first->second.ToString()); MarkConflicted(confirm.hashBlock, confirm.block_height, range.first->second); } range.first++; @@ -1036,9 +1053,9 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Co mweb_wallet->RewindOutputs(tx); - bool fExisted = mapWallet.count(tx.GetHash()) != 0; + bool fExisted = mapWallet.count(hash) != 0; if (fExisted && !fUpdate) return false; - if (fExisted || IsMine(tx) || IsFromMe(tx, boost::none)) + if (fExisted || IsMine(tx, mweb_wtx_info) || IsFromMe(tx, mweb_wtx_info)) { /* Check if any keys in the wallet keypool that were supposed to be unused * have appeared in a new transaction. If so, remove those keys from the keypool. @@ -1047,7 +1064,7 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Co */ // loop though all outputs - for (const CTxOutput& txout : tx.GetOutputs()) { + for (const CTxOutput& txout : wtx.GetOutputs()) { DestinationAddr dest; if (ExtractDestinationScript(txout, dest)) { for (const auto& spk_man_pair : m_spk_managers) { @@ -1065,7 +1082,7 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Co // Block disconnection override an abandoned tx as unconfirmed // which means user may have to call abandontransaction again - return AddToWallet(MakeTransactionRef(tx), /* mweb_wtx_info */ boost::none, confirm, /* update_wtx= */ nullptr, /* fFlushOnClose= */ false); + return AddToWallet(MakeTransactionRef(tx), mweb_wtx_info, confirm, /* update_wtx= */ nullptr, /* fFlushOnClose= */ false); } } return false; @@ -1078,9 +1095,9 @@ bool CWallet::TransactionCanBeAbandoned(const uint256& hashTx) const return wtx && !wtx->isAbandoned() && wtx->GetDepthInMainChain() == 0 && !wtx->InMempool(); } -void CWallet::MarkInputsDirty(const CTransactionRef& tx) +void CWallet::MarkInputsDirty(const CWalletTx& wtx) { - for (const CTxInput& txin : tx->GetInputs()) { + for (const CTxInput& txin : wtx.GetInputs()) { CWalletTx* prev = FindPrevTx(txin); if (prev != nullptr) { prev->MarkDirty(); @@ -1136,7 +1153,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx) } // If a transaction changes 'conflicted' state, that changes the balance // available of the outputs it spends. So force those to be recomputed - MarkInputsDirty(wtx.tx); + MarkInputsDirty(wtx); } } @@ -1191,25 +1208,25 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c } // If a transaction changes 'conflicted' state, that changes the balance // available of the outputs it spends. So force those to be recomputed - MarkInputsDirty(wtx.tx); + MarkInputsDirty(wtx); } } } -void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool update_tx) +void CWallet::SyncTransaction(const CTransactionRef& ptx, const boost::optional& mweb_wtx_info, CWalletTx::Confirmation confirm, bool update_tx) { - if (!AddToWalletIfInvolvingMe(ptx, confirm, update_tx)) + if (!AddToWalletIfInvolvingMe(ptx, mweb_wtx_info, confirm, update_tx)) return; // Not one of ours // If a transaction changes 'conflicted' state, that changes the balance // available of the outputs it spends. So force those to be // recomputed, also: - MarkInputsDirty(ptx); + MarkInputsDirty(CWalletTx(this, ptx, mweb_wtx_info)); } void CWallet::transactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) { LOCK(cs_wallet); - SyncTransaction(tx, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); + SyncTransaction(tx, boost::none, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); auto it = mapWallet.find(tx->GetHash()); if (it != mapWallet.end()) { @@ -1223,6 +1240,17 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe if (it != mapWallet.end()) { it->second.fInMempool = false; } + + for (const mw::Hash& output_id : tx->mweb_tx.GetOutputIDs()) { + auto out_iter = mapOutputsMWEB.find(output_id); + if (out_iter != mapOutputsMWEB.end()) { + auto tx_iter = mapWallet.find(out_iter->second); + if (tx_iter != mapWallet.end()) { + tx_iter->second.fInMempool = false; + } + } + } + // Handle transactions that were removed from the mempool because they // conflict with transactions in a newly connected block. if (reason == MemPoolRemovalReason::CONFLICT) { @@ -1251,7 +1279,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe // distinguishing between conflicted and unconfirmed transactions are // imperfect, and could be improved in general, see // https://github.com/bitcoin-core/bitcoin-devwiki/wiki/Wallet-Transaction-Conflict-Tracking - SyncTransaction(tx, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); + SyncTransaction(tx, boost::none, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); } } @@ -1263,7 +1291,7 @@ void CWallet::blockConnected(const CBlock& block, int height) m_last_block_processed_height = height; m_last_block_processed = block_hash; for (size_t index = 0; index < block.vtx.size(); index++) { - SyncTransaction(block.vtx[index], {CWalletTx::Status::CONFIRMED, height, block_hash, (int)index}); + SyncTransaction(block.vtx[index], boost::none, {CWalletTx::Status::CONFIRMED, height, block_hash, (int)index}); transactionRemovedFromMempool(block.vtx[index], MemPoolRemovalReason::BLOCK, 0 /* mempool_sequence */); } @@ -1279,12 +1307,28 @@ void CWallet::blockConnected(const CBlock& block, int height) } } + CWalletTx* hogex_wtx = GetWalletTx(block.vtx.back()->GetHash()); + if (hogex_wtx != nullptr) { + hogex_wtx->pegout_indices.clear(); + hogex_wtx->pegout_indices.push_back({mw::Hash(), 0}); // HogAddr doesn't have a corresponding kernel + + for (const Kernel& kernel : block.mweb_block.m_block->GetKernels()) { + const auto& kernel_pegouts = kernel.GetPegOuts(); + for (size_t pegout_idx = 0; pegout_idx < kernel_pegouts.size(); pegout_idx++) { + hogex_wtx->pegout_indices.push_back({kernel.GetKernelID(), pegout_idx}); + } + } + + assert(hogex_wtx->tx->vout.size() == hogex_wtx->pegout_indices.size()); + WalletBatch(*database).WriteTx(*hogex_wtx); + } + mw::Coin mweb_coin; - for (const mw::Hash& output_id : block.mweb_block.GetOutputIDs()) { - if (mweb_wallet->RewindOutput(block.mweb_block.m_block, output_id, mweb_coin)) { - auto wtx = FindWalletTx(output_id); + for (const Output& output : block.mweb_block.m_block->GetOutputs()) { + if (mweb_wallet->RewindOutput(output, mweb_coin)) { + auto wtx = FindWalletTx(output.GetOutputID()); if (wtx != nullptr) { - SyncTransaction(wtx->tx, {CWalletTx::Status::CONFIRMED, height, block_hash, wtx->m_confirm.nIndex}); + SyncTransaction(wtx->tx, wtx->mweb_wtx_info, {CWalletTx::Status::CONFIRMED, height, block_hash, wtx->m_confirm.nIndex}); transactionRemovedFromMempool(wtx->tx, MemPoolRemovalReason::BLOCK, 0 /* mempool_sequence */); } else { AddToWallet( @@ -1309,7 +1353,7 @@ void CWallet::blockDisconnected(const CBlock& block, int height) m_last_block_processed_height = height - 1; m_last_block_processed = block.hashPrevBlock; for (const CTransactionRef& ptx : block.vtx) { - SyncTransaction(ptx, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); + SyncTransaction(ptx, boost::none, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); } if (!block.mweb_block.IsNull()) { @@ -1319,17 +1363,27 @@ void CWallet::blockDisconnected(const CBlock& block, int height) std::pair range = mapTxSpends.equal_range(spent_id); // MWEB: We just choose the first spend. In the future, we may need a better approach for handling conflicted txs if (range.first != range.second) { - auto ptx = mapWallet.find(range.first->second)->second.tx; - SyncTransaction(ptx, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); + auto tx_iter = mapWallet.find(range.first->second); + SyncTransaction( + tx_iter->second.tx, + tx_iter->second.mweb_wtx_info, + {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0} + ); } } } - for (const mw::Hash& output_id : block.mweb_block.GetOutputIDs()) { - if (mweb_wallet->RewindOutput(block.mweb_block.m_block, output_id, coin)) { - auto wtx = FindWalletTx(output_id); + // MW: TODO - Pegout kernels? + + for (const Output& output : block.mweb_block.m_block->GetOutputs()) { + if (mweb_wallet->RewindOutput(output, coin)) { + auto wtx = FindWalletTx(output.GetOutputID()); if (wtx != nullptr) { - SyncTransaction(wtx->tx, {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0}); + SyncTransaction( + wtx->tx, + wtx->mweb_wtx_info, + {CWalletTx::Status::UNCONFIRMED, /* block height */ 0, /* block hash */ {}, /* index */ 0} + ); } } } @@ -1484,7 +1538,7 @@ CAmount CWallet::GetChange(const CTxOutput& output) const return (IsChange(output) ? amount : 0); } -bool CWallet::IsMine(const CTransaction& tx) const +bool CWallet::IsMine(const CTransaction& tx, const boost::optional& mweb_wtx_info) const { AssertLockHeld(cs_wallet); for (const CTxOutput& txout : tx.GetOutputs()) @@ -1497,6 +1551,12 @@ bool CWallet::IsMine(const CTransaction& tx) const } } + if (mweb_wtx_info && mweb_wtx_info->received_coin) { + if (mweb_wtx_info->received_coin->IsMine()) { + return true; + } + } + return false; } @@ -1971,17 +2031,17 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc break; } for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { - SyncTransaction(block.vtx[posInBlock], {CWalletTx::Status::CONFIRMED, block_height, block_hash, (int)posInBlock}, fUpdate); + SyncTransaction(block.vtx[posInBlock], boost::none, {CWalletTx::Status::CONFIRMED, block_height, block_hash, (int)posInBlock}, fUpdate); } if (!block.mweb_block.IsNull()) { // MW: TODO - Pegouts? mw::Coin mweb_coin; - for (const mw::Hash& output_id : block.mweb_block.GetOutputIDs()) { - if (mweb_wallet->RewindOutput(block.mweb_block.m_block, output_id, mweb_coin)) { + for (const Output& output : block.mweb_block.m_block->GetOutputs()) { + if (mweb_wallet->RewindOutput(output, mweb_coin)) { const CWalletTx* wtx = FindWalletTx(mweb_coin.output_id); if (wtx) { - // MW: TODO - Update height + SyncTransaction(wtx->tx, wtx->mweb_wtx_info, {CWalletTx::Status::CONFIRMED, block_height, block_hash, wtx->m_confirm.nIndex}, fUpdate); } else { AddToWallet( MakeTransactionRef(), @@ -1996,14 +2056,26 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc for (const mw::Hash& spent_id : block.mweb_block.GetSpentIDs()) { if (IsMine(CTxInput(spent_id))) { - // MW: TODO - Check for zapped transactions with matching spent IDs - AddToWallet( - MakeTransactionRef(), - boost::make_optional(spent_id), - {CWalletTx::Status::CONFIRMED, block_height, block_hash, 0}, - nullptr, - false - ); + auto spend_iter = mapTxSpends.find(spent_id); + if (spend_iter != mapTxSpends.end()) { + auto tx_iter = mapWallet.find(spend_iter->second); + if (tx_iter != mapWallet.end()) { + SyncTransaction( + tx_iter->second.tx, + tx_iter->second.mweb_wtx_info, + {CWalletTx::Status::CONFIRMED, block_height, block_hash, 0}, + fUpdate + ); + } + } else { + AddToWallet( + MakeTransactionRef(), + boost::make_optional(spent_id), + {CWalletTx::Status::CONFIRMED, block_height, block_hash, 0}, + nullptr, + false + ); + } CWalletTx* prev = FindPrevTx(spent_id); if (prev != nullptr) { @@ -2589,7 +2661,7 @@ std::map> CWallet::ListCoins() const if (ExtractOutputDestination(FindNonChangeParentOutput(*wtx->tx, output_idx), address)) { if (output_idx.type() == typeid(mw::Hash)) { mw::Coin coin; - if (GetCoin(boost::get(output_idx), coin) && coin.IsMine()) { + if (GetCoin(boost::get(output_idx), coin) && coin.IsMine() && coin.HasSpendKey()) { result[address].emplace_back(MWOutput{coin, depth, boost::get(address), wtx}); } } else { @@ -4073,7 +4145,10 @@ bool CWallet::Lock() // MWEB: Unload MWEB keychain auto mweb_spk_man = GetScriptPubKeyMan(OutputType::MWEB, false); if (mweb_spk_man) { - mweb_spk_man->UnloadMWEBKeychain(); + const mw::Keychain::Ptr& keychain = mweb_spk_man->GetMWEBKeychain(); + if (keychain) { + keychain->Lock(); + } } NotifyStatusChanged(this); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index afdfc5835..bd49efb1a 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -195,6 +195,41 @@ static inline void WriteOrderPos(const int64_t& nOrderPos, mapValue_t& mapValue) mapValue["n"] = ToString(nOrderPos); } +static inline void ReadPegoutIndices(std::vector>& pegout_indices, const mapValue_t& mapValue) +{ + if (!mapValue.count("pegout_indices")) { + return; + } + + std::vector bytes = ParseHex(mapValue.at("pegout_indices")); + CDataStream s((const char*)bytes.data(), (const char*)bytes.data() + bytes.size(), SER_DISK, PROTOCOL_VERSION); + + size_t num_indices = ReadVarInt(s); + for (size_t i = 0; i < num_indices; i++) { + mw::Hash kernel_id; + s >> kernel_id; + size_t sub_idx = ReadVarInt(s); + + pegout_indices.push_back({std::move(kernel_id), sub_idx}); + } +} + +static inline void WritePegoutIndices(const std::vector>& pegout_indices, mapValue_t& mapValue) +{ + if (pegout_indices.empty()) { + return; + } + + CDataStream s(SER_DISK, PROTOCOL_VERSION); + WriteVarInt(s, pegout_indices.size()); + for (const auto& pegout_idx : pegout_indices) { + pegout_idx.first.Serialize(s); + WriteVarInt(s, pegout_idx.second); + } + + mapValue["pegout_indices"] = HexStr(std::vector{s.begin(), s.end()}); +} + struct COutputEntry { CTxDestination destination; @@ -289,6 +324,7 @@ public: std::multimap::const_iterator m_it_wtxOrdered; boost::optional mweb_wtx_info; + std::vector> pegout_indices; // memory only enum AmountType { DEBIT, CREDIT, IMMATURE_CREDIT, AVAILABLE_CREDIT, AMOUNTTYPE_ENUM_ELEMENTS }; @@ -371,6 +407,7 @@ public: mapValueCopy["timesmart"] = strprintf("%u", nTimeSmart); } + WritePegoutIndices(pegout_indices, mapValueCopy); if (mweb_wtx_info) { mapValueCopy["mweb_info"] = mweb_wtx_info->ToHex(); } @@ -412,6 +449,8 @@ public: ReadOrderPos(nOrderPos, mapValue); nTimeSmart = mapValue.count("timesmart") ? (unsigned int)atoi64(mapValue["timesmart"]) : 0; + + ReadPegoutIndices(pegout_indices, mapValue); mweb_wtx_info = mapValue.count("mweb_info") ? boost::make_optional(MWEB::WalletTxInfo::FromHex(mapValue["mweb_info"])) : boost::none; mapValue.erase("fromaccount"); @@ -419,6 +458,7 @@ public: mapValue.erase("n"); mapValue.erase("timesmart"); mapValue.erase("mweb_info"); + mapValue.erase("pegout_indices"); } void SetTx(CTransactionRef arg) @@ -758,6 +798,10 @@ private: * Used to keep track of which CWalletTx an MWEB output came from. */ std::map mapOutputsMWEB GUARDED_BY(cs_wallet); + /** + * Used to keep track of which CWalletTx an MWEB kernel is in. + */ + std::map mapKernelsMWEB GUARDED_BY(cs_wallet); // MW: TODO - Could be multiple transactions. Need to handle conflicts? void AddMWEBOrigins(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** @@ -773,19 +817,19 @@ private: * Abandoned state should probably be more carefully tracked via different * posInBlock signals or by checking mempool presence when necessary. */ - bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, const boost::optional& mweb_wtx_info, CWalletTx::Confirmation confirm, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /* Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */ void MarkConflicted(const uint256& hashBlock, int conflicting_height, const uint256& hashTx); /* Mark a transaction's inputs dirty, thus forcing the outputs to be recomputed */ - void MarkInputsDirty(const CTransactionRef& tx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void MarkInputsDirty(const CWalletTx& wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void SyncMetaData(std::pair) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /* Used by TransactionAddedToMemorypool/BlockConnected/Disconnected/ScanForWalletTransactions. * Should be called with non-zero block_hash and posInBlock if this is for a transaction that is included in a block. */ - void SyncTransaction(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + void SyncTransaction(const CTransactionRef& tx, const boost::optional& mweb_wtx_info, CWalletTx::Confirmation confirm, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); std::atomic m_wallet_flags{0}; @@ -904,6 +948,7 @@ public: /** Interface for accessing chain state. */ interfaces::Chain& chain() const { assert(m_chain); return *m_chain; } + CWalletTx* GetWalletTx(const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); const CWalletTx* GetWalletTx(const uint256& hash) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsTrusted(const CWalletTx& wtx, std::set& trusted_parents) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); @@ -1162,7 +1207,7 @@ public: bool IsChange(const CTxOutput& output) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool IsChange(const DestinationAddr& script) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); CAmount GetChange(const CTxOutput& output) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool IsMine(const CTransaction& tx) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool IsMine(const CTransaction& tx, const boost::optional& mweb_wtx_info) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** should probably be renamed to IsRelevantToMe */ bool IsFromMe(const CTransaction& tx, const boost::optional& mweb_wtx_info) const; CAmount GetDebit(const CTransaction& tx, const boost::optional& mweb_wtx_info, const isminefilter& filter) const; diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 76ab77191..49f6b9b7c 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -511,7 +511,7 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, } if (!!keyMeta.mweb_index) { - chain.nVersion = CHDChain::VERSION_HD_MWEB; + chain.nVersion = std::max(chain.nVersion, CHDChain::VERSION_HD_MWEB_WATCH); chain.nMWEBIndexCounter = std::max(chain.nMWEBIndexCounter, *keyMeta.mweb_index); } else if (internal) { chain.nVersion = std::max(chain.nVersion, CHDChain::VERSION_HD_CHAIN_SPLIT); diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 815393f2d..3cc2a9525 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -93,11 +93,13 @@ public: uint32_t nInternalChainCounter; uint32_t nMWEBIndexCounter; CKeyID seed_id; //!< seed hash160 + boost::optional mweb_scan_key; static const int VERSION_HD_BASE = 1; static const int VERSION_HD_CHAIN_SPLIT = 2; static const int VERSION_HD_MWEB = 3; - static const int CURRENT_VERSION = VERSION_HD_MWEB; + static const int VERSION_HD_MWEB_WATCH = 4; + static const int CURRENT_VERSION = VERSION_HD_MWEB_WATCH; int nVersion; CHDChain() { SetNull(); } @@ -112,6 +114,10 @@ public: if (obj.nVersion >= VERSION_HD_MWEB) { READWRITE(obj.nMWEBIndexCounter); } + + if (obj.nVersion >= VERSION_HD_MWEB_WATCH) { + READWRITE(obj.mweb_scan_key); + } } void SetNull() @@ -121,6 +127,7 @@ public: nInternalChainCounter = 0; nMWEBIndexCounter = 0; seed_id.SetNull(); + mweb_scan_key = boost::none; } bool operator==(const CHDChain& chain) const