Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <psbt.h>
#include <set>
#include <string>
Expand Down Expand Up @@ -115,6 +116,10 @@ class Wallet
//! Abort a rescan.
virtual void abortRescan() = 0;

//! The height startRescan(false) starts at, when this node has pruned
//! blocks it would read; nullopt when it has them all.
virtual std::optional<int> rescanPrunedFrom() = 0;

//! Lock masternode collaterals
virtual void autoLockMasternodeCollaterals() = 0;

Expand Down Expand Up @@ -142,6 +147,10 @@ class Wallet
//! Sign special transaction payload
virtual bool signSpecialTxPayload(const uint256& hash, const CKeyID& keyid, std::vector<unsigned char>& vchSig) = 0;

//! Whether this wallet can derive Platform keys: SUCCESS for a descriptor
//! wallet with private keys and a single HD root, whether locked or not.
virtual wallet::PlatformKeyStatus platformKeySourceStatus() = 0;

//! Derive and return a public Platform key. The mnemonic-owning key
//! manager performs derivation without exposing the wallet's root seed.
virtual wallet::PlatformKeyResult<CPubKey> getPlatformPubKey(const wallet::PlatformKeyRequest& request) = 0;
Expand All @@ -156,6 +165,12 @@ class Wallet
virtual wallet::PlatformKeyResult<SecureVector> platformECDHSecret(const wallet::IdentityAuthKey& key,
const CPubKey& counterparty) = 0;

//! DIP-15 accountReference of a contact request sending compact_xpub,
//! keyed by the identity ENCRYPTION key, for account 0 and version 0 as
//! DashPay iOS sends it. The key never leaves the wallet.
virtual wallet::PlatformKeyResult<uint32_t> platformAccountReference(const wallet::IdentityAuthKey& key,
const wallet::CompactXpub& compact_xpub) = 0;

//! Ensure our private DIP-15 receiving chain is imported as a ranged
//! descriptor and return the corresponding public chain. Derivation,
//! descriptor update and result publication are one wallet-locked action.
Expand Down Expand Up @@ -238,6 +253,15 @@ class Wallet
int& change_pos,
CAmount& fee) = 0;

//! Create a signed, uncommitted asset lock converting credit_amount duffs
//! into Platform credits. Its single P2PKH credit output pays the key the
//! wallet derives for funding_key, so the credits stay recoverable from
//! the wallet's seed.
virtual util::Result<CTransactionRef> createAssetLockTransaction(CAmount credit_amount,
const wallet::AssetLockFundingKey& funding_key,
const wallet::CCoinControl& coin_control,
CAmount& fee) = 0;

//! Fund a transaction template per the provider-transaction funding policy:
//! inputs are selected only from coins received at the single specified
//! fee-source destination, change is returned to that same destination, and
Expand All @@ -250,8 +274,9 @@ class Wallet
//! Sign every wallet-owned input of a transaction and report whether signing is complete.
virtual util::Result<WalletTxSignResult> signTransaction(const CMutableTransaction& tx) = 0;

//! Commit transaction.
virtual void commitTransaction(CTransactionRef tx,
//! Commit transaction to the wallet and broadcast it. Returns an error
//! when it could not be broadcast; it stays committed to the wallet.
virtual util::Result<void> commitTransaction(CTransactionRef tx,
WalletValueMap value_map,
WalletOrderForm order_form) = 0;

Expand Down
28 changes: 28 additions & 0 deletions src/qt/test/wallettests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qt/test/wallettests.h>
#include <qt/test/masternodetestutil.h>
#include <qt/test/util.h>

#include <chainparams.h>
Expand Down Expand Up @@ -321,3 +322,30 @@ void WalletTests::walletTests()
#endif
TestGUI(m_node);
}

