Skip to content

Conversation

@Sjors
Copy link
Member

@Sjors Sjors commented Aug 5, 2025

BIP 379 (Miniscript) allows relative height and time locks that have no consensus meaning in BIP 68 (relative timelocks) / BIP 112 (CHECKSEQUENCEVERIFY). This is (ab)used by some protocols, e.g. by Lightning to encode extra data, but is unsafe when used unintentionally: older(65536) is equivalent to older(1).

This PR emits a warning when importdescriptors contains such a descriptor.

The first commit makes SEQUENCE_LOCKTIME flags reusable by other tests.

The main commit adds the ForEachNode helper to miniscript.h which is then used in the MiniscriptDescriptor constructor to check for Fragment::OLDER with unsafe values. These are stored in m_warnings, which the RPC code then collects via Warnings().

It adds both a unit and functional test.


A previous version of this PR prevented the import, unless the user opted in with an unsafe flag. It also used string parsing in the RPC code.


Based on:

@DrahtBot DrahtBot added the Wallet label Aug 5, 2025
@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 5, 2025

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

Code Coverage & Benchmarks

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

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK rkrux, brunoerg, pythcoiner, achow101

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #33392 (wallet, rpc: add UTXO set check and incremental rescan to importdescriptors by musaHaruna)
  • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
  • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
  • #31713 (miniscript refactor: Remove unique_ptr-indirection by hodlinator)
  • #31668 (Added rescan option for import descriptors by saikiran57)
  • #29136 (wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor by achow101)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

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

I've reviewed the code and it looks great - Just a question: Instead of having this new field and prevent the import (any user have reported an accidental import?), couldn't we just throw a warning?

@Sjors
Copy link
Member Author

Sjors commented Aug 22, 2025

@brunoerg perhaps a warning is good enough yes. In that case we wouldn't need the unsafe option. Will give it some thought.

@Sjors Sjors force-pushed the 2025/08/older-safety branch from fac503e to 8c6b70b Compare September 1, 2025 07:48
@Sjors Sjors changed the title wallet: prevent accidental unsafe older() import wallet: warn against accidental unsafe older() import Sep 1, 2025
@Sjors
Copy link
Member Author

Sjors commented Sep 1, 2025

I switched to emitting a warning.

I'll look into modifying miniscript / descriptor classes to avoid the string parsing. Though unless people really don't like it, I think it could be done as a followup.

@Sjors Sjors force-pushed the 2025/08/older-safety branch from 8c6b70b to 3aa611d Compare September 9, 2025 16:12
@Sjors
Copy link
Member Author

Sjors commented Sep 9, 2025

Rebased. Fixed bug in the functional test found by an AI overlord, apparently using is isn't safe: https://stackoverflow.com/a/28864111

I'll look into modifying miniscript / descriptor classes to avoid the string parsing.

Done, and also added a unit test. It's a bit more code and complexity, but more robust and extendable than parsing strings.

Since c1e6a9f1e7331fa2c1c73a0dd17056570283e443 is no longer relevant for this PR I dropped it.

@Sjors Sjors force-pushed the 2025/08/older-safety branch 2 times, most recently from 2f684f2 to fe06b92 Compare September 9, 2025 17:14
Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

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

ACK fe06b92429206aee66d33c843195235f4bb27181 - This new approach of just warning is better than the previous one, nice! I haven't reviewed the code in depth, just did a light code review but tested the behavior in practice and worked fine.

