What changed, and why it matters
This commit adds a helper function that calculates when a Bitcoin transaction becomes valid based on time or block height locks inside a Partially Signed Bitcoin Transaction (PSBT). It also makes the existing unsigned-transaction builder use this new calculation instead of always using a simple fallback value. The change appears to be a correctness improvement for PSBT version 2 locktime handling, not an obvious security fix, but it prevents a PSBT from being turned into a transaction when its inputs disagree about whether the lock is based on time or block height.
Review as part of normal PSBTv2 correctness work. Verify that `ComputeTimeLock()` matches BIP370 semantics, especially the interaction between mixed time/height locks and the fallback locktime. No immediate security patch appears required, but downstream callers that rely on `GetUnsignedTx()` should be checked for changed behavior when PSBTv2 inputs contain locktime fields.
Security signals we found
New validation logic that rejects inconsistent PSBTv2 locktime fields
Change from unconditional fallback locktime to computed/validated locktime
Potential denial-of-construction if conflicting locktime types are present
Evidence from the diff
The patch introduces PartiallySignedTransaction::ComputeTimeLock(). For PSBT version 2, it inspects each input’s optional time_locktime and height_locktime. If any input has only a time lock and none has a height lock, the result is the maximum time lock; conversely for height locks. If inputs mix the two types, or if no lock can be determined, it returns std::nullopt. GetUnsignedTx() now calls ComputeTimeLock() and fails if it returns no value. This prevents constructing an unsigned transaction with an inconsistent or invalid locktime derived from conflicting PSBTv2 input fields.
Changed components
src/psbt.cppsrc/psbt.hPartiallySignedTransaction::GetUnsignedTx()PSBT version 2 locktime handlingInspect captured patch +40 / −1
diff --git a/src/psbt.cpp b/src/psbt.cpp
index a8707961..6109d943 100644
--- a/src/psbt.cpp
+++ b/src/psbt.cpp
@@ -61,11 +61,49 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt)
return true;
}
+std::optional<uint32_t> PartiallySignedTransaction::ComputeTimeLock() const
+{
+ if (GetVersion() >= 2) {
+ std::optional<uint32_t> time_lock{0};
+ std::optional<uint32_t> height_lock{0};
+ for (const PSBTInput& input : inputs) {
+ if (input.time_locktime.has_value() && !input.height_locktime.has_value()) {
+ height_lock.reset(); // Transaction can no longer have a height locktime
+ if (!time_lock.has_value()) {
+ return std::nullopt;
+ }
+ } else if (!input.time_locktime.has_value() && input.height_locktime.has_value()) {
+ time_lock.reset(); // Transaction can no longer have a time locktime
+ if (!height_lock.has_value()) {
+ return std::nullopt;
+ }
+ }
+ if (input.time_locktime && time_lock.has_value()) {
+ time_lock = std::max(time_lock, input.time_locktime);
+ }
+ if (input.height_locktime && height_lock.has_value()) {
+ height_lock = std::max(height_lock, input.height_locktime);
+ }
+ }
+ if (height_lock.has_value() && *height_lock > 0) {
+ return *height_lock;
+ }
+ if (time_lock.has_value() && *time_lock > 0) {
+ return *time_lock;
+ }
+ }
+ return fallback_locktime.value_or(0);
+}
+
std::optional<CMutableTransaction> PartiallySignedTransaction::GetUnsignedTx() const
{
CMutableTransaction mtx;
mtx.version = tx_version;
- mtx.nLockTime = fallback_locktime.value_or(0);
+ std::optional<uint32_t> locktime = ComputeTimeLock();
+ if (!locktime) {
+ return std::nullopt;
+ }
+ mtx.nLockTime = *locktime;
uint32_t max_sequence = CTxIn::SEQUENCE_FINAL;
for (const PSBTInput& input : inputs) {
CTxIn txin;
diff --git a/src/psbt.h b/src/psbt.h
index f46e625b..efb1ee1f 100644
--- a/src/psbt.h
+++ b/src/psbt.h
@@ -1256,6 +1256,7 @@ public:
[[nodiscard]] bool Merge(const PartiallySignedTransaction& psbt);
bool AddInput(const PSBTInput& psbtin);
bool AddOutput(const PSBTOutput& psbtout);
+ std::optional<uint32_t> ComputeTimeLock() const;
std::optional<CMutableTransaction> GetUnsignedTx() const;
std::optional<Txid> GetUniqueID() const;
explicit PartiallySignedTransaction(const CMutableTransaction& tx);
Why this scored 27/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.