8000 wallet: Pass through transaction locktime and preset input sequences and scripts to CreateTransaction by achow101 · Pull Request #25273 · bitcoin/bitcoin · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

wallet: Pass through transaction locktime and preset input sequences and scripts to CreateTransaction #25273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 11, 2023
Merged
127 changes: 100 additions & 27 deletions src/wallet/coincontrol.cpp
8000
Original file line number Diff line number Diff line change
Expand Up @@ -14,69 +14,142 @@ CCoinControl::CCoinControl()

bool CCoinControl::HasSelected() const
{
return !m_selected_inputs.empty();
return !m_selected.empty();
}

bool CCoinControl::IsSelected(const COutPoint& output) const
bool CCoinControl::IsSelected(const COutPoint& outpoint) const
{
return m_selected_inputs.count(output) > 0;
return m_selected.count(outpoint) > 0;
}

bool CCoinControl::IsExternalSelected(const COutPoint& output) const
bool CCoinControl::IsExternalSelected(const COutPoint& outpoint) const
{
return m_external_txouts.count(output) > 0;
const auto it = m_selected.find(outpoint);
return it != m_selected.end() && it->second.HasTxOut();
}

std::optional<CTxOut> CCoinControl::GetExternalOutput(const COutPoint& outpoint) const
{
const auto ext_it = m_external_txouts.find(outpoint);
if (ext_it == m_external_txouts.end()) {
const auto it = m_selected.find(outpoint);
if (it == m_selected.end() || !it->second.HasTxOut()) {
Copy link
Member
@josibake josibake Dec 8, 2023

Choose a reason for hiding this comment

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

in 7b5d22f:

Is there a situation where an internal input would have a value for TxOut, accidentally or purposefully? Only asking because it feels slightly more fragile to rely only on this to determine if something is external or internal vs having two separate vectors, but I also don't have a good sense if what I'm worried about is even possible (internal having TxOut set) or if its even that bad if we confuse external and internal inputs 🤷

Copy link
Member Author

Choose a reason for hiding this comment

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

The consequence is that size estimation will overestimate that input's size by 1 byte, which is up to 4 weight units of overestimation and therefore fees. However it should not be possible to accidentally set an internal input to be external as all calls to SelectExternal are guarded by a !IsMine().

return std::nullopt;
}
return it->second.GetTxOut();
}

return std::make_optional(ext_it->second);
PreselectedInput& CCoinControl::Select(const COutPoint& outpoint)
{
auto& input = m_selected[outpoint];
input.SetPosition(m_selection_pos);
++m_selection_pos;
return input;
}
void CCoinControl::UnSelect(const COutPoint& outpoint)
{
m_selected.erase(outpoint);
}

void CCoinControl::Select(const COutPoint& output)
void CCoinControl::UnSelectAll()
{
m_selected_inputs.insert(output);
m_selected.clear();
}

void CCoinControl::SelectExternal(const COutPoint& outpoint, const CTxOut& txout)
std::vector<COutPoint> CCoinControl::ListSelected() const
{
m_selected_inputs.insert(outpoint);
m_external_txouts.emplace(outpoint, txout);
std::vector<COutPoint> outpoints;
std::transform(m_selected.begin(), m_selected.end(), std::back_inserter(outpoints),
[](const std::map<COutPoint, PreselectedInput>::value_type& pair) {
return pair.first;
});
return outpoints;
}

void CCoinControl::UnSelect(const COutPoint& output)
void CCoinControl::SetInputWeight(const COutPoint& outpoint, int64_t weight)
{
m_selected_inputs.erase(output);
m_selected[outpoint].SetInputWeight(weight);
}

void CCoinControl::UnSelectAll()
std::optional<int64_t> CCoinControl::GetInputWeight(const COutPoint& outpoint) const
{
m_selected_inputs.clear();
const auto it = m_selected.find(outpoint);
return it != m_selected.end() ? it->second.GetInputWeight() : std::nullopt;
}

std::vector<COutPoint> CCoinControl::ListSelected() const
std::optional<uint32_t> CCoinControl::GetSequence(const COutPoint& outpoint) const
{
return {m_selected_inputs.begin(), m_selected_inputs.end()};
const auto it = m_selected.find(outpoint);
return it != m_selected.end() ? it->second.GetSequence() : std::nullopt;
}

void CCoinControl::SetInputWeight(const COutPoint& outpoint, int64_t weight)
std::pair<std::optional<CScript>, std::optional<CScriptWitness>> CCoinControl::GetScripts(const COutPoint& outpoint) const
{
const auto it = m_selected.find(outpoint);
return it != m_selected.end() ? m_selected.at(outpoint).GetScripts() : std::make_pair(std::nullopt, std::nullopt);
}

void PreselectedInput::SetTxOut(const CTxOut& txout)
{
m_txout = txout;
}

CTxOut PreselectedInput::GetTxOut() const
{
assert(m_txout.has_value());
return m_txout.value();
}

bool PreselectedInput::HasTxOut() const
{
return m_txout.has_value();
}

void PreselectedInput::SetInputWeight(int64_t weight)
{
m_weight = weight;
}

std::optional<int64_t> PreselectedInput::GetInputWeight() const
{
return m_weight;
}

void PreselectedInput::SetSequence(uint32_t sequence)
{
m_sequence = sequence;
}

std::optional<uint32_t> PreselectedInput::GetSequence() const
{
return m_sequence;
}

void PreselectedInput::SetScriptSig(const CScript& script)
{
m_script_sig = script;
}

void PreselectedInput::SetScriptWitness(const CScriptWitness& script_wit)
{
m_script_witness = script_wit;
}

bool PreselectedInput::HasScripts() const
{
return m_script_sig.has_value() || m_script_witness.has_value();
}

std::pair<std::optional<CScript>, std::optional<CScriptWitness>> PreselectedInput::GetScripts() const
{
m_input_weights[outpoint] = weight;
return {m_script_sig, m_script_witness};
}

bool CCoinControl::HasInputWeight(const COutPoint& outpoint) const
void PreselectedInput::SetPosition(unsigned int pos)
{
return m_input_weights.count(outpoint) > 0;
m_pos = pos;
}

int64_t CCoinControl::GetInputWeight(const COutPoint& outpoint) const
std::optional<unsigned int> PreselectedInput::GetPosition() const
{
auto it = m_input_weights.find(outpoint);
assert(it != m_input_weights.end());
return it->second;
return m_pos;
}
} // namespace wallet
101 changes: 81 additions & 20 deletions src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,58 @@ const int DEFAULT_MAX_DEPTH = 9999999;
//! Default for -avoidpartialspends
static constexpr bool DEFAULT_AVOIDPARTIALSPENDS = false;

