Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FLIP 275: Removal of Types in Contract Updates #276

Merged
merged 6 commits into from
Oct 30, 2024

Conversation

dsainati1
Copy link
Contributor

@dsainati1 dsainati1 commented Jun 4, 2024

Closes #275 and onflow/cadence#3210

This is a FLIP for a new pragma that would allow types to be removed in contract updates.

As also mentioned in the FLIP itself, a prototype implementation was added in onflow/cadence#3376 and onflow/cadence#3380. This is currently disabled by default, and has been added only as a proof of concept and to be able to test in local environments.

@dsainati1 dsainati1 requested review from a team June 4, 2024 15:37
@dsainati1 dsainati1 self-assigned this Jun 4, 2024
@turbolent
Copy link
Member

I guess this proposal does not prevent a #replacedType pragma being added in the future?

Copy link
Member

@SupunS SupunS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
```

Placing the pragma at the top level, or nested inside another definition in `C` has no effect, and does not permit the removal of `R`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make this an error/warning?

@bluesign
Copy link
Collaborator

bluesign commented Jun 4, 2024

I see too many edge cases to be honest, maybe at least we can limit this to migration. I always suggested composeability to people, instead of copying to reuse ( not that it worked out much so far ), I feel this has opposite effect.

@austinkline
Copy link
Contributor

My main concern here is that permission to remove types effectively means a contract cannot revoke its keys if it has any dependency because of the risk that it could permanently break.

I'd like to more clearly understand the reason that this is needed beyond just "unblocking the FiatToken contract", what has to change that cannot be without this? Is there another path to doing it?

Ideally, Circle should chime in here, maybe some of us can help them find another way.

@bluesign
Copy link
Collaborator

bluesign commented Jun 4, 2024

My main concern here is that permission to remove types effectively means a contract cannot revoke its keys if it has any dependency because of the risk that it could permanently break.

It is not only that, imagine having a field that using a struct with type that is removed.

On Circle side, only explanation makes sense is they lost the keys.

@austinkline
Copy link
Contributor

It is not only that, imagine having a field that using a struct with type that is removed.

Or an interface type definition that is the bedrock for other contracts downstream

@bluesign
Copy link
Collaborator

bluesign commented Jun 4, 2024

copied from Discord to keep discussion in one place:

I think the main issue is you it feels like you are committed to this ( at least in FiatToken case ), unless some security issues comes out of this, I don't feel that it would be rejected.

I am reading it more like; If nothing critical identified, we keep this till migration, then we can always remove if not liked.

@austinkline
Copy link
Contributor

Is this FLIP restricted only to concrete types or interface types as well?

If interface types as well, what is the difference between this FLIP and just allowing contract upgrades to remove conformance types?

@dsainati1
Copy link
Contributor Author

Is this FLIP restricted only to concrete types or interface types as well?

In the design section of the FLIP it says that the current design proposal only allows removing concrete types, not interfaces.

@bluesign
Copy link
Collaborator

bluesign commented Jun 4, 2024

btw if we stay out of Circle situation; and focus on the FLIP:

The Cadence 1.0 upgrade in particular has exacerbated this need, as the changes to access control have
rendered a number of types obselete.

I think would be nice to have some examples on this.

Additionally as all contracts on chain need to be updated for Crescendo, the ability to remove dependencies on contracts that are difficult or otherwise challenging to update has become more important.

This part I totally agree but maybe this can be relaxed with one time contract update checking for Crescendo. ( But also it is also tricky )

@SupunS
Copy link
Member

SupunS commented Jun 4, 2024

I think would be nice to have some examples on this.

I remember running into some cases when updating nft/ft code bases to match C1.0, e.g: https://github.com/onflow/flow-nft/blob/f741cb5352f1889984e60437e0805b8371dcd7fe/contracts/ExampleNFT.cdc#L157C34-L157C41 where it would have been cleaner to be able to remove certain type definitions. In the above case it was an interface, and would have of-course needed some addition relaxations (e.g: being able to remove a conformance of a removed type) to make it really useful for that particular case. Perhaps replacedType(T, R) would have been a better option for that. But just wanted to highlight that there are cases that could benefit from a solution along these lines.
Similar case with the NonFungibleToken as well.

@austinkline
Copy link
Contributor

Similar case with the NonFungibleToken as well.

This case is quite different fwiw. All existing marketplace contracts (including Flow's own NFTStandards) use NFT.CollectionPublic capabilities in their types, removal of this isn't an option like the ExampleNFT interface might be

@bluesign
Copy link
Collaborator

bluesign commented Jun 5, 2024

thanks @SupunS , yeah the problem is it makes sense in the first look, but then it becomes tricky when someone else relies on it somewhere. Thats the reason I pushed forbidding interface removal in the original issue.
for example like this:

https://github.com/bluesign/mainnet-contracts/blob/cfcb76228edd5711d9892ce5cb99a178d5caaae5/contracts/0x4eded0de73020ca5/TopShotEscrowV3.cdc#L90

On greater perspective, small suggestion; I think we need to set up some principle rules for language evolution formally. Till 1.0 we had big freedom, but I think there is a need for some guarantees if we want composeability to succeed. If we had a document why we don't allow type removal before ( which would include all this cases came up here and more ) then maybe we would not even consider this. ( or at least we could weight cons pros easier )

```cadence
access(all) contract C {

#removedType(R)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this work as well:

resource R {
        #removedType
    }

This might be a bit cleared because the type is still in the contract code its just flagged as removed.

@turbolent
Copy link
Member

turbolent commented Jun 6, 2024

Thank you everyone for your great feedback!

Let me provide some additional context, answer your questions, and clarify some misunderstandings.

Context

The feature proposed in this FLIP has been requested for several years. So far it had not been realized because there was no urgent need for it. Developers asked to be allowed to remove type definitions in contract updates, just like how it is already possible to remove e.g. function definitions.

The purpose and goal of the contract update checking is to prevent contract updates to circumvent the safety mechanisms of the system. The contract update checking does not have as a goal to ensure that contract updates will not also break downstream contracts that import the updated contract. That is a common misunderstanding, please have a look at the Validation Goals section of the documentation for more details and examples of goals and non-goals.

The contract update checking currently prevents the removal of type definitions. However, the removal of type definitions, just like the removal of e.g. function definitions, is safe.

You are of course absolutely right to point out that such removals might break downstream code! I'll try to address this down below in the section "Breaking downstream code".

With the update to Cadence 1.0, requests for the type removal feature increased. Not significantly, but still. Developers used to have type definitions, e.g. interfaces, which got mainly used for access control purposes (restricted types), which are no longer needed in Cadence 1.0 given that entitlements are now used for access control.

Supun therefore created a feature issue at the beginning of April, onflow/cadence#3210, to ideate and look into how such a feature could look like.

In May we realized that such a feature could help a particular developer (Circle) in their process to update to Cadence 1.0, which further increased the priority of the feature. We thus implemented the existing idea and provided it in the preview release to gather feedback on it, based on a real-world problem.

This FLIP is now trying to gather even more feedback and proposes to properly include the feature into the language.

FLIP

we are still soliciting feedback on its design

There might have been some misunderstandings in the team as well. As with every significant language change, we are trying to use the FLIP process to discuss and get feedback on the change, and end up with a decision. This feature / FLIP is no different. We are not still soliciting feedback, we are always trying to solicit feedback.

Most of the time we have a design proposal before we have a prototype implementation, sometimes (like for this proposal), it is the other way around.
Having an implementation is useful to evaluate the proposed design and gather feedback from developers.
A merged implementation does not mean that a decision has been made!

if a critical issue is identified with this feature, we still have time to change and/or remove the feature as necessary, ... a primitive version of it is necessary currently.

I think the main issue is you it feels like you are committed to this ( at least in FiatToken case ), unless some security issues comes out of this, I don't feel that it would be rejected.

I am reading it more like; If nothing critical identified, we keep this till migration, then we can always remove if not liked.

Again, sorry for the confusion.

This feature is being proposed like any other feature, and the feature will only get included in a proper release if the feature gets approved.
The proposal will not only be rejected if a critical issue is identified – it will be approved or rejected based on the normal FLIP process.

We as the core team have discussed this feature internally, as we do for all feature additions and changes, and found enough agreement to advance in the process opened a FLIP for it.
As with every FLIP, we are looking for feedback, concerns, issues we didn't see, things we missed, etc.
All the note was meant to say is that so far we had not identified any critical issues and did not expect there to be any.

Therefore, yes, any proposer of a FLIP is obviously committed to what they are proposing. Just because the team is proposing the feature does not mean a decision has been made. If the consensus is that the proposal should be rejected, then the feature will get removed.

Similar concerns were raised in a previous FLIP. I had written a similar explanation there, please have a look at the "Implementation" section in #53 (comment) (unfortunately I cannot link to it directly).

Reason for rejection

If we had a document why we don't allow type removal before

The only reason why type removals are currently not allowed is again to ensure safety, not to prevent downstream code to get broken. Specifically, it prevents a type with the same name, but different definition, to being added back.

For example, the re-added type definition could potentially have a different kind (e.g. resource to struct), different fields (e.g. new field added, field type changed, etc.), which are all invalid contract update changes (which again would subvert safety).

The "tombstoning" approach prevents the re-adding and thus ensures safety.

Breaking downstream code

I see too many edge cases to be honest, maybe at least we can limit this to migration. I always suggested composeability to people, instead of copying to reuse ( not that it worked out much so far ), I feel this has opposite effect.

My main concern here is that permission to remove types effectively means a contract cannot revoke its keys if it has any dependency because of the risk that it could permanently break.

AFAICS, the impact of removing type definitions on downstream code seems to be the main concern so far.

There are two important features / needs of developers:

  1. Evolve a contract. Developers want to be able update contracts e.g. to add additional, change existing, and remove functionality.

    Direct support of contract updatability is a core feature of Cadence / Flow, and helps developers extend and fix their contracts.

  2. Import of on-chain code. Developers want to be able to use and built on other on-chain code.
    To do so reliably, they need guarantees that changes to imported code work reliably and will not break their programs.

    Direct support for on-chain imports is a core feature of Cadence / Flow, and helps with composability.

The vision is to extend these features more and more. The problem is that they are naturally at odds with each other: While one contract author might want to evolve their contract, another developer that imports the contract wants to make sure there are no changes that could break their code.

Currently there is already fairly good support for updating contracts (see all changes that are possible in the contract updatability documentation), though we still are lacking some features like adding fields.
This FLIP proposes an improvement to this feature.

Support for the second need/feature, ensuring that imported code will not break your code, is lacking and is now starting to become a bigger concern. Up until the release of Cadence 1.0, providing functionality to guarantee to a dependent that an import would never cause any breakage was futile, as the language itself did not provide any such guarantees and could still cause breakage.

With Cadence 1.0 and its backward-compatibility guarantee coming soon, it now makes sense to increase support for the second feature. Since the beginning of the design of Cadence in '19 and introduction of the contract updatability feature, we have had ideas how to enable both features at the same time. I hope that soon after releasing Cadence 1.0 we can finally get to continue these ideas and turn them into concrete proposals.

Concretely, we've thought of different "levels" for updatability: At first a contract might be in a "fully-updatable" state, where authors may change anything (according to the contract updatability rules), but dependents have no/little guarantee their code will not break. A more restrictive level would then e.g. only allow adding new functionality (e.g. adding functions), but not e.g. change function signatures – this would give dependents some more guarantees. Finally, a "completely locked" contract would be not changeable at all, and would provide full guarantee for dependents that there will be no breakage. These levels could be first quite granular, and later split up into more fine-grained ones. Again, these are just ideas.

Note that needs for different kinds of contracts are different:

  • "Standards" (contract interfaces) are heavily imported, so contract changes have a huge impact. Dependents have a strong need for guarantees that their implementation will not break
  • "Leaf" contracts are not imported at all, so contract have no impact. Contract authors have a strong need to evolve their contracts.

We ideally want to assist all kinds of contracts and contract developers.

Fin

I hope that putting this FLIP into context will help everyone to evaluate it on its own merits and see it in the bigger picture of evolving Cadence.

I also hope it will assure you that the core team is thinking about it in the bigger picture, but as you can see we are trying to deliver on it incrementally, in smaller pieces.

In a way I have a deja-vu with yet another (group of FLIPs): When the team proposed the mutability restriction functionality in smaller FLIPs, those were also seen at micro-level, and the bigger picture was not clearly communicated.
We resolved this with a larger vision document (https://github.com/onflow/flips/blob/main/cadence/vision/mutability-restrictions.md), which then made it easer to evaluate the individual FLIPs in that context.
Though we are currently busy with the release of Cadence 1.0 and I cannot promise we will be able to deliver something similar soon, I hope that we can at at least "latest" prioritize it first thing after the release.

@bluesign
Copy link
Collaborator

bluesign commented Jun 7, 2024

Concretely, we've thought of different "levels" for updatability:

This is a great idea actually. I agree lack of macro view creates problems, to be fair, so far we always had micro level FLIPs. It is really hard to see bigger picture like this. I think this FLIP doesn't sound that bad when we read with that prespective.

A merged implementation does not mean that a decision has been made!

I don't know any project ( with a process to similar to FLIP process ) can claim that. But to be fair on this, I am not involved in much of anything else other than Cadence on this level.

@turbolent
Copy link
Member

turbolent commented Jun 11, 2024

To make it clearer that the feature has not been approved, the implementation of it is now behind a feature flag and disabled by default: onflow/cadence#3410

@turbolent
Copy link
Member

Thank you for updating the FLIP to the current, post-Crescendo, status @SupunS. The changes look great 👍

@SupunS
Copy link
Member

SupunS commented Oct 22, 2024

Circling back to this again, now that Cadence 1.0 is out. Thanks everyone for the feedback so far!

I updated the FLIP and the description to match the current state of things. I also added some more explanations on things like why the change is needed, and reasons for the existing behaviour, etc., and cleaned up some outdated information. Hope that would clear-out some doubts/confusions if there was any. Please have a look 🙏

(and you beat me to it, Bastian :D )

@SupunS SupunS assigned SupunS and unassigned dsainati1 Oct 22, 2024
@turbolent turbolent changed the title Add FLIP for type removal pragma FLIP 275: Removal of Types in Contract Updates Oct 23, 2024
@SupunS
Copy link
Member

SupunS commented Oct 30, 2024

Thank you again everyone for taking time to review and provide feedback. As we discussed in the last working group session, I'm going to consider this FLIP as accepted 🎉

The concerns regarding breaking of downstream dependencies (from type-removals, and any other changes such as removing functions, removing fields, etc.) would be addressed separately as part of the discussion for "making contracts immutable", and a separate FLIP would be proposed in future.

@SupunS SupunS merged commit 1544cb3 into main Oct 30, 2024
@SupunS SupunS deleted the sainati/type-removal-flip branch October 30, 2024 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Type Removal In Contract Updates
6 participants