Skip to content

descriptor: reduce tr() keys to x-only for taproot expansion - #10952

Open
fametrano wants to merge 1 commit into
spesmilo:masterfrom
fametrano:btclib-1872-tr-expand-xonly
Open

fametrano wants to merge 1 commit into
spesmilo:masterfrom
fametrano:btclib-1872-tr-expand-xonly

Conversation

@fametrano

@fametrano fametrano commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

A key in a tr() descriptor can resolve to a 33-byte compressed point. This happens when it is derived from an extended key, or written as a raw compressed key. Taproot needs a 32-byte x-only key, and Electrum does not reduce it. The two places where this happens fail in different ways:

  • Internal key. TRDescriptor.expand passes the 33 bytes to taproot_output_script, and assert len(pubkey32) == 32 fails. So tr(<xpub>/<path>), the usual single-key taproot descriptor, cannot be expanded.
  • pk() leaf key. Expansion succeeds, but the leaf script pushes a 33-byte key. BIP-0342 treats a key that is neither 0 nor 32 bytes as an unknown public key type, and signature validation for it succeeds. So under consensus anyone can spend through that leaf with any non-empty signature. Only relay policy makes such a spend non-standard.

Bitcoin Core builds the x-only key by dropping the parity byte (XOnlyPubKey(CPubKey)). This PR does the same. A small helper reduces a 33-byte key to its 32-byte x-only form. It runs on the internal key and on pk() leaf keys, both raw and derived from an extended key. pk() is the only leaf function Electrum's parser accepts. A 32-byte key is unchanged. Signing is not touched.

The test checks the internal key, raw and derived from an extended key, and a raw compressed pk() leaf key. The leaf case must give the BIP-0386 x-only vector.

Made with my usual tools: a computer, the Internet and an LLM. The mistakes, as usual, are all mine.

A tr() internal key, or a pk() tapscript leaf key, can resolve to a
33-byte compressed point when it is derived from an extended key or
written as a raw compressed key. Taproot uses x-only public keys, so
such a key is reduced to its 32-byte x-coordinate before it reaches
taproot_output_script, dropping the parity byte as Bitcoin Core does.
This is correct for either parity, since taproot keys are lifted to
even-y when tweaked. A key already in x-only form is left unchanged.
@fametrano

Copy link
Copy Markdown
Contributor Author

Sorry, I rebased #10948, #10951 and #10952 onto current master, which put their tests and regtest runs back into approval. All three were green before the rebase. Could a maintainer approve the runs and take a look when there's time?

@fametrano

Copy link
Copy Markdown
Contributor Author

Thanks @f321x. The one failure is tests/test_onion_message.py::TestOnionMessageManager::test_forward on py3.14, debug-mode; this PR only touches electrum/descriptor.py and its tests, and #10948 and #10951 pass that job on the same base. Could it be re-run?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant