-
Notifications
You must be signed in to change notification settings - Fork 23
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 for entitlements and safe reference downcasting #54
Conversation
cadence/2022-12-14-auth-remodel.md
Outdated
|
||
The second, more complex part of this proposal, is a change to the behavior of references to allow the `auth` modifier not to | ||
refer to the entire referenced value, but to the specific set of interfaces to which the referenced value has `auth` permissions. | ||
To express this, the `auth` keyword can now be used with similar syntax to that of restricted types: `auth{T1, T2, ...}`, where the `T`s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will the old syntax (i.e: without curly braces) continue to be supported, say for restricted types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally the old syntax would become deprecated eventually, as currently the only reasonable way to interpret it is as a reference that is auth
over the entirety of the referenced type, which in the new model would be a very dangerous behavior to have as a default. What would be the use case for keeping the old auth
syntax that does not require a restriction set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweet, then we can reuse the restricted types syntax when we add algebraic effects 🥲
cadence/2022-12-14-auth-remodel.md
Outdated
Then, someone with a `&{Balance}` reference would be able to cast this to a `&{Receiver}` and call `deposit` on it. | ||
They would also be able to cast this to a `&Vault` refrence, but because this reference is non-`auth` for `Provider`, | ||
they would be unable to call the `withdraw` function. However, if a user possessed a `auth{Provider} &{Balance}` reference, | ||
they would be able to cast this to `auth{Provider} &{Provider}` and call `withdraw`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auth{T} &{U}
syntax is a bit confusing to me.
My first concern is, in a concrete type (struct/resource) scenario, the reference-taking syntax is let ref = &r as auth &T
, but for a restricted type, the user has to specify the type twice let ref = &r as auth{T} &{T}
. Maybe make the T
in auth{T}
optional if both the auth-type and the restricted type are the same? Anyway they can't downcast it further I guess.
The second concern is it is easy to confuse the type of the resulting reference. For eg: auth{Provider} &{Balance}
made me wonder whether I can (or why I can't) call withdraw
on the resulting reference.
I don't really have any suggestions for an alternative syntax, but if we can find one/improve this to iron out any potential confusion, that would be great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the syntax is a little bit confusing; at first glance it seems like you have to specify the type twice (once in the auth{T}
and once in the &{T}
, but they are really doing quite different things. I think it is going to be quite rare for users to want to declare a reference value this way, however. In fact, with these changes there is little reason to ever actually create a reference value with anything other than its underlying concrete type: let ref = &r as auth{Provider} &Vault
makes a lot more sense than let ref = &r as auth{Provider} &{Provider, Receiver}
given that reference types can now be downcast. The "security" aspect of restricted types is effectively being moved to the auth
part of the type, while the actual reference type itself is now only being used for interface conformance checking.
I suspect that if we implemented this change, we would only really see restricted types being used as supertypes; i.e. if I want to write a function that operates over all Provider
s I would write it as fun(_ p: {Provider})
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I initially had feedback similar to Supun's, but ended up not posting it, so it's great it came up again:
Restrictions (Us
in T{Us}
) are not really restrictions anymore, as downcasting is now allowed.
We still might want to express something like T{Us}
(where Us
is a set of interfaces) for the case where T
is AnyStruct
/AnyResource
. Maybe we should just make the syntax {Us}
and call it an "interface set"?
The continued existence of restricted types (T{Us}
) maybe cause confusion for developers, so maybe we should just get rid of them along the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I really like this idea; the interface set type makes a lot of sense and is more intuitive than restricted types anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With restricted types gone, {Us}
sounds more like an 'intersection type' 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, {Us}
has been always pretty much like TypeScript's intersection types. Swift has protocol composition, a similar feature (even uses the same U1 & U2
syntax as TS), though there such a type is not first class (e.g a variable cannot have such a type)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for changing restricted types to intersection types. Allowing interfaces in type position (even if they need to be qualified somehow) would obviate the need for a separate Interface
proxy type, making Cadence issue #2233 trivial to implement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe replacing restricted types with standalone intersection types should be its own FLIP?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great proposal! 👏
I read through everything except for the attachments discussion point.
In general this a great proposal, with great user benefits: it improves usability, i.e. working with references; and at the same time also increases safety / reduces accidents / the potential of foot-guns. It would be great to get the proposed changes in 👍
Maybe: In the alternatives considered, describe the earlier idea to allow implementors of concrete types to specify auth
for interfaces in the conformance list and compare it to the current proposal:
- The earlier idea had one "decision point"
- Interface authors did not have to, but also could not specify which functions are auth or not
- Implementors of concrete types could specify
auth
for interfaces in the conformance list - Issuer of references did not have to, but also could not specify which interfaces are auth or not
- The current proposal has a mix of "decision points":
- Interface authors must determine which functions should be authorized
- Implementors of concrete types do not have to, but also cannot make any decisions (probably OK)
- Issuers of references can decide auth restrictions
The biggest issue for the proposal is not the proposed changes themselves, but its rollout, i.e. the compatibility problems: interface authors need to change access modifier from pub
to auth
, e.g. for FungibleToken.Provider
. I do not think this is a blocker, but will be a challenge.
The second biggest issue is the effort the implementation effort: The existing reference and restriction rules and their implementation are very complex and need to be removed, the new rules are complex too. As this changes the core primitives of access control, the new implementation must be absolutely secure.
Thank you for adding the discussion points at the end. I quite like the idea mentioned at the end, to issue auth/non-auth references based on path domain.
|
||
Currently in Cadence, when users are in possession of a reference value, unless that reference is declared | ||
with an `auth` type, users are unable to downcast that reference to a more specific type. This restriction | ||
exists for security reasons; it should not be possible for someone with a `&{Balance}` reference to downcast that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make a general point, and then mention (and link to) the Fungible Token standard contract as a concrete example, before referring to specific types defined in that contract – readers may not (immediately) know what those types (Balance
, Vault
) refer to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems likely to me that anybody reading or contributing to a language FLIP for Cadence or Flow is aware of the basic Balance
, Vault
, Provider
and Receiver
types.
to a `&Vault`. Cadence's current access control model relies on exposing limited interfaces to grant others capabilities | ||
to resources they own. | ||
|
||
However, this can often result in restrictions that are unnecessary and onerous to users. In the above example, a user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Fungible Token contract is a great running example 👍
Maybe mention that:
Balance
provides access to the read-only fieldbalance
Receiver
provides access to a mutating functiondeposit
, but that mutation is "safe" in the sense that a call by any party is OKDeposit
provides access to a mutating functionwithdraw
, but that mutation is "unsafe", it should not be possible for any party to call the function, only certain authorized parties
The Balance
and Receiver
types are already mentioned, but maybe help the reader and explain why their use is sade, so they do not have to look up the details in the code, and also mention the Deposit
type explicitly and why access should be restricted.
Co-authored-by: Bastian Müller <[email protected]>
…inati/auth-fields
Yes, I think this is going to be by far the hardest part of the change; I believe we should evaluate the proposal on its own merits, but I also think that "this is a good idea but it's not feasible to roll out" is also a reasonable stance to take here. |
Co-authored-by: Naomi Liu <[email protected]> Co-authored-by: Bastian Müller <[email protected]>
This is a great proposal but how to migrate storage ? Stored capabilities with restricted references for example. |
This is a great question and probably one of the more challenging parts of this problem. One potential option would be to transform any existing references in storage into the new format based on their borrow type. I.e., any old reference with type |
Feedback from @sisyphusSmiling on the new attachments preview release has identified an attachments use case (in addition to the iteration case we already know about) that would benefit a lot from this new feature. In the demo he is working on, he has an attachment for In his use case, he'd like to be able to call
Then, someone with just a reference to the base |
Regarding this proposed example above, after considering this and starting work on a prototype for it, I realized that allowing entitlement mappings to be declared inside structs and resources necessitates the question of how these values are inherited from interfaces and how conformance checks work for them. This introduces unnecessary complexity with unclear benefit, so unless there is a compelling reason to do otherwise I am going to opt against this proposed change. |
A single member definition can include multiple entitlements, using either a `|` or a `,` separator when defining the list. | ||
|
||
An entitlement list defined using a `|` functions like a disjunction (or an "or"); it is accessible to any `auth` reference with any of those entitlements. | ||
An entitlement list defined using a `,` functions like a conjunction set (or an "and"); it is accessible only to an `auth` reference with all of those entitlements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this section should have some note on precedence (eg. How do we interpret auth(A, B | C)
.) FWIW- My suggestion is that any mix of AND and OR operators must include parens for precedence (making my example a syntax error that would require the author to explicitly write auth((A, B) | C)
or auth(A, (B | C))
.
I also think that +
would be better than ,
for this: eg. auth(A+B)
instead of auth(A,B)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will specify this in the FLIP directly, but the reason there's no note on precedence for these operators here is because you cannot mix them: for simplicity's sake an entitlements set can either be a disjunction or a conjunction, but not both. This is both a simplification for the users (especially because the actual use case for a mixed set would be very rare) and also on the implementation side (since we won't need to implement a full-on boolean algebra solver to decide entitlement relations).
As for +
, I think this would be confusing, since +
often means "or" in other languages, especially those with algebraic types, where the "sum" type represents the union/disjunction between two types and the "product" type(which is often denoted with a ,
like in a tuple type) represents the intersection/conjunction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally i feel that || and && are more natural operators to use.
|
||
let entitledRef = &r as auth(OuterEntitlement) &OuterResource | ||
let entiteldSubRef = r.ref // `OuterEntitlement` is defined to map to `SubEntitlement`, so this access yields a value of type `auth(SubEntitlement) &SubResource` | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hrm. This might point to a bigger problem. Using references as data members of Resource objects isn't really great for this example, either.
But! It raises a bigger question. If the standard way of storing a Resource inside a Resource is not via a reference, what happens here:
resource R {
access(all) let inner: auth(X) @SomeType
access(all) fun typeFun() {
self.inner // what is the type of this statement?
}
}
fun test(r: &R) {
r.inner // What is the type of this statement?
}
You'd think that self.inner
would be type @SomeType
. And, intuitively, I would say that r.inner
would be of type auth(X) &SomeType
. But why is one a reference and one direct access? Could we say that self.inner
is actually auth(owner) &SomeType
instead(at least, when used as an r-value)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand the question here; the auth(X) @SomeType
can't actually exist in the language (unless you are proposing here that we add it), as only references can posses an auth
modifier. I've rewritten this example to use accessor functions, so hopefully this will clarify the point more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we talk live about this? One of the big motivations for the mapping feature was to make using the composition pattern much smoother in Cadence. If a Resource that contains a Resource can only grant access to the inner Resource via a method call, I feel like we're falling short. Maybe it's a future enhancement tho?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was discussed as part of the "external mutability" discussions we have recently. There, one idea was that field access on owned fields keeps resulting in the owned type (in typeFun
, self.inner
has type @SomeType
), as it does currently, but external field access on references results in a reference (in test
, r.inner
has type &SomeType
).
Agreed that it might be better to discuss this topic separately from this FLIP.
If this were to succeed, then a user could create an `OuterResource` with only a `B`-entitled reference to `SubResource`, and use the mapping on it to get a `D`-entitled | ||
reference to the `SubResource`, since the owner of the `OuterResource` can get any entitled reference to it. In order to be a valid initial value for the `self.ref` field here, | ||
the `ref` argument to the constructor would need to have type `auth(B, D, E) &SubResource`. This is most easily achievable if the creator of the `OuterResource` is also | ||
the owner of the inner resource. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this Flip should include a minimal set of entitlements for the built-in containers, and this would be a great time to include a concrete example of how they could be used, which I think would motivate the reader to better understand the power of mappings.
So, assume that we have Insert
, Replace
, and Remote
entitlements for the built-in dictionary types. A neat example of using mappings would be something like this:
entitlement mapping UserListUpdates {
AddUser -> Insert
RemoveUser -> Remove
UpdateUser -> Replace
all -> all
}
access (UserListUpdates) let userList: auth(UserListUpdates) userList{String: UserData}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe @SupunS was looking into potentially adding entitlement support for using entitlements to handle mutability on built in container types. I am in favor of adding these, but IMO we shouldn't expand the scope of this particular FLIP any further than it's already grown. These can be proposed in a follow-up FLIP that expands upon the existing entitlements to add them to containers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above: using entitlements to address the "external mutability" problem is a great idea, and so it proposing entitlements for built-in types.
However, such proposals should probably be done in separate FLIP(s), as this is one is already very large.
Let's continue discussing these ideas as part of the "external mutability" problem discussions.
r.foo() | ||
let ref: auth(E) &R = &r | ||
ref.foo() | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll be honest. I would love to remove or at least deprecate pub
and priv
. If this code examples (for example) used access(self)
and access(all)
it'd be easier to parse (visually, as a human) and more consistent.
pub resource R {
access(E) fun foo() { ... }
access(all) fun bar() {
self.foo()
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree 100%. I'll update the examples in the FLIP, but IMO a proposal to remove and replace pub
and priv
deserves its own FLIP.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes it simpler, and I like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been busy so have not had time to look at this or follow this much at all.
Is cadence supposed to be simple and readable?
Some of these advanced entitlement mapping are not simple and not easy to understand. What is the added benefit here? I really second what @dete is saying here in that we need practical examples not abstract ones.
r.foo() | ||
let ref: auth(E) &R = &r | ||
ref.foo() | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes it simpler, and I like that.
A single member definition can include multiple entitlements, using either a `|` or a `,` separator when defining the list. | ||
|
||
An entitlement list defined using a `|` functions like a disjunction (or an "or"); it is accessible to any `auth` reference with any of those entitlements. | ||
An entitlement list defined using a `,` functions like a conjunction set (or an "and"); it is accessible only to an `auth` reference with all of those entitlements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally i feel that || and && are more natural operators to use.
I completely agree when the purpose of these examples is to teach users how the language works and tutorialize the new features for new users. When we write the documentation for entitlements, we should make sure that the examples we use are relevant to the kind of code smart contract developers actually write. We will also need to update the existing Cadence tutorial to use the new entitlements in their implementation, which I think will go a long way towards making this feature simple and comprehensible to people. However, the purpose of this FLIP is not to explain how this feature is going to work for users, and should not be used as reference documentation after the feature is deployed. The FLIP is a reference document for the designers and implementors of the language, and needs to be precise and detailed in its language, and simple (to the point of abstraction) in its examples so that people who are debating the design and implementation of the feature have all the information they need to make decisions. This means that the FLIP is necessarily going to be more complex and more abstract than the actual documentation will be, and I wouldn't use this FLIP as a basis for what the documentation will look like when it's finished. |
Hi everyone! Apologies as well for only just not commenting on this FLIP, I'll try to make a note to come back to these more often in the future but it has been hard to keep track of them lately. I wanted to summarize some points after our breakout session for Hybrid Custody Wednesday and a Design meeting yesterday (Thursday), and share some thoughts that I'm hoping we can clarify a path for so that the Hybrid Custody initiative can continue uninterrupted
Some thoughts: Breaking CapabilitiesBreaking Capabilities isn't really an option, not just for Hybrid Custody but for lots of products out there which store capabilities which they are not able to reissue. For instance, if we broken capabilities, all storefront listings across all products (TopShot, All Day, Flowty, Gaia, Find) would all break and only users would be able to fix them. In general, I do not think we can consider any change which will break existing Capabilities. Cap Cons has an edge case I think we can live with (if at the time of migration, the capability's link is not valid, it will not be valid), but my understanding on Entitlements after hearing information about it in our calls this week is that they would all be invalid. It sounds like this is a tradeoff to prevent malicious access of public function modifiers but please correct me if I'm wrong. Let's assume breaking capabilities is not an option, here. What other ways are there to handle this migration? I've seen it mentioned that you could make all capabilities that currently exist into AuthAccount type definitionsAccount linking has been out for a while at this point (a month?). If we change the type definition of Accounts such that What is the plan to move forward, there? What I don't want is a world where we have to pause all of our Hybrid Custody work because changes like this are in-flight. It could be months before this/stable cadence are out so waiting on them isn't a great option for us. Daniel mentioned some ability to make a type alias to keep AuthAccount for legacy reasons such as what I'm describing here, is that the plan? And if not, how would transaction prepare statements have to change and what's the plan to get all transactions updated? Especially for Dapper Wallet, that will be a huge challenge. Pub access modifierBased on the last two meetings described earlier, any functions with the @bluesign mentioned the following in the hybrid custody discord channel as an option:
We don't want to throw a huge wrench in Entitlements or Hybrid Custody, and we also want to make sure that other products not related to either initiative don't break irreparably when this feature goes out. What would make the most sense in terms of tackling these challenges? We mentioned a breakout session in our last call, can we start there? We're trying pretty hard to keep momentum on the Hybrid Custody front, so the more momentum we can maintain there the better |
Thanks for your thoughts @austinkline, these are great. I'll address them in reverse order (from least to most complex).
I just published #84, which proposes that we remove the
Based on what you've outlined here I think it makes sense to leave
In the above point, we would be able to define
These two assumptions do not hold in general, which is what makes it very complicated to figure out how to migrate an existing We are going to have a breakout session to discuss this further (please attend if you are interested in this topic!), because I agree that we can't just break all existing capabilities, so we need to come up with a migration strategy. One potential option would be to allow contract upgrades to be scheduled in advance, and use the entitlements added in those prescheduled upgrades to give us the answer to point 2 above, which would allow us to migrate references of type |
When is entitlements comming out then? Because that cannot happen at the same time as this. One has to come first to make people safe and then we can fix things with entitlments later. We have to do this in multiple steps even though i hate breaking backwards compatability often. So in my eyes it looks like stable cadence will have to atleast be a double rocket. First we launch prelimiary changes, everybody have to fix things, then we do another change and people have to fix again and then we are flying to the moon? |
@bjartek Changing |
Ok, that is better then them working and beeing exploitable atleast. |
@austinkline Thank you for the great feedback, questions, and concerns! We will work through these points in the coming weeks. In the meantime, please do not consider this FLIP or the current lack of answers for your points be any blocker for any work that you have in progress. |
Thanks @turbolent and @dsainati1! I'll keep an eye out for follow-up discussion. Please feel free to loop me in if I can assist in any way |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of minor nitpicks:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
file name should be 20221214-...
Co-authored-by: Bastian Müller <[email protected]>
…inati/auth-fields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
Spoke with Dete offline about his concerns and addressed them.
This adds a FLIP proposing a Cadence change adding a new entitlement construct, a new access control modifier using entitlements, and changing the behavior of references to permit safe downcasting.