//! An unlock handed from one owner to another relocks the wallet once, when
//! its last owner goes away.
void WalletTests::unlockContextMove()
{
TestChain100Setup test;
m_node.setContext(&test.m_node);
WalletContext& context{*m_node.walletLoader().context()};
const auto wallet{MasternodeTestUtil::MakeCoinbaseWallet(m_node, context, test, "unlock", /*encrypt=*/true)};
QVERIFY(wallet != nullptr);
MasternodeTestUtil::WalletGuard guard{context, wallet};
MasternodeTestUtil::GuiModels models{m_node};
QVERIFY(models.ok);
WalletModel wallet_model(interfaces::MakeWallet(context, wallet), models.client);
connect(&wallet_model, &WalletModel::requireUnlock, [&wallet_model] {
wallet_model.setWalletLocked(/*locked=*/false, "test passphrase");
});
QCOMPARE(wallet_model.getEncryptionStatus(), WalletModel::Locked);

// emplace moves the returned context; the moved-from one must not relock.
std::optional<WalletModel::UnlockContext> held;
held.emplace(wallet_model.requestUnlock());
QVERIFY(held->isValid());
QCOMPARE(wallet_model.getEncryptionStatus(), WalletModel::Unlocked);
held.reset();
QCOMPARE(wallet_model.getEncryptionStatus(), WalletModel::Locked);
}
1 change: 1 addition & 0 deletions src/qt/test/wallettests.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class WalletTests : public QObject

private Q_SLOTS:
void walletTests();
void unlockContextMove();
};