class PreselectedInput
Copy link
Contributor

Choose a reason for hiding this comment

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

in 59a7c74

Naming is hard. We already have struct PreSelectedInputs which is quite confusing as it refers to different data.

PreSelectedInputs is basically set<COutput>. It seems we should find a better name for PreSelectedInputs. Maybe rename it to PreSelectedCoins?

It'd be nice to have consistent terms to refer to different data.
How about we use input when we talk about tx.in and related info.
And we use coin when its enriched data in context of coin selection.

Perhaps @murchandamus have thought about terminology.

Copy link
Member Author

Choose a reason for hiding this comment

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

🤷

{
private:
//! The previous output being spent by this input
std::optional<CTxOut> m_txout;
//! The input weight for spending this input
std::optional<int64_t> m_weight;
//! The sequence number for this input
std::optional<uint32_t> m_sequence;
//! The scriptSig for this input
std::optional<CScript> m_script_sig;
Copy link
Contributor

Choose a reason for hiding this comment

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

in 53cb734

There is redundancy between m_script_sig + m_script_witness and m_weight. One can set all of them and reach and inconsistent state, which will lead to error.

Can we make the interface better?

Copy link
Member Author

Choose a reason for hiding this comment

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

It is actually expected that both can be provided, and not match. We may be funding a transaction with other inputs that already have some signatures (e.g. a multisig where one of the parties is adding another input for fees, and one other party has already signed with sighash_single), so m_script_sig or m_script_witness may already exist, but are incomplete. m_weight may then also be provided, and it wouldn't match since it would account for the signatures that have not yet been added.

//! The scriptWitness for this input
std::optional<CScriptWitness> m_script_witness;
//! The position in the inputs vector for this input
std::optional<unsigned int> m_pos;

public:
/**
* Set the previous output for this input.
* Only necessary if the input is expected to be an external input.
*/
void SetTxOut(const CTxOut& txout);
/** Retrieve the previous output for this input. */
CTxOut GetTxOut() const;
/** Return whether the previous output is set for this input. */
bool HasTxOut() const;

/** Set the weight for this input. */
void SetInputWeight(int64_t weight);
/** Retrieve the input weight for this input. */
std::optional<int64_t> GetInputWeight() const;

/** Set the sequence for this input. */
void SetSequence(uint32_t sequence);
/** Retrieve the sequence for this input. */
std::optional<uint32_t> GetSequence() const;

/** Set the scriptSig for this input. */
void SetScriptSig(const CScript& script);
/** Set the scriptWitness for this input. */
void SetScriptWitness(const CScriptWitness& script_wit);
/** Return whether either the scriptSig or scriptWitness are set for this input. */
bool HasScripts() const;
/** Retrieve both the scriptSig and the scriptWitness. */
std::pair<std::optional<CScript>, std::optional<CScriptWitness>> GetScripts() const;

/** Store the position of this input. */
void SetPosition(unsigned int pos);
/** Retrieve the position of this input. */
std::optional<unsigned int> GetPosition() const;
};

