Support partial rewind of outputs for locked wallets, and UpgradeCoins() function to finish rewinding once unlocked.

This commit is contained in:
David Burkett 2022-03-18 06:56:27 -04:00 committed by Loshan T
parent c59ac6d8a5
commit 63147da911
16 changed files with 346 additions and 126 deletions

View File

@ -22,6 +22,8 @@ public:
secret_key_t(std::array<uint8_t, NUM_BYTES>&& bytes) : m_value(BigInt<NUM_BYTES>(std::move(bytes))) { }
secret_key_t(const uint8_t* bytes) : m_value(BigInt<NUM_BYTES>(bytes)) { }
static secret_key_t Null() { return secret_key_t<NUM_BYTES>(); }
static secret_key_t Random()
{
secret_key_t<NUM_BYTES> key;

View File

@ -30,14 +30,14 @@ static constexpr uint32_t UNKNOWN_INDEX{std::numeric_limits<uint32_t>::max()};
/// </summary>
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<SecretKey> key;
boost::optional<SecretKey> 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<StealthAddress> 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<SecretKey> 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);
}
}
};

View File

@ -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;

View File

@ -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<uint32_t>(index)
.Append(m_scanSecret)

View File

@ -91,7 +91,7 @@ TxBuilder::Inputs TxBuilder::CreateInputs(const std::vector<mw::Coin>& 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<mw::Coin>& 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;
}
);

View File

@ -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<mw::Coin> Wallet::RewindOutputs(const CTransaction& tx)
{
std::vector<mw::Coin> 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<mw::Coin> Wallet::RewindOutputs(const CTransaction& tx)
return coins;
}
bool Wallet::RewindOutput(const boost::variant<mw::Block::CPtr, mw::Transaction::CPtr>& 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<mw::Block::CPtr>(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<mw::Transaction::CPtr>(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;
}

View File

@ -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<mw::Coin> RewindOutputs(const CTransaction& tx);
bool RewindOutput(
const boost::variant<mw::Block::CPtr, mw::Transaction::CPtr>& 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;

View File

@ -894,7 +894,7 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t key_exp_index, Span<const c
if (Func("mweb", expr)) {
auto pubkey = ParsePubkey(key_exp_index, expr, false, out, error);
if (!pubkey) return nullptr;
return MakeUnique<MWEBDescriptor>(std::move(pubkey), SecretKey()); // MW: TODO - Lookup scan_secret
return MakeUnique<MWEBDescriptor>(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<Descriptor> InferDescriptor(const DestinationAddr& script, const
if (script.IsMWEB()) {
// MW: TODO - Lookup scan_secret
CPubKey pubkey(script.GetMWEBAddress().GetSpendPubKey().vec());
return MakeUnique<MWEBDescriptor>(InferPubkey(pubkey, ParseScriptContext::TOP, provider), SecretKey());
return MakeUnique<MWEBDescriptor>(InferPubkey(pubkey, ParseScriptContext::TOP, provider), SecretKey::Null());
}
return InferScript(script.GetScript(), ParseScriptContext::TOP, provider);

View File

@ -182,7 +182,7 @@ void test_one_input(const std::vector<uint8_t>& 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);

View File

@ -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;
}

View File

@ -1718,20 +1718,28 @@ std::set<CKeyID> 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<mw::Keychain>(
*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<mw::Keychain>(
*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<DestinationAddr> DescriptorScriptPubKeyMan::GetScriptPubKeys()
script_pub_keys.push_back(script_pub_key.first);
}
return script_pub_keys;
}
bool DescriptorScriptPubKeyMan::LoadMWEBKeychain()
{
return false;
}

View File

@ -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<CKeyID> 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<DestinationAddr> GetScriptPubKeys() const;
bool LoadMWEBKeychain() override;
};
#endif // BITCOIN_WALLET_SCRIPTPUBKEYMAN_H

View File

@ -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<uint256, CWalletTx>::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::WalletTxInfo>& 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<TxSpends::const_iterator, TxSpends::const_iterator> 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::WalletTxInfo>& 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<TxSpends::const_iterator, TxSpends::const_iterator> 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::WalletTxInfo>& 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<MWEB::WalletTxInfo>(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<MWEB::WalletTxInfo>(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<CTxDestination, std::vector<COutputCoin>> 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<mw::Hash>(output_idx), coin) && coin.IsMine()) {
if (GetCoin(boost::get<mw::Hash>(output_idx), coin) && coin.IsMine() && coin.HasSpendKey()) {
result[address].emplace_back(MWOutput{coin, depth, boost::get<StealthAddress>(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);

View File

@ -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<std::pair<mw::Hash, size_t>>& pegout_indices, const mapValue_t& mapValue)
{
if (!mapValue.count("pegout_indices")) {
return;
}
std::vector<uint8_t> 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<CDataStream, VarIntMode::DEFAULT, size_t>(s);
for (size_t i = 0; i < num_indices; i++) {
mw::Hash kernel_id;
s >> kernel_id;
size_t sub_idx = ReadVarInt<CDataStream, VarIntMode::DEFAULT, size_t>(s);
pegout_indices.push_back({std::move(kernel_id), sub_idx});
}
}
static inline void WritePegoutIndices(const std::vector<std::pair<mw::Hash, size_t>>& pegout_indices, mapValue_t& mapValue)
{
if (pegout_indices.empty()) {
return;
}
CDataStream s(SER_DISK, PROTOCOL_VERSION);
WriteVarInt<CDataStream, VarIntMode::DEFAULT, size_t>(s, pegout_indices.size());
for (const auto& pegout_idx : pegout_indices) {
pegout_idx.first.Serialize(s);
WriteVarInt<CDataStream, VarIntMode::DEFAULT, size_t>(s, pegout_idx.second);
}
mapValue["pegout_indices"] = HexStr(std::vector<uint8_t>{s.begin(), s.end()});
}
struct COutputEntry
{
CTxDestination destination;
@ -289,6 +324,7 @@ public:
std::multimap<int64_t, CWalletTx*>::const_iterator m_it_wtxOrdered;
boost::optional<MWEB::WalletTxInfo> mweb_wtx_info;
std::vector<std::pair<mw::Hash, size_t>> 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<mw::Hash, uint256> mapOutputsMWEB GUARDED_BY(cs_wallet);
/**
* Used to keep track of which CWalletTx an MWEB kernel is in.
*/
std::map<mw::Hash, uint256> 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::WalletTxInfo>& 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<TxSpends::iterator, TxSpends::iterator>) 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::WalletTxInfo>& mweb_wtx_info, CWalletTx::Confirmation confirm, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
std::atomic<uint64_t> 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<uint256>& 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::WalletTxInfo>& mweb_wtx_info) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/** should probably be renamed to IsRelevantToMe */
bool IsFromMe(const CTransaction& tx, const boost::optional<MWEB::WalletTxInfo>& mweb_wtx_info) const;
CAmount GetDebit(const CTransaction& tx, const boost::optional<MWEB::WalletTxInfo>& mweb_wtx_info, const isminefilter& filter) const;

View File

@ -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);

View File

@ -93,11 +93,13 @@ public:
uint32_t nInternalChainCounter;
uint32_t nMWEBIndexCounter;
CKeyID seed_id; //!< seed hash160
boost::optional<SecretKey> 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