Skip to content

Commit 8f0bd88

Browse files
jgriffithsjkauffman1
authored andcommitted
psbt: detect and mark change outputs when signing PSBT/PSET
For singlesig, we use the internal chain as a change indicator unconditionally. For multisig, for each asset that the wallet contributed to the transaction, we have two possible cases that result in an output being marked as change: 1. The asset is sent outside of the wallet and to one or more wallet outputs. 2. There is no external send, but multiple wallet outputs for the asset. In both cases, the first wallet output is marked as change. There will only be multiple wallet outputs where (1) the caller has deliberately constructed an inefficient send to the same subaccount with change, or (2) the caller is sending the asset to a different subaccount with change going back to the sending subaccount (cross-subaccount spend). Note we do not attempt to identify (2) at this stage, this may be considered for a later non-bugfix release.
1 parent 6d885e9 commit 8f0bd88

File tree

2 files changed

+55
-12
lines changed

2 files changed

+55
-12
lines changed

src/ga_psbt.cpp

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,12 @@ namespace sdk {
184184
const auto policy_asset = net_params.get_policy_asset();
185185
const Tx tx(extract());
186186

187-
auto inputs = inputs_to_json(session, std::move(details.at("utxos")));
188-
auto outputs = outputs_to_json(session, tx);
187+
auto inputs_and_assets = inputs_to_json(session, std::move(details.at("utxos")));
188+
auto outputs = outputs_to_json(session, tx, inputs_and_assets.second);
189189
amount fee, fee_output;
190190
bool use_error = false;
191191
std::string error;
192-
for (const auto& txin : inputs) {
192+
for (const auto& txin : inputs_and_assets.first) {
193193
auto txin_error = j_str_or_empty(txin, "error");
194194
if (txin_error.empty()) {
195195
const auto asset_id = j_asset(net_params, txin);
@@ -217,8 +217,9 @@ namespace sdk {
217217
}
218218
// Calculated fee must match fee output for Liquid unless an error occurred
219219
GDK_RUNTIME_ASSERT(!m_is_liquid || fee == fee_output || !error.empty());
220-
nlohmann::json result = { { "transaction", tx.to_hex() }, { "transaction_inputs", std::move(inputs) },
221-
{ "transaction_outputs", std::move(outputs) } };
220+
nlohmann::json result
221+
= { { "transaction", tx.to_hex() }, { "transaction_inputs", std::move(inputs_and_assets.first) },
222+
{ "transaction_outputs", std::move(outputs) } };
222223
result["fee"] = m_is_liquid ? fee_output.value() : fee.value();
223224
result["network_fee"] = 0;
224225
update_tx_info(session, tx, result);
@@ -228,8 +229,11 @@ namespace sdk {
228229
return result;
229230
}
230231

231-
nlohmann::json Psbt::inputs_to_json(session_impl& session, nlohmann::json utxos) const
232+
std::pair<nlohmann::json, std::set<std::string>> Psbt::inputs_to_json(
233+
session_impl& session, nlohmann::json utxos) const
232234
{
235+
const auto& net_params = session.get_network_parameters();
236+
std::set<std::string> wallet_assets;
233237
nlohmann::json::array_t inputs;
234238
inputs.resize(get_num_inputs());
235239
for (size_t i = 0; i < inputs.size(); ++i) {
@@ -243,6 +247,7 @@ namespace sdk {
243247
utxo.erase("user_status");
244248
utxo_add_paths(session, utxo);
245249
input_utxo = std::move(utxo);
250+
wallet_assets.insert(j_asset(net_params, input_utxo));
246251
break;
247252
}
248253
}
@@ -276,17 +281,20 @@ namespace sdk {
276281
input_utxo["redeem_script"] = b2h(redeem_script.value());
277282
}
278283
}
279-
return inputs;
284+
return std::make_pair(std::move(inputs), std::move(wallet_assets));
280285
}
281286

282-
nlohmann::json Psbt::outputs_to_json(session_impl& session, const Tx& tx) const
287+
nlohmann::json Psbt::outputs_to_json(
288+
session_impl& session, const Tx& tx, const std::set<std::string>& wallet_assets) const
283289
{
284290
const auto& net_params = session.get_network_parameters();
291+
const bool is_electrum = net_params.is_electrum();
292+
std::set<std::string> spent_assets;
293+
std::map<std::string, std::vector<size_t>> asset_outputs;
285294

286295
nlohmann::json::array_t outputs;
287296
outputs.resize(get_num_outputs());
288297
for (size_t i = 0; i < outputs.size(); ++i) {
289-
// TODO: change identification
290298
const auto& txout = get_output(i);
291299
auto& jsonout = outputs[i];
292300
if (!m_is_liquid) {
@@ -323,7 +331,8 @@ namespace sdk {
323331
jsonout["scriptpubkey"] = b2h({ txout.script, txout.script_len });
324332
}
325333
auto output_data = session.get_scriptpubkey_data({ txout.script, txout.script_len });
326-
if (output_data.empty()) {
334+
const bool is_wallet_output = !output_data.empty();
335+
if (!is_wallet_output) {
327336
jsonout["address"] = get_address_from_scriptpubkey(net_params, { txout.script, txout.script_len });
328337
} else {
329338
if (m_is_liquid) {
@@ -345,6 +354,37 @@ namespace sdk {
345354
confidentialize_address(net_params, unconf_addr, jsonout.at("blinding_key"));
346355
jsonout["address"] = std::move(unconf_addr.at("address"));
347356
}
357+
// Change detection
358+
auto asset_id = j_asset(net_params, jsonout);
359+
if (wallet_assets.count(asset_id)) {
360+
if (!is_electrum) {
361+
// Multisig: Collect info to compute change below
362+
if (is_wallet_output) {
363+
asset_outputs[asset_id].push_back(i);
364+
} else {
365+
spent_assets.emplace(std::move(asset_id));
366+
}
367+
} else if (is_wallet_output) {
368+
// Singlesig: Outputs on the internal chain are change
369+
jsonout["is_change"] = j_bool_or_false(jsonout, "is_internal");
370+
}
371+
}
372+
}
373+
if (!is_electrum) {
374+
// Multisig change detection (heuristic)
375+
for (const auto& o : asset_outputs) {
376+
if (wallet_assets.count(o.first)) {
377+
// This is an asset that we contributed an input to
378+
const bool is_spent_externally = spent_assets.count(o.first) != 0;
379+
const auto num_wallet_outputs = o.second.size();
380+
if (is_spent_externally || num_wallet_outputs > 1) {
381+
// We sent this asset elsewhere and also to the wallet, or
382+
// we have multiple wallet outputs for the same asset.
383+
// Mark the first (possibly only) wallet output as change.
384+
outputs[o.second.front()]["is_change"] = true;
385+
}
386+
}
387+
}
348388
}
349389
return outputs;
350390
}

src/ga_psbt.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#pragma once
44

55
#include <memory>
6+
#include <set>
67
#include <string>
78

89
#include <nlohmann/json_fwd.hpp>
@@ -49,8 +50,10 @@ namespace sdk {
4950
const struct wally_psbt_output& get_output(size_t index) const;
5051

5152
private:
52-
nlohmann::json inputs_to_json(session_impl& session, nlohmann::json utxos) const;
53-
nlohmann::json outputs_to_json(session_impl& session, const Tx& tx) const;
53+
std::pair<nlohmann::json, std::set<std::string>> inputs_to_json(
54+
session_impl& session, nlohmann::json utxos) const;
55+
nlohmann::json outputs_to_json(
56+
session_impl& session, const Tx& tx, const std::set<std::string>& wallet_assets) const;
5457

5558
struct psbt_deleter {
5659
void operator()(struct wally_psbt* p);

0 commit comments

Comments
 (0)