/** Coin Control Features. */
class CCoinControl
{
Expand Down Expand Up @@ -59,6 +111,10 @@ class CCoinControl
int m_max_depth = DEFAULT_MAX_DEPTH;
//! SigningProvider that has pubkeys and scripts to do spend size estimation for external inputs
FlatSigningProvider m_external_provider;
//! Locktime
std::optional<uint32_t> m_locktime;
Copy link
Contributor

Choose a reason for hiding this comment

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

in 0342002

locktime doesn't belong to CoinControl because it has nothing to do with coins being spent. I'd prefer if we keep CoinControl scoped down to only containing data necessary to select and spend coins.

Same is true for some other fields in CoinControl. Maybe we should extract parameters not related to coins into a separate struct?

Copy link
Member Author

Choose a reason for hiding this comment

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

With miniscript, it does matter since scripts can contain locktimes.

//! Version
std::optional<uint32_t> m_version;
Copy link
Contributor

Choose a reason for hiding this comment

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

in c854439

Same comment as for locktime

Copy link
Member Author

Choose a reason for hiding this comment

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

With miniscript, it does matter, as it controls whether something with OP_CSV is spendable.


CCoinControl();

Expand All @@ -69,11 +125,11 @@ class CCoinControl
/**
* Returns true if the given output is pre-selected.
*/
bool IsSelected(const COutPoint& output) const;
bool IsSelected(const COutPoint& outpoint) const;
/**
* Returns true if the given output is selected as an external input.
*/
bool IsExternalSelected(const COutPoint& output) const;
bool IsExternalSelected(const COutPoint& outpoint) const;
/**
* Returns the external output for the given outpoint if it exists.
*/
Expand All @@ -82,16 +138,11 @@ class CCoinControl
* Lock-in the given output for spending.
* The output will be included in the transaction even if it's not the most optimal choice.
*/
void Select(const COutPoint& output);
/**
* Lock-in the given output as an external input for spending because it is not in the wallet.
* The output will be included in the transaction even if it's not the most optimal choice.
*/
void SelectExternal(const COutPoint& outpoint, const CTxOut& txout);
PreselectedInput& Select(const COutPoint& outpoint);
/**
* Unselects the given output.
*/
void UnSelect(const COutPoint& output);
void UnSelect(const COutPoint& outpoint);
/**
* Unselects all outputs.
*/
Expand All @@ -104,23 +155,33 @@ class CCoinControl
* Set an input's weight.
*/
void SetInputWeight(const COutPoint& outpoint, int64_t weight);
/**
* Returns true if the input weight is set.
*/
bool HasInputWeight(const COutPoint& outpoint) const;
/**
* Returns the input weight.
*/
int64_t GetInputWeight(const COutPoint& outpoint) const;
std::optional<int64_t> GetInputWeight(const COutPoint& outpoint) const;
/** Retrieve the sequence for an input */
std::optional<uint32_t> GetSequence(const COutPoint& outpoint) const;
/** Retrieves the scriptSig and scriptWitness for an input. */
std::pair<std::optional<CScript>, std::optional<CScriptWitness>> GetScripts(const COutPoint& outpoint) const;

bool HasSelectedOrder() const
{
return m_selection_pos > 0;
}

std::optional<unsigned int> GetSelectionPos(const COutPoint& outpoint) const
{
const auto it = m_selected.find(outpoint);
if (it == m_selected.end()) {
return std::nullopt;
}
return it->second.GetPosition();
}

private:
//! Selected inputs (inputs that will be used, regardless of whether they're optimal or not)
std::set<COutPoint> m_selected_inputs;
//! Map of external inputs to include in the transaction
//! These are not in the wallet, so we need to track them separately
std::map<COutPoint, CTxOut> m_external_txouts;
//! Map of COutPoints to the maximum weight for that input
std::map<COutPoint, int64_t> m_input_weights;
std::map<COutPoint, PreselectedInput> m_selected;
unsigned int m_selection_pos{0};
};
} // namespace wallet