#endif // BITCOIN_QT_TEST_WALLETTESTS_H
9 changes: 9 additions & 0 deletions src/qt/walletmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,15 @@ WalletModel::UnlockContext::UnlockContext(WalletModel *_wallet, bool _valid, boo
{
}

WalletModel::UnlockContext::UnlockContext(UnlockContext&& other) noexcept :
wallet(other.wallet),
valid(other.valid),
was_locked(other.was_locked),
was_mixing(other.was_mixing)
{
other.valid = false;
}

WalletModel::UnlockContext::~UnlockContext()
{
if(valid && (was_locked || was_mixing))
Expand Down
10 changes: 5 additions & 5 deletions src/qt/walletmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,17 +123,17 @@ class WalletModel : public QObject

bool isValid() const { return valid; }

// Disable unused copy/move constructors/assignments explicitly.
// The moved-from context no longer relocks: exactly one owner does.
UnlockContext(UnlockContext&& other) noexcept;
UnlockContext(const UnlockContext&) = delete;
UnlockContext(UnlockContext&&) = delete;
UnlockContext& operator=(const UnlockContext&) = delete;
UnlockContext& operator=(UnlockContext&&) = delete;

private:
WalletModel *wallet;
const bool valid;
const bool was_locked;
const bool was_mixing;
bool valid;
bool was_locked;
bool was_mixing;
};

UnlockContext requestUnlock(bool fForMixingOnly = false);
Expand Down
4 changes: 4 additions & 0 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <wallet/coinselection.h>

#include <algorithm>
#include <cstdint>
#include <map>
#include <optional>
#include <set>
Expand Down Expand Up @@ -61,6 +62,9 @@ class CCoinControl
FlatSigningProvider m_external_provider;
//! Controls which types of coins are allowed to be used (default: ALL_COINS)
CoinType nCoinType = CoinType::ALL_COINS;
//! Payload version of the asset lock built for Platform recipients, if not
//! set CAssetLockPayload::CURRENT_VERSION
std::optional<uint8_t> m_asset_lock_version;

CCoinControl(CoinType coin_type = CoinType::ALL_COINS);

Expand Down
75 changes: 62 additions & 13 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include <wallet/hdchain.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/walletutil.h>
#include <evo/assetlocktx.h>
#include <evo/deterministicmns.h>
#include <masternode/sync.h>
#include <txdb.h>
Expand Down Expand Up @@ -187,20 +188,24 @@ class WalletImpl : public Wallet
{
return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase);
}
wallet::RescanStatus startRescan(bool from_genesis) override
//! Where a rescan from the birth of the wallet's first key starts.
int rescanStartHeight()
{
int rescan_height{0};
if (!from_genesis) {
std::optional<int64_t> time_first_key;
for (auto spk_man : m_wallet->GetAllScriptPubKeyMans()) {
int64_t time = spk_man->GetTimeFirstKey();
if (!time_first_key || time < *time_first_key) time_first_key = time;
}
if (time_first_key) {
m_wallet->chain().findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height,
FoundBlock().height(rescan_height));
}
std::optional<int64_t> time_first_key;
for (auto spk_man : m_wallet->GetAllScriptPubKeyMans()) {
int64_t time = spk_man->GetTimeFirstKey();
if (!time_first_key || time < *time_first_key) time_first_key = time;
}
if (time_first_key) {
m_wallet->chain().findFirstBlockWithTimeAndHeight(*time_first_key - TIMESTAMP_WINDOW, rescan_height,
FoundBlock().height(rescan_height));
}
return rescan_height;
}
wallet::RescanStatus startRescan(bool from_genesis) override
{
const int rescan_height{from_genesis ? 0 : rescanStartHeight()};

WalletRescanReserver reserver(*m_wallet);
if (!reserver.reserve()) {
Expand All @@ -219,6 +224,14 @@ class WalletImpl : public Wallet
return wallet::RescanStatus::FAILURE; // fallback for release builds
}
void abortRescan() override { m_wallet->AbortRescan(); }
std::optional<int> rescanPrunedFrom() override
{
if (!m_wallet->chain().havePruned()) return std::nullopt;
LOCK(m_wallet->cs_wallet);
const int rescan_height{rescanStartHeight()};
if (m_wallet->chain().hasBlocks(m_wallet->GetLastBlockHash(), rescan_height)) return std::nullopt;
return rescan_height;
}
void autoLockMasternodeCollaterals() override { m_wallet->AutoLockMasternodeCollaterals(); }
bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); }
bool autoBackupWallet(const fs::path& wallet_path, bilingual_str& error_string, std::vector<bilingual_str>& warnings) override
Expand Down Expand Up @@ -248,6 +261,7 @@ class WalletImpl : public Wallet
{
return m_wallet->SignSpecialTxPayload(hash, keyid, vchSig);
}
wallet::PlatformKeyStatus platformKeySourceStatus() override { return m_wallet->GetPlatformKeySourceStatus(); }
wallet::PlatformKeyResult<CPubKey> getPlatformPubKey(const wallet::PlatformKeyRequest& request) override
{
return m_wallet->GetPlatformPubKey(request);
Expand All @@ -262,6 +276,11 @@ class WalletImpl : public Wallet
{
return m_wallet->PlatformECDHSecret(key, counterparty);
}
wallet::PlatformKeyResult<uint32_t> platformAccountReference(const wallet::IdentityAuthKey& key,
const wallet::CompactXpub& compact_xpub) override
{
return m_wallet->PlatformAccountReference(key, compact_xpub);
}
wallet::PlatformKeyResult<wallet::FriendshipXpub> ensureFriendshipReceivingKeychain(
const wallet::FriendshipKeychainRequest& request) override
{
Expand Down Expand Up @@ -450,6 +469,36 @@ class WalletImpl : public Wallet

return txr.tx;
}
util::Result<CTransactionRef> createAssetLockTransaction(CAmount credit_amount,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description:

interfaces::Wallet::createAssetLockTransaction builds, funds and signs a version 1 asset lock

why exactly asset lock version 1 is needed here in PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because version 1 is the only asset lock that works on both sides of the v24 boundary today:

  • Before v24, CheckAssetLockTx rejects any payload version above INITIAL_VERSION (bad-assetlocktx-version-2, src/evo/assetlocktx.cpp:59).
  • After v24, IsStandardSpecialTx treats a version 2 payload as non-standard (assetlocktx-version-2, src/evo/specialtxman.cpp:1964). That is your fix: make huge asset lock tx non-standard #7359, which keeps v2 locks out of relay until Platform is ready for them.

The wallet's default is CAssetLockPayload::CURRENT_VERSION (2, src/wallet/spend.cpp:837), so a DashPay registration or top-up built the normal way would be refused by the mempool. The seam therefore sets version 1 on its own copy of the coin control, and leaves the existing sendtoaddress Platform path on v2 with its -acceptnonstdtxn=1 hint. Once v2 locks are made standard, the seam can drop the override.


🤖 Posted autonomously by Claude on behalf of pasta.

const AssetLockFundingKey& funding_key,
const CCoinControl& coin_control,
CAmount& fee) override
{
// CreateTransaction accepts a zero Platform credit, which CheckAssetLockTx refuses,
// and bounds no amount from above before coin selection doubles it.
if (credit_amount <= 0 || !MoneyRange(credit_amount)) return util::Error{_("Invalid amount")};

LOCK(m_wallet->cs_wallet);
// A wallet unlocked only for mixing keeps its keys but must not spend.
if (m_wallet->IsLocked()) {
return util::Error{_("Please enter the wallet passphrase with walletpassphrase first.")};
}
const auto funding_pubkey{m_wallet->GetPlatformPubKey(
std::visit([](const auto& key) { return PlatformKeyRequest{key}; }, funding_key))};
if (!funding_pubkey) return util::Error{_("Could not derive the asset lock funding key")};

// Version 1 stays consensus-valid after v24 and is the only version the
// mempool relays today (IsStandardSpecialTx).
CCoinControl version_1_control{coin_control};
version_1_control.m_asset_lock_version = CAssetLockPayload::INITIAL_VERSION;
auto res{CreateTransaction(*m_wallet,
{CRecipient{GetScriptForDestination(PKHash{funding_pubkey.value}), credit_amount,
/*fSubtractFeeFromAmount=*/false, /*fPlatformTransfer=*/true}},
RANDOM_CHANGE_POSITION, version_1_control, /*sign=*/true)};
if (!res) return util::Error{util::ErrorString(res)};
fee = res->fee;
return res->tx;
}
util::Result<CTransactionRef> fundTransaction(const CMutableTransaction& tx_template,
const CTxDestination& fund_destination) override
{
Expand Down Expand Up @@ -533,12 +582,12 @@ class WalletImpl : public Wallet
errors.push_back(std::move(error));
return WalletTxSignResult{MakeTransactionRef(std::move(signed_tx)), complete, std::move(errors)};
}
void commitTransaction(CTransactionRef tx,
util::Result<void> commitTransaction(CTransactionRef tx,
WalletValueMap value_map,
WalletOrderForm order_form) override
{
LOCK(m_wallet->cs_wallet);
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
return m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form));
}
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
bool transactionCanBeResent(const uint256& txid) override { return m_wallet->TransactionCanBeResent(txid); }
Expand Down
36 changes: 35 additions & 1 deletion src/wallet/platformkeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include <wallet/platformkeys.h>

