You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We have some changes coming up that potentially change the rules around simplicity. This issue is to discuss various options and caveats.
Problem
When new jets are added to simplicity, elements/liquid must be updated to execute them. This would produce a hard fork since older versions will explicitly fail on unknown jets.
Possible Solutions
1. No tapleaf changes, elements updated.
This is certainly the simplest, however it is a hard fork. Upgraded nodes will allow the transaction, older nodes will reject it.
2. Update tapleaf version
Currently the simplicity version is stored in the leaf as 0xbe. There are restrictions on valid versions, so potentially 0xc2 or 0xc4. I'm not sure that doing this will prevent a hardfork. New transactions using this leaf should be rejected on old nodes regardless. This would be a soft fork.
One downside though, is that if a new version is updated, the tapleaf_version() -> u8 jet will change it's result for some transactions. This is mostly fine, but a covenant that started on 0xbe would most likely be stuck on 0xbe. Mixing utxos with different versions may break contracts, although this is probably unlikely.
Are there other solutions? IMO it seems like we just need to hardfork (option 1). Perhaps there is a use case for changing the version byte in future? I would suggest that we reserve changing the version byte for cases where the simplicity program encoding changes.
I'm not sure that doing this will prevent a hardfork. New transactions using this leaf should be rejected on old nodes regardless.
New tapleaf versions are soft forks, they are consensus valid but "non-standard" - so they will be accepted in blocks but not relayed in the mempools of unupgraded nodes
As suggested by @Arvolear, we might want to add another byte for versioning somewhere so that we don't burn through tapleaf versions or overlap with another protocol that possibly emerges in future.
Maybe we can increase the tapleaf version once to 0xc2 (or 0xbc) and then change the encoding of the simplicity program in the witness to add a single byte versioning in front. This would change the data stored in the control block, and we'd need to change some Simplicity programs that calculate covenants. Perhaps we need a new Simplicity binary encoding that natively adds a node for the minimum supported simplicity version.
Maybe in the next Simplicity version, we also want some "nop" jets. There are a couple approaches we could take:
we can add "op_success" jets, which if they appear in a program, cause it to succeed without being evaluated. (Though these would make disconnect much more dangerous to use, to the point where maybe they'd need to be non-disconnect-only and the complexity would be more than it's worth...IIRC we discussed this some time ago and the disconnect issue killed the idea)
we can add "op_nop" "jets", which have trivial cost and which act like unit (have a free input and unit output); these can be later softforked in to have fixed input types and to assert properties on their inputs, though because we cannot change the jet cost in a softfork, these properties would need to be trivial to check
we could attempt to add nops with nontrivial costs, by e.g. directly encoding the cost into the jet encoding ... this would be wasteful but if it lets us increase our iteration speed on Liquid then maybe it's worth the waste
In particular, the trivial "nop" jets could be used for things like these "op_return" jets, which have ~0 cost since they just check transaction data which is precomputed and has ~0 marginal cost to access from within a Simplicity program.
I agree with the nop jets. If it is possible somehow, it would be good to specify a cost, e.g. nop(85) so that we can have older nodes get to the same cost.
For the most part, we allow overpaying, but I think we might be very strict if there is annex padding involved. I'd have to confirm, but I think the padding needs to be exactly the right amount, not over. Perhaps we should also relax that rule.
I agree with the nop jets. If it is possible somehow, it would be good to specify a cost, e.g. nop(85) so that we can have older nodes get to the same cost.
Yeah, I'm kinda tempted to do something like "16 NOPs each at 16 different costs".
Unfortunately I don't think my "have a free input type" proposal would work because of sharing and type inference ... I think this is what killed Russell's original idea to do this, which was based on Chialisp which does something very similar.
Perhaps we should also relax that rule.
We cannot relax that rule without introducing transaction-stuffing vulnerabilities in multiparty protocols. (I assume by "overpaying" you mean "add more padding than is required".)
Activity
delta1 commentedon Jun 4, 2026
New tapleaf versions are soft forks, they are consensus valid but "non-standard" - so they will be accepted in blocks but not relayed in the mempools of unupgraded nodes
stringhandler commentedon Jun 4, 2026
As suggested by @Arvolear, we might want to add another byte for versioning somewhere so that we don't burn through tapleaf versions or overlap with another protocol that possibly emerges in future.
Maybe we can increase the tapleaf version once to
0xc2(or0xbc) and then change the encoding of the simplicity program in the witness to add a single byte versioning in front. This would change the data stored in the control block, and we'd need to change some Simplicity programs that calculate covenants. Perhaps we need a new Simplicity binary encoding that natively adds a node for the minimum supported simplicity version.apoelstra commentedon Jun 5, 2026
Maybe in the next Simplicity version, we also want some "nop" jets. There are a couple approaches we could take:
disconnectmuch more dangerous to use, to the point where maybe they'd need to be non-disconnect-only and the complexity would be more than it's worth...IIRC we discussed this some time ago and thedisconnectissue killed the idea)unit(have a free input and unit output); these can be later softforked in to have fixed input types and to assert properties on their inputs, though because we cannot change the jet cost in a softfork, these properties would need to be trivial to checkIn particular, the trivial "nop" jets could be used for things like these "op_return" jets, which have ~0 cost since they just check transaction data which is precomputed and has ~0 marginal cost to access from within a Simplicity program.
stringhandler commentedon Jun 22, 2026
I agree with the
nopjets. If it is possible somehow, it would be good to specify a cost, e.g.nop(85)so that we can have older nodes get to the same cost.For the most part, we allow overpaying, but I think we might be very strict if there is annex padding involved. I'd have to confirm, but I think the padding needs to be exactly the right amount, not over. Perhaps we should also relax that rule.
apoelstra commentedon Jun 22, 2026
Yeah, I'm kinda tempted to do something like "16 NOPs each at 16 different costs".
Unfortunately I don't think my "have a free input type" proposal would work because of sharing and type inference ... I think this is what killed Russell's original idea to do this, which was based on Chialisp which does something very similar.
We cannot relax that rule without introducing transaction-stuffing vulnerabilities in multiparty protocols. (I assume by "overpaying" you mean "add more padding than is required".)