Current behaviour
In tr(), miniscript refuses or_i(pk(X),pk(02X)) for duplicate keys but accepts or_i(pk(X),pk(03X)). Tapscript keys are x-only, so both describe a leaf with the same 32-byte key X twice. BIP379 restricts its malleability analysis to scripts where no public key is repeated, and this one is malleable: one signature satisfies both branches, so anyone can flip the branch selector.
KeyParser::KeyCompare in src/script/descriptor.cpp compares full 33-byte keys, parity byte included, and a 32-byte X is parsed as 02X.
v31.1.0 behaves the same.
Expected behaviour
Both refused, as or_i(pk(X),pk(X)) is.
Steps to reproduce
H=50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0
X=6116733562ba4df653e4ec6b98c904c8dd3c677428264d33f4ae0089b72abaa4
bitcoin-cli getdescriptorinfo "tr($H,or_i(pk($X),pk(02$X)))" # error: ... is not sane: contains duplicate public keys
bitcoin-cli getdescriptorinfo "tr($H,or_i(pk($X),pk(03$X)))" # accepted
The accepted descriptor's leaf script is OP_IF <X> OP_CHECKSIG OP_ELSE <X> OP_CHECKSIG OP_ENDIF. On regtest I funded its address and signed a spend with descriptorprocesspsbt, which used the empty selector. Setting the selector to 01, without any key, gives a transaction that also passes testmempoolaccept, with the same txid and a different wtxid.
How did you obtain Bitcoin Core
Compiled from source
What version of Bitcoin Core are you using?
master@69142eacd1
Operating system and version
macOS 27.0.1
Made with my usual tools: a computer, the Internet and an LLM. The mistakes, as usual, are all mine.