onflow / flips

Flow Improvement Proposals
25 stars 23 forks source link

FLIP 242: Improve Public Capability Acquisition #242

Closed austinkline closed 5 months ago

austinkline commented 6 months ago

Related discussion

One of the big missing pieces here when we were originally discussing this problem was about what to set the ID of a capability if it is not valid. Coming out of today's discussion in discord, it looks like setting id to 0 is the way to go, and also helps solve another problem which would be that capabilities which are not successfully migrated would crash/panic if their id was accessed. It sounds like we might have an opportunity to tackle both problems here with this zero-value ID approach

bluesign commented 6 months ago

I am adding alternative approach I suggested on discord to here also:

We can also migrate public capabilities ( pointing to a public path ) even also change behaviour a bit, and add optional PublicPath to capability. Then public capabilities ( capabilities with public path set ) can go over Path->Capability Controller, instead of ID->Capability. This way we can ensure if I change a capability in my public path, all existing ones will point to that too. ( For example changing my FlowToken Receiver to FungibleToken Switchboard )

I think this also solves the optional Capability problem, and helps with migration problems.

turbolent commented 6 months ago

Nice!

Though I felt like having an optional returning type adequate, I see how having the address is useful.

I also understand the sentiment in regards to the effect of the previous change of changing the function from non-optional to optional, which results in additional code being necessary: like before, once the capability was obtained, it still needs to be checked/borrowed, but now also the "state of publication" (was a capability published under the requested path or not) must be handled with additional code (check for nil / unwrap).

Given how frequently the capability API is used, basically in all transactions, ergonomics are important. I like the simplicity of the change, the required effort is commensurate to the improvements in ergonomics.

austinkline commented 6 months ago

Thanks for the suggestions @turbolent! Accepted them all.

@bluesign I see what you mean, but wonder what kinds of issues might come up with some capabilities being able to be hot-swapped, and some not. Would that difference in behavior be a problem?

bluesign commented 6 months ago

@bluesign I see what you mean, but wonder what kinds of issues might come up with some capabilities being able to be hot-swapped, and some not. Would that difference in behavior be a problem?

I am not sure, but I think currently all is hot swappable anyway. ( you can change the object in storage )

austinkline commented 6 months ago

@bluesign I see what you mean, but wonder what kinds of issues might come up with some capabilities being able to be hot-swapped, and some not. Would that difference in behavior be a problem?

I am not sure, but I think currently all is hot swappable anyway. ( you can change the object in storage )

A great point for sure, hadn't thought about that

bjartek commented 6 months ago

I agree with the changes proposed in this FLIP.

bluesign commented 5 months ago

I think this is very good change and very much needed

joshuahannan commented 5 months ago

I looked through most of the contracts and transactions in our repos and it looks like most of them won't be affected by this change, so that means it is probably pretty likely that most of the staged contracts on testnet won't be broken if we introduce this either.

This does affect many transactions though because a lot of transactions use the pattern of getting the capability unwrapping it with the nil-coalescing operator, and borrowing a reference. This is fine though since they are just transactions. Only two of the ledger transactions are affected, because most of them use ccount.capabilities.borrow. This won't affect account.capabilities.borrow, right?

So it looks good from my perspective

turbolent commented 5 months ago

@joshuahannan Nice, thanks for looking into usage!

This won't affect account.capabilities.borrow, right?

Correct, this won't affect borrow, only getborrow has always been optional, and will stay so.

jacob-tucker commented 5 months ago

Just wanted to say I also support this, it is a very nice thing to keep. I always have empty capabilities because having address is quite useful!

turbolent commented 5 months ago

@dsainati1 Good point/question!

In the case where the requested path has a capability published to it, but the requested type does not "match" the capability's type, instead of returning nil, the function returns an invalid capability (ID = 0) with the requested type (like in the current version of Cadence (v0.42)).

I see two options here. Given a case where a value of type &X is published at a path but attempted to be borrowed with type &Y where Y is not a supertype of X, we can either:

  • Create a capability whose static and runtime borrow type is both &Y
  • Create a capability with static type &Y but runtime borrow type &Never

What is the difference between the two? Does the latter have any benefit?

dsainati1 commented 5 months ago

What is the difference between the two?

The main behavioral difference is that in the former case, an invalid capability created from a call to get<&Y> will succeed on a runtime cast to Capability<&Y>, while in the latter case such a cast would fail.

Does the latter have any benefit?

I don't really know to be honest, but I also felt originally that having get return an optional was a UX improvement, so I've accepted that I don't have a perfect grasp on what developers want from this API in the first place.

dsainati1 commented 5 months ago

To give maybe a more useful example: if you have some array of type [Capability] that you got through any various means, and you would like to filter this array for any Capability values that are borrowable as NonFungibleTokens, one way you might do this is by iterating over the array and doing an optional cast cap as? Capability<&{NonFungibleToken}>, and storing the successful results in a new array.

Would we like invalid capabilities to pass this cast (and be included in the filtered array) or fail (and be excluded)?

bluesign commented 5 months ago

Would we like invalid capabilities to pass this cast (and be included in the filtered array) or fail (and be excluded)?

I think passing this cast not failing is better option. In any case, there is no guarantee that capability will check/borrow, so filtering has no added benefit here.

Also failing case would be strange:

var cap: Capability<&{NonFungibleToken}> = capabilities.get<&{NonFungibleToken}>(....)
// cap != nil
var c:Capability = cap 
var x = c as? Capability<&{NonFungibleToken}>
// x = nil
turbolent commented 5 months ago

@dsainati1 Please feel free to suggest additions to the FLIP contents to clarify the proposed design

turbolent commented 5 months ago

@austinkline Could you please accept the suggestions? Once updated, PTAL everyone

turbolent commented 5 months ago

In today's working group call we had another round of discussion about this FLIP

Unless there are any further concerns, the FLIP is planned to be accepted by Tuesday next week.

turbolent commented 5 months ago

@austinkline The FLIP has been approved 🎉 Could you please merge the status update change? Once the status got updated, we can merge