Also, I ran a mutation analysis on these changes and the only uncaught mutant is:

         // Traverse miniscript tree for unsafe use of older()
         miniscript::ForEachNode(*m_node, [&](const miniscript::Node<uint32_t>& node) {
-            if (node.fragment == miniscript::Fragment::OLDER) {
+            if (1==1) {
                 const uint32_t raw = node.k;
                 const uint32_t value_part = raw & ~CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG;
                 if (value_part > CTxIn::SEQUENCE_LOCKTIME_MASK) {

it means we're not really testing that the warning is produced exclusively for the older() fragment, nothing critical and can be ignored but I think a simple test case could solve it. e.g.:

diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp
index 9e06e78fe9..fb35dfd05a 100644
--- a/src/test/descriptor_tests.cpp
+++ b/src/test/descriptor_tests.cpp
@@ -1260,6 +1260,15 @@ BOOST_AUTO_TEST_CASE(descriptor_test)

 BOOST_AUTO_TEST_CASE(descriptor_older_warnings)
 {
+    {
+        FlatSigningProvider keys;
+        std::string err;
+        // Using after() with a large timestamp (> 65535)
+        auto descs = Parse("wsh(and_v(v:pk(0379e45b3cf75f9c5f9befd8e9506fb962f6a9d185ac87001ec44a8d3df8d4a9e3),after(1000000)))", keys, err, /*require_checksum=*/false);
+        BOOST_REQUIRE(!descs.empty());
+        BOOST_CHECK(descs[0]->Warnings().empty()); // Ensure the older() warning is really being applied only for this fragment
+    }
+

@Sjors Sjors force-pushed the 2025/08/older-safety branch 2 times, most recently from 9431dc3 to 5e9737f Compare September 10, 2025 06:34
@Sjors
Copy link
Member Author

Sjors commented Sep 10, 2025

@brunoerg thanks, I added your test against false positive warnings for after(), which passes.

fe06b92429206aee66d33c843195235f4bb27181 -> 5e9737ff493a96566f4fc11b1733b4b5a5393f6b: compare

Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

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

reACK 5e9737ff493a96566f4fc11b1733b4b5a5393f6b

@@ -7,8 +7,10 @@

#include <algorithm>
#include <compare>
#include <concepts>
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not sure if those includes are stricly required?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's some sort of linter command that can check this, but I forgot which.

Copy link
Contributor

Choose a reason for hiding this comment

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

build success even w/o those 2 (added) includes on my side

Copy link
Member Author

Choose a reason for hiding this comment

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

That doesn't really say much, because indirect includes will succeed but we try to avoid those. Though maybe not for std stuff?

Copy link
Member Author

Choose a reason for hiding this comment

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

@@ -58,6 +59,7 @@ def test_importdesc(self, req, success, error_code=None, error_message=None, war
if 'warnings' in result[0]:
observed_warnings = result[0]['warnings']
assert_equal("\n".join(sorted(warnings)), "\n".join(sorted(observed_warnings)))
self.log.debug(result)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we keep this debug log?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's only printed when you set the log level to debug and it's useful for debugging if the test breaks.

@pythcoiner
Copy link
Contributor

tACK 5e9737ff49 code review + test importing few "Liana" descriptor with timelocks > 65535

if (value_part > CTxIn::SEQUENCE_LOCKTIME_MASK) {
const bool is_time = (raw & CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) != 0;
if (is_time) {
m_warnings.push_back(strprintf("older(%u - 1<<22) > 65535 is unsafe", raw));
Copy link
Member

Choose a reason for hiding this comment

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

In 5e9737ff493a96566f4fc11b1733b4b5a5393f6b "wallet: warn against accidental unsafe older() import"

This warning is basically incomprehensible. It would be better to actually write out what it means, i.e. "time based relative locktime > 65535 is unsafe"

Copy link
Member Author

@Sjors Sjors Sep 18, 2025

Choose a reason for hiding this comment

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

I went for a slight variant: relative locktime > 65535 * 512 seconds is unsafe

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/actions/runs/19535494720/job/55931077158
LLM reason (✨ experimental): Lint failures caused by missing SEQUENCE_LOCKTIME_TYPE_FLAG in test_framework.script during Python lint checks.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@Sjors
Copy link
Member Author

Sjors commented Nov 20, 2025

Lost the first commit in that rebase, let's try again...

@Sjors Sjors force-pushed the 2025/08/older-safety branch from 6edd8d4 to bbfa35d Compare November 20, 2025 13:01
fanquake added a commit that referenced this pull request Nov 27, 2025
c0bfe72 Change Parse descriptor argument to string_view (Sjors Provoost)

Pull request description:

  While investigating a silent merge conflict in #33135 I noticed that #32983 changed the descriptor `Parse` function signature from `const std::string& descriptor` to `std::span<const char> descriptor`.

  Calling that new version of `Parse` with a string literal will trigger a confusing "Invalid characters in payload" due to the trailing "\0".

  It can be worked around by having (the test) wrap string literals in `std::string()`, but that's easy to forget.

  Using `string_view` is easier and more compact than (as a previous version of this PR did) checking for trailing `\0`.

  Also add a test.

ACKs for top commit:
  maflcko:
    review ACK c0bfe72 🍨
  enirox001:
    tACK c0bfe72
  stickies-v:
    ACK c0bfe72
  rkrux:
    crACK c0bfe72

Tree-SHA512: 6b20307f834dae66826c8763f6c2ba0071f4e369375184cb5ff8543b85220fcaf33a47ddb065e418d1af3ed9a3fac401a7854f8924f52aab2b000b1f65328f2c
@Sjors Sjors force-pushed the 2025/08/older-safety branch from bbfa35d to 189437a Compare November 27, 2025 10:12
@Sjors
Copy link
Member Author

Sjors commented Nov 27, 2025

Rebased after #33914 so the test should pass again.

@Sjors Sjors marked this pull request as ready for review November 27, 2025 10:13
Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

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

lgtm ACK 189437a3dedefce3945ab2a8c05785eb283c2c6f

Shared a non-blocking nit.

Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

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

reACK 189437a3dedefce3945ab2a8c05785eb283c2c6f

@Sjors Sjors force-pushed the 2025/08/older-safety branch from 189437a to 45eb536 Compare December 2, 2025 08:51
BIP 379 allows height and time locks that have no consensus meaning in BIP 68 / BIP 112.
This is used by some protocols like Lightning to encode extra data, but is unsafe when
used unintentionally. E.g. older(65536) is equivalent to older(1).

This commit emits a warning when importing such a descriptor.

It introduces a helper ForEachNode to traverse all miniscript nodes.
@Sjors Sjors force-pushed the 2025/08/older-safety branch from 45eb536 to 76c092f Compare December 2, 2025 11:24
@DrahtBot DrahtBot removed the CI failed label Dec 2, 2025
Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

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

lgtm re-ACK 76c092f

@Sjors
Copy link
Member Author

Sjors commented Dec 29, 2025

@brunoerg @pythcoiner can you re-review the latest push?

Copy link
Contributor

@brunoerg brunoerg left a comment

Choose a reason for hiding this comment

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

reACK 76c092f

@pythcoiner
Copy link
Contributor

reACK 76c092f

@achow101
Copy link
Member

achow101 commented Jan 3, 2026

ACK 76c092f

@achow101 achow101 merged commit 2628de7 into bitcoin:master Jan 3, 2026
25 checks passed
kevkevinpal added a commit to kevkevinpal/bitcoin that referenced this pull request Jan 4, 2026
…followup item from PR bitcoin#33135 by refactoring descriptor classes to avoid string parsing instead of emitting warnings.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants