Skip to content

Conversation

@JeremyRubin
Copy link
Contributor

This patch adds a "redundant" rule with the annex discouragement, to discourage Annexes that are present but do not get signed.

This PR does not include tests, as the code paths are entirely redundant (and small!) with existing annex discouragement. If desired, tests could be made.

This new policy would come into play if someone somewhere for some reason decided that they were going to modify standard policy on their node to allow annexes even though it is abundantly clear that annex policy should not change until there is a semantic meaning for them (see the Taproot BIPs for more context), they can leave this lighter policy intact (or add it as a backport if they have already started accepting annexes).

Given recent discussion, it seems clear that certain nodes will remove such limitations, and having this code present in core provides an encouragement to retain this safety oriented policy, even if Annexes are desired prematurely.

Annexes should be signed because if they are not, certain output types (e.g. and OP_TRUE script in a taproot output, used only as an anchor) would become malleable to third parties, introducing griefing and transaction pinning vectors. If the annexes will come anyways, they must at least be specifically requested under this rule.

image

@DrahtBot
Copy link
Contributor

DrahtBot commented May 8, 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/32453.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK Christewart, 1440000bytes

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #32998 (Bump SCRIPT_VERIFY flags to 64 bit by ajtowns)
  • #31989 (BIP-119 (OP_CHECKTEMPLATEVERIFY) (regtest only) by jamesob)
  • #29247 (CAT in Tapscript (BIP-347) by arminsabouri)

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.

@rot13maxi
Copy link

Im surprised we haven’t seen any weird annex-use yet. This seems like a good safety policy to have in here until the annex’s use gets consensus. Could someone remove this? Sure, but at least theres sort of a warning sign that they might not want to.

@DrahtBot
Copy link
Contributor

DrahtBot commented May 8, 2025

🚧 At least one of the CI tasks failed.
Task CentOS, depends, gui: https://github.com/bitcoin/bitcoin/runs/41906525804
LLM reason (✨ experimental): The CI failure is due to the test_bitcoin-qt test failing and bench_sanity_check being aborted.

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

Sjors commented May 9, 2025

If desired, tests could be made.

I don't understand annexes at all, so having some tests to illustrate the issue would be handy.

fixme: Improve comment to make grammatically more clear that m_annex_present requires initialization

FIXME: Add Flag to tests
@JeremyRubin
Copy link
Contributor Author

squashed the CI fixmes to get "test each commit" to pass.

@Sjors no one understands the annex, so you're not alone. You can see https://groups.google.com/g/bitcoindev/c/Q5j2Kb6XeHI?pli=1 for some context on efforts to "standardize" the annex currently ongoing.

@JeremyRubin
Copy link
Contributor Author

JeremyRubin commented May 10, 2025

@1440000bytes

This comment was marked as abuse.

@instagibbs
Copy link
Member

I don't love the script-time check, but this level of granularity may make sense.

I've been mulling over future interactions with things like CTV, where enabling CTV (which doesn't commit to annex) is vulnerable to witness inflation in the taproot context. It's not immediately clear to me if that means the annex should be committed or not. This patchset would at least be a minimal relaxation of annex policy, if paired with another annex relay PR such as petertodd@04c8e44 . Future "keyless" relaxations can be done later.

I have a suggestion for the code that feels a bit less hide and go seek (that may be broken, no tests):

diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp
index 15239e182a..c0f91b8a6d 100644
--- a/src/script/interpreter.cpp
+++ b/src/script/interpreter.cpp
@@ -378,10 +378,13 @@ static bool EvalChecksigTapscript(const valtype& sig, const valtype& pubkey, Scr
         if ((flags & SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE) != 0) {
             return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_PUBKEYTYPE);
         }
     }
 
