rpc: Merge joinpsbts locktimes correctly #35100

pull nervana21 wants to merge 1 commits into bitcoin:master from nervana21:20260416_locktime changing 2 files +63 −8
  1. nervana21 commented at 6:12 PM on April 17, 2026: contributor

    joinpsbts previously took the minimum nLockTime across the PSBTs being merged, so the combined transaction could be minable earlier than the strictest party’s locktime. A merged PSBT should use the maximum nLockTime amongst the PSBTs that have at least one input with nSequence != SEQUENCE_FINAL.

    PSBTs whose inputs are all SEQUENCE_FINAL do not constrain the locktime.

    This PR implements the fix, updates RPC documentation, and extends rpc_psbt tests.

  2. DrahtBot added the label RPC/REST/ZMQ on Apr 17, 2026
  3. DrahtBot commented at 6:13 PM on April 17, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35100.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK w0xlt
    Concept ACK rkrux
    Stale ACK ViniciusCestarii

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. in src/rpc/rawtransaction.cpp:1835 in 0b00625fc8 outdated
    1833 | +        const bool locktime_enabled = std::any_of(psbtx.tx->vin.begin(), psbtx.tx->vin.end(), [](const CTxIn& txin) {
    1834 | +            return txin.nSequence != CTxIn::SEQUENCE_FINAL;
    1835 | +        });
    1836 | +        if (locktime_enabled && (!has_locktime_constraint || psbtx.tx->nLockTime > max_locktime)) {
    1837 | +            max_locktime = psbtx.tx->nLockTime;
    1838 | +            has_locktime_constraint = true;
    


    Bicaru20 commented at 5:47 PM on April 20, 2026:

    I don't understand why you need the has_locktime_constarint. If I am not wrong, remvoing it would not alter the logic of the if as it would still enter always that the current locktime is higher. I would suggest to eliminate it:

            // Choose the highest lock time
            if (locktime_enabled && psbtx.tx->nLockTime > max_locktime) {
                max_locktime = psbtx.tx->nLockTime;
    

    nervana21 commented at 8:26 PM on April 22, 2026:

    has_locktime_constraint is meant to be based on the logic found here:

    https://github.com/bitcoin/bitcoin/blob/744d47fcee0d32a71154292699bfdecf954a6065/src/consensus/tx_verify.cpp#L24-L35

    It’s my understanding that not all inputs’ locktimes should contribute to the overall transaction locktime constraint. Specifically, if all inputs have nSequence set to SEQUENCE_FINAL, then nLockTime is ignored. So, in joinpsbts, I think we should only consider a PSBT’s nLockTime if it has at least one input with nSequence != SEQUENCE_FINAL.

    Does that make sense, or am I still overlooking something?


    Bicaru20 commented at 3:56 PM on April 23, 2026:

    Yes, if in al the inputs the nSequence is set to SEQUENCE_FINAL the locktime shouldn't be considered. However, I do not see how the variable has_locktime_constraint helps you checks this. This is done in locktime_enabled, which checks if at least one inputs doesn't have SEQUENCE_FINAL.

    My logic tells me that has_locktime_constraint will just be needed the first time this condition is evaluated:

     if (locktime_enabled && (!has_locktime_constraint || psbtx.tx->nLockTime > max_locktime)) {
    

    The locktime_enabled is needed since it is checking that at least one input has SEQUENCE_FINAL.

    The !has_locktime_constraint will always be true the first time since it is initialized to false and then it is changed to true after the first valid assignment.

    On the other hand, the condition psbtx.tx->nLockTime > max_locktime will also typically be true on the first iteration, since max_locktime is initialized to 0. Therefore, almost any locktime will satisfy it. But even if the LockTime = 0, the behavior remains consistent, since max_locktime would remain unchanged.

    What I am trying to say is that has_locktime_constraint is redundant since usually psbtx.tx->nLockTime > max_locktime will also usually be staisfied. And even when it is not statifaied (Locktime = 0), there is no need to enter in the if.

    I hope I explained myself clearly


    nervana21 commented at 4:20 PM on April 27, 2026:

    Okay, I see. I've updated the code.

  5. in test/functional/rpc_psbt.py:932 in 0b00625fc8
     923 | @@ -923,6 +924,29 @@ def test_psbt_input_keys(psbt_input, keys):
     924 |          assert "final_scriptwitness" not in joined_decoded['inputs'][3]
     925 |          assert "final_scriptSig" not in joined_decoded['inputs'][3]
     926 |  
     927 | +        # Check that joinpsbts ignores locktime when all inputs use SEQUENCE_FINAL
     928 | +        addr5 = self.nodes[1].getnewaddress("", "bech32m")
     929 | +        utxo5 = self.create_outpoints(self.nodes[0], outputs=[{addr5: 5}])[0]
     930 | +        self.generate(self.nodes[0], 6)
     931 | +        vin = [{"txid": utxo4["txid"], "vout": utxo4["vout"], "sequence": SEQUENCE_FINAL}]
     932 | +        psbt_50 = self.nodes[1].createpsbt(vin, {self.nodes[0].getnewaddress():Decimal('4.999')}, locktime=50)
    


    Bicaru20 commented at 6:08 PM on April 20, 2026:

    Even if the sequence was valid, the psbt with locktime= 50 would be ignore as the maximum is 200. I think it is better to put a higher locktime than 200 to show the locktime is ignored

            psbt_250 = self.nodes[1].createpsbt(vin, {self.nodes[0].getnewaddress():Decimal('4.999')}, locktime=250)
    

    nervana21 commented at 8:25 PM on April 22, 2026:

    taken, thanks!

  6. Bicaru20 commented at 8:41 PM on April 20, 2026: none

    Left some comment son the code.

    Still, I think it should be discuss how to approach the case where we have a height-based timelock and time-based timelock. If I am not wrong, it is considered height-based if 0 < locktime < 500_000_000, and if it is higher than that it is time-based.

    With the current approach, if we had a locktime of each type, the time-based would always be the max_locktime, whether or not the height-based is older. I am not sure if this should be the expected behavior, or if it is at least better than what we currently have, which selects the min_locktime.

  7. nervana21 commented at 8:54 PM on April 22, 2026: contributor

    With the current approach, if we had a locktime of each type, the time-based would always be the max_locktime, whether or not the height-based is older. I am not sure if this should be the expected behavior, or if it is at least better than what we currently have, which selects the min_locktime.

    Thanks for pointing this out. Yea, with respect to this issue, I think it would make the most sense if we never tried to compare height-based locktimes and time-based locktimes (only compare "apples to apples"). I'll look more for examples of how the is handled elsewhere in the codebase.

  8. nervana21 force-pushed on Apr 22, 2026
  9. Bicaru20 commented at 4:03 PM on April 23, 2026: none

    Thanks for pointing this out. Yea, with respect to this issue, I think it would make the most sense if we never tried to compare height-based locktimes and time-based locktimes (only compare "apples to apples"). I'll look more for examples of how the is handled elsewhere in the codebase.

    It may be enough for now since there is almost no usage of time-based locktimes (you can check the mainnet explorer to see the exact %). And it would be even less the % of psbt convined with diferent locktime. However, I think that ideally we should think a way to handel this cases, somthing like make the user choose one of the locktimes or set a new one, or maybe even set it to 0. I am not really sure which one would be the best option.

  10. GerardoTaboada commented at 3:52 PM on April 25, 2026: contributor

    Yeah, the old min-locktime behavior was a real footgun. Anyone who signed first with a strict locktime would silently get overridden by whoever signed last with a looser one, which kind of defeats the whole point. Glad to see this fixed.

    A couple of things on top of what @Bicaru20 already raised:

    Agree that has_locktime_constraint is doing nothing once max_locktime starts at 0 and the locktime-enabled gate already filters out the all-SEQUENCE_FINAL PSBTs. The plain > comparison covers the first iteration just as well.

    The height-vs-time locktime thing is worth discussing more before settling. A single tx only has one nLockTime field with one interpretation, so if some incoming PSBTs are height-based and others are time-based there's no merged value that respects everyone's intent. Picking the numeric max can produce a tx that semantically locks none of the original parties the way they expected. Not sure what the cleanest answer is, could be rejecting the join, could be something else, but it feels worth nailing down before this lands rather than leaving the behavior implicit.

    One test case I'd add: all PSBTs have every input as SEQUENCE_FINAL, so nobody contributes a constraint and the merged tx should come out with nLockTime = 0. The current tests cover the "one is final-only, one isn't" case but not the "all final-only" one, and that's the path where max_locktime stays at its initial value, so it'd be nice to lock that down.

    Tiny doc nit: the new help line is accurate but a reader who doesn't already know the rule might not get why SEQUENCE_FINAL matters. Tagging on something like "(PSBTs with all inputs final do not constrain the locktime)" would make the help readable on its own.

  11. nervana21 commented at 4:23 PM on April 27, 2026: contributor

    @GerardoTaboada Thanks for the feedback!

    I've updated the code to address your concerns/suggestions. This implementation disallows joining pbsts with different locktime types.

  12. nervana21 force-pushed on Apr 27, 2026
  13. ViniciusCestarii commented at 7:48 PM on April 28, 2026: contributor

    tACK 077457b4103038cff8e781e7b09573ad73cbaa70 built and ran new functional tests and tried a single PSBT with mixed inputs (one SEQUENCE_FINAL, one non-final) and a time-based nLockTime, joined with a height-based PSBT.

  14. Bicaru20 commented at 6:06 PM on May 4, 2026: none

    This implementation disallows joining pbsts with different locktime types.

    I'm still not conviced this is the right choice, specially if the current behavior is to allow psbts with diferent locktimes to be joined. For this case, I think it would be better to leave the psbt to 0 or leave the lowest locktime (as we do rigth now) and maybe throw a warning message so the user can see two different lockitmes are present in the psbts.

    It would be nice to hear other people opinion on this.

  15. ViniciusCestarii commented at 8:11 PM on May 4, 2026: contributor

    There's precedent for this RPC to throw an error when something is ambiguous:

    joinpsbts does this on duplicate inputs rawtransaction.cpp: two PSBTs claiming the same UTXO is a real conflict the RPC could paper over by dropping one, but it throws instead. Mixed height/time locktimes is the same shape of conflict in the same RPC, so rejecting is consistent.

    It's about not producing a PSBT that can violate someone's explicit constraint.

    Rejection forces the caller to confront the conflict explicitly and resolve it.

    That said, the breaking change concern is valid. My intuition is that few callers actually rely on the current behavior of silently picking the lower locktime but I can't back that up with data, and I genuinely don't know what the project's bar is for a breaking change of this kind.

  16. DrahtBot added the label Needs rebase on May 5, 2026
  17. rpc: Merge joinpsbts locktimes correctly
    A merged PSBT should use the maximum `nLockTime` amongst the PSBTs that
    have at least one input with `nSequence` != `SEQUENCE_FINAL`. PSBTs
    whose inputs are all `SEQUENCE_FINAL` do not constrain the locktime.
    
    Document this in the help text and extend rpc_psbt tests.
    5c7a61a4b0
  18. nervana21 force-pushed on May 17, 2026
  19. nervana21 commented at 10:12 PM on May 17, 2026: contributor

    Updated to rebase off master and changed the structure of functional tests. I think this approach is easier to follow and best allows for modification if a different behavior is eventually chosen.

  20. DrahtBot removed the label Needs rebase on May 17, 2026
  21. in test/functional/rpc_psbt.py:1045 in 5c7a61a4b0
    1040 | +                joined_lt_decoded = self.nodes[0].decodepsbt(joined_lt)
    1041 | +                assert_equal(joined_lt_decoded['tx']['locktime'], expected_locktime)
    1042 | +
    1043 | +        psbt_150 = make_locktime_psbt(150, locktime=150)
    1044 | +        psbt_200 = make_locktime_psbt(200, locktime=200)
    1045 | +        psbt_250_seq_final = make_locktime_psbt(250, locktime=250, sequence=SEQUENCE_FINAL)
    


    w0xlt commented at 7:58 PM on May 20, 2026:

    Test for non-final nLockTime = 0

    <details> <summary>diff</summary>

    diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
    index f441290210..55144be83c 100755
    --- a/test/functional/rpc_psbt.py
    +++ b/test/functional/rpc_psbt.py
    @@ -1042,6 +1042,7 @@ class PSBTTest(BitcoinTestFramework):
     
             psbt_150 = make_locktime_psbt(150, locktime=150)
             psbt_200 = make_locktime_psbt(200, locktime=200)
    +        psbt_0 = make_locktime_psbt(0, locktime=0)
             psbt_250_seq_final = make_locktime_psbt(250, locktime=250, sequence=SEQUENCE_FINAL)
             psbt_300_seq_final = make_locktime_psbt(300, locktime=300, sequence=SEQUENCE_FINAL)
             psbt_time = make_locktime_psbt(LOCKTIME_THRESHOLD, locktime=LOCKTIME_THRESHOLD)
    @@ -1059,6 +1060,9 @@ class PSBTTest(BitcoinTestFramework):
             # joinpsbts uses the maximum time-based locktime
             assert_joined_locktime([psbt_time, psbt_later_time], expected_locktime=LOCKTIME_THRESHOLD + 1)
     
    +        # joinpsbts ignores zero locktime even when nSequence is non-final
    +        assert_joined_locktime([psbt_0, psbt_time], expected_locktime=LOCKTIME_THRESHOLD)
    +
             # joinpsbts ignores SEQUENCE_FINAL locktimes before rejecting mixed locktime types
             assert_joined_locktime([psbt_250_seq_final, psbt_time], expected_locktime=LOCKTIME_THRESHOLD)
    

    </details>

  22. in test/functional/rpc_psbt.py:1068 in 5c7a61a4b0
    1063 | +        assert_joined_locktime([psbt_250_seq_final, psbt_time], expected_locktime=LOCKTIME_THRESHOLD)
    1064 | +
    1065 | +        # joinpsbts rejects mixed height-based and time-based locktimes
    1066 | +        assert_raises_rpc_error(-8, "Cannot join PSBTs with mixed height-based and time-based locktimes", self.nodes[0].joinpsbts, [psbt_150, psbt_time])
    1067 | +        assert_raises_rpc_error(-8, "Cannot join PSBTs with mixed height-based and time-based locktimes", self.nodes[0].joinpsbts, [psbt_time, psbt_150])
    1068 | +
    


    w0xlt commented at 7:58 PM on May 20, 2026:

    The CLTV regression test from #35326 can be added here too.

    <details> <summary>diff</summary>

    diff --git a/test/functional/rpc_psbt.py b/test/functional/rpc_psbt.py
    index f441290210..b5eddfd727 100755
    --- a/test/functional/rpc_psbt.py
    +++ b/test/functional/rpc_psbt.py
    @@ -5,9 +5,11 @@
     """Test the Partially Signed Transaction RPCs.
     """
     from decimal import Decimal
    +from io import BytesIO
     from itertools import product
     from random import randbytes
     
    +from test_framework.address import script_to_p2wsh
     from test_framework.blocktools import (
         MAX_STANDARD_TX_WEIGHT,
     )
    @@ -17,6 +19,7 @@ from test_framework.messages import (
         COutPoint,
         CTransaction,
         CTxIn,
    +    CTxInWitness,
         CTxOut,
         MAX_BIP125_RBF_SEQUENCE,
         SEQUENCE_FINAL,
    @@ -41,7 +44,15 @@ from test_framework.psbt import (
         PSBT_OUT_TAP_TREE,
         PSBT_OUT_SCRIPT,
     )
    -from test_framework.script import CScript, LOCKTIME_THRESHOLD, OP_TRUE, SIGHASH_ALL, SIGHASH_ANYONECANPAY
    +from test_framework.script import (
    +    CScript,
    +    LOCKTIME_THRESHOLD,
    +    OP_CHECKLOCKTIMEVERIFY,
    +    OP_DROP,
    +    OP_TRUE,
    +    SIGHASH_ALL,
    +    SIGHASH_ANYONECANPAY,
    +)
     from test_framework.script_util import MIN_STANDARD_TX_NONWITNESS_SIZE
     from test_framework.test_framework import BitcoinTestFramework
     from test_framework.util import (
    @@ -470,6 +481,56 @@ class PSBTTest(BitcoinTestFramework):
             assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].converttopsbt, hexstring=rawtx, psbt_version=1)
             assert_raises_rpc_error(-8, "The PSBT version can only be 2 or 0", self.nodes[0].psbtbumpfee, txid=tobump, psbt_version=1)
     
    +    def test_joinpsbts_cltv_locktime(self):
    +        self.log.info("Test that joining PSBTs preserves the higher CLTV height locktime")
    +        node = self.nodes[0]
    +        amount = Decimal("1.0")
    +        spend_amount = Decimal("0.999")
    +        sequence = 0xfffffffe
    +        current_height = node.getblockcount()
    +        locktimes = [current_height - 2, current_height - 1]
    +
    +        witness_scripts = [
    +            CScript([locktime, OP_CHECKLOCKTIMEVERIFY, OP_DROP])
    +            for locktime in locktimes
    +        ]
    +        utxos = self.create_outpoints(node, outputs=[
    +            {script_to_p2wsh(witness_script): amount}
    +            for witness_script in witness_scripts
    +        ])
    +        self.generate(node, 1)
    +
    +        psbts = [
    +            node.createpsbt(
    +                inputs=[{**utxo, "sequence": sequence}],
    +                outputs=[{node.getnewaddress(): spend_amount}],
    +                locktime=locktime,
    +                replaceable=False,
    +                psbt_version=0,
    +            )
    +            for utxo, locktime in zip(utxos, locktimes)
    +        ]
    +
    +        joined = node.joinpsbts(psbts)
    +        joined_psbt = PSBT.from_base64(joined)
    +        joined_tx = CTransaction()
    +        joined_tx.deserialize(BytesIO(joined_psbt.g.map[PSBT_GLOBAL_UNSIGNED_TX]))
    +        assert_equal(joined_tx.nLockTime, max(locktimes))
    +
    +        # joinpsbts may reorder inputs, so attach each CLTV witness script by prevout.
    +        scripts_by_prevout = {
    +            (int(utxo["txid"], 16), utxo["vout"]): witness_script
    +            for utxo, witness_script in zip(utxos, witness_scripts)
    +        }
    +        joined_tx.wit.vtxinwit = [CTxInWitness() for _ in joined_tx.vin]
    +        for i, txin in enumerate(joined_tx.vin):
    +            joined_tx.wit.vtxinwit[i].scriptWitness.stack = [
    +                CScript([OP_TRUE]),
    +                scripts_by_prevout[(txin.prevout.hash, txin.prevout.n)],
    +            ]
    +        test_accept = node.testmempoolaccept([joined_tx.serialize().hex()], maxfeerate=0)[0]
    +        assert_equal(test_accept["allowed"], True)
    +
         def run_test(self):
             # Create and fund a raw tx for sending 10 BTC
             psbtx1 = self.nodes[0].walletcreatefundedpsbt([], {self.nodes[2].getnewaddress():10})['psbt']
    @@ -1065,6 +1126,7 @@ class PSBTTest(BitcoinTestFramework):
             # joinpsbts rejects mixed height-based and time-based locktimes
             assert_raises_rpc_error(-8, "Cannot join PSBTs with mixed height-based and time-based locktimes", self.nodes[0].joinpsbts, [psbt_150, psbt_time])
             assert_raises_rpc_error(-8, "Cannot join PSBTs with mixed height-based and time-based locktimes", self.nodes[0].joinpsbts, [psbt_time, psbt_150])
    +        self.test_joinpsbts_cltv_locktime()
     
             # Check that joining shuffles the inputs and outputs
             # 10 attempts should be enough to get a shuffled join
    

    </details>


    rkrux commented at 1:11 PM on May 21, 2026:

    This is a good test case, please add it in a separate commit.

  23. w0xlt commented at 7:59 PM on May 20, 2026: contributor

    ACK 5c7a61a4b094232bf4c44c4a75afa75524ca4ad2 mod some suggestions.

  24. in src/rpc/rawtransaction.cpp:1911 in 5c7a61a4b0
    1910 | -        uint32_t psbt_locktime = psbtx.fallback_locktime.value_or(0);
    1911 | -        if (psbt_locktime < best_locktime) {
    1912 | -            best_locktime = psbt_locktime;
    1913 | +        // If every input has nSequence == SEQUENCE_FINAL, nLockTime is ignored
    1914 | +        const bool locktime_enabled = std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), [](const PSBTInput& input) {
    1915 | +            const uint32_t sequence_final = CTxIn::SEQUENCE_FINAL;
    


    rkrux commented at 12:41 PM on May 21, 2026:

    This assignment seems redundant, the following line can use the constant directly.

  25. in src/rpc/rawtransaction.cpp:1910 in 5c7a61a4b0
    1905 | @@ -1903,17 +1906,25 @@ static RPCMethod joinpsbts()
    1906 |          if (psbtx.tx_version > best_version) {
    1907 |              best_version = psbtx.tx_version;
    1908 |          }
    1909 | -        // Choose the lowest lock time
    1910 | -        uint32_t psbt_locktime = psbtx.fallback_locktime.value_or(0);
    1911 | -        if (psbt_locktime < best_locktime) {
    1912 | -            best_locktime = psbt_locktime;
    1913 | +        // If every input has nSequence == SEQUENCE_FINAL, nLockTime is ignored
    1914 | +        const bool locktime_enabled = std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), [](const PSBTInput& input) {
    


    rkrux commented at 12:41 PM on May 21, 2026:

    Since it's a bool, s/locktime_enabled/is_locktime_enabled.

  26. in src/rpc/rawtransaction.cpp:1914 in 5c7a61a4b0
    1913 | +        // If every input has nSequence == SEQUENCE_FINAL, nLockTime is ignored
    1914 | +        const bool locktime_enabled = std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), [](const PSBTInput& input) {
    1915 | +            const uint32_t sequence_final = CTxIn::SEQUENCE_FINAL;
    1916 | +            return input.sequence.value_or(sequence_final) != sequence_final;
    1917 | +        });
    1918 | +        const uint32_t psbt_locktime = psbtx.fallback_locktime.value_or(0);
    


    rkrux commented at 12:43 PM on May 21, 2026:

    The default for optional is being assigned too early and being used in the condition below. For presence type checks, the has_value() notation reads better imho.

  27. in src/rpc/rawtransaction.cpp:1894 in 5c7a61a4b0
    1890 | @@ -1888,7 +1891,7 @@ static RPCMethod joinpsbts()
    1891 |      }
    1892 |  
    1893 |      uint32_t best_version = 1;
    1894 | -    uint32_t best_locktime = 0xffffffff;
    1895 | +    uint32_t max_locktime = 0;
    


    rkrux commented at 12:44 PM on May 21, 2026:

    Same for this variable as well - the sentinel value is used where an optional can be helpful for readability.

  28. in test/functional/rpc_psbt.py:1026 in 5c7a61a4b0
    1022 | @@ -1022,6 +1023,49 @@ def test_psbt_input_keys(psbt_input, keys):
    1023 |          assert "final_scriptwitness" not in joined_decoded['inputs'][3]
    1024 |          assert "final_scriptSig" not in joined_decoded['inputs'][3]
    1025 |  
    1026 | +        def make_locktime_psbt(prevout_id, locktime, sequence=0):
    


    rkrux commented at 12:48 PM on May 21, 2026:

    This is long file and the run_test function spans over 1000 lines including this diff!

    Let's avoid adding more code directly in this function and split out the functionality in a function named test_joinpsbts like few functions at the end of run_test do. If inclined, the existing 4 call sites of joinpsbts can also go in there.

  29. rkrux changes_requested
  30. rkrux commented at 12:55 PM on May 21, 2026: contributor

    Concept ACK 5c7a61a4b094232bf4c44c4a75afa75524ca4ad2 ISTM that it's correct to use the max locktime of the psbts being joined.

    Combined diff for the rawtransaction.cpp suggestions:

    diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
    index 4b3024a942..e27ceec0fd 100644
    --- a/src/rpc/rawtransaction.cpp
    +++ b/src/rpc/rawtransaction.cpp
    @@ -1891,7 +1891,7 @@ static RPCMethod joinpsbts()
         }
     
         uint32_t best_version = 1;
    -    uint32_t max_locktime = 0;
    +    std::optional<uint32_t> max_locktime = std::nullopt;
         for (unsigned int i = 0; i < txs.size(); ++i) {
             util::Result<PartiallySignedTransaction> psbt_res = DecodeBase64PSBT(txs[i].get_str());
             if (!psbt_res) {
    @@ -1907,24 +1907,23 @@ static RPCMethod joinpsbts()
                 best_version = psbtx.tx_version;
             }
             // If every input has nSequence == SEQUENCE_FINAL, nLockTime is ignored
    -        const bool locktime_enabled = std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), [](const PSBTInput& input) {
    -            const uint32_t sequence_final = CTxIn::SEQUENCE_FINAL;
    -            return input.sequence.value_or(sequence_final) != sequence_final;
    +        const bool is_locktime_enabled = std::any_of(psbtx.inputs.begin(), psbtx.inputs.end(), [](const PSBTInput& input) {
    +            return input.sequence.value_or(CTxIn::SEQUENCE_FINAL) != CTxIn::SEQUENCE_FINAL;
             });
    -        const uint32_t psbt_locktime = psbtx.fallback_locktime.value_or(0);
    -        if (locktime_enabled && psbt_locktime != 0) {
    -            const bool is_time_locktime{psbt_locktime >= LOCKTIME_THRESHOLD};
    -            if (max_locktime != 0 && (max_locktime >= LOCKTIME_THRESHOLD) != is_time_locktime) {
    +        auto psbt_locktime = psbtx.fallback_locktime;
    +        if (is_locktime_enabled && psbt_locktime.has_value()) {
    +            const bool is_time_locktime{*psbt_locktime >= LOCKTIME_THRESHOLD};
    +            if (max_locktime.has_value() && (*max_locktime >= LOCKTIME_THRESHOLD) != is_time_locktime) {
                     throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot join PSBTs with mixed height-based and time-based locktimes");
                 }
    -            max_locktime = std::max(max_locktime, psbt_locktime);
    +            max_locktime = std::max(*max_locktime, *psbt_locktime);
             }
         }
     
         // Create a blank psbt where everything will be added
         CMutableTransaction tx;
         tx.version = best_version;
    -    tx.nLockTime = max_locktime;
    +    tx.nLockTime = max_locktime.value_or(0);
         PartiallySignedTransaction merged_psbt(tx, psbtxs.at(0).GetVersion());
     
         // Merge
    
    

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-05-22 22:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me