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
32 changes: 29 additions & 3 deletions src/interfaces/wallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
#include <util/fs.h>
#include <util/message.h>
#include <util/result.h>
#include <util/translation.h> // For bilingual_str
#include <util/ui_change_type.h>
#include <wallet/platformtypes.h>

#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <psbt.h>
#include <set>
#include <string>
Expand All @@ -35,7 +37,6 @@ class CKey;
class CRPCCommand;
enum class FeeReason;
enum class TransactionError;
struct bilingual_str;
struct PartiallySignedTransaction;
namespace node {
struct NodeContext;
Expand Down Expand Up @@ -152,9 +153,17 @@ class Wallet

//! ECDH shared secret between the identity authentication key
//! and a counterparty public key, using the libsecp256k1 ECDH KDF.
//! Refuses key_index 0 (the identity MASTER key).
virtual wallet::PlatformKeyResult<SecureVector> platformECDHSecret(const wallet::IdentityAuthKey& key,
const CPubKey& counterparty) = 0;

//! DIP-15 accountReference MAC: HMAC-SHA256 keyed by the identity
//! authentication (ENCRYPTION) private key over the compact xpub sent in
//! a contact request. Refuses key_index 0. The caller masks the account
//! index with the result; the key never leaves the wallet.
virtual wallet::PlatformKeyResult<uint256> platformAccountReferenceMac(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 @@ -249,8 +258,21 @@ 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,
//! Create a signed (uncommitted) asset lock transaction converting
//! credit_amount duffs into Platform credits, with a single P2PKH credit
//! output to credit_pubkey (typically a registration funding key from
//! getPlatformPubKey). Broadcast the result with commitTransaction().
virtual util::Result<CTransactionRef> createAssetLockTransaction(CAmount credit_amount,
const CPubKey& credit_pubkey,
const wallet::CCoinControl& coin_control) = 0;

//! Commit transaction. Returns the mempool rejection reason when the
//! transaction was committed to the wallet but could not be accepted to
//! the mempool for broadcast; the caller may abandon it to release its
//! inputs. Returns std::nullopt when the transaction was accepted or when
//! wallet broadcasting is disabled (-walletbroadcast=0), which the caller
//! cannot distinguish here.
virtual std::optional<bilingual_str> commitTransaction(CTransactionRef tx,
WalletValueMap value_map,
WalletOrderForm order_form) = 0;

Expand Down Expand Up @@ -560,6 +582,10 @@ struct WalletTxStatus
bool is_in_main_chain;
bool is_chainlocked;
bool is_islocked;
//! A conflicting transaction is in a ChainLocked block, so this one can
//! never confirm; a conflict in a block not ChainLocked yet may still be
//! reorganized away.
bool is_conflict_chainlocked;
};

//! Wallet transaction output.
Expand Down
70 changes: 68 additions & 2 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
#include <wallet/hdchain.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/walletutil.h>
#include <evo/assetlocktx.h>
#include <evo/deterministicmns.h>
#include <evo/specialtx.h>
#include <masternode/sync.h>
#include <txdb.h>
#include <node/context.h>
Expand Down Expand Up @@ -137,6 +139,10 @@ WalletTxStatus MakeWalletTxStatus(const CWallet& wallet, const CWalletTx& wtx)
result.is_in_main_chain = wallet.IsTxInMainChain(wtx);
result.is_chainlocked = wallet.IsTxChainLocked(wtx);
result.is_islocked = wallet.IsTxLockedByInstantSend(wtx);
const auto* conflict{wtx.state<TxStateConflicted>()};
result.is_conflict_chainlocked = conflict && wallet.HaveChain() &&
wallet.chain().hasChainLock(conflict->conflicting_block_height,
conflict->conflicting_block_hash);
return result;
}

