psbt: add tx input and output fields in PSBTInput and PSBTOutput
What changed, and why it matters
This commit is a small internal refactor of how Bitcoin Core builds Partially Signed Bitcoin Transactions (PSBTs). It makes each PSBT input and output remember its corresponding transaction details (previous transaction ID, output index, sequence number, amount, and script) directly, instead of only storing them inside the global unsigned transaction. The change prepares the code for a future PSBT version (PSBTv2) where these fields are stored separately. There is no obvious security bug in the diff itself, but it touches serialization and RPC code paths that handle user-provided transaction data.
Review as normal code maintenance. Verify that the new PSBTInput/PSBTOutput fields are kept consistent with the global unsigned tx during future merges and PSBTv2 serialization, and ensure fuzz/serialization round-trips still cover edge cases such as empty vin/vout.
Security signals we found
Constructor signature change in PSBTInput/PSBTOutput now requires transaction-derived fields
Deserialization path creates PSBTInput/PSBTOutput using data from the global unsigned tx
RPC createpsbt/converttopsbt now rely on a single constructor instead of manual vector population
No bounds checks or validation logic added for the newly stored fields
No explicit security claim or bug fix in commit message
Evidence from the diff
The patch modifies PSBTInput and PSBTOutput constructors to require and store prev_txid/prev_out/sequence and amount/script respectively. It updates PartiallySignedTransaction construction to populate these from the global CMutableTransaction, and adjusts PSBT deserialization to pass the matching global tx fields when creating inputs/outputs. Two RPC methods (createpsbt and converttopsbt) are simplified to use the new constructor. Fuzz targets are updated to supply dummy values. The change is preparatory for PSBTv2 and does not alter consensus rules or network behavior.
Changed components
src/psbt.cppsrc/psbt.hsrc/rpc/rawtransaction.cppsrc/test/fuzz/deserialize.cppInspect captured patch +30 / −26
diff --git a/src/psbt.cpp b/src/psbt.cpp
index 416eae11..29f16e73 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -15,8 +15,14 @@ using common::PSBTError;
PartiallySignedTransaction::PartiallySignedTransaction(const CMutableTransaction& tx) : tx(tx)
{
- inputs.resize(tx.vin.size(), PSBTInput(GetVersion()));
- outputs.resize(tx.vout.size(), PSBTOutput(GetVersion()));
+ inputs.reserve(tx.vin.size());
+ for (const CTxIn& input : tx.vin) {
+ inputs.emplace_back(GetVersion(), input.prevout.hash, input.prevout.n, input.nSequence);
+ }
+ outputs.reserve(tx.vout.size());
+ for (const CTxOut& output : tx.vout) {
+ outputs.emplace_back(GetVersion(), output.nValue, output.scriptPubKey);
+ }
}
bool PartiallySignedTransaction::IsNull() const
diff --git a/src/psbt.h b/src/psbt.h
index 4272cd3a..dcf0d6bd 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -280,6 +280,10 @@ public:
std::map<uint160, std::vector<unsigned char>> hash160_preimages;
std::map<uint256, std::vector<unsigned char>> hash256_preimages;
+ Txid prev_txid;
+ uint32_t prev_out;
+ std::optional<uint32_t> sequence;
+
// Taproot fields
std::vector<unsigned char> m_tap_key_sig;
std::map<std::pair<XOnlyPubKey, uint256>, std::vector<unsigned char>> m_tap_script_sigs;
@@ -304,8 +308,11 @@ public:
void FromSignatureData(const SignatureData& sigdata);
void Merge(const PSBTInput& input);
uint32_t GetVersion() const { return m_psbt_version; }
- explicit PSBTInput(uint32_t psbt_version)
- : m_psbt_version(psbt_version)
+ explicit PSBTInput(uint32_t psbt_version, const Txid& prev_txid, uint32_t prev_out, std::optional<uint32_t> sequence = std::nullopt)
+ : m_psbt_version(psbt_version),
+ prev_txid(prev_txid),
+ prev_out(prev_out),
+ sequence(sequence)
{
assert(m_psbt_version == 0);
}
@@ -816,13 +823,18 @@ public:
std::map<std::vector<unsigned char>, std::vector<unsigned char>> unknown;
std::set<PSBTProprietary> m_proprietary;
+ CAmount amount;
+ CScript script;
+
bool IsNull() const;
void FillSignatureData(SignatureData& sigdata) const;
void FromSignatureData(const SignatureData& sigdata);
void Merge(const PSBTOutput& output);
uint32_t GetVersion() const { return m_psbt_version; }
- explicit PSBTOutput(uint32_t psbt_version)
- : m_psbt_version(psbt_version)
+ explicit PSBTOutput(uint32_t psbt_version, CAmount amount, const CScript& script)
+ : m_psbt_version(psbt_version),
+ amount(amount),
+ script(script)
{
assert(m_psbt_version == 0);
}
@@ -1264,7 +1276,7 @@ public:
// Read input data
unsigned int i = 0;
while (!s.empty() && i < tx->vin.size()) {
- PSBTInput input(psbt_ver);
+ PSBTInput input(psbt_ver, tx->vin[i].prevout.hash, tx->vin[i].prevout.n, tx->vin[i].nSequence);
s >> input;
inputs.push_back(input);
@@ -1287,7 +1299,7 @@ public:
// Read output data
i = 0;
while (!s.empty() && i < tx->vout.size()) {
- PSBTOutput output(psbt_ver);
+ PSBTOutput output(psbt_ver, tx->vout[i].nValue, tx->vout[i].scriptPubKey);
s >> output;
outputs.push_back(output);
++i;
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
index 67067110..517a02bf 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -1649,14 +1649,7 @@ static RPCMethod createpsbt()
CMutableTransaction rawTx = ConstructTransaction(request.params[0], request.params[1], request.params[2], rbf, self.Arg<uint32_t>("version"));
// Make a blank psbt
- PartiallySignedTransaction psbtx;
- psbtx.tx = rawTx;
- for (unsigned int i = 0; i < rawTx.vin.size(); ++i) {
- psbtx.inputs.emplace_back(0);
- }
- for (unsigned int i = 0; i < rawTx.vout.size(); ++i) {
- psbtx.outputs.emplace_back(0);
- }
+ PartiallySignedTransaction psbtx(rawTx);
// Serialize the PSBT
DataStream ssTx{};
@@ -1717,14 +1710,7 @@ static RPCMethod converttopsbt()
}
// Make a blank psbt
- PartiallySignedTransaction psbtx;
- psbtx.tx = tx;
- for (unsigned int i = 0; i < tx.vin.size(); ++i) {
- psbtx.inputs.emplace_back(0);
- }
- for (unsigned int i = 0; i < tx.vout.size(); ++i) {
- psbtx.outputs.emplace_back(0);
- }
+ PartiallySignedTransaction psbtx(tx);
// Serialize the PSBT
DataStream ssTx{};
diff --git a/src/test/fuzz/deserialize.cpp b/src/test/fuzz/deserialize.cpp
index b3f35baa..15150c4a 100644
--- a/src/test/fuzz/deserialize.cpp
+++ b/src/test/fuzz/deserialize.cpp
@@ -192,11 +192,11 @@ FUZZ_TARGET_DESERIALIZE(prefilled_transaction_deserialize, {
DeserializeFromFuzzingInput(buffer, prefilled_transaction);
})
FUZZ_TARGET_DESERIALIZE(psbt_input_deserialize, {
- PSBTInput psbt_input(0);
+ PSBTInput psbt_input(0, Txid{}, 0);
DeserializeFromFuzzingInput(buffer, psbt_input);
})
FUZZ_TARGET_DESERIALIZE(psbt_output_deserialize, {
- PSBTOutput psbt_output(0);
+ PSBTOutput psbt_output(0, 0, CScript());
DeserializeFromFuzzingInput(buffer, psbt_output);
})
FUZZ_TARGET_DESERIALIZE(block_deserialize, {
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.