There is the annex, leaf version, ext_flag, OP_SUCCESS, unknown pubkey types and you could probably include the existing witness version too. I believe that list is exhaustive.
The ext_flag isn’t so much an extension mechanism in itself; more a structure that permits safely reusing the sighashing code (as opposed to needing a new tag or other mechanism for collision avoidance).
- Leaf versions: for revamping script semantics
- OP_SUCCESSx: for new opcodes, without coordinating new version
- Pubkey types: for new sighash flags/cryptography without needing an explosion of new opcodes
- Annex: for effectively adding new fields like nLockTime
Leaf versions were really only added because we had a few bits to spare in the control block, and it seemed wasteful to reserve them. They’re mostly a convenience, I think, as OP_SUCCESSx can achieve the same (add an OP_V2 etc).
As the annex isn’t committed to by the scriptPubKey, it’s more a way to extend witness possibilities than it is directly a way to add new semantics.
So is there something an annex can do that a new leaf version can’t? I think they’re orthogonal.
For example, a feature where you could restrict a tx to only be valid in a chain that contains a certain block hash. It can’t be done with a leaf version, as it’s a sign-time thing. I believe the annex can’t be used to introduce new script conditions. A new leaf version could introduce its own annex-like thing, but that wouldn’t be able to apply to old leaf versions.
The motivating example for the annex is this. Imagine a new opcode is added that needs few bytes but has high CPU cost. You’d want a high weight budget per such opcode, but that may require actually stuffing the witness with dummy data just to get the necessary budget. Instead, it would be nice if there could just be a marker on the input that says “increment the apparent weight (and corresponding op budget) by N”, without taking N bytes. Logistically, it would be annoying if that marker can only be parsed when the spent output is available. The annex is recognizable without context, mostly by exploiting a thankful coincidence about what set of witnesses is valid for v0 (first byte of last witness stack item can only take on certain byte values). This, I think, can’t be accomplished with a new leaf version.
This question was answered by Pieter Wuille on Twitter.