Expand Down Expand Up @@ -262,6 +268,11 @@ class WalletImpl : public Wallet
{
return m_wallet->PlatformECDHSecret(key, counterparty);
}
wallet::PlatformKeyResult<uint256> platformAccountReferenceMac(const wallet::IdentityAuthKey& key,
const wallet::CompactXpub& compact_xpub) override
{
return m_wallet->PlatformAccountReferenceMac(key, compact_xpub);
}
wallet::PlatformKeyResult<wallet::FriendshipXpub> ensureFriendshipReceivingKeychain(
const wallet::FriendshipKeychainRequest& request) override
{
Expand Down Expand Up @@ -526,12 +537,67 @@ 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<CTransactionRef> createAssetLockTransaction(CAmount credit_amount,
const CPubKey& credit_pubkey,
const CCoinControl& coin_control) override
{
// Platform recovers a compressed key from the identity registration
// signature and matches its hash against the credit output, so any
// other key would lock funds that can never be claimed.
if (credit_amount <= 0 || !credit_pubkey.IsFullyValid() || !credit_pubkey.IsCompressed()) {

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.

🔴 Blocking: Reject out-of-range credit amounts before funding

This guard accepts positive amounts above MAX_MONEY, and neither the asset-lock payload constructor nor CreateTransaction validates their upper bound. With an unlocked wallet, a valid compressed funding key, and an amount of 5000000000000000000 duffs, funding can reach GenerateChangeTarget through spend.cpp:921 before checking available funds. GenerateChangeTarget evaluates payment_value * 2 at coinselection.cpp:556, causing signed integer overflow and undefined behavior rather than safely rejecting the amount. Reject amounts outside MoneyRange at this new API boundary, and extend create_asset_lock_transaction to assert that a large positive out-of-range amount returns the invalid-parameters error.

Suggested change
if (credit_amount <= 0 || !credit_pubkey.IsFullyValid() || !credit_pubkey.IsCompressed()) {
if (credit_amount <= 0 || !MoneyRange(credit_amount) || !credit_pubkey.IsFullyValid() || !credit_pubkey.IsCompressed()) {

source: gpt-6.1-sol (phase2-reviewer: general)

return util::Error{Untranslated("invalid asset lock parameters")};
}

LOCK(m_wallet->cs_wallet);
Comment thread
PastaPastaPasta marked this conversation as resolved.
// 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.")};
}

CMutableTransaction mtx;
mtx.nVersion = 3;
mtx.nType = TRANSACTION_ASSET_LOCK;
// Version 1 admits exactly the P2PKH credit output built here, stays
// consensus-valid after v24 and is the only version the mempool
// relays today (IsStandardSpecialTx), so it is used unconditionally.
const CAssetLockPayload payload{{CTxOut{credit_amount, GetScriptForDestination(PKHash{credit_pubkey})}},
CAssetLockPayload::INITIAL_VERSION};
SetTxPayload(mtx, payload);

// The single OP_RETURN "burn" output must carry the total credit
// amount (see CheckAssetLockTx).
const std::vector<CRecipient> recipients{{CScript() << OP_RETURN << OP_0, credit_amount, /*fSubtractFeeFromAmount=*/false}};
auto res = CreateTransaction(*m_wallet, recipients, RANDOM_CHANGE_POSITION, coin_control,
/*sign=*/false, static_cast<int>(mtx.vExtraPayload.size()));
if (!res) return util::Error{util::ErrorString(res)};

// Graft the funded inputs/outputs and the anti-fee-sniping locktime
// onto the special tx and sign it as a whole (input signatures commit
// to nType and vExtraPayload).
mtx.vin = res->tx->vin;
mtx.vout = res->tx->vout;
mtx.nLockTime = res->tx->nLockTime;
if (!m_wallet->SignTransaction(mtx)) {
return util::Error{_("Signing transaction failed")};
}
CTransactionRef tx{MakeTransactionRef(std::move(mtx))};
// Too many inputs or too large a transaction would be committed but
// never relayed; fail here like CreateTransaction does for Platform
// recipients.
if (std::string reason; m_wallet->chain().isNonStandardSpecialTx(tx, reason)) {
return util::Error{strprintf(_("Transaction is non-standard (%s) and would not relay"), reason)};
}
return tx;
}
std::optional<bilingual_str> 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));
bilingual_str broadcast_error;
m_wallet->CommitTransaction(std::move(tx), std::move(value_map), std::move(order_form), &broadcast_error);
if (broadcast_error.empty()) return std::nullopt;
return broadcast_error;
}
bool transactionCanBeAbandoned(const uint256& txid) override { return m_wallet->TransactionCanBeAbandoned(txid); }
bool transactionCanBeResent(const uint256& txid) override { return m_wallet->TransactionCanBeResent(txid); }
Expand Down
27 changes: 26 additions & 1 deletion src/wallet/platformkeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <wallet/platformkeys.h>

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