Expand Down
10 changes: 4 additions & 6 deletions src/wallet/feebumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,9 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
errors.push_back(Untranslated(strprintf("%s:%u is already spent", txin.prevout.hash.GetHex(), txin.prevout.n)));
return Result::MISC_ERROR;
}
if (wallet.IsMine(txin.prevout)) {
new_coin_control.Select(txin.prevout);
} else {
new_coin_control.SelectExternal(txin.prevout, coin.out);
PreselectedInput& preset_txin = new_coin_control.Select(txin.prevout);
if (!wallet.IsMine(txin.prevout)) {
preset_txin.SetTxOut(coin.out);
}
input_value += coin.out.nValue;
spent_outputs.push_back(coin.out);
Expand Down Expand Up @@ -317,8 +316,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
// We cannot source new unconfirmed inputs(bip125 rule 2)
new_coin_control.m_min_depth = 1;

constexpr int RANDOM_CHANGE_POSITION = -1;
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, new_coin_control, false);
auto res = CreateTransaction(wallet, recipients, std::nullopt, new_coin_control, false);
if (!res) {
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + util::ErrorString(res));
return Result::WALLET_ERROR;
Expand Down
4 changes: 2 additions & 2 deletions src/wallet/interfaces.cpp
Original file line number Diff line number Diff line change
4D1F Expand Up @@ -281,12 +281,12 @@ class WalletImpl : public Wallet
CAmount& fee) override
{
LOCK(m_wallet->cs_wallet);
auto res = CreateTransaction(*m_wallet, recipients, change_pos,
auto res = CreateTransaction(*m_wallet, recipients, change_pos == -1 ? std::nullopt : std::make_optional(change_pos),
Copy link
Member
@josibake josibake Dec 8, 2023

Choose a reason for hiding this comment

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

in 3d1a5f2:

note for a follow-up: would be nice if we could use this convention throughout the codebase instead of switching the magic number into a std::optional, like we are doing here.

coin_control, sign);
if (!res) return util::Error{util::ErrorString(res)};
const auto& txr = *res;
fee = txr.fee;
change_pos = txr.change_pos;
change_pos = txr.change_pos ? *txr.change_pos : -1;

return txr.tx;
}
Expand Down
Loading
0