+    assert(execdata.m_annex_init);
+    execdata.m_annex_committed = true;
+
     return true;
 }
 
 /** Helper for OP_CHECKSIG, OP_CHECKSIGVERIFY, and (in Tapscript) OP_CHECKSIGADD.
  *
@@ -431,16 +434,10 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
     bool fRequireMinimal = (flags & SCRIPT_VERIFY_MINIMALDATA) != 0;
     uint32_t opcode_pos = 0;
     execdata.m_codeseparator_pos = 0xFFFFFFFFUL;
     execdata.m_codeseparator_pos_init = true;
 
-    bool unsigned_annex_discouragement_needed = false;
-    if (execdata.m_annex_init && execdata.m_annex_present) {
-        if (flags & SCRIPT_VERIFY_DISCOURAGE_UNSIGNED_ANNEX) {
-            unsigned_annex_discouragement_needed = true;
-        }
-    }
     try
     {
         for (; pc < pend; ++opcode_pos) {
             bool fExec = vfExec.all_true();
 
@@ -1074,12 +1071,10 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
                     bool fSuccess = true;
                     if (!EvalChecksig(vchSig, vchPubKey, pbegincodehash, pend, execdata, flags, checker, sigversion, serror, fSuccess)) return false;
                     popstack(stack);
                     popstack(stack);
                     stack.push_back(fSuccess ? vchTrue : vchFalse);
-                    if (fSuccess)
-                        unsigned_annex_discouragement_needed = false;
                     if (opcode == OP_CHECKSIGVERIFY)
                     {
                         if (fSuccess)
                             popstack(stack);
                         else
@@ -1104,12 +1099,10 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
                     if (!EvalChecksig(sig, pubkey, pbegincodehash, pend, execdata, flags, checker, sigversion, serror, success)) return false;
                     popstack(stack);
                     popstack(stack);
                     popstack(stack);
                     stack.push_back((num + (success ? 1 : 0)).getvch());
-                    if (success)
-                        unsigned_annex_discouragement_needed = false;
                 }
                 break;
 
                 case OP_CHECKMULTISIG:
                 case OP_CHECKMULTISIGVERIFY:
@@ -1235,11 +1228,13 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
     catch (...)
     {
         return set_error(serror, SCRIPT_ERR_UNKNOWN_ERROR);
     }
 
-    if (unsigned_annex_discouragement_needed) {
+    if (flags & SCRIPT_VERIFY_DISCOURAGE_UNSIGNED_ANNEX &&
+        execdata.m_annex_present &&
+        !execdata.m_annex_committed) {
         return set_error(serror, SCRIPT_ERR_TAPSCRIPT_UNSIGNED_ANNEX);
     }
     if (!vfExec.empty())
         return set_error(serror, SCRIPT_ERR_UNBALANCED_CONDITIONAL);
 
diff --git a/src/script/interpreter.h b/src/script/interpreter.h
index e6206f06fa..5a859d1dce 100644
--- a/src/script/interpreter.h
+++ b/src/script/interpreter.h
@@ -212,10 +212,12 @@ struct ScriptExecutionData
 
     //! Whether m_annex_present is initialized and (only when needed) whether m_annex_hash is initialized.
     bool m_annex_init = false;
     //! Whether an annex is present.
     bool m_annex_present;
+    //! Whether the annex was commited to by a signature
+    bool m_annex_committed = false;
     //! Hash of the annex data.
     uint256 m_annex_hash;
 
     //! Whether m_validation_weight_left is initialized.
     bool m_validation_weight_left_init = false;

@JeremyRubin
Copy link
Contributor Author

good alternative approach -- I can change to that, after there's a clear preference from a couple devs who look. I don't know if people like altering execdata if there isn't a clear API reason to want to propagate that data (I think of it as only relevant when we really need to pass the information across, whereas the change as written is fully local to script exec).

@joshdoman
Copy link
Contributor

joshdoman commented May 12, 2025

@JeremyRubin I think this is a great idea. I made a PR into Peter Todd's repo doing something similar, which may or may not be useful (petertodd#10).

Primary difference is I modified execdata like @instagibbs described and add a single line execdata.m_annex_committed = execdata.m_annex_present; to the end of CheckSchnorrSignature. This eliminates ~6 lines of code and doesn't assume that future public key versions commit to the annex (though that's probably a safe assumption to make).

@Christewart
Copy link
Contributor

Concept ACK

existing annex discouragement.

Could you link to these?

Else tests would be nice.

@JeremyRubin
Copy link
Contributor Author

if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) {
// Annexes are nonstandard as long as no semantics are defined for them.
return false;
}

achow101 added a commit that referenced this pull request Oct 7, 2025
652424a test: additional test coverage for script_verify_flags (Anthony Towns)
417437e script/verify_flags: extend script_verify_flags to 64 bits (Anthony Towns)
3cbbcb6 script/interpreter: make script_verify_flag_name an ordinary enum (Anthony Towns)
bddcade script/verify_flags: make script_verify_flags type safe (Anthony Towns)
a5ead12 script/interpreter: introduce script_verify_flags typename (Anthony Towns)
4577fb2 rpc: have getdeploymentinfo report script verify flags (Anthony Towns)
a398693 validation: export GetBlockScriptFlags() (Anthony Towns)
5db8cd2 Move mapFlagNames and FormatScriptFlags logic to script/interpreter.h (Anthony Towns)

Pull request description:

  We currently use 21 of 32 possible bits for `SCRIPT_VERIFY_*` flags, with open PRs that may use 8 more (#29247, #31989, #32247, #32453). The mutinynet fork that has included many experimental soft fork features is [already reusing bits here](https://github.com/benthecarman/bitcoin/blob/d4a86277ed8a0712e03fbbce290e9209165e049c/src/script/interpreter.h#L175-L195). Therefore, bump this to 64 bits.

  In order to make it easier to update this logic in future, this PR also introduces a dedicated type for the script flags, and disables implicit conversion between that type and the underlying integer type. To make verifying that this change doesn't cause flags to disappear, this PR also resurrects the changes from #28806 so that the script flags that are consensus enforced on each block can be queried via getdeploymentinfo.

ACKs for top commit:
  instagibbs:
    reACK 652424a
  achow101:
    ACK 652424a
  darosior:
    ACK 652424a
  theStack:
    Code-review ACK 652424a 🎏

Tree-SHA512: 7b30152196cdfdef8b9700b571b7d7d4e94d28fbc5c26ea7532788037efc02e4b1d8de392b0b20507badfdc26f5c125f8356a479604a9149b8aae23a7cf5549f
@DrahtBot
Copy link
Contributor

DrahtBot commented Oct 8, 2025

🐙 This pull request conflicts with the target branch and needs rebase.

@instagibbs
Copy link
Member

(since I was tagged)

I don't think this PR makes sense stand-alone, but should definitely be considered if any sort of annex relay is to be considered standard in some future.

@maflcko
Copy link
Member

maflcko commented Oct 22, 2025

So I guess it can be closed for now?

@achow101 achow101 closed this Oct 22, 2025
@ajtowns
Copy link
Contributor

ajtowns commented Oct 23, 2025

I don't think this PR makes sense stand-alone, but should definitely be considered if any sort of annex relay is to be considered standard in some future.

For me, I think there's two blockers: (a) we shouldn't add dead code that will only take effect if someone forks the code; since we won't relay any annex content, this is just dead code. if there was some option to enable relaying of txs with some annex content, maybe that would be different (b) before relaying any txs with annexes, we should have a BIP that documents the way those annexes will be interpreted; even if that's only "we'll relay annexes beginning with \x00 of length up to 520 bytes, and apply no interpretation to that content". Personally I don't think "we'll relay this data and ignore it entirely" is a useful enough upgrade over existing approaches with inscriptions or OP_RETURN to be worth prioritising.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.