#include <crypto/common.h>
#include <crypto/hmac_sha256.h>
#include <secp256k1.h>
#include <secp256k1_ecdh.h>

Expand Down Expand Up @@ -88,8 +90,15 @@ bool DeriveExtKey(const CExtKey& parent, const Path& path, ExtKey256& out)

CKey key{parent.key};
ChainCode chaincode{parent.chaincode};
std::array<uint8_t, 4> parent_fingerprint{};

for (size_t i{0}; i < path.size(); ++i) {
const auto& element{path[i]};
if (i + 1 == path.size()) {
const CKeyID parent_id{key.GetPubKey().GetID()};
std::copy_n(parent_id.begin(), parent_fingerprint.size(), parent_fingerprint.begin());
}

for (const auto& element : path) {
CKey child_key;
ChainCode child_cc;
bool ok{false};
Expand All @@ -107,6 +116,7 @@ bool DeriveExtKey(const CExtKey& parent, const Path& path, ExtKey256& out)

out.key = key;
out.chaincode = chaincode;
out.parent_fingerprint = parent_fingerprint;
return true;
}

Expand Down Expand Up @@ -144,10 +154,34 @@ bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVecto
return true;
}

bool ComputeAccountReferenceMac(const CKey& key, const CompactXpub& compact_xpub, uint256& mac_out)
{
if (!key.IsValid()) return false;
CHMAC_SHA256{key.begin(), key.size()}.Write(compact_xpub.data(), compact_xpub.size()).Finalize(mac_out.begin());
return true;
}

uint32_t AccountReferenceFromMac(const uint256& mac)
{
// DIP-15: (version << 28) | (ASK28 ^ account). With version 0 and
// account 0, the reference is ASK28, the top 28 bits of the MAC's last
// four bytes.
return ReadBE32(mac.begin() + 28) >> 4;
}

} // namespace wallet::platformkeys

namespace wallet {

bool CompactXpubBytes(const FriendshipXpub& xpub, CompactXpub& compact_out)
{
if (!xpub.pubkey.IsCompressed()) return false;
auto it{std::copy(xpub.parent_fingerprint.begin(), xpub.parent_fingerprint.end(), compact_out.begin())};
it = std::copy(xpub.chaincode.begin(), xpub.chaincode.end(), it);
std::copy(xpub.pubkey.begin(), xpub.pubkey.end(), it);
return true;
}

bool DeriveFriendshipPaymentDestination(const FriendshipXpub& xpub, uint32_t index, CTxDestination& destination_out)
{
platformkeys::ExtPubKey256 child;
Expand Down
Loading
Loading