Expand Down Expand Up @@ -88,8 +89,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 +115,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 +153,26 @@ 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;
}

} // 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
15 changes: 13 additions & 2 deletions src/wallet/platformkeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ inline constexpr uint32_t AUTH_KEY_TYPE_ECDSA{0};
inline constexpr uint32_t AUTH_KEY_TYPE_BLS{1};

//! An extended key produced by walking a (possibly 256-bit) derivation path.
//! Unlike CExtKey this does not carry BIP32 serialization metadata; DIP-14
//! extended-key serialization tracks the path separately.
//! Unlike CExtKey this does not carry BIP32 serialization metadata beyond the
//! parent fingerprint; DIP-14 extended-key serialization tracks the path
//! separately.
struct ExtKey256 {
CKey key;
ChainCode chaincode;
//! BIP32 fingerprint of the key the last path step was derived from
//! (Hash160 prefix of its public key); the DIP-15 compact xpub carries it.
std::array<uint8_t, 4> parent_fingerprint{};

ExtKey256() = default;
};
Expand Down Expand Up @@ -100,6 +104,13 @@ Path FriendshipPath(uint32_t coin_type, uint32_t account, Span<const uint8_t> us
//! encryption. Returns a 32-byte secret.
[[nodiscard]] bool ComputeECDHSecret(const CKey& key, const CPubKey& counterparty, SecureVector& secret_out);

//! DIP-15 accountReference MAC: HMAC-SHA256 keyed by the sender's ENCRYPTION
//! private key over the compact xpub sent in the contact request. Matches
//! rs-platform-encryption's calculate_account_reference, which then masks
//! the account index with the low bits of this digest; the masking itself
//! is not done here.
[[nodiscard]] bool ComputeAccountReferenceMac(const CKey& key, const CompactXpub& compact_xpub, uint256& mac_out);

} // namespace wallet::platformkeys

#endif // BITCOIN_WALLET_PLATFORMKEYS_H
16 changes: 16 additions & 0 deletions src/wallet/platformtypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <support/allocators/secure.h>
#include <uint256.h>

#include <array>
#include <cstddef>
#include <cstdint>
#include <variant>

Expand Down Expand Up @@ -63,8 +65,22 @@ struct FriendshipKeychainRequest {
struct FriendshipXpub {
CPubKey pubkey;
ChainCode chaincode;
//! BIP32 fingerprint (Hash160 prefix) of the key one level up, i.e. of
//! m/9'/coin'/15'/account'/<user_a> for the receiving chain.
std::array<uint8_t, 4> parent_fingerprint{};
};

//! DIP-15 compact extended public key: parentFingerprint(4) || chainCode(32)
//! || pubKey(33). This is the contactRequest encryptedPublicKey plaintext and
//! the accountReference MAC input; unlike a BIP32/DIP-14 serialization it
//! carries no version, depth or child number.
inline constexpr size_t COMPACT_XPUB_SIZE{4 + 32 + 33};
using CompactXpub = std::array<uint8_t, COMPACT_XPUB_SIZE>;

//! Serialize a friendship xpub in the compact form. Fails for a xpub whose
//! key is not compressed, as externally supplied contact xpubs may be.
[[nodiscard]] bool CompactXpubBytes(const FriendshipXpub& xpub, CompactXpub& compact_out);

//! Derive one contact payment destination from a DIP-15 friendship xpub.
//! This is public-only key math and does not require a wallet instance.
bool DeriveFriendshipPaymentDestination(const FriendshipXpub& xpub, uint32_t index, CTxDestination& destination_out);
Expand Down
Loading
Loading