Skip to content

Discussion on versioning and hardforks #341

Open

Description

@stringhandler

Background

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.

Activity

  1. delta1 commented on Jun 4, 2026

    @delta1
    Contributor

    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

  2. stringhandler commented on Jun 4, 2026

    @stringhandler
    Author

    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.

  3. apoelstra commented on Jun 5, 2026

    @apoelstra
    Collaborator

    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.

  4. stringhandler commented on Jun 22, 2026

    @stringhandler
    Author

    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.

  5. apoelstra commented on Jun 22, 2026

    @apoelstra
    Collaborator

    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".)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions